From 585f03bf45df4ac7ed61d22c9dbe10d8e453199d Mon Sep 17 00:00:00 2001
From: alyssaf16 <alyssa_feinberg@brown.edu>
Date: Wed, 5 Jun 2024 15:06:15 -0400
Subject: Flashcards changes

---
 .../views/collections/CollectionCarouselView.tsx   | 51 +++++++++++++++-------
 1 file changed, 35 insertions(+), 16 deletions(-)

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

diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 2adad68e0..2893de762 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -2,7 +2,7 @@
 /* eslint-disable jsx-a11y/click-events-have-key-events */
 /* eslint-disable react/jsx-props-no-spreading */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { computed, makeObservable } from 'mobx';
+import { computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { StopEvent, returnFalse, returnOne, returnTrue, returnZero } from '../../../ClientUtils';
@@ -24,6 +24,7 @@ enum cardMode {
     PRACTICE = 'practice',
     STAR = 'star',
     QUIZ = 'quiz',
+    ALL = 'all',
 }
 enum practiceVal {
     MISSED = 'missed',
@@ -32,12 +33,15 @@ enum practiceVal {
 @observer
 export class CollectionCarouselView extends CollectionSubView() {
     private _dropDisposer?: DragManager.DragDropDisposer;
+    @observable private _message = 'Drag a document';
     get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore
     get starField()     { return this.fieldKey + "_star"; } // prettier-ignore
 
     constructor(props: any) {
         super(props);
         makeObservable(this);
+        this.layoutDoc.filterOp = cardMode.ALL;
+        this.layoutDoc._carousel_index = 0;
     }
 
     componentWillUnmount() {
@@ -61,7 +65,7 @@ export class CollectionCarouselView extends CollectionSubView() {
     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) {
+            while (!match(this.carouselItems?.[startInd].layout) && (startInd + this.carouselItems.length) % this.carouselItems.length !== this.layoutDoc._carousel_index) {
                 startInd = (startInd + dir + this.carouselItems.length) % this.carouselItems.length;
             }
             if (match(this.carouselItems?.[startInd].layout)) {
@@ -73,16 +77,19 @@ export class CollectionCarouselView extends CollectionSubView() {
         switch (StrCast(this.layoutDoc.filterOp)) {
             case cardMode.STAR:  // go to a flashcard that is starred, skip the ones that aren't
                 if (!moveToCardWithField((doc: Doc) => !!doc[this.starField])) {
+                    // this.layoutDoc._carousel_index
                     this.layoutDoc.filterOp = undefined;  // if there aren't any starred, show all cards
+                    this._message = 'No starred items. Return to All to star documents.'
                 }
                 break;
             case cardMode.PRACTICE: // go to a new index that is missed, skip the ones that are correct
                 if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT)) {
                     this.layoutDoc.filterOp = undefined;  // if all of the cards are correct, show all cards and exit practice mode
-
+                    this._message = 'Finished! Return to All.'
                     this.carouselItems.forEach(item => { // reset all the practice values
                         item.layout[this.practiceField] = undefined;
                     });
+                    // this.layoutDoc._carousel_index = -1;
                 }
                 break;
             default: moveToCardWithField(returnTrue);
@@ -112,6 +119,7 @@ export class CollectionCarouselView extends CollectionSubView() {
         e.stopPropagation();
         const curDoc = this.carouselItems[NumCast(this.layoutDoc._carousel_index)];
         curDoc.layout[this.starField] = curDoc.layout[this.starField] ? undefined : true;
+        // this.layoutDoc._carousel_index = undefined;
     };
 
     /*
@@ -123,7 +131,9 @@ export class CollectionCarouselView extends CollectionSubView() {
         curDoc.layout[this.practiceField] = val;
         this.advance(e);
     };
-
+    clearContent = () => {
+        this.carouselItems?.map(doc => (doc.layout[this.practiceField] = undefined));
+    };
     captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string): any => {
         // first look for properties on the document in the carousel, then fallback to properties on the container
         const childValue = doc?.['caption_' + property] ? this._props.styleProvider?.(doc, captionProps, property) : undefined;
@@ -133,16 +143,22 @@ export class CollectionCarouselView extends CollectionSubView() {
     onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
     onContentClick = () => ScriptCast(this.layoutDoc.onChildClick);
     captionWidth = () => this._props.PanelWidth() - 2 * this.marginX;
+    setFilterMode = (mode: cardMode) => {
+        this.layoutDoc.filterOp = mode;
+        if (mode == cardMode.STAR) this.move(1);
+        this.clearContent();
+    };
     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 = cardMode.STAR;},     icon: 'star',}); // prettier-ignore
-        revealItems.push({description: 'Practice Mode', event: () => {this.layoutDoc.filterOp = cardMode.PRACTICE;}, icon: 'check',}); // prettier-ignore
-        revealItems.push({description: 'Quiz Cards',    event: () => {this.layoutDoc.filterOp = cardMode.QUIZ;},     icon: 'pencil',}); // prettier-ignore
+        revealItems.push({description: 'All',           event: () => {this.setFilterMode(cardMode.ALL);},         icon: 'layer-group',}); // prettier-ignore
+        revealItems.push({description: 'Star',          event: () => {this.setFilterMode(cardMode.STAR);},     icon: 'star',}); // prettier-ignore
+        revealItems.push({description: 'Practice Mode', event: () => {this.setFilterMode(cardMode.PRACTICE);}, icon: 'check',}); // prettier-ignore
+        cm.addItem({description: 'Quiz Cards',    event: () => {this.setFilterMode(cardMode.QUIZ);},     icon: 'pencil',}); // prettier-ignore
         !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
+        //cm.addItem({description: 'Quiz Cards',    event: () => {this.layoutDoc.filterOp = cardMode.QUIZ; this.clearContent()});
     };
     @computed get content() {
         const index = NumCast(this.layoutDoc._carousel_index);
@@ -221,17 +237,15 @@ export class CollectionCarouselView extends CollectionSubView() {
                     background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor),
                     color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color),
                 }}>
-                {this.content}
+                {this.layoutDoc.filterOp ? this.content : <p className="message">{this._message}</p>}
+                {/* {(this.carouselItems?.filter(doc => doc.layout[this.practiceField] !== practiceVal.CORRECT)).length == 0 ? null : this.content} */}
                 {/* Displays a message to the user to add more flashcards if they are in practice mode and no flashcards are there. */}
                 <p
+                    className="message"
                     style={{
                         display: !this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] && this.layoutDoc.filterOp === cardMode.PRACTICE ? 'flex' : 'none',
-                        justifyContent: 'center',
-                        alignItems: 'center',
-                        height: '100%',
-                        zIndex: '-1',
                     }}>
-                    Add flashcards!
+                    Add flashcards
                 </p>
                 {/* Displays a message to the user that a flashcard was recently missed if they had previously gotten it wrong. */}
                 <p
@@ -241,11 +255,16 @@ export class CollectionCarouselView extends CollectionSubView() {
                         position: 'relative',
                         left: '10px',
                         top: '10px',
-                        display: this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] ? (this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.practiceField] === practiceVal.MISSED ? 'block' : 'none') : 'none',
+                        width: '10px',
+                        display: this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)]
+                            ? this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.practiceField] === practiceVal.MISSED && this.layoutDoc.filterOp === cardMode.PRACTICE
+                                ? 'block'
+                                : 'none'
+                            : 'none',
                     }}>
                     Recently missed!
                 </p>
-                {this.Document._chromeHidden ? null : this.buttons}
+                {this.Document._chromeHidden || !this.layoutDoc.filterOp ? null : this.buttons}
             </div>
         );
     }
-- 
cgit v1.2.3-70-g09d2


From c76505f56a83625e3993427838aaee0c54c5fb81 Mon Sep 17 00:00:00 2001
From: alyssaf16 <alyssa_feinberg@brown.edu>
Date: Thu, 6 Jun 2024 09:53:12 -0400
Subject: Fixes to chatcard and quizcard

---
 src/client/apis/gpt/GPT.ts                         |  2 +-
 .../views/collections/CollectionCarouselView.tsx   |  7 ++--
 src/client/views/nodes/ComparisonBox.tsx           | 43 ++++++++++++++++++----
 src/client/views/nodes/DocumentView.tsx            | 19 +---------
 4 files changed, 40 insertions(+), 31 deletions(-)

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 05007960d..b036349dc 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -51,7 +51,7 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
         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',
+        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 the Rubric is incorrect, explain why. If there are no differences, say correct. If it is empty, say there is nothing for me to evaluate.',
     },
 };
 
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 2893de762..53d14e6e0 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -35,6 +35,7 @@ export class CollectionCarouselView extends CollectionSubView() {
     private _dropDisposer?: DragManager.DragDropDisposer;
     @observable private _message = 'Drag a document';
     get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore
+    get sideField() { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore
     get starField()     { return this.fieldKey + "_star"; } // prettier-ignore
 
     constructor(props: any) {
@@ -131,9 +132,6 @@ export class CollectionCarouselView extends CollectionSubView() {
         curDoc.layout[this.practiceField] = val;
         this.advance(e);
     };
-    clearContent = () => {
-        this.carouselItems?.map(doc => (doc.layout[this.practiceField] = undefined));
-    };
     captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string): any => {
         // first look for properties on the document in the carousel, then fallback to properties on the container
         const childValue = doc?.['caption_' + property] ? this._props.styleProvider?.(doc, captionProps, property) : undefined;
@@ -146,7 +144,8 @@ export class CollectionCarouselView extends CollectionSubView() {
     setFilterMode = (mode: cardMode) => {
         this.layoutDoc.filterOp = mode;
         if (mode == cardMode.STAR) this.move(1);
-        this.clearContent();
+        if (mode == cardMode.QUIZ) this.carouselItems?.map(doc => (doc.layout[this.sideField] = undefined));
+        this.carouselItems?.map(doc => (doc.layout[this.practiceField] = undefined));
     };
     specificMenu = (): void => {
         const cm = ContextMenu.Instance;
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index c0c173d9a..6d4af2f3e 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -25,6 +25,8 @@ import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import ReactLoading from 'react-loading';
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
 
 @observer
 export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@@ -282,7 +284,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         // Call the GPT model and get the output
         this.layoutDoc[`_${this._props.fieldKey}_usePath`] = 'alternate';
         this._outputValue = '';
-        if (this._inputValue) this.askGPT();
+        if (this._inputValue) this.askGPT(GPTCallType.QUIZ);
     };
 
     @action handleRenderClick = () => {
@@ -290,12 +292,22 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         this.layoutDoc[`_${this._props.fieldKey}_usePath`] = undefined;
     };
 
-    animateRes = (resIndex: number, newText: string) => {
+    animateRes = (resIndex: number, newText: string, callType: GPTCallType) => {
         if (resIndex < newText.length) {
             // const marks = this._editorView?.state.storedMarks ?? [];
-            this._outputValue += newText[resIndex];
+            switch (callType) {
+                case GPTCallType.CHATCARD:
+                    DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text += newText[resIndex];
+                    break;
+                case GPTCallType.QUIZ:
+                    this._outputValue += newText[resIndex];
+                    break;
+                default:
+                    return;
+            }
+
             // this._editorView?.dispatch(this._editorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(this._outputValue));
-            setTimeout(() => this.animateRes(resIndex + 1, newText), 20);
+            setTimeout(() => this.animateRes(resIndex + 1, newText, callType), 20);
         }
     };
 
@@ -303,18 +315,22 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
      * 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> => {
+    askGPT = async (callType: GPTCallType): Promise<string | undefined> => {
         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;
         this._loading = true;
+        if (callType == GPTCallType.CHATCARD) {
+            DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = '';
+            this.flipFlashcard();
+        }
         try {
-            const res = await gptAPICall(queryText, GPTCallType.QUIZ);
+            const res = await gptAPICall(callType == GPTCallType.QUIZ ? queryText : questionText, callType);
             if (!res) {
                 console.error('GPT call failed');
                 return;
             }
-            this.animateRes(0, '\n\n' + res);
+            this.animateRes(0, res, callType);
             // this._outputValue = res;
             console.log(res);
         } catch (err) {
@@ -325,6 +341,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     layoutWidth = () => NumCast(this.layoutDoc.width, 200);
     layoutHeight = () => NumCast(this.layoutDoc.height, 200);
 
+    specificMenu = (): void => {
+        const cm = ContextMenu.Instance;
+        cm.addItem({ description: 'Create an Answer on the Back', event: () => this.askGPT(GPTCallType.CHATCARD), icon: 'pencil' });
+    };
+
     render() {
         const clearButton = (which: string) => (
             <Tooltip title={<div className="dash-tooltip">remove</div>}>
@@ -425,7 +446,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         </div>
                         <div className="submit-button" style={{ overflow: 'hidden', marginBottom: '2px', display: this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? 'flex' : 'none' }}>
                             <button type="button" onClick={this.handleRenderClick} style={{ borderRadius: '2px' }}>
-                                Try Again
+                                Redo the Question
                             </button>
                         </div>
                     </div>
@@ -436,6 +457,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             return (
                 <div
                     className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */
+                    onContextMenu={this.specificMenu}
                     style={{ display: 'flex', flexDirection: 'column' }}
                     onMouseEnter={() => {
                         this.hoverFlip('alternate');
@@ -446,6 +468,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     // onPointerUp={() => (this._isAnyChildContentActive = true)}
                 >
                     {displayBox(`${this.fieldKey}_${side === 0 ? 1 : 0}`, side, this._props.PanelWidth() - 3)}
+                    {this._loading ? (
+                        <div className="loading-spinner" style={{ position: 'absolute' }}>
+                            <ReactLoading type="spin" height={30} width={30} color={'blue'} />
+                        </div>
+                    ) : null}
                     {this.overlayAlternateIcon}
                 </div>
             );
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 8bf7b094f..a25249eac 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -111,6 +111,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     private _downTime: number = 0;
     private _lastTap: number = 0;
     private _doubleTap = false;
+    private _loading = false;
     private _mainCont = React.createRef<HTMLDivElement>();
     private _titleRef = React.createRef<EditableView>();
     private _dropDisposer?: DragManager.DragDropDisposer;
@@ -501,21 +502,6 @@ 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 {
-            const 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();
@@ -574,9 +560,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => DocumentView.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 an answer on the back', 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' });
-- 
cgit v1.2.3-70-g09d2


From d1b7e29761fa0395263bff3521f148170659ff62 Mon Sep 17 00:00:00 2001
From: alyssaf16 <alyssa_feinberg@brown.edu>
Date: Fri, 7 Jun 2024 21:48:34 -0400
Subject: Flashcard menus

---
 src/client/views/MainView.tsx                      |  1 +
 .../views/collections/CollectionCarouselView.scss  | 42 ++++++++++++-
 .../views/collections/CollectionCarouselView.tsx   | 53 ++++++++++++----
 src/client/views/nodes/ComparisonBox.tsx           | 73 +++++++++++++++++-----
 src/client/views/nodes/DocumentView.tsx            | 14 ++---
 5 files changed, 147 insertions(+), 36 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 31d88fb87..541b15006 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -548,6 +548,7 @@ export class MainView extends ObservableReactComponent<{}> {
                 fa.faRobot,
                 fa.faSatellite,
                 fa.faStar,
+                fa.faFilePen,
             ]
         );
     }
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss
index 975b352cf..c4679e888 100644
--- a/src/client/views/collections/CollectionCarouselView.scss
+++ b/src/client/views/collections/CollectionCarouselView.scss
@@ -50,7 +50,7 @@
 }
 .carouselView-star {
     top: 0;
-    right: 20;
+    left: 0;
 }
 .carouselView-remove {
     top: 80%;
@@ -60,6 +60,46 @@
     top: 80%;
     right: 52%;
 }
+.carouselView-quiz {
+    position: absolute;
+    display: flex;
+    top: 5px;
+    right: 8px;
+    &:hover {
+        color: white;
+    }
+}
+
+.carouselView-practice {
+    position: absolute;
+    display: flex;
+    top: 22px;
+    right: 8px;
+    &:hover {
+        color: white;
+    }
+}
+.carouselView-starFilter {
+    position: absolute;
+    display: flex;
+    top: 40px;
+    right: 7px;
+    &:hover {
+        color: white;
+    }
+}
+
+.carouselView-menu {
+    position: absolute;
+    display: flex;
+    top: 2px;
+    right: 2px;
+    width: 30;
+    height: 60;
+    border-radius: 5px;
+    color: rgba(255, 255, 255, 0.5);
+    background: rgba(0, 0, 0, 0.1);
+}
 
 .carouselView-back:hover,
 .carouselView-fwd:hover {
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 53d14e6e0..8da808065 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -19,6 +19,7 @@ import { FieldViewProps } from '../nodes/FieldView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import './CollectionCarouselView.scss';
 import { CollectionSubView } from './CollectionSubView';
+import { Tooltip } from '@mui/material';
 
 enum cardMode {
     PRACTICE = 'practice',
@@ -143,22 +144,26 @@ export class CollectionCarouselView extends CollectionSubView() {
     captionWidth = () => this._props.PanelWidth() - 2 * this.marginX;
     setFilterMode = (mode: cardMode) => {
         this.layoutDoc.filterOp = mode;
+        console.log('MODE' + mode);
+        console.log('FILT' + this.layoutDoc.filterOp + ';');
         if (mode == cardMode.STAR) this.move(1);
-        if (mode == cardMode.QUIZ) this.carouselItems?.map(doc => (doc.layout[this.sideField] = undefined));
+        if (mode == cardMode.QUIZ) {
+            this.carouselItems?.map(doc => (doc.layout[this.sideField] = undefined));
+        }
         this.carouselItems?.map(doc => (doc.layout[this.practiceField] = undefined));
     };
-    specificMenu = (): void => {
-        const cm = ContextMenu.Instance;
+    // 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.setFilterMode(cardMode.ALL);},         icon: 'layer-group',}); // prettier-ignore
-        revealItems.push({description: 'Star',          event: () => {this.setFilterMode(cardMode.STAR);},     icon: 'star',}); // prettier-ignore
-        revealItems.push({description: 'Practice Mode', event: () => {this.setFilterMode(cardMode.PRACTICE);}, icon: 'check',}); // prettier-ignore
-        cm.addItem({description: 'Quiz Cards',    event: () => {this.setFilterMode(cardMode.QUIZ);},     icon: 'pencil',}); // prettier-ignore
-        !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
-        //cm.addItem({description: 'Quiz Cards',    event: () => {this.layoutDoc.filterOp = cardMode.QUIZ; this.clearContent()});
-    };
+    //     const revealOptions = cm.findByDescription('Filter Flashcards');
+    //     const revealItems: ContextMenuProps[] = revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : [];
+    //     revealItems.push({description: 'All',           event: () => {this.setFilterMode(cardMode.ALL);},         icon: 'layer-group',}); // prettier-ignore
+    //     revealItems.push({description: 'Star',          event: () => {this.setFilterMode(cardMode.STAR);},     icon: 'star',}); // prettier-ignore
+    //     revealItems.push({description: 'Practice Mode', event: () => {this.setFilterMode(cardMode.PRACTICE);}, icon: 'check',}); // prettier-ignore
+    //     cm.addItem({description: 'Quiz Cards',    event: () => {this.setFilterMode(cardMode.QUIZ);},     icon: 'pencil',}); // prettier-ignore
+    //     !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
+    //     //cm.addItem({description: 'Quiz Cards',    event: () => {this.layoutDoc.filterOp = cardMode.QUIZ; this.clearContent()});
+    // };
     @computed get content() {
         const index = NumCast(this.layoutDoc._carousel_index);
         const curDoc = this.carouselItems?.[index];
@@ -226,6 +231,28 @@ export class CollectionCarouselView extends CollectionSubView() {
         );
     }
 
+    @computed get menu() {
+        return (
+            <div className="carouselView-menu">
+                <Tooltip title="Practice flashcards using GPT">
+                    <div key="back" className="carouselView-quiz" onClick={e => (this.layoutDoc.filterOp === cardMode.QUIZ ? this.setFilterMode(cardMode.ALL) : this.setFilterMode(cardMode.QUIZ))}>
+                        <FontAwesomeIcon icon="file-pen" color={this.layoutDoc.filterOp === cardMode.QUIZ ? 'white' : 'light gray'} size="1x" />
+                    </div>
+                </Tooltip>
+                <Tooltip title={this.layoutDoc.filterOp === cardMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}>
+                    <div key="back" className="carouselView-practice" onClick={e => (this.layoutDoc.filterOp === cardMode.PRACTICE ? this.setFilterMode(cardMode.ALL) : this.setFilterMode(cardMode.PRACTICE))}>
+                        <FontAwesomeIcon icon="check" color={this.layoutDoc.filterOp === cardMode.PRACTICE ? 'white' : 'light gray'} size="1x" />
+                    </div>
+                </Tooltip>
+                <Tooltip title={this.layoutDoc.filterOp === cardMode.STAR ? 'Show all cards' : 'Show only starred cards'}>
+                    <div key="back" className="carouselView-starFilter" onClick={e => (!this.layoutDoc.filterOp ? this.setFilterMode(cardMode.ALL) : this.setFilterMode(cardMode.STAR))}>
+                        <FontAwesomeIcon icon="filter" color={this.layoutDoc.filterOp === cardMode.STAR || !this.layoutDoc.filterOp ? 'white' : 'light gray'} size="1x" />
+                    </div>
+                </Tooltip>
+            </div>
+        );
+    }
+
     render() {
         return (
             <div
@@ -250,6 +277,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                 <p
                     style={{
                         color: 'red',
+                        fontWeight: 'bold',
                         zIndex: '999',
                         position: 'relative',
                         left: '10px',
@@ -263,6 +291,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                     }}>
                     Recently missed!
                 </p>
+                {this.menu}
                 {this.Document._chromeHidden || !this.layoutDoc.filterOp ? null : this.buttons}
             </div>
         );
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index f844892c5..084723d56 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -45,6 +45,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     @observable private _loading = false;
     @observable private _errorMessage = '';
     @observable private _outputMessage = '';
+    @observable private _isEmpty = false;
+
+    public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
 
     @action handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
         this._inputValue = e.target.value;
@@ -162,23 +165,29 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     };
 
     clearDoc = undoable((fieldKey: string) => {
-        delete this.dataDoc[fieldKey];
-        this.dataDoc[fieldKey] = 'empty';
+        // delete this.dataDoc[fieldKey];
+        this.dataDoc[fieldKey] = undefined;
+        this._isEmpty = true;
+        // this.dataDoc[fieldKey] = 'empty';
+        console.log('HERE' + fieldKey + ';');
     }, 'clear doc');
     // clearDoc = (fieldKey: string) => delete this.dataDoc[fieldKey];
 
     moveDoc = (doc: Doc, addDocument: (document: Doc | Doc[]) => boolean, which: string) => this.remDoc(doc, which) && addDocument(doc);
     addDoc = (doc: Doc, which: string) => {
-        if (this.dataDoc[which] && this.dataDoc[which] !== 'empty') return false;
+        if (this.dataDoc[which] && !this._isEmpty) return false;
         this.dataDoc[which] = doc;
         return true;
     };
     remDoc = (doc: Doc, which: string) => {
         if (this.dataDoc[which] === doc) {
-            this.dataDoc[which] = 'empty';
+            this._isEmpty = true;
+            // this.dataDoc[which] = 'empty';
+            console.log('HEREEEE');
             this.dataDoc[which] = undefined;
             return true;
         }
+        console.log('FALSE');
         return false;
     };
 
@@ -268,8 +277,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             if (!this.layoutDoc[`_${this._props.fieldKey}_revealOp`] || this.layoutDoc[`_${this._props.fieldKey}_revealOp`] === 'flip') {
                                 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 ?? ''));
+                                // 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 ?? ''));
                             }
                         })
                     }
@@ -297,10 +306,31 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             <div className="dash-tooltip">Ask GPT to create an answer on the back side of the flashcard</div>
                         )
                     }>
-                    <div style={{ position: 'absolute', bottom: '3px', right: '55px', cursor: 'pointer' }} onPointerDown={e => (!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? this.askGPT(GPTCallType.CHATCARD) : null)}>
+                    <div style={{ position: 'absolute', bottom: '3px', right: '56px', cursor: 'pointer' }} onPointerDown={e => (!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? this.askGPT(GPTCallType.CHATCARD) : null)}>
                         <FontAwesomeIcon icon="lightbulb" size="xl" />
                     </div>
                 </Tooltip>
+                <Tooltip title={<div>Create a flashcard pile</div>}>
+                    <div
+                        style={{ position: 'absolute', bottom: '3px', right: '80px', cursor: 'pointer' }}
+                        onPointerDown={e => {
+                            const collectionArr: Doc[] = [];
+                            collectionArr.push(this.Document);
+                            const newCol = Docs.Create.CarouselDocument(collectionArr, {
+                                _width: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 250),
+                                _height: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 200),
+                                _layout_fitWidth: false,
+                                _layout_autoHeight: true,
+                            });
+                            newCol['x'] = e.clientX - 820;
+                            newCol['y'] = e.clientY - 640;
+                            this._props.addDocument?.(newCol);
+                            this._props.removeDocument?.(this.Document);
+                            this.Document.embedContainer = newCol;
+                        }}>
+                        <FontAwesomeIcon icon="folder-plus" size="xl" />
+                    </div>
+                </Tooltip>
                 <Tooltip title={<div className="dash-tooltip">Hover to reveal</div>}>
                     <div
                         style={{ position: 'absolute', bottom: '3px', right: '25px', cursor: 'pointer' }}
@@ -308,11 +338,18 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="layer-group" size="xl" />
                     </div>
                 </Tooltip>
-                {this.overlayAlternateIcon}
+                {/* <Tooltip title={<div className="dash-tooltip">Remove this side of the flashcard</div>}>
+                    <div
+                        style={{ position: 'absolute', bottom: '3px', right: '80px', cursor: 'pointer' }}
+                        onPointerDown={e => this.closeDown(e, this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this._props.fieldKey + '_1' : this._props.fieldKey + '_0')}>
+                        <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="trash-can" size="xl" />
+                    </div>
+                </Tooltip> */}
+                {/* {this.overlayAlternateIcon} */}
             </div>
         );
     }
-
+    // this.closeDown(e, this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this.fieldKey + '_0' : this.fieldKey + '_1')}
     @action activateContent = () => {
         this.childActive = true;
     };
@@ -434,7 +471,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         hideLinkButton
                         pointerEvents={this.childActive ? undefined : returnNone}
                     />
-                    <div style={{ position: 'absolute', top: '-5px', left: '2px' }}>{layoutString ? null : clearButton(whichSlot)}</div>
+                    {/* <div style={{ position: 'absolute', top: '-5px', left: '2px' }}>{layoutString ? null : clearButton(whichSlot)}</div> */}
                 </> // placeholder image if doc is missingleft: `${NumCast(this.layoutDoc.width, 200) - 33}px`
             ) : (
                 <div className="placeholder">
@@ -452,7 +489,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     this.activateContent();
                 }}
                 ref={ele => this.createDropTarget(ele, which, index)}>
-                {displayDoc(which)}
+                {!this._isEmpty ? displayDoc(which) : null}
+                {/* {this.dataDoc[this.fieldKey + '_0'] !== 'empty' ? displayDoc(which) : null} */}
             </div>
         );
 
@@ -460,7 +498,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             const side = this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? 1 : 0;
 
             // add text box to each side when comparison box is first created
-            if (!(this.dataDoc[this.fieldKey + '_0'] || this.dataDoc[this.fieldKey + '_0'] === 'empty')) {
+            // (!this.dataDoc[this.fieldKey + '_0'] && this.dataDoc[this._props.fieldKey + '_0'] !== 'empty')
+            if (!this.dataDoc[this.fieldKey + '_0'] && !this._isEmpty) {
                 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.
@@ -468,7 +507,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 newDoc[DocData].text = dataSplit[1];
                 this.addDoc(newDoc, this.fieldKey + '_0');
             }
-            if (!(this.dataDoc[this.fieldKey + '_1'] || this.dataDoc[this.fieldKey + '_1'] === 'empty')) {
+            if (!this.dataDoc[this.fieldKey + '_1'] && !this._isEmpty) {
                 const dataSplit = StrCast(this.dataDoc.data).split('Answer');
                 const newDoc = Docs.Create.TextDocument(dataSplit[0]);
                 // if there is text from the pdf ai cards, put the answer on the alternate side.
@@ -478,6 +517,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             }
 
             // render the QuizCards
+            console.log('GERE' + DocCast(this.Document.embedContainer).filterOp);
             if (DocCast(this.Document.embedContainer) && DocCast(this.Document.embedContainer).filterOp === 'quiz') {
                 const text = StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text);
                 return (
@@ -489,6 +529,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                                 value={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this._outputValue : this._inputValue}
                                 onChange={this.handleInputChange}
                                 onScroll={e => e.stopPropagation()}
+                                placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''}
                                 readOnly={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate'}></textarea>
 
                             {this._loading ? (
@@ -516,7 +557,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 <div
                     className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */
                     // onContextMenu={this.specificMenu}
-                    style={{ display: 'flex', flexDirection: 'column' }}
+                    style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
                     onMouseEnter={() => {
                         this.hoverFlip('alternate');
                     }}
@@ -531,8 +572,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             <ReactLoading type="spin" height={30} width={30} color={'blue'} />
                         </div>
                     ) : null}
-                    {this.flashcardMenu}
-                    {/* {this.overlayAlternateIcon} */}
+                    {this._props.isContentActive() ? this.flashcardMenu : null}
+                    {this.overlayAlternateIcon}
                 </div>
             );
         }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 2f3357791..d4c31a5b3 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -566,13 +566,13 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             !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' }); // prettier-ignore
-                revealItems.push({ description: 'Flip', event: () => { this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip'; }, icon: 'rotate' }); // prettier-ignore
-                !revealOptions && cm.addItem({ description: 'Reveal Options', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
-            }
+            // 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' }); // prettier-ignore
+            //     revealItems.push({ description: 'Flip', event: () => { this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip'; }, icon: 'rotate' }); // prettier-ignore
+            //     !revealOptions && cm.addItem({ description: 'Reveal Options', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
+            // }
 
             if (this._props.bringToFront) {
                 const zorders = cm.findByDescription('ZOrder...');
-- 
cgit v1.2.3-70-g09d2


From ab162dc8027508253accdbc4259646af7f4b9b0a Mon Sep 17 00:00:00 2001
From: alyssaf16 <alyssa_feinberg@brown.edu>
Date: Sun, 9 Jun 2024 00:01:22 -0400
Subject: filter fixed

---
 .../views/collections/CollectionCarouselView.tsx   | 207 +++++++++++++++------
 src/client/views/nodes/ComparisonBox.tsx           |   4 +-
 2 files changed, 153 insertions(+), 58 deletions(-)

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

diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 8da808065..979f7ea2a 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -2,7 +2,7 @@
 /* eslint-disable jsx-a11y/click-events-have-key-events */
 /* eslint-disable react/jsx-props-no-spreading */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { computed, makeObservable, observable } from 'mobx';
+import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { StopEvent, returnFalse, returnOne, returnTrue, returnZero } from '../../../ClientUtils';
@@ -22,11 +22,16 @@ import { CollectionSubView } from './CollectionSubView';
 import { Tooltip } from '@mui/material';
 
 enum cardMode {
-    PRACTICE = 'practice',
+    // PRACTICE = 'practice',
     STAR = 'star',
-    QUIZ = 'quiz',
+    // QUIZ = 'quiz',
     ALL = 'all',
 }
+enum practiceMode {
+    PRACTICE = 'practice',
+    QUIZ = 'quiz',
+    NORMAL = 'normal',
+}
 enum practiceVal {
     MISSED = 'missed',
     CORRECT = 'correct',
@@ -34,7 +39,8 @@ enum practiceVal {
 @observer
 export class CollectionCarouselView extends CollectionSubView() {
     private _dropDisposer?: DragManager.DragDropDisposer;
-    @observable private _message = 'Drag a document';
+    @observable private _practiceMessage: string | undefined;
+    @observable private _filterMessage: string | undefined;
     get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore
     get sideField() { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore
     get starField()     { return this.fieldKey + "_star"; } // prettier-ignore
@@ -43,6 +49,7 @@ export class CollectionCarouselView extends CollectionSubView() {
         super(props);
         makeObservable(this);
         this.layoutDoc.filterOp = cardMode.ALL;
+        this.layoutDoc.practiceMode = practiceMode.NORMAL;
         this.layoutDoc._carousel_index = 0;
     }
 
@@ -64,6 +71,13 @@ export class CollectionCarouselView extends CollectionSubView() {
         return NumCast(this.layoutDoc.caption_xMargin, 50);
     }
 
+    @action setPracticeMessage = (mes: string | undefined) => {
+        this._practiceMessage = mes;
+    };
+    @action setFilterMessage = (mes: string | undefined) => {
+        this._filterMessage = mes;
+    };
+
     move = (dir: number) => {
         const moveToCardWithField = (match: (doc: Doc) => boolean): boolean => {
             let startInd = (NumCast(this.layoutDoc._carousel_index) + dir) % this.carouselItems.length;
@@ -76,26 +90,69 @@ export class CollectionCarouselView extends CollectionSubView() {
             }
             return match(this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout);
         };
-        switch (StrCast(this.layoutDoc.filterOp)) {
-            case cardMode.STAR:  // go to a flashcard that is starred, skip the ones that aren't
-                if (!moveToCardWithField((doc: Doc) => !!doc[this.starField])) {
-                    // this.layoutDoc._carousel_index
-                    this.layoutDoc.filterOp = undefined;  // if there aren't any starred, show all cards
-                    this._message = 'No starred items. Return to All to star documents.'
-                }
-                break;
-            case cardMode.PRACTICE: // go to a new index that is missed, skip the ones that are correct
-                if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT)) {
-                    this.layoutDoc.filterOp = undefined;  // if all of the cards are correct, show all cards and exit practice mode
-                    this._message = 'Finished! Return to All.'
-                    this.carouselItems.forEach(item => { // reset all the practice values
-                        item.layout[this.practiceField] = undefined;
-                    });
-                    // this.layoutDoc._carousel_index = -1;
-                }
-                break;
-            default: moveToCardWithField(returnTrue);
-        } // prettier-ignore
+
+        if (this.layoutDoc.practiceMode === practiceMode.PRACTICE && this.layoutDoc.filterOp !== cardMode.STAR) {
+            if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT)) {
+                //this.layoutDoc.filterOp = undefined;  // if all of the cards are correct, show all cards and exit practice mode
+                this._practiceMessage = 'Finished! Return to All - BUTTONS??.';
+                this.carouselItems.forEach(item => {
+                    // reset all the practice values
+                    item.layout[this.practiceField] = undefined;
+                });
+                // this.layoutDoc._carousel_index = -1;
+            }
+        } else if (this.layoutDoc.practiceMode !== practiceMode.PRACTICE && this.layoutDoc.filterOp === cardMode.STAR) {
+            if (!moveToCardWithField((doc: Doc) => !!doc[this.starField])) {
+                //this.layoutDoc.filterOp = undefined;  // if there aren't any starred, show all cards
+                this._filterMessage = 'No starred items. Unselect this view to see all flashcards and star them.';
+            }
+        } else if (this.layoutDoc.practiceMode === practiceMode.PRACTICE && this.layoutDoc.filterOp === cardMode.STAR) {
+            if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT && doc[this.starField] === true)) {
+                this._practiceMessage = 'New message.';
+            }
+        } else {
+            moveToCardWithField(returnTrue);
+        }
+
+        // if (this.layoutDoc.practiceMode === practiceMode.PRACTICE && this.layoutDoc.filterOp === cardMode.STAR) {
+        //     if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT) && !moveToCardWithField((doc: Doc) => doc[this.starField] === true)) {
+        //         this._practiceMessage = 'New message.';
+        //     } else moveToCardWithField(returnTrue);
+        //     return;
+        // }
+        // switch (StrCast(this.layoutDoc.practiceMode)) {
+        //     // case practiceMode.QUIZ:  // go to a flashcard that is starred, skip the ones that aren't
+        //     //     if (!moveToCardWithField((doc: Doc) => !!doc[this.starField])) {
+        //     //         // this.layoutDoc._carousel_index
+        //     //         //this.layoutDoc.filterOp = undefined;  // if there aren't any starred, show all cards
+        //     //         this._message = 'No starred items. Unselect this view to see all flashcards and star them.'
+        //     //     }
+        //     //     break;
+        //     case practiceMode.PRACTICE: // go to a new index that is missed, skip the ones that are correct
+        //         if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT)) {
+        //             //this.layoutDoc.filterOp = undefined;  // if all of the cards are correct, show all cards and exit practice mode
+        //             this._practiceMessage = 'Finished! Return to All - BUTTONS??.'
+        //             this.carouselItems.forEach(item => { // reset all the practice values
+        //                 item.layout[this.practiceField] = undefined;
+        //             });
+        //             // this.layoutDoc._carousel_index = -1;
+        //         }
+        //         break;
+        //     default: moveToCardWithField(returnTrue);
+
+        // } // prettier-ignore
+
+        // switch (StrCast(this.layoutDoc.filterOp)) {
+        //     case cardMode.STAR: // go to a flashcard that is starred, skip the ones that aren't
+        //         if (!moveToCardWithField((doc: Doc) => !!doc[this.starField])) {
+        //             //this.layoutDoc.filterOp = undefined;  // if there aren't any starred, show all cards
+        //             this._filterMessage = 'No starred items. Unselect this view to see all flashcards and star them.';
+        //         }
+        //         break;
+        //     default:
+        //         break;
+        //     // if (this.layoutDoc.practiceMode === practiceMode.PRACTICE) moveToCardWithField(returnTrue);
+        // }
     };
 
     /**
@@ -121,6 +178,7 @@ export class CollectionCarouselView extends CollectionSubView() {
         e.stopPropagation();
         const curDoc = this.carouselItems[NumCast(this.layoutDoc._carousel_index)];
         curDoc.layout[this.starField] = curDoc.layout[this.starField] ? undefined : true;
+        // if (!curDoc.layout[this.starField]) this.move(1);
         // this.layoutDoc._carousel_index = undefined;
     };
 
@@ -142,16 +200,42 @@ export class CollectionCarouselView extends CollectionSubView() {
     onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
     onContentClick = () => ScriptCast(this.layoutDoc.onChildClick);
     captionWidth = () => this._props.PanelWidth() - 2 * this.marginX;
+    setPracticeMode = (mode: practiceMode) => {
+        this.layoutDoc.practiceMode = mode;
+        this.carouselItems?.map(doc => (doc.layout[this.practiceField] = undefined));
+        switch (mode) {
+            case practiceMode.QUIZ:
+                this.carouselItems?.map(doc => (doc.layout[this.sideField] = undefined));
+                break;
+            case practiceMode.NORMAL:
+                this.setPracticeMessage(undefined);
+                break;
+            default:
+                break;
+        }
+    };
     setFilterMode = (mode: cardMode) => {
         this.layoutDoc.filterOp = mode;
-        console.log('MODE' + mode);
-        console.log('FILT' + this.layoutDoc.filterOp + ';');
-        if (mode == cardMode.STAR) this.move(1);
-        if (mode == cardMode.QUIZ) {
-            this.carouselItems?.map(doc => (doc.layout[this.sideField] = undefined));
+        switch (mode) {
+            case cardMode.STAR:
+                this.move(1);
+                break;
+            default:
+                this.setFilterMessage(undefined);
         }
-        this.carouselItems?.map(doc => (doc.layout[this.practiceField] = undefined));
+
+        // if (mode === practiceMode.QUIZ) {
+        //     this.carouselItems?.map(doc => (doc.layout[this.sideField] = undefined));
+        //     this.layoutDoc.practiceMode = mode;
+        // }
+        // if (mode === practiceMode.PRACTICE) this.layoutDoc.practiceMode = mode;
+        // if (mode === practiceMode.NORMAL) {
+        //     this.layoutDoc.practiceMode = mode;
+        //     if (this.layoutDoc.filterOp === cardMode.STAR) this.setMessage('No starred items. Unselect this filter to star cards.');
+        // }
+        // this.carouselItems?.map(doc => (doc.layout[this.practiceField] = undefined));
     };
+
     // specificMenu = (): void => {
     //     const cm = ContextMenu.Instance;
 
@@ -221,32 +305,43 @@ export class CollectionCarouselView extends CollectionSubView() {
                 <div key="star" className="carouselView-star" onClick={this.star}>
                     <FontAwesomeIcon icon="star" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" />
                 </div>
-                <div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)} style={{ visibility: this.layoutDoc.filterOp === cardMode.PRACTICE ? 'visible' : 'hidden' }}>
-                    <FontAwesomeIcon icon="xmark" color="red" size="1x" />
-                </div>
-                <div key="check" className="carouselView-check" onClick={e => this.setPracticeVal(e, practiceVal.CORRECT)} style={{ visibility: this.layoutDoc.filterOp === cardMode.PRACTICE ? 'visible' : 'hidden' }}>
-                    <FontAwesomeIcon icon="check" color="green" size="1x" />
-                </div>
+                {this.layoutDoc.practiceMode === practiceMode.PRACTICE ? (
+                    <div>
+                        <div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)}>
+                            <FontAwesomeIcon icon="xmark" color="red" size="1x" />
+                        </div>
+                        <div key="check" className="carouselView-check" onClick={e => this.setPracticeVal(e, practiceVal.CORRECT)}>
+                            <FontAwesomeIcon icon="check" color="green" size="1x" />
+                        </div>
+                    </div>
+                ) : null}
             </>
         );
     }
 
+    togglePracticeMode = (mode: practiceMode) => {
+        if (mode === this.layoutDoc.practiceMode) this.setPracticeMode(practiceMode.NORMAL);
+        else this.setPracticeMode(mode);
+    };
+    toggleFilterMode = () => { this.layoutDoc.filterOp === cardMode.STAR ? this.setFilterMode(cardMode.ALL) : this.setFilterMode(cardMode.STAR)}; //prettier-ignore
+    setColor = (mode: practiceMode | cardMode, which: string) => { return which === mode ? 'white' : 'light gray'}; //prettier-ignore
+
     @computed get menu() {
         return (
             <div className="carouselView-menu">
                 <Tooltip title="Practice flashcards using GPT">
-                    <div key="back" className="carouselView-quiz" onClick={e => (this.layoutDoc.filterOp === cardMode.QUIZ ? this.setFilterMode(cardMode.ALL) : this.setFilterMode(cardMode.QUIZ))}>
-                        <FontAwesomeIcon icon="file-pen" color={this.layoutDoc.filterOp === cardMode.QUIZ ? 'white' : 'light gray'} size="1x" />
+                    <div key="back" className="carouselView-quiz" onClick={e => this.togglePracticeMode(practiceMode.QUIZ)}>
+                        <FontAwesomeIcon icon="file-pen" color={this.setColor(practiceMode.QUIZ, StrCast(this.layoutDoc.practiceMode))} size="1x" />
                     </div>
                 </Tooltip>
-                <Tooltip title={this.layoutDoc.filterOp === cardMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}>
-                    <div key="back" className="carouselView-practice" onClick={e => (this.layoutDoc.filterOp === cardMode.PRACTICE ? this.setFilterMode(cardMode.ALL) : this.setFilterMode(cardMode.PRACTICE))}>
-                        <FontAwesomeIcon icon="check" color={this.layoutDoc.filterOp === cardMode.PRACTICE ? 'white' : 'light gray'} size="1x" />
+                <Tooltip title={this.layoutDoc.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}>
+                    <div key="back" className="carouselView-practice" onClick={e => this.togglePracticeMode(practiceMode.PRACTICE)}>
+                        <FontAwesomeIcon icon="check" color={this.setColor(practiceMode.PRACTICE, StrCast(this.layoutDoc.practiceMode))} size="1x" />
                     </div>
                 </Tooltip>
                 <Tooltip title={this.layoutDoc.filterOp === cardMode.STAR ? 'Show all cards' : 'Show only starred cards'}>
-                    <div key="back" className="carouselView-starFilter" onClick={e => (!this.layoutDoc.filterOp ? this.setFilterMode(cardMode.ALL) : this.setFilterMode(cardMode.STAR))}>
-                        <FontAwesomeIcon icon="filter" color={this.layoutDoc.filterOp === cardMode.STAR || !this.layoutDoc.filterOp ? 'white' : 'light gray'} size="1x" />
+                    <div key="back" className="carouselView-starFilter" onClick={e => this.toggleFilterMode()}>
+                        <FontAwesomeIcon icon="filter" color={this.setColor(cardMode.STAR, StrCast(this.layoutDoc.filterOp))} size="1x" />
                     </div>
                 </Tooltip>
             </div>
@@ -258,22 +353,22 @@ export class CollectionCarouselView extends CollectionSubView() {
             <div
                 className="collectionCarouselView-outer"
                 ref={this.createDashEventsTarget}
-                onContextMenu={this.specificMenu}
+                // onContextMenu={this.specificMenu}
                 style={{
                     background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor),
                     color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color),
                 }}>
-                {this.layoutDoc.filterOp ? this.content : <p className="message">{this._message}</p>}
+                {!this._practiceMessage && !this._filterMessage ? (
+                    this.content
+                ) : (
+                    <p className="message">
+                        {this._filterMessage}
+                        {'\n'}
+                        {this._practiceMessage}
+                    </p>
+                )}
                 {/* {(this.carouselItems?.filter(doc => doc.layout[this.practiceField] !== practiceVal.CORRECT)).length == 0 ? null : this.content} */}
-                {/* Displays a message to the user to add more flashcards if they are in practice mode and no flashcards are there. */}
-                <p
-                    className="message"
-                    style={{
-                        display: !this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] && this.layoutDoc.filterOp === cardMode.PRACTICE ? 'flex' : 'none',
-                    }}>
-                    Add flashcards
-                </p>
-                {/* Displays a message to the user that a flashcard was recently missed if they had previously gotten it wrong. */}
+                {!this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] && this.layoutDoc.practiceMode === practiceMode.PRACTICE ? <p className="message">Add flashcards </p> : null}
                 <p
                     style={{
                         color: 'red',
@@ -284,7 +379,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                         top: '10px',
                         width: '10px',
                         display: this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)]
-                            ? this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.practiceField] === practiceVal.MISSED && this.layoutDoc.filterOp === cardMode.PRACTICE
+                            ? this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.practiceField] === practiceVal.MISSED && this.layoutDoc.practiceMode === practiceMode.PRACTICE && !this._practiceMessage
                                 ? 'block'
                                 : 'none'
                             : 'none',
@@ -292,7 +387,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                     Recently missed!
                 </p>
                 {this.menu}
-                {this.Document._chromeHidden || !this.layoutDoc.filterOp ? null : this.buttons}
+                {this.Document._chromeHidden || (!this._filterMessage && !this._practiceMessage) ? this.buttons : null}
             </div>
         );
     }
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 084723d56..02ab76c2a 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -517,8 +517,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             }
 
             // render the QuizCards
-            console.log('GERE' + DocCast(this.Document.embedContainer).filterOp);
-            if (DocCast(this.Document.embedContainer) && DocCast(this.Document.embedContainer).filterOp === 'quiz') {
+            // console.log('GERE' + DocCast(this.Document.embedContainer).filterOp);
+            if (DocCast(this.Document.embedContainer) && DocCast(this.Document.embedContainer).practiceMode === 'quiz') {
                 const text = StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text);
                 return (
                     <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} style={{ display: 'flex', flexDirection: 'column' }}>
-- 
cgit v1.2.3-70-g09d2


From 10f0a232bc40a17f0151430bf7cc3ba3c10ede67 Mon Sep 17 00:00:00 2001
From: alyssaf16 <alyssa_feinberg@brown.edu>
Date: Sun, 9 Jun 2024 13:21:27 -0400
Subject: filter fixes

---
 .../views/collections/CollectionCarouselView.tsx   | 114 +++++----------------
 1 file changed, 24 insertions(+), 90 deletions(-)

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

diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 979f7ea2a..9cf7765dd 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -91,68 +91,27 @@ export class CollectionCarouselView extends CollectionSubView() {
             return match(this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout);
         };
 
-        if (this.layoutDoc.practiceMode === practiceMode.PRACTICE && this.layoutDoc.filterOp !== cardMode.STAR) {
-            if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT)) {
-                //this.layoutDoc.filterOp = undefined;  // if all of the cards are correct, show all cards and exit practice mode
-                this._practiceMessage = 'Finished! Return to All - BUTTONS??.';
-                this.carouselItems.forEach(item => {
-                    // reset all the practice values
-                    item.layout[this.practiceField] = undefined;
-                });
-                // this.layoutDoc._carousel_index = -1;
-            }
-        } else if (this.layoutDoc.practiceMode !== practiceMode.PRACTICE && this.layoutDoc.filterOp === cardMode.STAR) {
-            if (!moveToCardWithField((doc: Doc) => !!doc[this.starField])) {
-                //this.layoutDoc.filterOp = undefined;  // if there aren't any starred, show all cards
-                this._filterMessage = 'No starred items. Unselect this view to see all flashcards and star them.';
-            }
-        } else if (this.layoutDoc.practiceMode === practiceMode.PRACTICE && this.layoutDoc.filterOp === cardMode.STAR) {
-            if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT && doc[this.starField] === true)) {
-                this._practiceMessage = 'New message.';
-            }
-        } else {
-            moveToCardWithField(returnTrue);
+        switch (this.layoutDoc.practiceMode && this.layoutDoc.filterOp) {
+            case practiceMode.PRACTICE && cardMode.ALL:
+                if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT)) {
+                    this._practiceMessage = 'Finished! Unselect practice mode to view all flashcards.';
+                    this.carouselItems.forEach(item => { item.layout[this.practiceField] = undefined}); //prettier-ignore
+                }
+                break;
+            case !practiceMode.PRACTICE && cardMode.STAR:
+                if (!moveToCardWithField((doc: Doc) => !!doc[this.starField])) {
+                    this._filterMessage = 'No starred items. Unselect this view to see all flashcards and star them.';
+                }
+                break;
+            case practiceMode.PRACTICE && cardMode.STAR:
+                if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT && doc[this.starField] === true)) {
+                    this._filterMessage = 'No flashcards to show! Unselect star mode to view all flashcards.';
+                    this._practiceMessage = 'Unselect practice mode to view all flashcards.';
+                }
+                break;
+            default:
+                moveToCardWithField(returnTrue);
         }
-
-        // if (this.layoutDoc.practiceMode === practiceMode.PRACTICE && this.layoutDoc.filterOp === cardMode.STAR) {
-        //     if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT) && !moveToCardWithField((doc: Doc) => doc[this.starField] === true)) {
-        //         this._practiceMessage = 'New message.';
-        //     } else moveToCardWithField(returnTrue);
-        //     return;
-        // }
-        // switch (StrCast(this.layoutDoc.practiceMode)) {
-        //     // case practiceMode.QUIZ:  // go to a flashcard that is starred, skip the ones that aren't
-        //     //     if (!moveToCardWithField((doc: Doc) => !!doc[this.starField])) {
-        //     //         // this.layoutDoc._carousel_index
-        //     //         //this.layoutDoc.filterOp = undefined;  // if there aren't any starred, show all cards
-        //     //         this._message = 'No starred items. Unselect this view to see all flashcards and star them.'
-        //     //     }
-        //     //     break;
-        //     case practiceMode.PRACTICE: // go to a new index that is missed, skip the ones that are correct
-        //         if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT)) {
-        //             //this.layoutDoc.filterOp = undefined;  // if all of the cards are correct, show all cards and exit practice mode
-        //             this._practiceMessage = 'Finished! Return to All - BUTTONS??.'
-        //             this.carouselItems.forEach(item => { // reset all the practice values
-        //                 item.layout[this.practiceField] = undefined;
-        //             });
-        //             // this.layoutDoc._carousel_index = -1;
-        //         }
-        //         break;
-        //     default: moveToCardWithField(returnTrue);
-
-        // } // prettier-ignore
-
-        // switch (StrCast(this.layoutDoc.filterOp)) {
-        //     case cardMode.STAR: // go to a flashcard that is starred, skip the ones that aren't
-        //         if (!moveToCardWithField((doc: Doc) => !!doc[this.starField])) {
-        //             //this.layoutDoc.filterOp = undefined;  // if there aren't any starred, show all cards
-        //             this._filterMessage = 'No starred items. Unselect this view to see all flashcards and star them.';
-        //         }
-        //         break;
-        //     default:
-        //         break;
-        //     // if (this.layoutDoc.practiceMode === practiceMode.PRACTICE) moveToCardWithField(returnTrue);
-        // }
     };
 
     /**
@@ -200,6 +159,7 @@ export class CollectionCarouselView extends CollectionSubView() {
     onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
     onContentClick = () => ScriptCast(this.layoutDoc.onChildClick);
     captionWidth = () => this._props.PanelWidth() - 2 * this.marginX;
+
     setPracticeMode = (mode: practiceMode) => {
         this.layoutDoc.practiceMode = mode;
         this.carouselItems?.map(doc => (doc.layout[this.practiceField] = undefined));
@@ -210,44 +170,19 @@ export class CollectionCarouselView extends CollectionSubView() {
             case practiceMode.NORMAL:
                 this.setPracticeMessage(undefined);
                 break;
-            default:
-                break;
         }
     };
+
     setFilterMode = (mode: cardMode) => {
         this.layoutDoc.filterOp = mode;
         switch (mode) {
             case cardMode.STAR:
                 this.move(1);
                 break;
-            default:
-                this.setFilterMessage(undefined);
+            default: this.setFilterMessage(undefined); // prettier-ignore
         }
-
-        // if (mode === practiceMode.QUIZ) {
-        //     this.carouselItems?.map(doc => (doc.layout[this.sideField] = undefined));
-        //     this.layoutDoc.practiceMode = mode;
-        // }
-        // if (mode === practiceMode.PRACTICE) this.layoutDoc.practiceMode = mode;
-        // if (mode === practiceMode.NORMAL) {
-        //     this.layoutDoc.practiceMode = mode;
-        //     if (this.layoutDoc.filterOp === cardMode.STAR) this.setMessage('No starred items. Unselect this filter to star cards.');
-        // }
-        // this.carouselItems?.map(doc => (doc.layout[this.practiceField] = undefined));
     };
 
-    // 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.setFilterMode(cardMode.ALL);},         icon: 'layer-group',}); // prettier-ignore
-    //     revealItems.push({description: 'Star',          event: () => {this.setFilterMode(cardMode.STAR);},     icon: 'star',}); // prettier-ignore
-    //     revealItems.push({description: 'Practice Mode', event: () => {this.setFilterMode(cardMode.PRACTICE);}, icon: 'check',}); // prettier-ignore
-    //     cm.addItem({description: 'Quiz Cards',    event: () => {this.setFilterMode(cardMode.QUIZ);},     icon: 'pencil',}); // prettier-ignore
-    //     !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
-    //     //cm.addItem({description: 'Quiz Cards',    event: () => {this.layoutDoc.filterOp = cardMode.QUIZ; this.clearContent()});
-    // };
     @computed get content() {
         const index = NumCast(this.layoutDoc._carousel_index);
         const curDoc = this.carouselItems?.[index];
@@ -353,7 +288,6 @@ export class CollectionCarouselView extends CollectionSubView() {
             <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),
@@ -367,9 +301,9 @@ export class CollectionCarouselView extends CollectionSubView() {
                         {this._practiceMessage}
                     </p>
                 )}
-                {/* {(this.carouselItems?.filter(doc => doc.layout[this.practiceField] !== practiceVal.CORRECT)).length == 0 ? null : this.content} */}
                 {!this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] && this.layoutDoc.practiceMode === practiceMode.PRACTICE ? <p className="message">Add flashcards </p> : null}
                 <p
+                    className="missed-message"
                     style={{
                         color: 'red',
                         fontWeight: 'bold',
-- 
cgit v1.2.3-70-g09d2


From 39784e909c68f139bb537151294d8db56d021158 Mon Sep 17 00:00:00 2001
From: alyssaf16 <alyssa_feinberg@brown.edu>
Date: Wed, 12 Jun 2024 12:31:50 -0400
Subject: flashcard

---
 .../views/collections/CollectionCarouselView.scss  | 11 +++-
 .../views/collections/CollectionCarouselView.tsx   | 55 ++++++++++++++-----
 src/client/views/nodes/ComparisonBox.scss          | 13 ++++-
 src/client/views/nodes/ComparisonBox.tsx           | 62 +++++++++++++++-------
 src/client/views/pdf/AnchorMenu.tsx                | 21 ++++++--
 src/client/views/pdf/PDFViewer.tsx                 |  1 +
 6 files changed, 124 insertions(+), 39 deletions(-)

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

diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss
index c4679e888..b402a7a32 100644
--- a/src/client/views/collections/CollectionCarouselView.scss
+++ b/src/client/views/collections/CollectionCarouselView.scss
@@ -27,10 +27,10 @@
 .carouselView-fwd,
 .carouselView-star,
 .carouselView-remove,
-.carouselView-check {
+.carouselView-check,
+.carouselView-add {
     position: absolute;
     display: flex;
-    top: 42.5%;
     width: 30;
     height: 30;
     align-items: center;
@@ -43,15 +43,22 @@
     }
 }
 .carouselView-fwd {
+    top: 42.5%;
     right: 20;
 }
 .carouselView-back {
+    top: 42.5%;
     left: 20;
 }
 .carouselView-star {
     top: 0;
     left: 0;
 }
+.carouselView-add {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+}
 .carouselView-remove {
     top: 80%;
     left: 52%;
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 9cf7765dd..b5b6e1f4b 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -4,6 +4,7 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
+import { Docs } from '../../documents/Documents';
 import * as React from 'react';
 import { StopEvent, returnFalse, returnOne, returnTrue, returnZero } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
@@ -48,7 +49,8 @@ export class CollectionCarouselView extends CollectionSubView() {
     constructor(props: any) {
         super(props);
         makeObservable(this);
-        this.layoutDoc.filterOp = cardMode.ALL;
+        // this.layoutDoc.filterOp = cardMode.ALL;
+        Doc.setDocFilter(this.Document, 'data_star', undefined, 'match');
         this.layoutDoc.practiceMode = practiceMode.NORMAL;
         this.layoutDoc._carousel_index = 0;
     }
@@ -177,9 +179,12 @@ export class CollectionCarouselView extends CollectionSubView() {
         this.layoutDoc.filterOp = mode;
         switch (mode) {
             case cardMode.STAR:
+                Doc.setDocFilter(this.Document, 'data_star', true, 'match');
                 this.move(1);
                 break;
-            default: this.setFilterMessage(undefined); // prettier-ignore
+            default:
+                this.setFilterMessage(undefined); // prettier-ignore
+                Doc.setDocFilter(this.Document, 'data_star', true, 'remove');
         }
     };
 
@@ -197,6 +202,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                         NativeHeight={returnZero}
                         fitWidth={undefined}
                         setContentViewBox={undefined}
+                        childFilters={this.childDocFilters}
                         onDoubleClickScript={this.onContentDoubleClick}
                         onClickScript={this.onContentClick}
                         isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
@@ -227,6 +233,16 @@ export class CollectionCarouselView extends CollectionSubView() {
             </>
         );
     }
+
+    containsDifTypes = (): boolean => {
+        return this.carouselItems.filter(doc => !doc.layout.isFlashcard).length === 0;
+    };
+
+    addFlashcard() {
+        const newDoc = Docs.Create.ComparisonDocument('', { _layout_isFlashcard: true, _width: 300, _height: 300 });
+        this.addDocument?.(newDoc);
+    }
+
     @computed get buttons() {
         if (!this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)]) return null;
         return (
@@ -237,17 +253,32 @@ export class CollectionCarouselView extends CollectionSubView() {
                 <div key="fwd" className="carouselView-fwd" onClick={this.advance}>
                     <FontAwesomeIcon icon="chevron-right" size="2x" />
                 </div>
-                <div key="star" className="carouselView-star" onClick={this.star}>
-                    <FontAwesomeIcon icon="star" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" />
-                </div>
+                {!this.containsDifTypes() ? (
+                    <div>
+                        <Tooltip title="star">
+                            <div key="star" className="carouselView-star" onClick={this.star}>
+                                <FontAwesomeIcon icon="star" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" />
+                            </div>
+                        </Tooltip>
+                        <Tooltip title="add new flashcard to pile">
+                            <div key="add" className="carouselView-add" onClick={this.addFlashcard}>
+                                <FontAwesomeIcon icon="plus" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" />
+                            </div>
+                        </Tooltip>
+                    </div>
+                ) : null}
                 {this.layoutDoc.practiceMode === practiceMode.PRACTICE ? (
                     <div>
-                        <div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)}>
-                            <FontAwesomeIcon icon="xmark" color="red" size="1x" />
-                        </div>
-                        <div key="check" className="carouselView-check" onClick={e => this.setPracticeVal(e, practiceVal.CORRECT)}>
-                            <FontAwesomeIcon icon="check" color="green" size="1x" />
-                        </div>
+                        <Tooltip title="Incorrect. View again later.">
+                            <div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)}>
+                                <FontAwesomeIcon icon="xmark" color="red" size="1x" />
+                            </div>
+                        </Tooltip>
+                        <Tooltip title="Correct">
+                            <div key="check" className="carouselView-check" onClick={e => this.setPracticeVal(e, practiceVal.CORRECT)}>
+                                <FontAwesomeIcon icon="check" color="green" size="1x" />
+                            </div>
+                        </Tooltip>
                     </div>
                 ) : null}
             </>
@@ -320,7 +351,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                     }}>
                     Recently missed!
                 </p>
-                {this.menu}
+                {!this.containsDifTypes() ? this.menu : null}
                 {this.Document._chromeHidden || (!this._filterMessage && !this._practiceMessage) ? this.buttons : null}
             </div>
         );
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index dc107b576..0b2c356e5 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -14,9 +14,8 @@
         -webkit-text-stroke-color: black;
         -webkit-text-stroke-width: 0.2px;
     }
-
     .input-box {
-        position: relative;
+        position: absolute;
         padding: 10px;
         width: 100%;
         height: 100%;
@@ -145,6 +144,15 @@
     }
 }
 
+.explain {
+    position: absolute;
+    top: 10px;
+    left: 10px;
+    z-index: 200;
+    padding: 5px;
+    background: #dfdfdf;
+}
+
 .comparisonBox-interactive {
     pointer-events: unset;
     cursor: ew-resize;
@@ -154,6 +162,7 @@
             display: flex;
         }
     }
+
     // .input-box {
     //     position: absolute;
     //     padding: 10px;
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 02ab76c2a..9eb5f6ca2 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -43,9 +43,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     @observable private _inputValue = '';
     @observable private _outputValue = '';
     @observable private _loading = false;
-    @observable private _errorMessage = '';
-    @observable private _outputMessage = '';
     @observable private _isEmpty = false;
+    @observable _yRelativeToTop: boolean = true;
 
     public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
 
@@ -76,8 +75,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     componentDidMount() {
         this._props.setContentViewBox?.(this);
         reaction(
-            () => this._props.isSelected(),
-            selected => !selected && (this.childActive = false)
+            () => this._props.isSelected(), // when this reaction should update
+            selected => !selected && (this.childActive = false) // what it should update to
         );
     }
     protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => {
@@ -283,8 +282,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         })
                     }
                     style={{
-                        background: usepath === 'alternate' ? 'white' : 'black',
-                        color: usepath === 'alternate' ? 'black' : 'white',
+                        background: this.revealOp === 'hover' ? 'gray' : usepath === 'alternate' ? 'white' : 'black',
+                        color: this.revealOp === 'hover' ? 'black' : usepath === 'alternate' ? 'black' : 'white',
                         display: 'inline-block',
                     }}>
                     <div key="alternate" className="formattedTextBox-flip">
@@ -303,7 +302,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? (
                             <div className="dash-tooltip">Flip to front side to use GPT</div>
                         ) : (
-                            <div className="dash-tooltip">Ask GPT to create an answer on the back side of the flashcard</div>
+                            <div className="dash-tooltip">Ask GPT to create an answer on the back side of the flashcard based on your question on the front</div>
                         )
                     }>
                     <div style={{ position: 'absolute', bottom: '3px', right: '56px', cursor: 'pointer' }} onPointerDown={e => (!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? this.askGPT(GPTCallType.CHATCARD) : null)}>
@@ -314,16 +313,17 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     <div
                         style={{ position: 'absolute', bottom: '3px', right: '80px', cursor: 'pointer' }}
                         onPointerDown={e => {
+                            // this.displayLabelHandler(e.target.value, e.clientY);
                             const collectionArr: Doc[] = [];
                             collectionArr.push(this.Document);
                             const newCol = Docs.Create.CarouselDocument(collectionArr, {
-                                _width: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 250),
-                                _height: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 200),
+                                _width: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 250) + 50,
+                                _height: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 200) + 50,
                                 _layout_fitWidth: false,
                                 _layout_autoHeight: true,
                             });
-                            newCol['x'] = e.clientX - 820;
-                            newCol['y'] = e.clientY - 640;
+                            newCol['x'] = this.layoutDoc['x'];
+                            newCol['y'] = NumCast(this.layoutDoc['y']) + 50;
                             this._props.addDocument?.(newCol);
                             this._props.removeDocument?.(this.Document);
                             this.Document.embedContainer = newCol;
@@ -332,9 +332,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     </div>
                 </Tooltip>
                 <Tooltip title={<div className="dash-tooltip">Hover to reveal</div>}>
-                    <div
-                        style={{ position: 'absolute', bottom: '3px', right: '25px', cursor: 'pointer' }}
-                        onClick={e => (this.revealOp === 'hover' ? (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip') : (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'hover'))}>
+                    <div style={{ position: 'absolute', bottom: '3px', right: '25px', cursor: 'pointer' }} onClick={e => this.handleHover()}>
                         <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="layer-group" size="xl" />
                     </div>
                 </Tooltip>
@@ -361,6 +359,17 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         if (this._inputValue) this.askGPT(GPTCallType.QUIZ);
     };
 
+    @action handleHover = () => {
+        if (this.revealOp === 'hover') {
+            this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip';
+            this.Document.forceActive = false;
+        } else {
+            this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'hover';
+            this.Document.forceActive = true;
+        }
+        //this.revealOp === 'hover' ? (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip') : (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'hover');
+    };
+
     @action handleRenderClick = () => {
         // Call the GPT model and get the output
         this.layoutDoc[`_${this._props.fieldKey}_usePath`] = undefined;
@@ -448,6 +457,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             const whichDoc = DocCast(this.dataDoc[whichSlot]);
             const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc);
             const layoutString = targetDoc ? '' : this.testForTextFields(whichSlot);
+            // whichDoc['backgroundColor'] = this.layoutDoc['backgroundColor'];
 
             return targetDoc || layoutString ? (
                 <>
@@ -500,20 +510,32 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             // add text box to each side when comparison box is first created
             // (!this.dataDoc[this.fieldKey + '_0'] && this.dataDoc[this._props.fieldKey + '_0'] !== 'empty')
             if (!this.dataDoc[this.fieldKey + '_0'] && !this._isEmpty) {
-                const dataSplit = StrCast(this.dataDoc.data).split('Answer');
+                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.
                 // eslint-disable-next-line prefer-destructuring
-                newDoc[DocData].text = dataSplit[1];
+                // newDoc.text = dataSplit[1];
+                newDoc['backgroundColor'] = 'lightgray';
                 this.addDoc(newDoc, this.fieldKey + '_0');
+                // DocCast(this.dataDoc[this.fieldKey + '_0'])[DocData].text = dataSplit[1];
+                // DocCast(this.dataDoc[this.fieldKey + '_0']).text = dataSplit[1];
+                // console.log('HI' + DocCast(this.dataDoc[this.fieldKey + '_0']).text);
+                console.log('HEREEE' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text));
             }
+
             if (!this.dataDoc[this.fieldKey + '_1'] && !this._isEmpty) {
-                const dataSplit = StrCast(this.dataDoc.data).split('Answer');
+                const dataSplit = StrCast(this.dataDoc.data).split('Answer: ');
                 const newDoc = Docs.Create.TextDocument(dataSplit[0]);
+                this.addDoc(newDoc, this.fieldKey + '_1');
                 // if there is text from the pdf ai cards, put the answer on the alternate side.
                 // eslint-disable-next-line prefer-destructuring
-                newDoc[DocData].text = dataSplit[0];
-                this.addDoc(newDoc, this.fieldKey + '_1');
+
+                // newDoc[DocData].text = dataSplit[0];
+                // console.log('HEREEE' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text));
+                // console.log('HI' + DocCast(this.dataDoc[this.fieldKey + '_1']).text);
+                // DocCast(this.dataDoc[this.props.fieldKey + '_1'])[DocData].text = dataSplit[0];
+                // console.log('HEREEE' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text));
+                // DocCast(this.dataDoc[this.fieldKey + '_1'])[DocData].text = dataSplit[0];
             }
 
             // render the QuizCards
@@ -552,6 +574,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 );
             }
 
+            console.log('HEREEE2' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text));
             // render a normal flashcard when not a QuizCard
             return (
                 <div
@@ -566,6 +589,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     }}
                     // onPointerUp={() => (this._isAnyChildContentActive = true)}
                 >
+                    {StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '' && !this.childActive ? <p className="explain">Enter text in the flashcard. </p> : null}
                     {displayBox(`${this.fieldKey}_${side === 0 ? 1 : 0}`, side, this._props.PanelWidth() - 3)}
                     {this._loading ? (
                         <div className="loading-spinner" style={{ position: 'absolute' }}>
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 2f6824466..cedd3c7c3 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -24,6 +24,8 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
     private _disposer: IReactionDisposer | undefined;
     private _commentRef = React.createRef<HTMLDivElement>();
     private _cropRef = React.createRef<HTMLDivElement>();
+    // @observable protected _top: number = -300;
+    // @observable protected _left: number = -300;
 
     constructor(props: any) {
         super(props);
@@ -38,10 +40,17 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
 
     // GPT additions
     @observable private _selectedText: string = '';
+    @observable private _x: number = 0;
+    @observable private _y: number = 0;
     @action
     public setSelectedText = (txt: string) => {
         this._selectedText = txt.trim();
     };
+    @action
+    public setLocation = (x: number, y: number) => {
+        this._x = x;
+        this._y = y;
+    };
 
     public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search
 
@@ -99,7 +108,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
      * Invokes the API with the selected text and stores it in the selected text.
      * @param e pointer down event
      */
-    gptFlashcards = async () => {
+    gptFlashcards = async (e: React.PointerEvent) => {
         const queryText = this._selectedText;
         try {
             const res = await gptAPICall(queryText, GPTCallType.FLASHCARD);
@@ -117,8 +126,8 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
      */
     transferToFlashcard = (text: string) => {
         // put each question generated by GPT on the front of the flashcard
-        const senArr = text.split('Question');
-        const collectionArr: Doc[] = [];
+        var senArr = text.trim().split('Question: ');
+        var collectionArr: Doc[] = [];
         for (let 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 });
@@ -133,6 +142,10 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
             _layout_autoHeight: true,
         });
 
+        newCol.x = this._x;
+        newCol.y = this._y;
+        newCol.zIndex = 100;
+
         this.addToCollection?.(newCol);
     };
 
@@ -221,7 +234,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
                     {/* Adds a create flashcards option to the anchor menu, which calls the gptFlashcard method. */}
                     <IconButton
                         tooltip="Create flashcards" //
-                        onPointerDown={this.gptFlashcards}
+                        onPointerDown={e => this.gptFlashcards(e)}
                         icon={<FontAwesomeIcon icon="id-card" size="lg" />}
                         color={SettingsManager.userColor}
                     />
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 6c1617c38..92f890e70 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -425,6 +425,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
         const sel = window.getSelection();
         if (sel) {
             AnchorMenu.Instance.setSelectedText(sel.toString());
+            AnchorMenu.Instance.setLocation(NumCast(this._props.layoutDoc['x']), NumCast(this._props.layoutDoc['y']));
         }
 
         if (sel?.type === 'Range') {
-- 
cgit v1.2.3-70-g09d2


From 91e465c9ba542b637e66c7091c444a44fdbe4f0c Mon Sep 17 00:00:00 2001
From: alyssaf16 <alyssa_feinberg@brown.edu>
Date: Thu, 13 Jun 2024 14:37:37 -0400
Subject: create flashcard stack and ui fixes

---
 .../views/collections/CollectionCarouselView.tsx   | 51 +++++++++---
 src/client/views/nodes/ComparisonBox.scss          |  8 +-
 src/client/views/nodes/ComparisonBox.tsx           | 90 +++++++++++++++-------
 src/client/views/pdf/AnchorMenu.tsx                | 13 +++-
 src/client/views/pdf/Annotation.scss               | 19 ++++-
 src/client/views/pdf/PDFViewer.tsx                 |  1 +
 6 files changed, 136 insertions(+), 46 deletions(-)

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

diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index b5b6e1f4b..f2d634eaa 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -21,6 +21,8 @@ import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import './CollectionCarouselView.scss';
 import { CollectionSubView } from './CollectionSubView';
 import { Tooltip } from '@mui/material';
+import { DocUtils } from '../../documents/DocUtils';
+import { Any } from '@react-spring/web';
 
 enum cardMode {
     // PRACTICE = 'practice',
@@ -49,7 +51,8 @@ export class CollectionCarouselView extends CollectionSubView() {
     constructor(props: any) {
         super(props);
         makeObservable(this);
-        // this.layoutDoc.filterOp = cardMode.ALL;
+        // this.setModes();
+        this.layoutDoc.filterOp = cardMode.ALL;
         Doc.setDocFilter(this.Document, 'data_star', undefined, 'match');
         this.layoutDoc.practiceMode = practiceMode.NORMAL;
         this.layoutDoc._carousel_index = 0;
@@ -67,6 +70,9 @@ export class CollectionCarouselView extends CollectionSubView() {
     };
 
     @computed get carouselItems() {
+        this.childLayoutPairs.map(pair => {
+            pair.layout.embedContainer = this.Document;
+        });
         return this.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK);
     }
     @computed get marginX() {
@@ -80,6 +86,13 @@ export class CollectionCarouselView extends CollectionSubView() {
         this._filterMessage = mes;
     };
 
+    setModes = () => {
+        this.layoutDoc.filterOp = cardMode.ALL;
+        Doc.setDocFilter(this.Document, 'data_star', undefined, 'match');
+        this.layoutDoc.practiceMode = practiceMode.NORMAL;
+        this.layoutDoc._carousel_index = 0;
+    };
+
     move = (dir: number) => {
         const moveToCardWithField = (match: (doc: Doc) => boolean): boolean => {
             let startInd = (NumCast(this.layoutDoc._carousel_index) + dir) % this.carouselItems.length;
@@ -107,8 +120,8 @@ export class CollectionCarouselView extends CollectionSubView() {
                 break;
             case practiceMode.PRACTICE && cardMode.STAR:
                 if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT && doc[this.starField] === true)) {
-                    this._filterMessage = 'No flashcards to show! Unselect star mode to view all flashcards.';
-                    this._practiceMessage = 'Unselect practice mode to view all flashcards.';
+                    this._filterMessage = 'No flashcards to show! Unselect mode to view all flashcards.';
+                    this._practiceMessage = undefined;
                 }
                 break;
             default:
@@ -179,20 +192,24 @@ export class CollectionCarouselView extends CollectionSubView() {
         this.layoutDoc.filterOp = mode;
         switch (mode) {
             case cardMode.STAR:
-                Doc.setDocFilter(this.Document, 'data_star', true, 'match');
+                // Doc.setDocFilter(this.Document, 'data_star', true, 'match');
                 this.move(1);
                 break;
             default:
                 this.setFilterMessage(undefined); // prettier-ignore
-                Doc.setDocFilter(this.Document, 'data_star', true, 'remove');
+            // Doc.setDocFilter(this.Document, 'data_star', true, 'remove');
         }
     };
 
     @computed get content() {
+        if (this.layoutDoc._carousel_index === this.carouselItems.length && this.layoutDoc._carousel_index !== 0) {
+            this.move(1);
+        }
         const index = NumCast(this.layoutDoc._carousel_index);
         const curDoc = this.carouselItems?.[index];
         const captionProps = { ...this._props, NativeScaling: returnOne, PanelWidth: this.captionWidth, fieldKey: 'caption', setHeight: undefined, setContentView: undefined };
         const carouselShowsCaptions = StrCast(this.layoutDoc._layout_showCaption);
+
         return !(curDoc?.layout instanceof Doc) ? null : (
             <>
                 <div className="collectionCarouselView-image" key="image">
@@ -207,6 +224,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                         onClickScript={this.onContentClick}
                         isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
                         isContentActive={this._props.childContentsActive ?? this._props.isContentActive() === false ? returnFalse : emptyFunction}
+                        addDocument={this._props.addDocument}
                         hideCaptions={!!carouselShowsCaptions} // hide captions if the carousel is configured to show the captions
                         renderDepth={this._props.renderDepth + 1}
                         LayoutTemplate={this._props.childLayoutTemplate}
@@ -235,12 +253,21 @@ export class CollectionCarouselView extends CollectionSubView() {
     }
 
     containsDifTypes = (): boolean => {
-        return this.carouselItems.filter(doc => !doc.layout.isFlashcard).length === 0;
+        return this.carouselItems.filter(doc => !doc.layout._layout_isFlashcard).length !== 0;
     };
 
     addFlashcard() {
         const newDoc = Docs.Create.ComparisonDocument('', { _layout_isFlashcard: true, _width: 300, _height: 300 });
         this.addDocument?.(newDoc);
+        // DocUtils.copyDragFactory(newDoc);
+        // this._props.addDocument?.();
+        // newDoc.layout = this.layoutDoc;
+        // newDoc.data = this.dataDoc;
+        // Doc.AddDocToList()
+        // this._props.parent._props.addDocument();
+        // this.childLayoutPairs.push({ newDoc.layout, newDoc.data});
+        // this._props.addDocument?.(newDoc);
+        // console.log('HERE');
     }
 
     @computed get buttons() {
@@ -260,11 +287,11 @@ export class CollectionCarouselView extends CollectionSubView() {
                                 <FontAwesomeIcon icon="star" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" />
                             </div>
                         </Tooltip>
-                        <Tooltip title="add new flashcard to pile">
+                        {/* <Tooltip title="add new flashcard to pile">
                             <div key="add" className="carouselView-add" onClick={this.addFlashcard}>
                                 <FontAwesomeIcon icon="plus" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" />
                             </div>
-                        </Tooltip>
+                        </Tooltip> */}
                     </div>
                 ) : null}
                 {this.layoutDoc.practiceMode === practiceMode.PRACTICE ? (
@@ -286,8 +313,10 @@ export class CollectionCarouselView extends CollectionSubView() {
     }
 
     togglePracticeMode = (mode: practiceMode) => {
-        if (mode === this.layoutDoc.practiceMode) this.setPracticeMode(practiceMode.NORMAL);
-        else this.setPracticeMode(mode);
+        if (mode === this.layoutDoc.practiceMode) {
+            this.setPracticeMode(practiceMode.NORMAL);
+            // this.setPracticeMessage("undefined");
+        } else this.setPracticeMode(mode);
     };
     toggleFilterMode = () => { this.layoutDoc.filterOp === cardMode.STAR ? this.setFilterMode(cardMode.ALL) : this.setFilterMode(cardMode.STAR)}; //prettier-ignore
     setColor = (mode: practiceMode | cardMode, which: string) => { return which === mode ? 'white' : 'light gray'}; //prettier-ignore
@@ -351,7 +380,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                     }}>
                     Recently missed!
                 </p>
-                {!this.containsDifTypes() ? this.menu : null}
+                {!this.containsDifTypes() && this.carouselItems.length !== 0 ? this.menu : null}
                 {this.Document._chromeHidden || (!this._filterMessage && !this._practiceMessage) ? this.buttons : null}
             </div>
         );
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index 0b2c356e5..f7389e39b 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -16,14 +16,15 @@
     }
     .input-box {
         position: absolute;
+        top: 50;
         padding: 10px;
         width: 100%;
-        height: 100%;
+        height: 70%;
         display: flex;
     }
 
     .submit-button {
-        position: relative;
+        position: absolute;
         padding-bottom: 10px;
         padding-left: 5px;
         padding-right: 5px;
@@ -31,6 +32,7 @@
         border-radius: 2px;
         height: 15%;
         display: flex;
+        bottom: 0;
 
         button {
             flex: 1;
@@ -149,7 +151,7 @@
     top: 10px;
     left: 10px;
     z-index: 200;
-    padding: 5px;
+    // padding: 5px;
     background: #dfdfdf;
 }
 
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 9eb5f6ca2..2fc297bec 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -28,6 +28,7 @@ import ReactLoading from 'react-loading';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { tickStep } from 'd3';
+import { CollectionCarouselView } from '../collections/CollectionCarouselView';
 
 @observer
 export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@@ -46,8 +47,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     @observable private _isEmpty = false;
     @observable _yRelativeToTop: boolean = true;
 
-    public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
-
     @action handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
         this._inputValue = e.target.value;
         console.log(this._inputValue);
@@ -248,6 +247,41 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
 
     _closeRef = React.createRef<HTMLDivElement>();
 
+    createFlashcardPile(collectionArr: Doc[], gpt: boolean) {
+        const newCol = Docs.Create.CarouselDocument(collectionArr, {
+            _width: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 250) + 50,
+            _height: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 200) + 50,
+            _layout_fitWidth: false,
+            _layout_autoHeight: true,
+        });
+        newCol['x'] = this.layoutDoc['x'];
+        newCol['y'] = NumCast(this.layoutDoc['y']) + 50;
+        newCol.type_collection = 'carousel';
+        console.log(newCol.data);
+
+        if (gpt) {
+            this._props.DocumentView?.()._props.addDocument?.(newCol);
+            this._props.removeDocument?.(this.Document);
+        } else {
+            this._props.addDocument?.(newCol);
+            this._props.removeDocument?.(this.Document);
+            this.Document.embedContainer = newCol;
+        }
+    }
+
+    gptFlashcardPile = async () => {
+        var text = (await this.askGPT(GPTCallType.FLASHCARD)) || '';
+
+        var senArr = text.trim().split('Question: ');
+        var collectionArr: Doc[] = [];
+        for (let i = 1; i < senArr.length; i++) {
+            const newDoc = Docs.Create.ComparisonDocument(senArr[i].trim(), { _layout_isFlashcard: true, _width: 300, _height: 300 });
+            newDoc.text = senArr[i].trim();
+            collectionArr.push(newDoc);
+        }
+        this.createFlashcardPile(collectionArr, true);
+    };
+
     /**
      * Flips a flashcard to the alternate side for the user to view.
      */
@@ -305,35 +339,27 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             <div className="dash-tooltip">Ask GPT to create an answer on the back side of the flashcard based on your question on the front</div>
                         )
                     }>
-                    <div style={{ position: 'absolute', bottom: '3px', right: '56px', cursor: 'pointer' }} onPointerDown={e => (!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? this.askGPT(GPTCallType.CHATCARD) : null)}>
+                    <div style={{ position: 'absolute', bottom: '3px', right: '50px', cursor: 'pointer' }} onPointerDown={e => (!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? this.askGPT(GPTCallType.CHATCARD) : null)}>
                         <FontAwesomeIcon icon="lightbulb" size="xl" />
                     </div>
                 </Tooltip>
-                <Tooltip title={<div>Create a flashcard pile</div>}>
-                    <div
-                        style={{ position: 'absolute', bottom: '3px', right: '80px', cursor: 'pointer' }}
-                        onPointerDown={e => {
-                            // this.displayLabelHandler(e.target.value, e.clientY);
-                            const collectionArr: Doc[] = [];
-                            collectionArr.push(this.Document);
-                            const newCol = Docs.Create.CarouselDocument(collectionArr, {
-                                _width: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 250) + 50,
-                                _height: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 200) + 50,
-                                _layout_fitWidth: false,
-                                _layout_autoHeight: true,
-                            });
-                            newCol['x'] = this.layoutDoc['x'];
-                            newCol['y'] = NumCast(this.layoutDoc['y']) + 50;
-                            this._props.addDocument?.(newCol);
-                            this._props.removeDocument?.(this.Document);
-                            this.Document.embedContainer = newCol;
-                        }}>
-                        <FontAwesomeIcon icon="folder-plus" size="xl" />
+                {DocCast(this.Document.embedContainer).type_collection === 'carousel' ? null : (
+                    <div>
+                        <Tooltip title={<div>Create a flashcard pile</div>}>
+                            <div style={{ position: 'absolute', bottom: '3px', right: '74px', cursor: 'pointer' }} onPointerDown={e => this.createFlashcardPile([this.Document], false)}>
+                                <FontAwesomeIcon icon="folder-plus" size="xl" />
+                            </div>
+                        </Tooltip>
+                        <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}>
+                            <div style={{ position: 'absolute', bottom: '3px', right: '104px', cursor: 'pointer' }} onClick={e => this.gptFlashcardPile()}>
+                                <FontAwesomeIcon icon="layer-group" size="xl" />
+                            </div>
+                        </Tooltip>
                     </div>
-                </Tooltip>
+                )}
                 <Tooltip title={<div className="dash-tooltip">Hover to reveal</div>}>
                     <div style={{ position: 'absolute', bottom: '3px', right: '25px', cursor: 'pointer' }} onClick={e => this.handleHover()}>
-                        <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="layer-group" size="xl" />
+                        <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="hand-point-up" size="xl" />
                     </div>
                 </Tooltip>
                 {/* <Tooltip title={<div className="dash-tooltip">Remove this side of the flashcard</div>}>
@@ -425,6 +451,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             if (callType == GPTCallType.QUIZ) this._outputValue = res;
             // DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res;
             // this._outputValue = res;
+
+            if (callType === GPTCallType.FLASHCARD) {
+                // console.log(res);
+                this._loading = false;
+                return res;
+            }
             console.log(res);
         } catch (err) {
             console.error('GPT call failed');
@@ -520,7 +552,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 // DocCast(this.dataDoc[this.fieldKey + '_0'])[DocData].text = dataSplit[1];
                 // DocCast(this.dataDoc[this.fieldKey + '_0']).text = dataSplit[1];
                 // console.log('HI' + DocCast(this.dataDoc[this.fieldKey + '_0']).text);
-                console.log('HEREEE' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text));
+                //console.log('HEREEE' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text));
             }
 
             if (!this.dataDoc[this.fieldKey + '_1'] && !this._isEmpty) {
@@ -545,7 +577,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 return (
                     <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} style={{ display: 'flex', flexDirection: 'column' }}>
                         <p style={{ color: 'white', padding: 10 }}>{text}</p>
-                        <p style={{ display: text === '' ? 'flex' : 'none', color: 'white', padding: 10 }}>Return to all flashcards and add text to both sides. </p>
+                        <p style={{ display: text === '' ? 'flex' : 'none', color: 'white', marginLeft: '10px' }}>Return to all flashcards and add text to both sides. </p>
                         <div className="input-box">
                             <textarea
                                 value={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this._outputValue : this._inputValue}
@@ -574,7 +606,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 );
             }
 
-            console.log('HEREEE2' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text));
+            //console.log('HEREEE2' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text));
             // render a normal flashcard when not a QuizCard
             return (
                 <div
@@ -589,7 +621,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     }}
                     // onPointerUp={() => (this._isAnyChildContentActive = true)}
                 >
-                    {StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '' && !this.childActive ? <p className="explain">Enter text in the flashcard. </p> : null}
+                    {!this.layoutDoc[`_${this._props.fieldKey}_usePath`] && StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '' && !this.childActive ? <p className="explain">Enter text in the flashcard. </p> : null}
                     {displayBox(`${this.fieldKey}_${side === 0 ? 1 : 0}`, side, this._props.PanelWidth() - 3)}
                     {this._loading ? (
                         <div className="loading-spinner" style={{ position: 'absolute' }}>
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index cedd3c7c3..c1198b4f7 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -15,6 +15,7 @@ import { LinkPopup } from '../linking/LinkPopup';
 import { DocumentView } from '../nodes/DocumentView';
 import './AnchorMenu.scss';
 import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup';
+import ReactLoading from 'react-loading';
 
 @observer
 export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -24,6 +25,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
     private _disposer: IReactionDisposer | undefined;
     private _commentRef = React.createRef<HTMLDivElement>();
     private _cropRef = React.createRef<HTMLDivElement>();
+    @observable private _loading = false;
     // @observable protected _top: number = -300;
     // @observable protected _left: number = -300;
 
@@ -110,15 +112,16 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
      */
     gptFlashcards = async (e: React.PointerEvent) => {
         const queryText = this._selectedText;
+        this._loading = true;
         try {
             const res = await gptAPICall(queryText, GPTCallType.FLASHCARD);
             console.log(res);
-            GPTPopup.Instance.setText(res || 'Something went wrong.');
+            // GPTPopup.Instance.setText(res || 'Something went wrong.');
             this.transferToFlashcard(res || 'Something went wrong');
         } catch (err) {
             console.error(err);
         }
-        GPTPopup.Instance.setLoading(false);
+        // GPTPopup.Instance.setLoading(false);
     };
 
     /*
@@ -147,6 +150,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
         newCol.zIndex = 100;
 
         this.addToCollection?.(newCol);
+        this._loading = false;
     };
 
     pointerDown = (e: React.PointerEvent) => {
@@ -263,6 +267,11 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
                             />
                         </div>
                     )}
+                    {this._loading ? (
+                        <div className="loading-spinner" style={{ position: 'absolute' }}>
+                            <ReactLoading type="spin" height={30} width={30} color={'white'} />
+                        </div>
+                    ) : null}
                 </>
             ) : (
                 <>
diff --git a/src/client/views/pdf/Annotation.scss b/src/client/views/pdf/Annotation.scss
index 1de60ffed..329819ea2 100644
--- a/src/client/views/pdf/Annotation.scss
+++ b/src/client/views/pdf/Annotation.scss
@@ -7,4 +7,21 @@
     &:hover {
         cursor: pointer;
     }
-}
\ No newline at end of file
+}
+.loading-spinner {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 90%;
+    width: 93%;
+    left: 10;
+    font-size: 20px;
+    font-weight: bold;
+    color: #0b0a0a;
+}
+
+@keyframes spin {
+    to {
+        transform: rotate(360deg);
+    }
+}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 92f890e70..af7a5774d 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -464,6 +464,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
             this._mainCont.current!.style.transform = '';
         }
         this._selectionContent = selRange.cloneContents();
+
         this._selectionText = this._selectionContent?.textContent || '';
 
         // clear selection
-- 
cgit v1.2.3-70-g09d2


From ad32c1606395cdc71ba50eb9b51d3a9d3b707ca0 Mon Sep 17 00:00:00 2001
From: alyssaf16 <alyssa_feinberg@brown.edu>
Date: Tue, 18 Jun 2024 15:23:12 -0400
Subject: working on various image flashcard ideas

---
 src/client/apis/gpt/GPT.ts                         |   7 +-
 src/client/views/MarqueeAnnotator.tsx              |   1 +
 .../views/collections/CollectionCardDeckView.tsx   |   2 +-
 .../views/collections/CollectionCarouselView.tsx   |   4 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |   2 +-
 src/client/views/nodes/ComparisonBox.tsx           |  18 +-
 src/client/views/nodes/ImageBox.scss               |  21 ++
 src/client/views/nodes/ImageBox.tsx                | 232 +++++++++++++++++++++
 src/client/views/pdf/AnchorMenu.tsx                |  47 ++---
 src/client/views/pdf/Annotation.scss               |  32 +--
 src/client/views/pdf/PDFViewer.scss                |  21 ++
 src/client/views/pdf/PDFViewer.tsx                 |  52 +++++
 12 files changed, 383 insertions(+), 56 deletions(-)

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index b036349dc..7bcd541c7 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -25,7 +25,7 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
     // newest model: gpt-4
     summary: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Summarize the text given in simpler terms.' },
     edit: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Reword the text.' },
-    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: ' },
+    flashcard: { model: 'gpt-4-turbo', maxTokens: 512, temp: 0.5, prompt: 'Make flashcards out of this text with each question and answer labeled as question and answer. Do not label each flashcard and do not include asterisks: ' },
     completion: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: "You are a helpful assistant. Answer the user's prompt." },
     mermaid: {
         model: 'gpt-4-turbo',
@@ -120,7 +120,7 @@ const gptGetEmbedding = async (src: string): Promise<number[]> => {
         return [];
     }
 };
-const gptImageLabel = async (src: string): Promise<string> => {
+const gptImageLabel = async (src: string, prompt: string): Promise<string> => {
     try {
         const response = await openai.chat.completions.create({
             model: 'gpt-4o',
@@ -128,7 +128,7 @@ const gptImageLabel = async (src: string): Promise<string> => {
                 {
                     role: 'user',
                     content: [
-                        { type: 'text', text: 'Give three to five labels to describe this image.' },
+                        { type: 'text', text: prompt },
                         {
                             type: 'image_url',
                             image_url: {
@@ -141,6 +141,7 @@ const gptImageLabel = async (src: string): Promise<string> => {
             ],
         });
         if (response.choices[0].message.content) {
+            console.log(response.choices[0].message.content);
             return response.choices[0].message.content;
         }
         return 'Missing labels';
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index c18ac6738..c7ffce2b4 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -34,6 +34,7 @@ export interface MarqueeAnnotatorProps {
     getPageFromScroll?: (top: number) => number;
     finishMarquee: (x?: number, y?: number) => void;
     anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
+    anchorMenuFlashcard?: () => Promise<String>;
     anchorMenuCrop?: (anchor: Doc | undefined, addCrop: boolean) => Doc | undefined;
     highlightDragSrcColor?: string;
 }
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index de46180e6..50af9df9e 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -373,7 +373,7 @@ export class CollectionCardView extends CollectionSubView() {
         const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
         try {
             const hrefBase64 = await CollectionCardView.imageUrlToBase64(hrefComplete);
-            const response = await gptImageLabel(hrefBase64);
+            const response = await gptImageLabel(hrefBase64, 'Give three to five labels to describe this image.');
             image[DocData].description = response.trim();
             return response; // Return the response from gptImageLabel
         } catch (error) {
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index f2d634eaa..6976deea5 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -46,14 +46,14 @@ export class CollectionCarouselView extends CollectionSubView() {
     @observable private _filterMessage: string | undefined;
     get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore
     get sideField() { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore
-    get starField()     { return this.fieldKey + "_star"; } // prettier-ignore
+    get starField()     { return "star"; } // prettier-ignore
 
     constructor(props: any) {
         super(props);
         makeObservable(this);
         // this.setModes();
         this.layoutDoc.filterOp = cardMode.ALL;
-        Doc.setDocFilter(this.Document, 'data_star', undefined, 'match');
+        Doc.setDocFilter(this.Document, 'star', undefined, 'match');
         this.layoutDoc.practiceMode = practiceMode.NORMAL;
         this.layoutDoc._carousel_index = 0;
     }
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index dc15c83c5..0afda3e64 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -440,7 +440,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.');
             return CollectionCardView.imageUrlToBase64(`${name}_o.${type}`).then(hrefBase64 =>
                 !hrefBase64 ? undefined : 
-                    gptImageLabel(hrefBase64).then(labels => 
+                    gptImageLabel(hrefBase64, 'Give three to five labels to describe this image.').then(labels => 
                         Promise.all(labels.split('\n').map(label => gptGetEmbedding(label))).then(embeddings => 
                             ({ doc, embeddings, labels }))) ); // prettier-ignore
         });
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 2fc297bec..3d33ff862 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -92,6 +92,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             Doc.SetContainer(droppedDocuments.lastElement(), this.dataDoc);
             !added && e.preventDefault();
             e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place
+            // this.childActive = false;
             return added;
         }
         return undefined;
@@ -257,7 +258,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         newCol['x'] = this.layoutDoc['x'];
         newCol['y'] = NumCast(this.layoutDoc['y']) + 50;
         newCol.type_collection = 'carousel';
-        console.log(newCol.data);
+        // console.log(newCol.data);
 
         if (gpt) {
             this._props.DocumentView?.()._props.addDocument?.(newCol);
@@ -270,13 +271,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     }
 
     gptFlashcardPile = async () => {
-        var text = (await this.askGPT(GPTCallType.FLASHCARD)) || '';
+        var text = await this.askGPT(GPTCallType.FLASHCARD);
 
-        var senArr = text.trim().split('Question: ');
+        var senArr = text?.split('Question: ');
         var collectionArr: Doc[] = [];
-        for (let i = 1; i < senArr.length; i++) {
-            const newDoc = Docs.Create.ComparisonDocument(senArr[i].trim(), { _layout_isFlashcard: true, _width: 300, _height: 300 });
-            newDoc.text = senArr[i].trim();
+        for (let i = 1; i < senArr?.length!; i++) {
+            const newDoc = Docs.Create.ComparisonDocument(senArr![i], { _layout_isFlashcard: true, _width: 300, _height: 300 });
+            newDoc.text = senArr![i];
             collectionArr.push(newDoc);
         }
         this.createFlashcardPile(collectionArr, true);
@@ -451,13 +452,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             if (callType == GPTCallType.QUIZ) this._outputValue = res;
             // DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res;
             // this._outputValue = res;
-
-            if (callType === GPTCallType.FLASHCARD) {
+            else if (callType === GPTCallType.FLASHCARD) {
                 // console.log(res);
                 this._loading = false;
                 return res;
             }
-            console.log(res);
+            // console.log(res);
         } catch (err) {
             console.error('GPT call failed');
         }
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 3ffda5a35..be68ac8cd 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -139,3 +139,24 @@
 .imageBox-fadeBlocker-hover {
     opacity: 0;
 }
+
+.loading-spinner {
+    position: absolute;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100%;
+    width: 100%;
+    // left: 50%;
+    // top: 50%;
+    z-index: 200;
+    font-size: 20px;
+    font-weight: bold;
+    color: #17175e;
+}
+
+@keyframes spin {
+    to {
+        transform: rotate(360deg);
+    }
+}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index e4b3a1b9b..ff938df78 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -35,6 +35,15 @@ import { FieldView, FieldViewProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
 import './ImageBox.scss';
 import { OpenWhere } from './OpenWhere';
+import { URLField } from '../../../fields/URLField';
+import { gptImageLabel } from '../../apis/gpt/GPT';
+import ReactLoading from 'react-loading';
+import { FollowLinkScript } from '../../documents/DocUtils';
+import { basename } from 'path';
+import { ImageUtility } from './generativeFill/generativeFillUtils/ImageHandler';
+import { dropActionType } from '../../util/DropActionTypes';
+import { canvasSize } from './generativeFill/generativeFillUtils/generativeFillConstants';
+import axios from 'axios';
 
 export class ImageEditorData {
     // eslint-disable-next-line no-use-before-define
@@ -59,6 +68,8 @@ export class ImageEditorData {
     public static get AddDoc()                   { return ImageEditorData.imageData.addDoc; } // prettier-ignore
     public static set AddDoc(addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) { ImageEditorData.set(this.imageData.open, this.imageData.rootDoc, this.imageData.source, addDoc); } // prettier-ignore
 }
+
+const API_URL = 'https://api.unsplash.com/search/photos';
 @observer
 export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
@@ -73,9 +84,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     private _marqueeref = React.createRef<MarqueeAnnotator>();
     private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
+    private _imageRef: HTMLImageElement | null = null; // <video> ref
+    @observable private _width: number = 0;
+    @observable private _height: number = 0;
+    @observable private searchInput = '';
     @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
     @observable _curSuffix = '';
     @observable _error = '';
+    @observable private _loading = false;
     @observable _isHovering = false; // flag to switch between primary and alternate images on hover
     _ffref = React.createRef<CollectionFreeFormView>();
 
@@ -148,6 +164,32 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         Object.values(this._disposers).forEach(disposer => disposer?.());
     }
 
+    fetchImages = async () => {
+        try {
+            const { data } = await axios.get(`${API_URL}?query=${this.searchInput}&page=1&per_page=${1}&client_id=${process.env.VITE_API_KEY}`);
+            console.log('data', data);
+            console.log(data.results);
+            const imageSnapshot = Docs.Create.ImageDocument(data.results[0].urls.small, {
+                _nativeWidth: Doc.NativeWidth(this.layoutDoc),
+                _nativeHeight: Doc.NativeHeight(this.layoutDoc),
+                x: NumCast(this.layoutDoc.x),
+                y: NumCast(this.layoutDoc.y),
+                onClick: FollowLinkScript(),
+                _width: 150,
+                _height: 150,
+                title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-',
+            });
+            this._props.addDocument?.(imageSnapshot);
+        } catch (error) {
+            console.log(error);
+        }
+    };
+
+    handleSelection = async (selection: string) => {
+        this.searchInput = selection;
+        const images = await this.fetchImages();
+    };
+
     @undoBatch
     drop = (e: Event, de: DragManager.DropEvent) => {
         if (de.complete.docDragData) {
@@ -259,10 +301,189 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return cropping;
     };
 
+    createCanvas = async (downX?: number, downY?: number, cb?: (filename: string, x: number | undefined, y: number | undefined) => void) => {
+        const width = NumCast(this.layoutDoc._width);
+        const canvas = document.createElement('canvas');
+        // canvas.width = 640;
+        // canvas.height = (640 * Doc.NativeHeight(this.layoutDoc)) / (Doc.NativeWidth(this.layoutDoc) || 1);
+        canvas.width = NumCast(this.layoutDoc._width);
+        canvas.height = NumCast(this.layoutDoc._height);
+        const ctx = canvas.getContext('2d'); // draw image to canvas. scale to target dimensions
+        if (ctx) {
+            // this._imageRef && ctx.drawImage(this._imageRef, 0, 0, canvas.width, canvas.height);
+            this._imageRef && ctx.drawImage(this._imageRef, NumCast(this._marqueeref.current?.left), NumCast(this._marqueeref.current?.top), this._width, this._height, 0, 0, 1000, 1000);
+            //this._imageRef && ctx.drawImage(this._imageRef, 0, 0, 2000, 1000, 0, 0, canvas.width, canvas.height);
+            // console.log(NumCast(this._marqueeref.current?.left) + 100);
+        }
+        const blob = await ImageUtility.canvasToBlob(canvas);
+        return ImageBox.selectUrlToBase64(blob);
+
+        // if (this._imageRef) {
+        //     const canv = ImageUtility.getCroppedImg(this._imageRef, this._imageRef.width, this._imageRef.height);
+        //     console.log(this._imageRef.width);
+        //     if (canv) {
+        //         const blob = await ImageUtility.canvasToBlob(canv);
+        //         return ImageBox.selectUrlToBase64(blob);
+        //     }
+        // }
+        if (!this._imageRef) {
+            const b = Docs.Create.LabelDocument({
+                x: NumCast(this.layoutDoc.x) + width,
+                y: NumCast(this.layoutDoc.y, 1),
+                _width: 150,
+                _height: 50,
+                // title: (this.layoutDoc._layout_currentTimecode || 0).toString(),
+                onClick: FollowLinkScript(),
+            });
+            this._props.addDocument?.(b);
+            DocUtils.MakeLink(b, this.Document, { link_relationship: 'image snapshot' });
+        } else {
+            // convert to desired file format
+            // const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
+            // // if you want to preview the captured image,
+            // const retitled = StrCast(this.Document.title).replace(/[ -.:]/g, '');
+            // const encodedFilename = encodeURIComponent(('snapshot' + retitled + '_' + (this.layoutDoc._layout_currentTimecode || 0).toString()).replace(/[./?=]/g, '_'));
+            // const filename = basename(encodedFilename);
+            // ClientUtils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY));
+        }
+        // convert to desired file format
+
+        // const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
+        // // if you want to preview the captured image,
+        // const retitled = StrCast(this.Document.title).replace(/[ -.:]/g, '');
+        // const encodedFilename = encodeURIComponent(('snapshot' + retitled + '_' + (this.layoutDoc._layout_currentTimecode || 0).toString()).replace(/[./?=]/g, '_'));
+        // const filename = basename(encodedFilename);
+        //ClientUtils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY));
+        // }
+        // const docViewContent = this.DocumentView?.().ContentDiv!;
+        // if (docViewContent instanceof HTMLCanvasElement) {
+        //     const canvas = docViewContent;
+        //     const img = document.createElement('img'); // create a Image Element
+        //     img.src = canvas.toDataURL(); // image sourcez
+        //     img.style.width = canvas.style.width;
+        //     img.style.height = canvas.style.height;
+        //     const parEle = newCan.parentElement as HTMLElement;
+        //     parEle.removeChild(newCan);
+        //     parEle.appendChild(img);
+        // }
+    };
+
+    createSnapshotLink = (imagePath: string, downX?: number, downY?: number) => {
+        const url = !imagePath.startsWith('/') ? ClientUtils.CorsProxy(imagePath) : imagePath;
+        const width = NumCast(this.layoutDoc._width) || 1;
+        const height = NumCast(this.layoutDoc._height);
+        const imageSnapshot = Docs.Create.ImageDocument(url, {
+            _nativeWidth: Doc.NativeWidth(this.layoutDoc),
+            _nativeHeight: Doc.NativeHeight(this.layoutDoc),
+            x: NumCast(this.layoutDoc.x) + width,
+            y: NumCast(this.layoutDoc.y),
+            onClick: FollowLinkScript(),
+            _width: 150,
+            _height: (height / width) * 150,
+            title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-',
+        });
+        Doc.SetNativeWidth(imageSnapshot[DocData], Doc.NativeWidth(this.layoutDoc));
+        Doc.SetNativeHeight(imageSnapshot[DocData], Doc.NativeHeight(this.layoutDoc));
+        this._props.addDocument?.(imageSnapshot);
+        DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' });
+        // link && (DocCast(link.link_anchor_2)[DocData].timecodeToHide = NumCast(DocCast(link.link_anchor_2).timecodeToShow) + 3);  // do we need to set an end time? should default to +0.1
+        setTimeout(() => downX !== undefined && downY !== undefined && DocumentView.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, dropActionType.move, true));
+    };
+
+    /**
+     * 
+        if (oldDiv instanceof HTMLCanvasElement) {
+            const canvas = oldDiv;
+            const img = document.createElement('img'); // create a Image Element
+            img.src = canvas.toDataURL(); // image sourcez
+            img.style.width = canvas.style.width;
+            img.style.height = canvas.style.height;
+            const newCan = newDiv as HTMLCanvasElement;
+            const parEle = newCan.parentElement as HTMLElement;
+            parEle.removeChild(newCan);
+            parEle.appendChild(img);
+        }
+     */
+
+    static selectUrlToBase64 = async (blob: Blob): Promise<string> => {
+        try {
+            return new Promise((resolve, reject) => {
+                const reader = new FileReader();
+                reader.readAsDataURL(blob);
+                reader.onloadend = () => resolve(reader.result as string);
+                reader.onerror = error => reject(error);
+            });
+        } catch (error) {
+            console.error('Error:', error);
+            throw error;
+        }
+    };
+
+    static imageUrlToBase64 = async (imageUrl: string): Promise<string> => {
+        try {
+            const response = await fetch(imageUrl);
+            const blob = await response.blob();
+
+            return new Promise((resolve, reject) => {
+                const reader = new FileReader();
+                reader.readAsDataURL(blob);
+                reader.onloadend = () => resolve(reader.result as string);
+                reader.onerror = error => reject(error);
+            });
+        } catch (error) {
+            console.error('Error:', error);
+            throw error;
+        }
+    };
+
+    getImageDesc = async () => {
+        // if (StrCast(this.dataDoc.description)) return StrCast(this.dataDoc.description); // Return existing description
+        const { href } = (this.dataDoc.data as URLField).url;
+        const hrefParts = href.split('.');
+        const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
+        this._loading = true;
+        try {
+            // const hrefBase64 = await ImageBox.imageUrlToBase64(hrefComplete);
+            const hrefBase64 = await this.createCanvas();
+            const response = await gptImageLabel(hrefBase64, 'Tell me what words you see on this image.');
+            //const response = await gptImageLabel(hrefBase64, 'Make flashcards out of this text with each question and answer labeled as question and answer. Do not label each flashcard and do not include asterisks: ');
+            console.log(response);
+            // AnchorMenu.Instance.transferToFlashcard(response);
+            // this.Document[DocData].description = response.trim();
+            // return response; // Return the response
+        } catch (error) {
+            console.log('Error');
+        }
+        this._loading = false;
+        // return '';
+    };
+
+    @action
+    setRef = (iref: HTMLImageElement | null) => {
+        this._imageRef = iref;
+        // if (iref) {
+        //     this._videoRef!.ontimeupdate = this.updateTimecode;
+        //     // @ts-ignore
+        //     // vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
+        //     this._disposers.reactionDisposer?.();
+        //     this._disposers.reactionDisposer = reaction(
+        //         () => NumCast(this.layoutDoc._layout_currentTimecode),
+        //         time => {
+        //             !this._playing && (vref.currentTime = time);
+        //         },
+        //         { fireImmediately: true }
+        //     );
+
+        //     (!this.dataDoc[this.fieldKey + '_thumbnails'] || StrListCast(this.dataDoc[this.fieldKey + '_thumbnails']).length !== VideoThumbnails.DENSE) && this.getVideoThumbnails();
+        // }
+    };
+
     specificContextMenu = (): void => {
         const field = Cast(this.dataDoc[this.fieldKey], ImageField);
         if (field) {
             const funcs: ContextMenuProps[] = [];
+            // funcs.push({ description: 'Create ai flashcards', event: () => this.getImageDesc(), icon: 'id-card' });
+            funcs.push({ description: 'Get Images', event: () => this.handleSelection('Cats'), icon: 'redo-alt' });
             funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' });
             funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' });
             funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' });
@@ -391,6 +612,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 <div className="imageBox-fader" style={{ opacity: backAlpha }}>
                     <img
                         alt=""
+                        ref={this.setRef}
                         key="paths"
                         src={srcpath}
                         style={{ transform, transformOrigin }}
@@ -432,6 +654,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 action(moveEv => {
                     MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
                     this._marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]);
+                    this._width = moveEv.clientX;
+                    this._height = moveEv.clientY;
                     return true;
                 }),
                 returnFalse,
@@ -443,6 +667,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @action
     finishMarquee = () => {
         this._getAnchor = AnchorMenu.Instance?.GetAnchor;
+        AnchorMenu.Instance.gptFlashcards = this.getImageDesc;
+        AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument;
         this._marqueeref.current?.onTerminateSelection();
         this._props.select(false);
     };
@@ -500,6 +726,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     addDocument={this.addDocument}>
                     {this.content}
                 </CollectionFreeFormView>
+                {this._loading ? (
+                    <div className="loading-spinner" style={{ position: 'absolute' }}>
+                        <ReactLoading type="spin" height={50} width={50} color={'blue'} />
+                    </div>
+                ) : null}
                 {this.annotationLayer}
                 {!this._mainCont.current || !this.DocumentView || !this._annotationLayer.current ? null : (
                     <MarqueeAnnotator
@@ -518,6 +749,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         marqueeContainer={this._mainCont.current}
                         highlightDragSrcColor=""
                         anchorMenuCrop={this.crop}
+                        // anchorMenuFlashcard={() => this.getImageDesc()}
                     />
                 )}
             </div>
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index c1198b4f7..777117f26 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -54,6 +54,9 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
         this._y = y;
     };
 
+    @computed public get selectedText() {
+        return this._selectedText;
+    }
     public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search
 
     public OnCrop: (e: PointerEvent) => void = unimplementedFunction;
@@ -68,6 +71,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
     public MakeTargetToggle: () => void = unimplementedFunction;
     public ShowTargetTrail: () => void = unimplementedFunction;
     public IsTargetToggler: () => boolean = returnFalse;
+    public gptFlashcards: () => void = unimplementedFunction;
     public get Active() {
         return this._left > 0;
     }
@@ -110,23 +114,23 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
      * Invokes the API with the selected text and stores it in the selected text.
      * @param e pointer down event
      */
-    gptFlashcards = async (e: React.PointerEvent) => {
-        const queryText = this._selectedText;
-        this._loading = true;
-        try {
-            const res = await gptAPICall(queryText, GPTCallType.FLASHCARD);
-            console.log(res);
-            // GPTPopup.Instance.setText(res || 'Something went wrong.');
-            this.transferToFlashcard(res || 'Something went wrong');
-        } catch (err) {
-            console.error(err);
-        }
-        // GPTPopup.Instance.setLoading(false);
-    };
+    // gptPDFFlashcards = async () => {
+    //     const queryText = this._selectedText;
+    //     this._loading = true;
+    //     try {
+    //         const res = await gptAPICall(queryText, GPTCallType.FLASHCARD);
+    //         console.log(res);
+    //         // GPTPopup.Instance.setText(res || 'Something went wrong.');
+    //         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.
-     */
+    // /*
+    //  * 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
         var senArr = text.trim().split('Question: ');
@@ -236,12 +240,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
                         />
                     )}
                     {/* Adds a create flashcards option to the anchor menu, which calls the gptFlashcard method. */}
-                    <IconButton
-                        tooltip="Create flashcards" //
-                        onPointerDown={e => this.gptFlashcards(e)}
-                        icon={<FontAwesomeIcon icon="id-card" size="lg" />}
-                        color={SettingsManager.userColor}
-                    />
+                    <IconButton tooltip="Create flashcards" onPointerDown={this.gptFlashcards} icon={<FontAwesomeIcon icon="id-card" size="lg" />} color={SettingsManager.userColor} />
                     {AnchorMenu.Instance.OnAudio === unimplementedFunction ? null : (
                         <IconButton
                             tooltip="Click to Record Annotation" //
@@ -267,11 +266,11 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
                             />
                         </div>
                     )}
-                    {this._loading ? (
+                    {/* {this._loading ? (
                         <div className="loading-spinner" style={{ position: 'absolute' }}>
                             <ReactLoading type="spin" height={30} width={30} color={'white'} />
                         </div>
-                    ) : null}
+                    ) : null} */}
                 </>
             ) : (
                 <>
diff --git a/src/client/views/pdf/Annotation.scss b/src/client/views/pdf/Annotation.scss
index 329819ea2..26856b74e 100644
--- a/src/client/views/pdf/Annotation.scss
+++ b/src/client/views/pdf/Annotation.scss
@@ -8,20 +8,20 @@
         cursor: pointer;
     }
 }
-.loading-spinner {
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    height: 90%;
-    width: 93%;
-    left: 10;
-    font-size: 20px;
-    font-weight: bold;
-    color: #0b0a0a;
-}
+// .loading-spinner {
+//     display: flex;
+//     justify-content: center;
+//     align-items: center;
+//     height: 90%;
+//     width: 93%;
+//     left: 10;
+//     font-size: 20px;
+//     font-weight: bold;
+//     color: #0b0a0a;
+// }
 
-@keyframes spin {
-    to {
-        transform: rotate(360deg);
-    }
-}
+// @keyframes spin {
+//     to {
+//         transform: rotate(360deg);
+//     }
+// }
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index d3dd9f727..e70102ce9 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -107,3 +107,24 @@
 .pdfViewerDash-interactive {
     pointer-events: all;
 }
+
+.loading-spinner {
+    position: absolute;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100%;
+    width: 100%;
+    // left: 50%;
+    // top: 50%;
+    z-index: 200;
+    font-size: 20px;
+    font-weight: bold;
+    color: #17175e;
+}
+
+@keyframes spin {
+    to {
+        transform: rotate(360deg);
+    }
+}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index af7a5774d..befbee48b 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -32,6 +32,8 @@ import { Annotation } from './Annotation';
 import { GPTPopup } from './GPTPopup/GPTPopup';
 import { Docs } from '../../documents/Documents';
 import './PDFViewer.scss';
+import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
+import ReactLoading from 'react-loading';
 
 // pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`;
 // The workerSrc property shall be specified.
@@ -70,6 +72,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
     @observable _textSelecting = true;
     @observable _showWaiting = true;
     @observable Index: number = -1;
+    @observable private _loading = false;
 
     private _pdfViewer: any;
     private _styleRule: any; // stylesheet rule for making hyperlinks clickable
@@ -406,6 +409,49 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
         }
     };
 
+    gptPDFFlashcards = async () => {
+        const queryText = this._selectionText;
+        this._loading = true;
+        try {
+            const res = await gptAPICall(queryText, GPTCallType.FLASHCARD);
+            console.log(res);
+            // GPTPopup.Instance.setText(res || 'Something went wrong.');
+            AnchorMenu.Instance.transferToFlashcard(res || 'Something went wrong');
+            // this.transferToFlashcard(res || 'Something went wrong');
+        } catch (err) {
+            console.error(err);
+        }
+        this._loading = false;
+        // GPTPopup.Instance.setLoading(false);
+    };
+
+    // transferToFlashcard = (text: string) => {
+    //     // put each question generated by GPT on the front of the flashcard
+    //     var senArr = text.trim().split('Question: ');
+    //     var collectionArr: Doc[] = [];
+    //     for (let 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);
+    //     }
+    //     // create a new carousel collection of these flashcards
+    //     const newCol = Docs.Create.CarouselDocument(collectionArr, {
+    //         _width: 250,
+    //         _height: 200,
+    //         _layout_fitWidth: false,
+    //         _layout_autoHeight: true,
+    //     });
+
+    //     newCol.x = this._props.layoutDoc['x'];
+    //     newCol.y = this._props.layoutDoc['y'];
+    //     newCol.zIndex = 100;
+
+    //     this._props.DocumentView?.()._props.addDocument?.(newCol);
+    //     console.log('HERE');
+    //     this._loading = false;
+    // };
+
     @action
     finishMarquee = (/* x?: number, y?: number */) => {
         this._getAnchor = AnchorMenu.Instance?.GetAnchor;
@@ -437,6 +483,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
         GPTPopup.Instance.addDoc = this._props.sidebarAddDoc;
         // allows for creating collection
         AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument;
+        AnchorMenu.Instance.gptFlashcards = this.gptPDFFlashcards;
     };
 
     @action
@@ -625,6 +672,11 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
                         />
                     )}
                 </div>
+                {this._loading ? (
+                    <div className="loading-spinner" style={{ position: 'absolute' }}>
+                        <ReactLoading type="spin" height={80} width={80} color={'blue'} />
+                    </div>
+                ) : null}
             </div>
         );
     }
-- 
cgit v1.2.3-70-g09d2


From 6131fd8fc6cb1245ccfbc5d03d33ffd1498e83f4 Mon Sep 17 00:00:00 2001
From: alyssaf16 <alyssa_feinberg@brown.edu>
Date: Mon, 8 Jul 2024 15:12:16 -0400
Subject: Speech to phonemes progress

---
 report.20231129.000028.55430.0.001.json            | 1343 ++++++++++++++++++++
 src/client/apis/gpt/GPT.ts                         |    9 +-
 src/client/util/CurrentUserUtils.ts                |    1 +
 src/client/util/DictationManager.ts                |    2 +-
 src/client/views/.MetadataEntryMenu.tsx.icloud     |  Bin 0 -> 171 bytes
 src/client/views/.Touchable.tsx.icloud             |  Bin 0 -> 161 bytes
 .../views/collections/CollectionCarouselView.tsx   |    5 +-
 src/client/views/nodes/.LinkAnchorBox.scss.icloud  |  Bin 0 -> 168 bytes
 src/client/views/nodes/AudioBox.tsx                |   19 +-
 src/client/views/nodes/ComparisonBox.scss          |   10 +-
 src/client/views/nodes/ComparisonBox.tsx           |  258 ++--
 src/client/views/nodes/FieldView.tsx               |    2 +-
 src/client/views/nodes/ImageBox.tsx                |   78 +-
 src/client/views/nodes/LinkAnchorBox.tsx           |  115 ++
 .../nodes/ae6d-ba67-4ace-93aa-0f9e0bd96b88.wav     |  Bin 0 -> 765006 bytes
 .../views/nodes/formattedText/FormattedTextBox.tsx |    7 +-
 src/fields/RichTextField.ts                        |    9 +-
 17 files changed, 1743 insertions(+), 115 deletions(-)
 create mode 100644 report.20231129.000028.55430.0.001.json
 create mode 100644 src/client/views/.MetadataEntryMenu.tsx.icloud
 create mode 100644 src/client/views/.Touchable.tsx.icloud
 create mode 100644 src/client/views/nodes/.LinkAnchorBox.scss.icloud
 create mode 100644 src/client/views/nodes/LinkAnchorBox.tsx
 create mode 100644 src/client/views/nodes/ae6d-ba67-4ace-93aa-0f9e0bd96b88.wav

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

diff --git a/report.20231129.000028.55430.0.001.json b/report.20231129.000028.55430.0.001.json
new file mode 100644
index 000000000..a29d550ae
--- /dev/null
+++ b/report.20231129.000028.55430.0.001.json
@@ -0,0 +1,1343 @@
+
+{
+  "header": {
+    "reportVersion": 1,
+    "event": "Allocation failed - JavaScript heap out of memory",
+    "trigger": "FatalError",
+    "filename": "report.20231129.000028.55430.0.001.json",
+    "dumpEventTime": "2023-11-29T00:00:28Z",
+    "dumpEventTimeStamp": "1701234028056",
+    "processId": 55430,
+    "cwd": "/Users/sarah/Desktop/dash/Dash-Web",
+    "commandLine": [
+      "/Users/sarah/.nvm/versions/node/v12.16.0/bin/node",
+      "--max-old-space-size=2048",
+      "/Users/sarah/Desktop/dash/Dash-Web/node_modules/ts-node-dev/lib/wrap.js",
+      "/Users/sarah/Desktop/dash/Dash-Web/node_modules/fork-ts-checker-webpack-plugin/lib/service.js"
+    ],
+    "nodejsVersion": "v12.16.0",
+    "wordSize": 64,
+    "arch": "x64",
+    "platform": "darwin",
+    "componentVersions": {
+      "node": "12.16.0",
+      "v8": "7.8.279.23-node.31",
+      "uv": "1.34.0",
+      "zlib": "1.2.11",
+      "brotli": "1.0.7",
+      "ares": "1.15.0",
+      "modules": "72",
+      "nghttp2": "1.40.0",
+      "napi": "5",
+      "llhttp": "2.0.4",
+      "http_parser": "2.9.3",
+      "openssl": "1.1.1d",
+      "cldr": "35.1",
+      "icu": "64.2",
+      "tz": "2019c",
+      "unicode": "12.1"
+    },
+    "release": {
+      "name": "node",
+      "lts": "Erbium",
+      "headersUrl": "https://nodejs.org/download/release/v12.16.0/node-v12.16.0-headers.tar.gz",
+      "sourceUrl": "https://nodejs.org/download/release/v12.16.0/node-v12.16.0.tar.gz"
+    },
+    "osName": "Darwin",
+    "osRelease": "22.6.0",
+    "osVersion": "Darwin Kernel Version 22.6.0: Wed Jul  5 22:21:56 PDT 2023; root:xnu-8796.141.3~6/RELEASE_X86_64",
+    "osMachine": "x86_64",
+    "cpus": [
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 19491620,
+        "nice": 0,
+        "sys": 10530730,
+        "idle": 128029940,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 1041080,
+        "nice": 0,
+        "sys": 832510,
+        "idle": 156711500,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 17230140,
+        "nice": 0,
+        "sys": 7308720,
+        "idle": 133550270,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 1101080,
+        "nice": 0,
+        "sys": 813690,
+        "idle": 156678120,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 14246350,
+        "nice": 0,
+        "sys": 5654800,
+        "idle": 138314740,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 1142930,
+        "nice": 0,
+        "sys": 790770,
+        "idle": 156668310,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 12228700,
+        "nice": 0,
+        "sys": 4533960,
+        "idle": 141551990,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 1165420,
+        "nice": 0,
+        "sys": 773220,
+        "idle": 156669740,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 9805920,
+        "nice": 0,
+        "sys": 3386750,
+        "idle": 145216650,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 1177080,
+        "nice": 0,
+        "sys": 759980,
+        "idle": 156674610,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 8806950,
+        "nice": 0,
+        "sys": 2889820,
+        "idle": 146774530,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 1166250,
+        "nice": 0,
+        "sys": 737790,
+        "idle": 156711830,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 7865920,
+        "nice": 0,
+        "sys": 2438950,
+        "idle": 148205800,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 1155370,
+        "nice": 0,
+        "sys": 719580,
+        "idle": 156743860,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 7478040,
+        "nice": 0,
+        "sys": 2248690,
+        "idle": 148810130,
+        "irq": 0
+      },
+      {
+        "model": "Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz",
+        "speed": 2300,
+        "user": 1131070,
+        "nice": 0,
+        "sys": 695000,
+        "idle": 156797450,
+        "irq": 0
+      }
+    ],
+    "networkInterfaces": [
+      {
+        "name": "lo0",
+        "internal": true,
+        "mac": "00:00:00:00:00:00",
+        "address": "127.0.0.1",
+        "netmask": "255.0.0.0",
+        "family": "IPv4"
+      },
+      {
+        "name": "lo0",
+        "internal": true,
+        "mac": "00:00:00:00:00:00",
+        "address": "::1",
+        "netmask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
+        "family": "IPv6",
+        "scopeid": 0
+      },
+      {
+        "name": "lo0",
+        "internal": true,
+        "mac": "00:00:00:00:00:00",
+        "address": "fe80::1",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 1
+      },
+      {
+        "name": "en5",
+        "internal": false,
+        "mac": "ac:de:48:00:11:22",
+        "address": "fe80::aede:48ff:fe00:1122",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 4
+      },
+      {
+        "name": "en0",
+        "internal": false,
+        "mac": "88:66:5a:29:28:77",
+        "address": "fe80::1467:9dad:c3d1:73a7",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 6
+      },
+      {
+        "name": "en0",
+        "internal": false,
+        "mac": "88:66:5a:29:28:77",
+        "address": "10.38.21.14",
+        "netmask": "255.255.192.0",
+        "family": "IPv4"
+      },
+      {
+        "name": "en0",
+        "internal": false,
+        "mac": "88:66:5a:29:28:77",
+        "address": "fde6:e673:d766:4b72:1481:e813:77a2:da34",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 0
+      },
+      {
+        "name": "en0",
+        "internal": false,
+        "mac": "88:66:5a:29:28:77",
+        "address": "2620:6e:6000:3100:148e:201a:1a33:145d",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 0
+      },
+      {
+        "name": "en0",
+        "internal": false,
+        "mac": "88:66:5a:29:28:77",
+        "address": "2620:6e:6000:3100:5c7f:8d3f:76ba:694b",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 0
+      },
+      {
+        "name": "awdl0",
+        "internal": false,
+        "mac": "12:df:77:23:4c:4e",
+        "address": "fe80::10df:77ff:fe23:4c4e",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 9
+      },
+      {
+        "name": "llw0",
+        "internal": false,
+        "mac": "12:df:77:23:4c:4e",
+        "address": "fe80::10df:77ff:fe23:4c4e",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 12
+      },
+      {
+        "name": "utun0",
+        "internal": false,
+        "mac": "00:00:00:00:00:00",
+        "address": "fe80::6616:29f1:d83d:2f8d",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 14
+      },
+      {
+        "name": "utun1",
+        "internal": false,
+        "mac": "00:00:00:00:00:00",
+        "address": "fe80::b4dc:ec9a:5d3d:18ab",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 15
+      },
+      {
+        "name": "utun2",
+        "internal": false,
+        "mac": "00:00:00:00:00:00",
+        "address": "fe80::ce81:b1c:bd2c:69e",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 16
+      },
+      {
+        "name": "utun3",
+        "internal": false,
+        "mac": "00:00:00:00:00:00",
+        "address": "fe80::bd9f:d9d8:700d:2011",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 17
+      },
+      {
+        "name": "utun4",
+        "internal": false,
+        "mac": "00:00:00:00:00:00",
+        "address": "fe80::9a49:1de7:e220:2bb",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 18
+      },
+      {
+        "name": "utun5",
+        "internal": false,
+        "mac": "00:00:00:00:00:00",
+        "address": "fe80::93fe:38cf:1393:3191",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 19
+      },
+      {
+        "name": "utun6",
+        "internal": false,
+        "mac": "00:00:00:00:00:00",
+        "address": "fe80::781d:651c:69a7:3d42",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 20
+      },
+      {
+        "name": "utun7",
+        "internal": false,
+        "mac": "00:00:00:00:00:00",
+        "address": "fe80::b39:d094:2a6d:e0c5",
+        "netmask": "ffff:ffff:ffff:ffff::",
+        "family": "IPv6",
+        "scopeid": 21
+      }
+    ],
+    "host": "sarahs-mbp-2.devices.brown.edu"
+  },
+  "javascriptStack": {
+    "message": "No stack.",
+    "stack": [
+      "Unavailable."
+    ]
+  },
+  "nativeStack": [
+    {
+      "pc": "0x000000010015c8ca",
+      "symbol": "report::TriggerNodeReport(v8::Isolate*, node::Environment*, char const*, char const*, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&, v8::Local<v8::String>) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x0000000100080f3e",
+      "symbol": "node::OnFatalError(char const*, char const*) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x0000000100185467",
+      "symbol": "v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x0000000100185403",
+      "symbol": "v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x000000010030b5f5",
+      "symbol": "v8::internal::Heap::FatalProcessOutOfMemory(char const*) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x000000010030ccc4",
+      "symbol": "v8::internal::Heap::RecomputeLimits(v8::internal::GarbageCollector) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x0000000100309b37",
+      "symbol": "v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x0000000100307afd",
+      "symbol": "v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x00000001003132ba",
+      "symbol": "v8::internal::Heap::AllocateRawWithLightRetry(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x0000000100313341",
+      "symbol": "v8::internal::Heap::AllocateRawWithRetryOrFail(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x00000001002e065b",
+      "symbol": "v8::internal::Factory::NewFillerObject(int, bool, v8::internal::AllocationType, v8::internal::AllocationOrigin) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x0000000100618a18",
+      "symbol": "v8::internal::Runtime_AllocateInYoungGeneration(int, unsigned long*, v8::internal::Isolate*) [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    },
+    {
+      "pc": "0x0000000100950c19",
+      "symbol": "Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_NoBuiltinExit [/Users/sarah/.nvm/versions/node/v12.16.0/bin/node]"
+    }
+  ],
+  "javascriptHeap": {
+    "totalMemory": 2154991616,
+    "totalCommittedMemory": 2153815368,
+    "usedMemory": 2136222480,
+    "availableMemory": 51343040,
+    "memoryLimit": 2197815296,
+    "heapSpaces": {
+      "read_only_space": {
+        "memorySize": 262144,
+        "committedMemory": 33088,
+        "capacity": 32808,
+        "used": 32808,
+        "available": 0
+      },
+      "new_space": {
+        "memorySize": 8388608,
+        "committedMemory": 8388528,
+        "capacity": 4189824,
+        "used": 69544,
+        "available": 4120280
+      },
+      "old_space": {
+        "memorySize": 1946566656,
+        "committedMemory": 1946346224,
+        "capacity": 1939183704,
+        "used": 1938974448,
+        "available": 209256
+      },
+      "code_space": {
+        "memorySize": 14323712,
+        "committedMemory": 13779552,
+        "capacity": 12481792,
+        "used": 12481792,
+        "available": 0
+      },
+      "map_space": {
+        "memorySize": 1576960,
+        "committedMemory": 1394440,
+        "capacity": 1280720,
+        "used": 1280720,
+        "available": 0
+      },
+      "large_object_space": {
+        "memorySize": 183824384,
+        "committedMemory": 183824384,
+        "capacity": 183380384,
+        "used": 183380384,
+        "available": 0
+      },
+      "code_large_object_space": {
+        "memorySize": 49152,
+        "committedMemory": 49152,
+        "capacity": 2784,
+        "used": 2784,
+        "available": 0
+      },
+      "new_large_object_space": {
+        "memorySize": 0,
+        "committedMemory": 0,
+        "capacity": 4189824,
+        "used": 0,
+        "available": 4189824
+      }
+    }
+  },
+  "resourceUsage": {
+    "userCpuSeconds": 306.691,
+    "kernelCpuSeconds": 15.3869,
+    "cpuConsumptionPercent": 11.2772,
+    "maxRss": 2231285841920,
+    "pageFaults": {
+      "IORequired": 110,
+      "IONotRequired": 2487718
+    },
+    "fsActivity": {
+      "reads": 0,
+      "writes": 0
+    }
+  },
+  "libuv": [
+  ],
+  "environmentVariables": {
+    "npm_config_save_dev": "",
+    "npm_config_legacy_bundling": "",
+    "npm_config_dry_run": "",
+    "npm_package_dependencies_request": "^2.88.2",
+    "npm_package_dependencies_express_flash": "0.0.2",
+    "npm_package_dependencies__fortawesome_fontawesome_svg_core": "^6.3.0",
+    "NVM_INC": "/Users/sarah/.nvm/versions/node/v12.16.0/include/node",
+    "npm_config_viewer": "man",
+    "npm_config_only": "",
+    "npm_config_commit_hooks": "true",
+    "npm_config_browser": "",
+    "npm_package_gitHead": "5754c1c056ec62e17b5d7badddc4a8fc3637e09f",
+    "npm_package_dependencies_webpack_dev_middleware": "^5.3.1",
+    "npm_package_dependencies_webpack_cli": "^4.10.0",
+    "npm_package_devDependencies_prettier": "^2.7.1",
+    "npm_package_devDependencies_awesome_typescript_loader": "^5.2.1",
+    "npm_package_devDependencies__types_archiver": "^3.1.1",
+    "npm_config_also": "",
+    "npm_package_dependencies_react_jsx_parser": "^1.29.0",
+    "npm_package_dependencies_mongoose": "^5.13.14",
+    "npm_package_dependencies_connect_flash": "^0.1.1",
+    "npm_package_browser_child_process": "false",
+    "npm_config_sign_git_commit": "",
+    "npm_config_rollback": "true",
+    "npm_package_dependencies_material_ui": "^0.20.2",
+    "npm_package_devDependencies__types_sharp": "^0.23.1",
+    "npm_package_devDependencies__types_passport_local": "^1.0.34",
+    "npm_package_devDependencies__types_dotenv": "^6.1.1",
+    "npm_package_devDependencies__types_cookie_parser": "^1.4.2",
+    "TERM_PROGRAM": "Apple_Terminal",
+    "NODE": "/Users/sarah/.nvm/versions/node/v12.16.0/bin/node",
+    "npm_config_usage": "",
+    "npm_config_audit": "true",
+    "npm_package_dependencies_reveal_js": "^4.3.0",
+    "npm_package_dependencies_process": "^0.11.10",
+    "npm_package_dependencies_pdfjs": "^2.4.7",
+    "npm_package_dependencies_html_to_image": "^0.1.3",
+    "npm_package_devDependencies_file_loader": "^3.0.1",
+    "npm_package_devDependencies__types_express_flash": "0.0.0",
+    "npm_package_scripts_monitor": "cross-env MONITORED=true NODE_OPTIONS=--max_old_space_size=4096 ts-node src/server/index.ts",
+    "INIT_CWD": "/Users/sarah/Desktop/dash/Dash-Web",
+    "npm_package_dependencies_rehype_raw": "^6.1.1",
+    "npm_package_dependencies_react_audio_waveform": "0.0.5",
+    "npm_package_dependencies_path_browserify": "^1.0.1",
+    "npm_package_dependencies_nodemailer": "^5.1.1",
+    "npm_package_dependencies_axios": "^0.19.2",
+    "npm_package_devDependencies_typescript": "^4.7.4",
+    "NVM_CD_FLAGS": "-q",
+    "npm_config_globalignorefile": "/Users/sarah/.nvm/versions/node/v12.16.0/etc/npmignore",
+    "npm_package_dependencies_react_grid_layout": "^1.3.4",
+    "npm_package_dependencies_prosemirror_find_replace": "^0.9.0",
+    "npm_package_dependencies_normalize_css": "^8.0.1",
+    "npm_package_devDependencies_mocha": "^5.2.0",
+    "npm_package_devDependencies__types_express_session": "^1.17.5",
+    "SHELL": "/bin/zsh",
+    "TERM": "xterm-256color",
+    "npm_config_shell": "/bin/zsh",
+    "npm_config_maxsockets": "50",
+    "npm_config_init_author_url": "",
+    "npm_package_dependencies_prosemirror_dev_tools": "^3.1.0",
+    "npm_package_dependencies_p_limit": "^2.2.0",
+    "npm_package_dependencies_bson": "^4.6.1",
+    "npm_package_dependencies__types_dom_speech_recognition": "0.0.1",
+    "npm_package_dependencies__emotion_styled": "^11.11.0",
+    "npm_package_devDependencies_style_loader": "^0.23.1",
+    "npm_package_devDependencies__types_react_datepicker": "^3.1.8",
+    "npm_config_shrinkwrap": "true",
+    "npm_config_parseable": "",
+    "npm_config_metrics_registry": "https://registry.npmjs.org/",
+    "npm_package_dependencies_xregexp": "^4.4.1",
+    "npm_package_dependencies_shelljs": "^0.8.5",
+    "npm_package_dependencies_bezier_curve": "^1.0.0",
+    "npm_package_dependencies__mui_icons_material": "^5.11.16",
+    "npm_package_devDependencies_tslint": "^5.20.1",
+    "npm_package_devDependencies__types_react_transition_group": "^4.4.5",
+    "npm_package_scripts_tsc": "tsc",
+    "TMPDIR": "/var/folders/yk/p_39q8jn673c5p8_66mcxm7r0000gn/T/",
+    "npm_config_timing": "",
+    "npm_config_init_license": "ISC",
+    "npm_package_dependencies_socket_io": "^2.5.0",
+    "npm_package_dependencies_probe_image_size": "^4.0.0",
+    "npm_package_dependencies_canvas": "^2.9.3",
+    "npm_package_dependencies__hig_theme_data": "^2.23.1",
+    "npm_package_devDependencies__types_react_select": "^3.1.2",
+    "npm_package_devDependencies__types_prosemirror_model": "^1.16.1",
+    "CONDA_SHLVL": "1",
+    "npm_config_if_present": "",
+    "npm_package_dependencies_typescript_collections": "^1.3.3",
+    "npm_package_dependencies_rimraf": "^3.0.0",
+    "npm_package_dependencies_react_autosuggest": "^9.4.3",
+    "npm_package_dependencies_flexlayout_react": "^0.3.11",
+    "npm_package_dependencies_find_in_files": "^0.5.0",
+    "npm_package_devDependencies__types_chai": "^4.3.0",
+    "CONDA_PROMPT_MODIFIER": "(base) ",
+    "TERM_PROGRAM_VERSION": "447",
+    "npm_package_dependencies_prosemirror_inputrules": "^1.1.3",
+    "npm_package_dependencies_bcrypt_nodejs": "0.0.3",
+    "npm_package_dependencies_async": "^2.6.2",
+    "npm_config_sign_git_tag": "",
+    "npm_config_init_author_email": "",
+    "npm_config_cache_max": "Infinity",
+    "npm_package_dependencies_uuid": "^3.4.0",
+    "npm_package_dependencies_supercluster": "^7.1.4",
+    "npm_package_dependencies_remark_gfm": "^3.0.1",
+    "npm_package_dependencies_connect_mongo": "^2.0.3",
+    "npm_package_dependencies_browser_assert": "^1.2.1",
+    "npm_package_devDependencies_sass_loader": "^7.3.1",
+    "npm_package_scripts_start_release_debug": "cross-env RELEASE=true USE_AZURE=true NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --inspect -- src/server/index.ts",
+    "npm_config_preid": "",
+    "npm_config_long": "",
+    "npm_config_local_address": "",
+    "npm_config_git_tag_version": "true",
+    "npm_config_cert": "",
+    "npm_package_dependencies_js_datepicker": "^4.6.6",
+    "npm_package_devDependencies__types_webpack_hot_middleware": "^2.25.6",
+    "npm_package_devDependencies__types_mongodb": "^3.6.20",
+    "npm_package_devDependencies__types_mocha": "^5.2.6",
+    "TERM_SESSION_ID": "4AACD142-8780-44F4-A4FA-BB53E1BF6081",
+    "npm_config_registry": "https://registry.npmjs.org/",
+    "npm_config_noproxy": "",
+    "npm_config_fetch_retries": "2",
+    "npm_package_dependencies_react_compound_slider": "^2.5.0",
+    "npm_package_dependencies_prosemirror_history": "^1.2.0",
+    "npm_package_devDependencies__types_react_color": "^2.17.6",
+    "npm_package_devDependencies__types_google_maps_react": "^2.0.5",
+    "npm_package_devDependencies__types_color": "^3.0.3",
+    "npm_package_dependencies_react_dom": "^18.2.0",
+    "npm_package_dependencies_passport_local": "^1.0.0",
+    "npm_package_dependencies__octokit_core": "^4.0.4",
+    "npm_package_devDependencies__types_async": "^2.4.1",
+    "npm_package_scripts_debug": "cross-env  NODE_OPTIONS=--max_old_space_size=8192 ts-node-dev --transpile-only --inspect -- src/server/index.ts",
+    "npm_package_scripts_oldstart": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --debug -- src/server/index.ts",
+    "npm_config_versions": "",
+    "npm_config_message": "%s",
+    "npm_config_key": "",
+    "npm_package_readmeFilename": "README.md",
+    "npm_package_dependencies_react_refresh_typescript": "^2.0.7",
+    "npm_package_dependencies_react_dropzone": "^14.2.3",
+    "npm_package_dependencies_image_size": "^0.7.5",
+    "npm_package_dependencies_html_to_text": "^5.1.1",
+    "npm_package_dependencies_express_validator": "^5.3.1",
+    "npm_package_devDependencies_eslint_plugin_jsx_a11y": "^6.6.0",
+    "npm_package_node_child_process": "empty",
+    "npm_package_dependencies_react_resizable_rotatable_draggable": "^0.2.0",
+    "npm_package_dependencies_got": "^12.0.1",
+    "npm_package_dependencies__types_d3_color": "^2.0.3",
+    "npm_package_devDependencies_webpack": "^5.69.1",
+    "npm_package_devDependencies__types_nodemailer": "^4.6.6",
+    "npm_package_description": "Install Node.js, then, from the project directory, run",
+    "NVM_DIR": "/Users/sarah/.nvm",
+    "USER": "sarah",
+    "npm_package_dependencies__types_d3_scale": "^3.3.2",
+    "npm_package_devDependencies_dotenv": "^8.6.0",
+    "npm_package_devDependencies__types_react": "^18.0.15",
+    "npm_package_devDependencies__types_prosemirror_transform": "^1.1.5",
+    "npm_package_devDependencies__types_prosemirror_history": "^1.0.3",
+    "npm_package_dependencies_readline": "^1.3.0",
+    "npm_package_dependencies__types_supercluster": "^7.1.0",
+    "npm_package_dependencies__azure_storage_blob": "^12.14.0",
+    "npm_config_globalconfig": "/Users/sarah/.nvm/versions/node/v12.16.0/etc/npmrc",
+    "npm_package_dependencies_depcheck": "^0.9.2",
+    "npm_package_dependencies__types_web": "0.0.53",
+    "CONDA_EXE": "/Users/sarah/miniconda3/bin/conda",
+    "npm_config_prefer_online": "",
+    "npm_config_logs_max": "10",
+    "npm_config_always_auth": "",
+    "npm_package_dependencies_react_icons": "^4.3.1",
+    "npm_package_dependencies_passport_google_oauth20": "^2.0.0",
+    "npm_package_devDependencies_webpack_dev_server": "^3.11.3",
+    "npm_package_devDependencies__types_brotli": "^1.3.1",
+    "npm_package_dependencies_url_loader": "^1.1.2",
+    "npm_package_dependencies_stream_browserify": "^3.0.0",
+    "npm_package_dependencies_prosemirror_transform": "^1.3.4",
+    "npm_package_dependencies_lodash": "^4.17.21",
+    "npm_package_dependencies_i": "^0.3.7",
+    "npm_package_devDependencies_tslint_loader": "^3.6.0",
+    "SSH_AUTH_SOCK": "/private/tmp/com.apple.launchd.dBaRxB6a53/Listeners",
+    "npm_package_dependencies_words_to_numbers": "^1.5.1",
+    "npm_package_dependencies_valid_url": "^1.0.9",
+    "npm_package_dependencies_styled_components": "^4.4.1",
+    "npm_package_dependencies_csv_parser": "^3.0.0",
+    "npm_package_dependencies_class_transformer": "^0.2.0",
+    "npm_package_devDependencies_eslint": "^8.36.0",
+    "npm_package_devDependencies__types_prosemirror_inputrules": "^1.0.4",
+    "npm_package_devDependencies__types_express": "^4.17.13",
+    "__CF_USER_TEXT_ENCODING": "0x1F5:0x0:0x0",
+    "npm_execpath": "/Users/sarah/.nvm/versions/node/v12.16.0/lib/node_modules/npm/bin/npm-cli.js",
+    "npm_config_global_style": "",
+    "npm_config_cache_lock_retries": "10",
+    "npm_package_dependencies_wikijs": "^6.3.3",
+    "npm_package_dependencies_bluebird": "^3.7.2",
+    "npm_package_devDependencies__types_react_typist": "^2.0.3",
+    "npm_config_update_notifier": "true",
+    "npm_config_cafile": "",
+    "npm_package_dependencies_util": "^0.12.4",
+    "npm_package_dependencies_raw_loader": "^1.0.0",
+    "npm_package_dependencies_https_browserify": "^1.0.0",
+    "npm_package_dependencies_brotli": "^1.3.3",
+    "npm_package_dependencies__mui_material": "^5.13.1",
+    "npm_package_dependencies__fortawesome_react_fontawesome": "^0.2.0",
+    "npm_package_devDependencies__types_passport_google_oauth20": "^2.0.11",
+    "npm_package_dependencies_cors": "^2.8.5",
+    "npm_package_dependencies_bezier_js": "^4.1.1",
+    "npm_package_dependencies__fortawesome_free_brands_svg_icons": "^6.3.0",
+    "npm_config_heading": "npm",
+    "npm_config_audit_level": "low",
+    "npm_package_dependencies_chrome": "^0.1.0",
+    "npm_package_dependencies__react_three_fiber": "^6.2.3",
+    "npm_package_devDependencies_eslint_plugin_prettier": "^4.2.1",
+    "npm_package_devDependencies_copy_webpack_plugin": "^4.6.0",
+    "npm_package_devDependencies__types_react_measure": "^2.0.8",
+    "npm_package_devDependencies__types_react_dom": "^18.0.6",
+    "npm_package_devDependencies__types_mobile_detect": "^1.3.4",
+    "_CE_CONDA": "",
+    "npm_config_searchlimit": "20",
+    "npm_config_read_only": "",
+    "npm_config_offline": "",
+    "npm_config_fetch_retry_mintimeout": "10000",
+    "npm_package_dependencies_react_typist": "^2.0.5",
+    "npm_package_dependencies_mobx_react_devtools": "^6.1.1",
+    "npm_package_dependencies_md5_file": "^5.0.0",
+    "npm_package_dependencies_forever_agent": "^0.6.1",
+    "npm_package_devDependencies__types_xregexp": "^4.4.0",
+    "npm_package_devDependencies__types_typescript": "^2.0.0",
+    "npm_package_devDependencies__types_request": "^2.48.8",
+    "npm_package_devDependencies__types_prosemirror_commands": "^1.0.4",
+    "npm_config_json": "",
+    "npm_config_access": "",
+    "npm_config_argv": "{\"remain\":[],\"cooked\":[\"start\"],\"original\":[\"start\"]}",
+    "npm_package_dependencies__fortawesome_free_solid_svg_icons": "^6.3.0",
+    "npm_package_devDependencies__types_socket_io": "^2.1.13",
+    "PATH": "/Users/sarah/.nvm/versions/node/v12.16.0/lib/node_modules/npm/node_modules/npm-lifecycle/node-gyp-bin:/Users/sarah/Desktop/dash/Dash-Web/node_modules/.bin:/Users/sarah/.nvm/versions/node/v12.16.0/bin:/Users/sarah/miniconda3/bin:/Users/sarah/miniconda3/condabin:/Users/sarah/.elan/bin:/Library/Frameworks/Python.framework/Versions/3.9/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin",
+    "npm_config_allow_same_version": "",
+    "npm_package_dependencies_webrtc_adapter": "^7.7.1",
+    "npm_package_dependencies_react_reveal": "^1.2.2",
+    "npm_package_dependencies_prosemirror_schema_list": "^1.1.6",
+    "npm_package_dependencies__material_ui_core": "^4.12.3",
+    "npm_package_devDependencies__types_rimraf": "^2.0.5",
+    "npm_package_devDependencies__types_connect_flash": "0.0.34",
+    "npm_config_https_proxy": "",
+    "npm_config_engine_strict": "",
+    "npm_config_description": "true",
+    "npm_package_dependencies_pug": "^2.0.4",
+    "npm_package_dependencies_prosemirror_keymap": "^1.1.5",
+    "npm_package_dependencies_pdfjs_dist": "^2.14.305",
+    "npm_package_dependencies_mobile_detect": "^1.4.5",
+    "npm_package_dependencies_image_size_stream": "^1.1.0",
+    "npm_package_dependencies_golden_layout": "^1.5.9",
+    "npm_package_dependencies_child_process": "^1.0.2",
+    "npm_package_dependencies__types_d3_axis": "^2.1.3",
+    "_": "/Users/sarah/Desktop/dash/Dash-Web/node_modules/.bin/cross-env",
+    "LaunchInstanceID": "16109DB5-CDB2-454A-9613-8F6F534702AC",
+    "npm_config_userconfig": "/Users/sarah/.npmrc",
+    "npm_config_init_module": "/Users/sarah/.npm-init.js",
+    "npm_package_dependencies__react_google_maps_api": "^2.7.0",
+    "CONDA_PREFIX": "/Users/sarah/miniconda3",
+    "__CFBundleIdentifier": "com.apple.Terminal",
+    "npm_config_cidr": "",
+    "npm_package_dependencies_puppeteer": "^3.3.0",
+    "npm_package_dependencies_prosemirror_view": "^1.26.5",
+    "npm_package_dependencies_mongodb": "^3.7.3",
+    "npm_package_dependencies_google_auth_library": "^4.2.4",
+    "npm_package_dependencies_bootstrap": "^4.6.1",
+    "npm_package_devDependencies_eslint_config_airbnb": "^19.0.4",
+    "PWD": "/Users/sarah/desktop/dash/dash-web",
+    "npm_config_user": "501",
+    "npm_config_node_version": "12.16.0",
+    "npm_package_dependencies_node_sass": "^4.14.1",
+    "npm_package_dependencies_howler": "^2.2.3",
+    "npm_package_dependencies_expressjs": "^1.0.1",
+    "npm_package_dependencies_core_js": "^3.28.0",
+    "npm_package_dependencies_browndash_components": "^0.1.36",
+    "npm_package_devDependencies_eslint_plugin_react_hooks": "^4.6.0",
+    "npm_package_devDependencies__types_lodash": "^4.14.179",
+    "JAVA_HOME": "/Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home",
+    "npm_lifecycle_event": "start",
+    "npm_package_dependencies_react_table": "^6.11.5",
+    "npm_package_dependencies_react_loading": "^2.0.3",
+    "npm_package_dependencies_mobx": "^5.15.7",
+    "npm_package_dependencies_babel": "^6.23.0",
+    "npm_package_devDependencies_jsdom": "^15.2.1",
+    "npm_package_devDependencies_chai": "^4.3.6",
+    "npm_config_save": "true",
+    "npm_config_ignore_prepublish": "",
+    "npm_config_editor": "vi",
+    "npm_config_auth_type": "legacy",
+    "npm_package_dependencies_npm": "^6.14.18",
+    "npm_package_dependencies_node_stream_zip": "^1.15.0",
+    "npm_package_dependencies_image_data_uri": "^2.0.1",
+    "npm_package_scripts_start_release": "cross-env RELEASE=true USE_AZURE=false NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev -- src/server/index.ts",
+    "npm_package_name": "dash",
+    "LANG": "en_US.UTF-8",
+    "npm_config_tag": "latest",
+    "npm_config_script_shell": "",
+    "npm_package_dependencies_query_string": "^6.14.1",
+    "npm_package_dependencies_mobx_utils": "^5.6.2",
+    "npm_package_dependencies_file_saver": "^2.0.5",
+    "npm_package_dependencies_body_parser": "^1.19.2",
+    "npm_package_dependencies__types_reveal": "^3.3.33",
+    "npm_package_devDependencies_eslint_plugin_import": "^2.26.0",
+    "npm_package_devDependencies__types_prosemirror_view": "^1.23.1",
+    "npm_config_progress": "true",
+    "npm_config_global": "",
+    "npm_config_before": "",
+    "npm_package_dependencies_xoauth2": "^1.2.0",
+    "npm_package_dependencies_standard_http_error": "^2.0.1",
+    "npm_package_dependencies_react_loader_spinner": "^5.3.4",
+    "npm_package_dependencies_http_browserify": "^1.7.0",
+    "npm_package_dependencies__types_d3_selection": "^2.0.1",
+    "npm_package_dependencies__hig_flyout": "^1.3.1",
+    "npm_package_devDependencies_fork_ts_checker_webpack_plugin": "^1.6.0",
+    "npm_package_scripts_build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 webpack --env production",
+    "npm_package_scripts_start": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --debug --transpile-only -- src/server/index.ts",
+    "npm_config_searchstaleness": "900",
+    "npm_config_optional": "true",
+    "npm_config_ham_it_up": "",
+    "npm_package_dependencies_sharp": "^0.23.4",
+    "npm_package_dependencies_rc_switch": "^1.9.2",
+    "npm_package_dependencies_googlephotos": "^0.2.5",
+    "npm_package_dependencies_exifr": "^7.1.3",
+    "npm_package_dependencies__types_google_maps": "^3.2.3",
+    "npm_package_dependencies__types_bezier_js": "^4.1.0",
+    "npm_package_dependencies__ffmpeg_core": "0.10.0",
+    "npm_package_devDependencies_ts_loader": "^5.3.3",
+    "npm_package_devDependencies__types_bcrypt_nodejs": "0.0.30",
+    "XPC_FLAGS": "0x0",
+    "npm_config_save_prod": "",
+    "npm_config_force": "",
+    "npm_config_bin_links": "true",
+    "npm_package_devDependencies__types_youtube": "0.0.39",
+    "npm_config_searchopts": "",
+    "npm_package_dependencies_react_beautiful_dnd": "^13.1.0",
+    "npm_package_dependencies_jszip": "^3.7.1",
+    "npm_package_dependencies_csv_stringify": "^6.3.0",
+    "npm_package_devDependencies__types_react_icons": "^3.0.0",
+    "npm_config_node_gyp": "/Users/sarah/.nvm/versions/node/v12.16.0/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js",
+    "npm_config_depth": "Infinity",
+    "npm_package_dependencies_google_maps_react": "^2.0.6",
+    "npm_package_dependencies_express_session": "^1.17.2",
+    "npm_package_devDependencies_eslint_plugin_node": "^11.1.0",
+    "npm_package_devDependencies_eslint_config_prettier": "^8.5.0",
+    "npm_package_main": "index.js",
+    "npm_config_sso_poll_frequency": "500",
+    "npm_config_rebuild_bundle": "true",
+    "npm_package_dependencies_chart_js": "^3.8.0",
+    "npm_package_dependencies__emotion_react": "^11.11.0",
+    "npm_package_devDependencies__types_prosemirror_menu": "^1.0.6",
+    "npm_package_devDependencies__types_prosemirror_keymap": "^1.0.4",
+    "npm_package_devDependencies__types_pdfjs_dist": "^2.10.378",
+    "npm_package_devDependencies__types_exif": "^0.6.3",
+    "npm_package_version": "1.0.0",
+    "_CE_M": "",
+    "XPC_SERVICE_NAME": "0",
+    "npm_config_unicode": "true",
+    "npm_package_dependencies_typescript_language_server": "^0.4.0",
+    "npm_package_dependencies_prosemirror_model": "^1.18.1",
+    "npm_package_dependencies__ffmpeg_ffmpeg": "0.10.0",
+    "SHLVL": "2",
+    "HOME": "/Users/sarah",
+    "npm_config_fetch_retry_maxtimeout": "60000",
+    "npm_package_dependencies_request_promise": "^4.2.6",
+    "npm_package_dependencies_react_markdown": "^8.0.3",
+    "npm_package_dependencies__hig_theme_context": "^2.1.3",
+    "npm_package_devDependencies__types_react_autosuggest": "^9.3.14",
+    "npm_package_devDependencies__types_mongoose": "^5.11.97",
+    "npm_package_devDependencies__types_d3": "^7.4.0",
+    "npm_package_devDependencies__types_animejs": "^2.0.2",
+    "npm_package_scripts_test": "mocha -r ts-node/register test/**/*.ts",
+    "npm_config_tag_version_prefix": "v",
+    "npm_config_strict_ssl": "true",
+    "npm_config_sso_type": "oauth",
+    "npm_config_scripts_prepend_node_path": "warn-only",
+    "npm_config_save_prefix": "^",
+    "npm_config_loglevel": "notice",
+    "npm_config_ca": "",
+    "npm_package_dependencies_three": "^0.127.0",
+    "npm_package_dependencies_openai": "^3.2.1",
+    "npm_package_dependencies_mobx_react": "^5.4.4",
+    "npm_package_dependencies_google_translate_api_browser": "^3.0.1",
+    "npm_package_dependencies_cookie_parser": "^1.4.6",
+    "npm_package_dependencies_adm_zip": "^0.4.16",
+    "npm_package_devDependencies_eslint_config_node": "^4.1.0",
+    "npm_config_save_exact": "",
+    "npm_config_group": "20",
+    "npm_config_fetch_retry_factor": "10",
+    "npm_config_dev": "",
+    "npm_package_devDependencies_webpack_hot_middleware": "^2.25.1",
+    "npm_package_devDependencies_cross_env": "^5.2.1",
+    "npm_config_version": "",
+    "npm_config_prefer_offline": "",
+    "npm_config_cache_lock_stale": "60000",
+    "npm_package_devDependencies__types_prosemirror_state": "^1.2.8",
+    "npm_package_devDependencies__types_body_parser": "^1.19.2",
+    "npm_config_otp": "",
+    "npm_config_cache_min": "10",
+    "npm_package_dependencies_react_color": "^2.19.3",
+    "npm_package_dependencies_d3": "^7.6.1",
+    "npm_package_devDependencies_ts_node": "^10.9.1",
+    "npm_package_devDependencies__types_react_grid_layout": "^1.3.2",
+    "npm_config_searchexclude": "",
+    "npm_config_cache": "/Users/sarah/.npm",
+    "npm_package_dependencies_tough_cookie": "^4.0.0",
+    "npm_package_dependencies_googleapis": "^40.0.0",
+    "npm_package_devDependencies__types_valid_url": "^1.0.3",
+    "npm_package_devDependencies__types_passport": "^1.0.9",
+    "npm_package_devDependencies__types_adm_zip": "^0.4.34",
+    "CONDA_PYTHON_EXE": "/Users/sarah/miniconda3/bin/python",
+    "LOGNAME": "sarah",
+    "npm_lifecycle_script": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --debug --transpile-only -- src/server/index.ts",
+    "npm_config_color": "true",
+    "npm_package_dependencies_solr_node": "^1.2.1",
+    "npm_package_dependencies_react_transition_group": "^4.4.2",
+    "npm_package_dependencies_iink_js": "^1.5.4",
+    "npm_package_dependencies_html_webpack_plugin": "^5.5.0",
+    "npm_config_proxy": "",
+    "npm_config_package_lock": "true",
+    "npm_package_dependencies_prosemirror_state": "^1.4.1",
+    "npm_package_dependencies_nodemon": "^1.19.4",
+    "npm_package_dependencies_function_plot": "^1.22.8",
+    "npm_package_dependencies_equation_editor_react": "github:bobzel/equation-editor-react#useLocally",
+    "npm_package_devDependencies__types_socket_io_parser": "^3.0.0",
+    "CLASSPATH": "/Users/sarah/Downloads/cs15/*:.",
+    "npm_config_package_lock_only": "",
+    "npm_config_fund": "true",
+    "npm_package_dependencies_react": "^18.2.0",
+    "npm_package_dependencies_bingmaps_react": "^1.2.10",
+    "npm_package_devDependencies_scss_loader": "0.0.1",
+    "npm_package_devDependencies__types_cookie_session": "^2.0.44",
+    "npm_config_save_optional": "",
+    "npm_package_dependencies_textarea_caret": "^3.1.0",
+    "npm_package_dependencies_react_measure": "^2.5.2",
+    "npm_package_dependencies_exif": "^0.6.0",
+    "NVM_BIN": "/Users/sarah/.nvm/versions/node/v12.16.0/bin",
+    "CONDA_DEFAULT_ENV": "base",
+    "npm_config_ignore_scripts": "",
+    "npm_config_user_agent": "npm/6.14.7 node/v12.16.0 darwin x64",
+    "npm_package_dependencies_react_resizable": "^1.11.1",
+    "npm_package_dependencies_prosemirror_commands": "^1.2.1",
+    "npm_package_dependencies_memorystream": "^0.3.1",
+    "npm_package_dependencies_formidable": "1.2.1",
+    "npm_package_devDependencies__types_uuid": "^3.4.10",
+    "npm_config_cache_lock_wait": "10000",
+    "npm_package_dependencies_socket_io_client": "^2.5.0",
+    "npm_package_dependencies_recharts": "^2.1.12",
+    "npm_package_dependencies_react_chartjs_2": "^4.3.0",
+    "npm_package_dependencies_fluent_ffmpeg": "^2.1.2",
+    "npm_package_dependencies__types_cors": "^2.8.12",
+    "npm_package_devDependencies__types_node": "^10.17.60",
+    "npm_package_devDependencies__types_file_saver": "^2.0.5",
+    "npm_config_production": "",
+    "npm_package_dependencies_jsonschema": "^1.4.0",
+    "npm_package_dependencies_ffmpeg": "0.0.4",
+    "npm_package_dependencies_cookie_session": "^2.0.0",
+    "npm_package_dependencies_color": "^3.2.1",
+    "npm_package_devDependencies__types_webpack": "^4.41.32",
+    "npm_package_devDependencies__types_request_promise": "^4.1.48",
+    "npm_package_devDependencies__types_prosemirror_schema_list": "^1.0.3",
+    "npm_config_send_metrics": "",
+    "npm_config_save_bundle": "",
+    "npm_package_dependencies_web_request": "^1.0.7",
+    "npm_package_dependencies_react_datepicker": "^3.8.0",
+    "npm_package_dependencies_express": "^4.17.3",
+    "npm_package_dependencies_D": "^1.0.0",
+    "npm_package_dependencies__types_formidable": "1.0.31",
+    "npm_package_devDependencies__types_rc_switch": "^1.9.2",
+    "npm_package_devDependencies__types_prosemirror_dev_tools": "^2.1.0",
+    "npm_package_devDependencies__types_jquery": "^3.5.14",
+    "npm_config_umask": "0022",
+    "npm_config_node_options": "",
+    "npm_config_init_version": "1.0.0",
+    "npm_package_dependencies_https": "^1.0.0",
+    "npm_package_dependencies_array_batcher": "^1.2.3",
+    "npm_package_dependencies__fortawesome_free_regular_svg_icons": "^6.3.0",
+    "npm_package_devDependencies__types_shelljs": "^0.8.11",
+    "npm_package_devDependencies__types_libxmljs": "^0.18.7",
+    "npm_package_devDependencies__types_express_validator": "^3.0.0",
+    "npm_package_devDependencies__types_bluebird": "^3.5.36",
+    "npm_config_init_author_name": "",
+    "npm_config_git": "git",
+    "npm_config_scope": "",
+    "npm_package_dependencies_react_select": "^3.2.0",
+    "npm_package_dependencies_pdf_parse": "^1.1.1",
+    "npm_package_dependencies_colors": "^1.4.0",
+    "npm_package_dependencies_archiver": "^3.1.1",
+    "npm_package_devDependencies_css_loader": "^2.1.1",
+    "npm_package_devDependencies__types_socket_io_client": "^1.4.36",
+    "SECURITYSESSIONID": "186a6",
+    "npm_config_unsafe_perm": "true",
+    "npm_config_tmp": "/var/folders/yk/p_39q8jn673c5p8_66mcxm7r0000gn/T",
+    "npm_config_onload_script": "",
+    "npm_package_dependencies_serializr": "^1.5.4",
+    "npm_package_dependencies_fit_curve": "^0.1.7",
+    "npm_package_dependencies__webscopeio_react_textarea_autocomplete": "^4.9.1",
+    "npm_package_dependencies__types_three": "^0.126.2",
+    "npm_package_devDependencies_ts_node_dev": "^2.0.0",
+    "npm_node_execpath": "/Users/sarah/.nvm/versions/node/v12.16.0/bin/node",
+    "npm_config_prefix": "/Users/sarah/.nvm/versions/node/v12.16.0",
+    "npm_config_link": "",
+    "npm_config_format_package_lock": "true",
+    "npm_package_dependencies_passport": "^0.4.0",
+    "npm_package_devDependencies_eslint_plugin_react": "^7.30.1",
+    "npm_package_devDependencies__types_react_table": "^6.8.9",
+    "npm_package_devDependencies__types_react_reconciler": "^0.26.4",
+    "NODE_OPTIONS": "--max_old_space_size=4096",
+    "TS_NODE_DEV": "true",
+    "_CLIENT_OPENAI_KEY": "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",
+    "VIPSHOME": "/usr/local/Cellar/vips/8.8.1",
+    "TYPESCRIPT_PATH": "/Users/sarah/Desktop/dash/Dash-Web/node_modules/typescript/lib/typescript.js",
+    "TSCONFIG": "/Users/sarah/Desktop/dash/Dash-Web/tsconfig.json",
+    "COMPILER_OPTIONS": "{}",
+    "TSLINT": "true",
+    "CONTEXT": "/Users/sarah/Desktop/dash/Dash-Web",
+    "TSLINTAUTOFIX": "false",
+    "ESLINT": "false",
+    "ESLINT_OPTIONS": "{}",
+    "WATCH": "",
+    "WORK_DIVISION": "1",
+    "MEMORY_LIMIT": "2048",
+    "CHECK_SYNTACTIC_ERRORS": "false",
+    "USE_INCREMENTAL_API": "true",
+    "VUE": "false"
+  },
+  "userLimits": {
+    "core_file_size_blocks": {
+      "soft": 0,
+      "hard": "unlimited"
+    },
+    "data_seg_size_kbytes": {
+      "soft": "unlimited",
+      "hard": "unlimited"
+    },
+    "file_size_blocks": {
+      "soft": "unlimited",
+      "hard": "unlimited"
+    },
+    "max_locked_memory_bytes": {
+      "soft": "unlimited",
+      "hard": "unlimited"
+    },
+    "max_memory_size_kbytes": {
+      "soft": "unlimited",
+      "hard": "unlimited"
+    },
+    "open_files": {
+      "soft": 1048575,
+      "hard": "unlimited"
+    },
+    "stack_size_bytes": {
+      "soft": 8388608,
+      "hard": 67104768
+    },
+    "cpu_time_seconds": {
+      "soft": "unlimited",
+      "hard": "unlimited"
+    },
+    "max_user_processes": {
+      "soft": 2784,
+      "hard": 4176
+    },
+    "virtual_memory_kbytes": {
+      "soft": "unlimited",
+      "hard": "unlimited"
+    }
+  },
+  "sharedObjects": [
+    "/Users/sarah/.nvm/versions/node/v12.16.0/bin/node",
+    "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation",
+    "/usr/lib/libobjc.A.dylib",
+    "/System/Library/PrivateFrameworks/CoreServicesInternal.framework/Versions/A/CoreServicesInternal",
+    "/usr/lib/liboah.dylib",
+    "/usr/lib/libfakelink.dylib",
+    "/usr/lib/libicucore.A.dylib",
+    "/usr/lib/libSystem.B.dylib",
+    "/System/Library/PrivateFrameworks/SoftLinking.framework/Versions/A/SoftLinking",
+    "/usr/lib/libc++abi.dylib",
+    "/usr/lib/libc++.1.dylib",
+    "/usr/lib/system/libcache.dylib",
+    "/usr/lib/system/libcommonCrypto.dylib",
+    "/usr/lib/system/libcompiler_rt.dylib",
+    "/usr/lib/system/libcopyfile.dylib",
+    "/usr/lib/system/libcorecrypto.dylib",
+    "/usr/lib/system/libdispatch.dylib",
+    "/usr/lib/system/libdyld.dylib",
+    "/usr/lib/system/libkeymgr.dylib",
+    "/usr/lib/system/libmacho.dylib",
+    "/usr/lib/system/libquarantine.dylib",
+    "/usr/lib/system/libremovefile.dylib",
+    "/usr/lib/system/libsystem_asl.dylib",
+    "/usr/lib/system/libsystem_blocks.dylib",
+    "/usr/lib/system/libsystem_c.dylib",
+    "/usr/lib/system/libsystem_collections.dylib",
+    "/usr/lib/system/libsystem_configuration.dylib",
+    "/usr/lib/system/libsystem_containermanager.dylib",
+    "/usr/lib/system/libsystem_coreservices.dylib",
+    "/usr/lib/system/libsystem_darwin.dylib",
+    "/usr/lib/system/libsystem_dnssd.dylib",
+    "/usr/lib/system/libsystem_featureflags.dylib",
+    "/usr/lib/system/libsystem_info.dylib",
+    "/usr/lib/system/libsystem_m.dylib",
+    "/usr/lib/system/libsystem_malloc.dylib",
+    "/usr/lib/system/libsystem_networkextension.dylib",
+    "/usr/lib/system/libsystem_notify.dylib",
+    "/usr/lib/system/libsystem_sandbox.dylib",
+    "/usr/lib/system/libsystem_secinit.dylib",
+    "/usr/lib/system/libsystem_kernel.dylib",
+    "/usr/lib/system/libsystem_platform.dylib",
+    "/usr/lib/system/libsystem_pthread.dylib",
+    "/usr/lib/system/libsystem_symptoms.dylib",
+    "/usr/lib/system/libsystem_trace.dylib",
+    "/usr/lib/system/libunwind.dylib",
+    "/usr/lib/system/libxpc.dylib",
+    "/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit",
+    "/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices",
+    "/usr/lib/libDiagnosticMessagesClient.dylib",
+    "/usr/lib/libenergytrace.dylib",
+    "/usr/lib/libbsm.0.dylib",
+    "/usr/lib/libz.1.dylib",
+    "/usr/lib/system/libkxld.dylib",
+    "/System/Library/Frameworks/CFNetwork.framework/Versions/A/CFNetwork",
+    "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/FSEvents",
+    "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/CarbonCore",
+    "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/Metadata.framework/Versions/A/Metadata",
+    "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/OSServices.framework/Versions/A/OSServices",
+    "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SearchKit.framework/Versions/A/SearchKit",
+    "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/AE.framework/Versions/A/AE",
+    "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/LaunchServices",
+    "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/DictionaryServices.framework/Versions/A/DictionaryServices",
+    "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SharedFileList.framework/Versions/A/SharedFileList",
+    "/System/Library/Frameworks/Security.framework/Versions/A/Security",
+    "/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration",
+    "/usr/lib/libapple_nghttp2.dylib",
+    "/usr/lib/libcompression.dylib",
+    "/usr/lib/libnetwork.dylib",
+    "/usr/lib/libsqlite3.dylib",
+    "/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation",
+    "/System/Library/Frameworks/Network.framework/Versions/A/Network",
+    "/usr/lib/libCoreEntitlements.dylib",
+    "/System/Library/PrivateFrameworks/MessageSecurity.framework/Versions/A/MessageSecurity",
+    "/System/Library/PrivateFrameworks/ProtocolBuffer.framework/Versions/A/ProtocolBuffer",
+    "/usr/lib/libMobileGestalt.dylib",
+    "/System/Library/PrivateFrameworks/AppleFSCompression.framework/Versions/A/AppleFSCompression",
+    "/usr/lib/libcoretls.dylib",
+    "/usr/lib/libcoretls_cfhelpers.dylib",
+    "/usr/lib/libpam.2.dylib",
+    "/usr/lib/libxar.1.dylib",
+    "/System/Library/PrivateFrameworks/CoreAutoLayout.framework/Versions/A/CoreAutoLayout",
+    "/System/Library/Frameworks/DiskArbitration.framework/Versions/A/DiskArbitration",
+    "/usr/lib/libarchive.2.dylib",
+    "/usr/lib/libxml2.2.dylib",
+    "/usr/lib/liblangid.dylib",
+    "/System/Library/Frameworks/Combine.framework/Versions/A/Combine",
+    "/usr/lib/swift/libswiftCore.dylib",
+    "/usr/lib/swift/libswiftCoreFoundation.dylib",
+    "/usr/lib/swift/libswiftDarwin.dylib",
+    "/usr/lib/swift/libswiftDispatch.dylib",
+    "/usr/lib/swift/libswiftIOKit.dylib",
+    "/usr/lib/swift/libswiftObjectiveC.dylib",
+    "/usr/lib/swift/libswiftXPC.dylib",
+    "/usr/lib/swift/libswift_Concurrency.dylib",
+    "/usr/lib/swift/libswift_StringProcessing.dylib",
+    "/usr/lib/swift/libswiftos.dylib",
+    "/System/Library/PrivateFrameworks/AppleSystemInfo.framework/Versions/A/AppleSystemInfo",
+    "/System/Library/PrivateFrameworks/IOMobileFramebuffer.framework/Versions/A/IOMobileFramebuffer",
+    "/System/Library/Frameworks/IOSurface.framework/Versions/A/IOSurface",
+    "/usr/lib/libpcap.A.dylib",
+    "/usr/lib/libdns_services.dylib",
+    "/usr/lib/liblzma.5.dylib",
+    "/usr/lib/libbz2.1.0.dylib",
+    "/usr/lib/libiconv.2.dylib",
+    "/usr/lib/libcharset.1.dylib",
+    "/usr/lib/swift/libswift_RegexParser.dylib",
+    "/usr/lib/libheimdal-asn1.dylib",
+    "/usr/lib/libCheckFix.dylib",
+    "/System/Library/PrivateFrameworks/TCC.framework/Versions/A/TCC",
+    "/System/Library/PrivateFrameworks/CoreNLP.framework/Versions/A/CoreNLP",
+    "/System/Library/PrivateFrameworks/MetadataUtilities.framework/Versions/A/MetadataUtilities",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate",
+    "/usr/lib/libmecab.dylib",
+    "/usr/lib/libCRFSuite.dylib",
+    "/usr/lib/libgermantok.dylib",
+    "/usr/lib/libThaiTokenizer.dylib",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vImage.framework/Versions/A/vImage",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/vecLib",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvMisc.dylib",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvDSP.dylib",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libLAPACK.dylib",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libLinearAlgebra.dylib",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libSparseBLAS.dylib",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libQuadrature.dylib",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBNNS.dylib",
+    "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libSparse.dylib",
+    "/System/Library/PrivateFrameworks/MIL.framework/Versions/A/MIL",
+    "/System/Library/Frameworks/OpenDirectory.framework/Versions/A/Frameworks/CFOpenDirectory.framework/Versions/A/CFOpenDirectory",
+    "/System/Library/Frameworks/OpenDirectory.framework/Versions/A/OpenDirectory",
+    "/System/Library/PrivateFrameworks/APFS.framework/Versions/A/APFS",
+    "/System/Library/Frameworks/SecurityFoundation.framework/Versions/A/SecurityFoundation",
+    "/usr/lib/libutil.dylib",
+    "/System/Library/PrivateFrameworks/InstalledContentLibrary.framework/Versions/A/InstalledContentLibrary",
+    "/System/Library/PrivateFrameworks/CoreServicesStore.framework/Versions/A/CoreServicesStore",
+    "/usr/lib/libapp_launch_measurement.dylib",
+    "/System/Library/PrivateFrameworks/AppleMobileFileIntegrity.framework/Versions/A/AppleMobileFileIntegrity",
+    "/usr/lib/libmis.dylib",
+    "/System/Library/PrivateFrameworks/MobileSystemServices.framework/Versions/A/MobileSystemServices",
+    "/System/Library/PrivateFrameworks/ConfigProfileHelper.framework/Versions/A/ConfigProfileHelper",
+    "/System/Library/PrivateFrameworks/CoreAnalytics.framework/Versions/A/CoreAnalytics",
+    "/System/Library/PrivateFrameworks/AppleSauce.framework/Versions/A/AppleSauce",
+    "/System/Library/PrivateFrameworks/LanguageModeling.framework/Versions/A/LanguageModeling",
+    "/usr/lib/libxslt.1.dylib",
+    "/usr/lib/libcmph.dylib",
+    "/System/Library/PrivateFrameworks/CoreEmoji.framework/Versions/A/CoreEmoji",
+    "/System/Library/PrivateFrameworks/LinguisticData.framework/Versions/A/LinguisticData",
+    "/System/Library/PrivateFrameworks/Lexicon.framework/Versions/A/Lexicon",
+    "/System/Library/PrivateFrameworks/BackgroundTaskManagement.framework/Versions/A/BackgroundTaskManagement",
+    "/usr/lib/libTLE.dylib",
+    "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices",
+    "/System/Library/Frameworks/CoreGraphics.framework/Versions/A/CoreGraphics",
+    "/System/Library/Frameworks/CoreText.framework/Versions/A/CoreText",
+    "/System/Library/Frameworks/ImageIO.framework/Versions/A/ImageIO",
+    "/System/Library/Frameworks/ColorSync.framework/Versions/A/ColorSync",
+    "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/ATS",
+    "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/HIServices",
+    "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/PrintCore.framework/Versions/A/PrintCore",
+    "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/QD.framework/Versions/A/QD",
+    "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ColorSyncLegacy.framework/Versions/A/ColorSyncLegacy",
+    "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/SpeechSynthesis.framework/Versions/A/SpeechSynthesis",
+    "/System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/SkyLight",
+    "/System/Library/PrivateFrameworks/FontServices.framework/libFontParser.dylib",
+    "/System/Library/PrivateFrameworks/RunningBoardServices.framework/Versions/A/RunningBoardServices",
+    "/System/Library/PrivateFrameworks/IOSurfaceAccelerator.framework/Versions/A/IOSurfaceAccelerator",
+    "/System/Library/PrivateFrameworks/WatchdogClient.framework/Versions/A/WatchdogClient",
+    "/System/Library/Frameworks/CoreDisplay.framework/Versions/A/CoreDisplay",
+    "/System/Library/Frameworks/CoreMedia.framework/Versions/A/CoreMedia",
+    "/System/Library/PrivateFrameworks/IOAccelerator.framework/Versions/A/IOAccelerator",
+    "/System/Library/Frameworks/Metal.framework/Versions/A/Metal",
+    "/System/Library/Frameworks/CoreVideo.framework/Versions/A/CoreVideo",
+    "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/MetalPerformanceShaders",
+    "/System/Library/PrivateFrameworks/MultitouchSupport.framework/Versions/A/MultitouchSupport",
+    "/System/Library/Frameworks/QuartzCore.framework/Versions/A/QuartzCore",
+    "/System/Library/Frameworks/VideoToolbox.framework/Versions/A/VideoToolbox",
+    "/System/Library/PrivateFrameworks/BaseBoard.framework/Versions/A/BaseBoard",
+    "/System/Library/PrivateFrameworks/AppleJPEG.framework/Versions/A/AppleJPEG",
+    "/usr/lib/libexpat.1.dylib",
+    "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libPng.dylib",
+    "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libTIFF.dylib",
+    "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libGIF.dylib",
+    "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJP2.dylib",
+    "/usr/lib/libate.dylib",
+    "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJPEG.dylib",
+    "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libRadiance.dylib",
+    "/System/Library/PrivateFrameworks/GPUWrangler.framework/Versions/A/GPUWrangler",
+    "/System/Library/PrivateFrameworks/IOPresentment.framework/Versions/A/IOPresentment",
+    "/System/Library/PrivateFrameworks/DSExternalDisplay.framework/Versions/A/DSExternalDisplay",
+    "/System/Library/PrivateFrameworks/GPUCompiler.framework/Versions/31001/Libraries/libllvm-flatbuffers.dylib",
+    "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCoreFSCache.dylib",
+    "/System/Library/PrivateFrameworks/GPUCompiler.framework/Versions/31001/Libraries/libGPUCompilerUtils.dylib",
+    "/System/Library/PrivateFrameworks/CMCaptureCore.framework/Versions/A/CMCaptureCore",
+    "/usr/lib/libspindump.dylib",
+    "/System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio",
+    "/System/Library/Frameworks/ExtensionFoundation.framework/Versions/A/ExtensionFoundation",
+    "/System/Library/PrivateFrameworks/CoreTime.framework/Versions/A/CoreTime",
+    "/System/Library/PrivateFrameworks/AppServerSupport.framework/Versions/A/AppServerSupport",
+    "/System/Library/PrivateFrameworks/perfdata.framework/Versions/A/perfdata",
+    "/System/Library/PrivateFrameworks/AudioToolboxCore.framework/Versions/A/AudioToolboxCore",
+    "/System/Library/PrivateFrameworks/caulk.framework/Versions/A/caulk",
+    "/usr/lib/libAudioStatistics.dylib",
+    "/System/Library/PrivateFrameworks/SystemPolicy.framework/Versions/A/SystemPolicy",
+    "/usr/lib/libSMC.dylib",
+    "/System/Library/Frameworks/CoreMIDI.framework/Versions/A/CoreMIDI",
+    "/usr/lib/libAudioToolboxUtility.dylib",
+    "/System/Library/PrivateFrameworks/OSAServicesClient.framework/Versions/A/OSAServicesClient",
+    "/usr/lib/libperfcheck.dylib",
+    "/System/Library/PrivateFrameworks/PlugInKit.framework/Versions/A/PlugInKit",
+    "/System/Library/PrivateFrameworks/AssertionServices.framework/Versions/A/AssertionServices",
+    "/System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL",
+    "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGLU.dylib",
+    "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGFXShared.dylib",
+    "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib",
+    "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGLImage.dylib",
+    "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCVMSPluginSupport.dylib",
+    "/usr/lib/libRosetta.dylib",
+    "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCoreVMClient.dylib",
+    "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSCore.framework/Versions/A/MPSCore",
+    "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSImage.framework/Versions/A/MPSImage",
+    "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSNeuralNetwork.framework/Versions/A/MPSNeuralNetwork",
+    "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSMatrix.framework/Versions/A/MPSMatrix",
+    "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSRayIntersector.framework/Versions/A/MPSRayIntersector",
+    "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSNDArray.framework/Versions/A/MPSNDArray",
+    "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSFunctions.framework/Versions/A/MPSFunctions",
+    "/System/Library/PrivateFrameworks/MetalTools.framework/Versions/A/MetalTools",
+    "/System/Library/PrivateFrameworks/AggregateDictionary.framework/Versions/A/AggregateDictionary",
+    "/usr/lib/libIOReport.dylib",
+    "/System/Library/Frameworks/CoreImage.framework/Versions/A/CoreImage",
+    "/System/Library/PrivateFrameworks/PhotosensitivityProcessing.framework/Versions/A/PhotosensitivityProcessing",
+    "/System/Library/Frameworks/OpenCL.framework/Versions/A/OpenCL",
+    "/System/Library/PrivateFrameworks/GraphVisualizer.framework/Versions/A/GraphVisualizer",
+    "/System/Library/PrivateFrameworks/FontServices.framework/Versions/A/FontServices",
+    "/System/Library/Frameworks/UniformTypeIdentifiers.framework/Versions/A/UniformTypeIdentifiers",
+    "/System/Library/PrivateFrameworks/OTSVG.framework/Versions/A/OTSVG",
+    "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/Resources/libFontRegistry.dylib",
+    "/System/Library/PrivateFrameworks/FontServices.framework/libhvf.dylib",
+    "/System/Library/PrivateFrameworks/FontServices.framework/libXTFontStaticRegistryData.dylib",
+    "/usr/lib/swift/libswiftMetal.dylib",
+    "/usr/lib/swift/libswiftsimd.dylib",
+    "/System/Library/PrivateFrameworks/VideoToolboxParavirtualizationSupport.framework/Versions/A/VideoToolboxParavirtualizationSupport",
+    "/System/Library/PrivateFrameworks/AppleVA.framework/Versions/A/AppleVA",
+    "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATSUI.framework/Versions/A/ATSUI",
+    "/usr/lib/libcups.2.dylib",
+    "/System/Library/Frameworks/Kerberos.framework/Versions/A/Kerberos",
+    "/System/Library/Frameworks/GSS.framework/Versions/A/GSS",
+    "/usr/lib/libresolv.9.dylib",
+    "/System/Library/PrivateFrameworks/Heimdal.framework/Versions/A/Heimdal",
+    "/System/Library/Frameworks/Kerberos.framework/Versions/A/Libraries/libHeimdalProxy.dylib",
+    "/System/Library/PrivateFrameworks/CommonAuth.framework/Versions/A/CommonAuth",
+    "/System/Library/Frameworks/AVFAudio.framework/Versions/A/AVFAudio",
+    "/System/Library/PrivateFrameworks/AXCoreUtilities.framework/Versions/A/AXCoreUtilities",
+    "/System/Library/Frameworks/AudioToolbox.framework/Versions/A/AudioToolbox",
+    "/System/Library/PrivateFrameworks/AudioSession.framework/Versions/A/AudioSession",
+    "/System/Library/Frameworks/IOBluetooth.framework/Versions/A/IOBluetooth",
+    "/System/Library/PrivateFrameworks/MediaExperience.framework/Versions/A/MediaExperience",
+    "/System/Library/PrivateFrameworks/AudioSession.framework/libSessionUtility.dylib",
+    "/System/Library/PrivateFrameworks/AudioResourceArbitration.framework/Versions/A/AudioResourceArbitration",
+    "/System/Library/PrivateFrameworks/PowerLog.framework/Versions/A/PowerLog",
+    "/System/Library/Frameworks/CoreData.framework/Versions/A/CoreData",
+    "/System/Library/Frameworks/CoreBluetooth.framework/Versions/A/CoreBluetooth",
+    "/System/Library/Frameworks/AudioUnit.framework/Versions/A/AudioUnit",
+    "/System/Library/PrivateFrameworks/CoreUtils.framework/Versions/A/CoreUtils",
+    "/System/Library/PrivateFrameworks/CoreUtilsExtras.framework/Versions/A/CoreUtilsExtras",
+    "/System/Library/PrivateFrameworks/IO80211.framework/Versions/A/IO80211",
+    "/System/Library/PrivateFrameworks/MobileKeyBag.framework/Versions/A/MobileKeyBag",
+    "/Users/sarah/Desktop/dash/Dash-Web/node_modules/fsevents/build/Release/fse.node"
+  ]
+}
\ No newline at end of file
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index caddc41a2..8e08265d6 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -13,6 +13,7 @@ enum GPTCallType {
     MERMAID = 'mermaid',
     DATA = 'data',
     STACK = 'stack',
+    PRONUNCIATION = 'pronunciation',
 }
 
 type GPTCallOpts = {
@@ -58,7 +59,13 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
         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 the Rubric is incorrect, explain why. If there are no differences, say correct. If it is empty, say there is nothing for me to evaluate.',
+        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 the Rubric is incorrect, explain why. If there are no differences, say correct. If it is empty, say there is nothing for me to evaluate. If it is comparing two words, look for spelling and not capitalization and not punctuation.',
+    },
+    pronunciation: {
+        model: 'gpt-4-turbo',
+        maxTokens: 1024,
+        temp: 0,
+        prompt: 'List unique differences between the pronunciation of the ',
     },
 };
 
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index e095bc659..c712dba21 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -262,6 +262,7 @@ export class CurrentUserUtils {
             }
             return plotly;
         }
+        
         const plotlyView = (opts:DocumentOptions) => {
             const rtfield = new RichTextField(JSON.stringify(
                 {doc: {type:"doc",content:[
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index bc9fe813f..b9a465515 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -350,7 +350,7 @@ export namespace DictationManager {
                         const head = 3;
                         const anchor = head + prompt.length;
                         const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"ordered_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`;
-                        proto.data = new RichTextField(proseMirrorState);
+                        proto.data = new RichTextField(proseMirrorState, prompt);
                         proto.backgroundColor = '#eeffff';
                         target.props.addDocTab(newBox, OpenWhere.addRight);
                     },
diff --git a/src/client/views/.MetadataEntryMenu.tsx.icloud b/src/client/views/.MetadataEntryMenu.tsx.icloud
new file mode 100644
index 000000000..ada9927c9
Binary files /dev/null and b/src/client/views/.MetadataEntryMenu.tsx.icloud differ
diff --git a/src/client/views/.Touchable.tsx.icloud b/src/client/views/.Touchable.tsx.icloud
new file mode 100644
index 000000000..e458bbe47
Binary files /dev/null and b/src/client/views/.Touchable.tsx.icloud differ
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 6976deea5..c45b1494b 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -2,7 +2,7 @@
 /* eslint-disable jsx-a11y/click-events-have-key-events */
 /* eslint-disable react/jsx-props-no-spreading */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, makeObservable, observable } from 'mobx';
+import { action, computed, makeObservable, observable, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import { Docs } from '../../documents/Documents';
 import * as React from 'react';
@@ -56,6 +56,8 @@ export class CollectionCarouselView extends CollectionSubView() {
         Doc.setDocFilter(this.Document, 'star', undefined, 'match');
         this.layoutDoc.practiceMode = practiceMode.NORMAL;
         this.layoutDoc._carousel_index = 0;
+        this.carouselItems.forEach(item => { item.layout[this.practiceField] = undefined}); //prettier-ignore
+        console.log(this.carouselItems.length);
     }
 
     componentWillUnmount() {
@@ -202,6 +204,7 @@ export class CollectionCarouselView extends CollectionSubView() {
     };
 
     @computed get content() {
+        trace();
         if (this.layoutDoc._carousel_index === this.carouselItems.length && this.layoutDoc._carousel_index !== 0) {
             this.move(1);
         }
diff --git a/src/client/views/nodes/.LinkAnchorBox.scss.icloud b/src/client/views/nodes/.LinkAnchorBox.scss.icloud
new file mode 100644
index 000000000..193619551
Binary files /dev/null and b/src/client/views/nodes/.LinkAnchorBox.scss.icloud differ
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 9deed4de4..5d0ed7eab 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -10,7 +10,7 @@ import { DateField } from '../../../fields/DateField';
 import { Doc } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { ComputedField } from '../../../fields/ScriptField';
-import { Cast, DateCast, NumCast } from '../../../fields/Types';
+import { Cast, DateCast, DocCast, NumCast } from '../../../fields/Types';
 import { AudioField, nullAudio } from '../../../fields/URLField';
 import { formatTime } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
@@ -29,6 +29,7 @@ import './AudioBox.scss';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { OpenWhere } from './OpenWhere';
+import axios from 'axios';
 
 /**
  * AudioBox
@@ -257,6 +258,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             const [{ result }] = await Networking.UploadFilesToServer({ file: e.data });
             if (!(result instanceof Error)) {
                 this.Document[this.fieldKey] = new AudioField(result.accessPaths.agnostic.client);
+                this.Document.url = result.accessPaths.agnostic.client;
+                await this.pushInfo();
             }
         };
         this._recordStart = new Date().getTime();
@@ -284,9 +287,23 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
     };
 
+    pushInfo = async () => {
+        const audio = {
+            file: this.path,
+        };
+        const response = await axios.post('http://localhost:105/recognize/', audio, {
+            headers: {
+                'Content-Type': 'application/json',
+            },
+        });
+        this.Document[DocData].phoneticTranscription = response.data['transcription'];
+        console.log('RESPONSE: ' + response.data['transcription']);
+    };
+
     // context menu
     specificContextMenu = (): void => {
         const funcs: ContextMenuProps[] = [];
+        // funcs.push({ description: 'Push info', event: this.pushInfo, icon: 'redo-alt' });
         funcs.push({
             description: (this.layoutDoc.hideAnchors ? "Don't hide" : 'Hide') + ' anchors',
             event: () => { this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors; }, // prettier-ignore
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index 41ff56514..56a1e4fcc 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -26,11 +26,12 @@
     .submit-button {
         position: absolute;
         padding-bottom: 10px;
+        padding-top: 5px;
         padding-left: 5px;
         padding-right: 5px;
         // width: 80%;
         border-radius: 2px;
-        height: 15%;
+        height: 17%;
         display: inline-block;
         bottom: 0;
         // right: 0;
@@ -48,8 +49,13 @@
             }
         }
 
+        &.pronunciation {
+            width: 40%;
+            align-items: center;
+            justify-content: center;
+        }
         &.submit {
-            width: 80%;
+            width: 40%;
             // float: right;
 
             // position: absolute;
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 3d2a1f0a9..ce29a63b4 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -1,4 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { MathJax, MathJaxContext } from 'better-react-mathjax';
 import { Tooltip } from '@mui/material';
 import { action, computed, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
@@ -33,6 +34,8 @@ import { FollowLinkScript } from '../../documents/DocUtils';
 import { schema } from '../nodes/formattedText/schema_rts';
 import { Id } from '../../../fields/FieldSymbols';
 import axios from 'axios';
+import ReactMarkdown from 'react-markdown';
+import { WebField, nullAudio } from '../../../fields/URLField';
 
 const API_URL = 'https://api.unsplash.com/search/photos';
 @observer
@@ -51,9 +54,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     @observable private _outputValue = '';
     @observable private _loading = false;
     @observable private _isEmpty = false;
+    @observable private _audio: Doc = Docs.Create.TextDocument('');
     @observable childActive = false;
     @observable _yRelativeToTop: boolean = true;
     @observable _animating = '';
+    @observable mathJaxConfig = {
+        loader: { load: ['input/asciimath'] },
+    };
     private _ref = React.createRef<HTMLDivElement>();
 
     get revealOp() {
@@ -237,11 +244,71 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         this.childActive = true;
     };
 
-    @action handleRenderGPTClick = () => {
+    @action handleRenderGPTClick = async () => {
         // Call the GPT model and get the output
+        // await this.pushInfo();
+        // console.log('PHONETIC TRANSCRIPTION: ' + DocCast(this._audio)[DocData]);
+        // this.Document.audio = this._audio;
+        console.log('Phonetic transcription: ' + DocCast(this.Document.audio).phoneticTranscription);
+        const phonTrans = DocCast(this.Document.audio).phoneticTranscription;
+        if (phonTrans) {
+            this._inputValue = StrCast(phonTrans);
+            console.log('INPUT:' + this._inputValue);
+            this.askGPTPhonemes(this._inputValue);
+        } else if (this._inputValue) this.askGPT(GPTCallType.QUIZ);
         this.layoutDoc[`_${this._props.fieldKey}_usePath`] = 'alternate';
         this._outputValue = '';
-        if (this._inputValue) this.askGPT(GPTCallType.QUIZ);
+    };
+
+    askGPTPhonemes = async (phonemes: string) => {
+        const question0 = 'These phonemes should match "what is your name": ' + phonemes + 'Use the structure of this response as guidance: "Your pronunciation of the vowel in "what" is not front enough. It should be pronounced like /uh/."';
+        const question =
+            'Match the following phonemes with each word in "what is your name": ' +
+            phonemes +
+            '. Note if a letter is added or missing that changes the meaning. If the mismatches are not allophones of the same phoneme and they are far away from each other on the vowel chart, list the difference. For the mismatches, use the structure of this response as guidance: "Your pronunciation of the vowel in "what" is not front enough. It should be pronounced like /uh/."';
+        const question1 =
+            'Consider all phonetic transcriptions of "what is your name" with different vowel pronunications. Compares these phonemes with that phonetic transcription: ' +
+            phonemes +
+            '. If the differences are not allophones of the same phoneme and they are far away from each other on the vowel chart, list the difference. If it is missing or added a letter, say that.';
+        //Only describe sound changes that will change the meaning drastically. Provide two sentences describing this. Do not list differences that do not change the meaning.';
+        const question2 = 'Is this a valid phonetic transcription of the phrase "what is your name": ' + phonemes + '.';
+        // If the difference found will definitely make the word be not understood and change the meaning, then list it. If the difference is minimal or the sound matches, do not list it.';
+        //These phonemes are supposed to match the pronunciation of ' +
+        //'hello: ' +
+        //phonemes +
+        //'. If there is a difference in sound that would change the meaning of the word or sentence, such as "pen" vs. "pin", describe that. Otherwise say "good job."';
+        // Identify any differences in pronunciation that would change the meaning of the intended word or sentence and only list differences that would change the meaning. If there are no major differences, say "Good job." If there are differences, describe it in terms of sounds in sentences.';
+        // const question =
+        //     'These phonemes are supposed to match the pronunciation of ' +
+        //     StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text) +
+        //     '. Identify any differences in pronunciation that would change the meaning of the intended word or sentence.';
+        console.log(question);
+        const res = await gptAPICall(question, GPTCallType.PRONUNCIATION);
+        console.log('GPT: ' + res);
+        if (!res) {
+            console.error('GPT call failed');
+            return;
+        }
+        // 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;
+        // this._loading = true;
+    };
+
+    pushInfo = async () => {
+        const formData = new FormData();
+
+        console.log(DocCast(this._audio).dataDoc);
+        const audio = {
+            file: this._audio.url,
+        };
+        const response = await axios.post('http://localhost:105/recognize/', audio, {
+            headers: {
+                'Content-Type': 'application/json',
+            },
+        });
+        this.Document.phoneticTranscription = response.data['transcription'];
+        console.log('RESPONSE: ' + response.data['transcription']);
     };
 
     @action handleHover = () => {
@@ -391,22 +458,41 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         var collectionArr: Doc[] = [];
         for (let i = 1; i < senArr?.length!; i++) {
             const newDoc = Docs.Create.ComparisonDocument(senArr![i], { _layout_isFlashcard: true, _width: 300, _height: 300 });
-            // newDoc.text = senArr![i];
-            // const dataSplit = StrCast(this.dataDoc.data).includes('Keyword: ') ? StrCast(this.dataDoc.data).split('Keyword: ') : StrCast(this.dataDoc.data).split('Answer: ');
-            // newDoc[this.fieldKey + '_0'] = Docs.Create.TextDocument(dataSplit[1]);
-            // newDoc[this.fieldKey + '_1'] = Docs.Create.TextDocument(dataSplit[0]);
-            // newDoc['backgroundColor'] = 'lightgray';
-            // newDoc.image =
+
             if (StrCast(senArr![i]).includes('Keyword: ')) {
-                console.log('Here');
-                const img = await this.fetchImages(StrCast(senArr![i]).split('Keyword: ')[1]);
-                console.log(img);
-                DocCast(newDoc).image = img;
-                // DocCast(DocCast(newDoc).dataDoc)['image'] = img;
-                Doc.AddToMyOverlay(img);
+                const question = StrCast(senArr![i]).split('Keyword: ');
+                const img = await this.fetchImages(question[1]);
+                // newDoc['image'] = img;
+                // const newDoc = Docs.Create.TextDocument(dataSplit[1]);
+                const textSide1 = question[0].includes('Answer: ') ? question[0].split('Answer: ')[0] : question[0];
+                const textDoc1 = Docs.Create.TextDocument(question[0]);
+                const rtfiel = new RichTextField(
+                    JSON.stringify({
+                        doc: {
+                            type: 'doc',
+                            content: [
+                                {
+                                    type: 'paragraph',
+                                    attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
+                                    content: [
+                                        { type: 'text', text: question[0].includes('Answer: ') ? question[0].split('Answer: ')[0] : question[0] },
+                                        { type: 'dashDoc', attrs: { width: '200px', height: '200px', title: 'dashDoc', float: 'unset', hidden: false, docId: img![Id] } },
+                                    ],
+                                },
+                            ],
+                        },
+                        selection: { type: 'text', anchor: 2, head: 2 },
+                    }),
+                    textSide1
+                );
+
+                textDoc1[DocData].text = rtfiel;
+                DocCast(newDoc)[DocData][this.fieldKey + '_1'] = textDoc1;
+
+                DocCast(newDoc)[DocData][this.fieldKey + '_0'] = Docs.Create.TextDocument(question[0].includes('Answer: ') ? question[0].split('Answer: ')[1] : question[1]);
+                // Doc.AddToMyOverlay(img!);
             }
 
-            console.log('ARR' + i + senArr![i]);
             collectionArr.push(newDoc);
         }
         this.createFlashcardPile(collectionArr, true);
@@ -473,7 +559,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             if (callType == GPTCallType.CHATCARD) {
                 DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res;
                 // this.flipFlashcard();
-            } else if (callType == GPTCallType.QUIZ) this._outputValue = res;
+            } else if (callType == GPTCallType.QUIZ) {
+                console.log(this._inputValue);
+                this._outputValue = res.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
+            }
             // DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res;
             // this._outputValue = res;
             else if (callType === GPTCallType.FLASHCARD) {
@@ -511,7 +600,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 if (i.className !== 'ProseMirror-separator') await this.getImageDesc(i.src);
             }
             this._loading = false;
-            this.flipFlashcard();
+            // this.flipFlashcard();
         }
         // console.log('HI' + this.ProseRef?.getElementsByTagName('img'));
     };
@@ -558,22 +647,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 _height: 150,
                 title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-',
             });
-            // return imageSnapshot;
             imageSnapshot['x'] = this.layoutDoc['x'];
             imageSnapshot['y'] = this.layoutDoc['y'];
-
-            // const newDoc = Docs.Create.TextDocument(selection);
-            // newDoc.text = selection;
-            // newDoc['backgroundColor'] = 'lightgray';
-
-            // Doc.AddToMyOverlay(imageSnapshot);
             return imageSnapshot;
-            return data.results[0].urls.small;
-            // Doc.AddEmbedding(newDoc, imageSnapshot);
-            // Doc.MakeEmbedding(imageSnapshot);
-            // return imageSnapshot;
-            // imageSnapshot['zIndex'] = 20000;
-            // this._props.DocumentView?.()._props.addDocument?.(newDoc);
         } catch (error) {
             console.log(error);
         }
@@ -660,6 +736,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         ContextMenu.Instance.displayMenu(x, y);
     };
 
+    evaluatePronunciation = () => {
+        const newAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, _height: 100 });
+        this.Document.audio = newAudio[DocData];
+        // DocCast(this.Document.embedContainer)()._props.addDocument?.(newAudio);
+        this._props.DocumentView?.()._props.addDocument?.(newAudio);
+        // Doc.AddToMyOverlay(newAudio);
+    };
+
     render() {
         const clearButton = (which: string) => (
             <Tooltip title={<div className="dash-tooltip">remove</div>}>
@@ -680,6 +764,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             // whichDoc['backgroundColor'] = this.layoutDoc['backgroundColor'];
 
             return targetDoc || layoutString ? (
+                // <MathJaxContext config={this.mathJaxConfig}>
+                //     <MathJax>
                 <>
                     <DocumentView
                         // eslint-disable-next-line react/jsx-props-no-spreading
@@ -701,8 +787,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         hideLinkButton
                         pointerEvents={this.childActive ? undefined : returnNone}
                     />
+                    {/* </MathJax> */}
                     {/* <div style={{ position: 'absolute', top: '-5px', left: '2px' }}>{layoutString ? null : clearButton(whichSlot)}</div> */}
-                </> // placeholder image if doc is missingleft: `${NumCast(this.layoutDoc.width, 200) - 33}px`
+                    {/* </MathJaxContext> // placeholder image if doc is missingleft: `${NumCast(this.layoutDoc.width, 200) - 33}px` */}
+                </>
             ) : (
                 <div className="placeholder">
                     <FontAwesomeIcon className="upload-icon" icon="cloud-upload-alt" size="lg" />
@@ -732,79 +820,36 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             if (!this.dataDoc[this.fieldKey + '_0'] && !this._isEmpty) {
                 const dataSplit = StrCast(this.dataDoc.data).includes('Keyword: ') ? StrCast(this.dataDoc.data).split('Keyword: ') : StrCast(this.dataDoc.data).split('Answer: ');
                 const newDoc = Docs.Create.TextDocument(dataSplit[1]);
-                if (this.Document.image) DocCast(newDoc).image = DocCast(this.Document.image);
-                // console.log('D' + this.Document.image);
-                //if (DocCast(DocCast(newDoc).dataDoc)) DocCast(DocCast(newDoc).dataDoc)['image'] = this.dataDoc['image'];
-
-                // console.log('HI' + this.Document.image);
-                // const imageSnapshot = Docs.Create.ImageDocument(StrCast(this.Document.image), {
-                //     _nativeWidth: Doc.NativeWidth(this.layoutDoc),
-                //     _nativeHeight: Doc.NativeHeight(this.layoutDoc),
-                //     x: NumCast(this.layoutDoc.x),
-                //     y: NumCast(this.layoutDoc.y),
-                //     onClick: FollowLinkScript(),
-                //     _width: 150,
-                //     _height: 150,
-                //     title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-',
-                // });
-                // // return imageSnapshot;
-                // imageSnapshot['x'] = this.layoutDoc['x'];
-                // imageSnapshot['y'] = this.layoutDoc['y'];
-
-                // const newDoc = Docs.Create.TextDocument(selection);
-                // newDoc.text = selection;
-                // newDoc['backgroundColor'] = 'lightgray';
-                // newDoc.data = imageSnapshot;
-                // this.createDropTarget(this., this.fieldKey + '_0', 0)
-                // Doc.AddEmbedding(imageSnapshot, newDoc);
-                // Doc.SetContainer(imageSnapshot, newDoc);
-                // Doc.AddToMyOverlay(imageSnapshot);
-
-                // if (StrCast(this.dataDoc.data).includes('Keyword: ')) {
-                //     console.log('HERE' + this.dataDoc.data);
-                //     this.fetchImages(StrCast(this.dataDoc.data).split('Keyword: ')[1]);
-                // }
-                //     // const node = schema.nodes.dashDoc.create({
-                //     //     width: NumCast(newDoc._width),
-                //     //     height: NumCast(newDoc._height),
-                //     //     title: 'dashDoc',
-                //     //     docId: newDoc[Id],
-                //     //     float: 'unset',
-                //     // });
-                //     // Doc.AddEmbedding(images!, newDoc);
-                //     // Doc.SetContainer(images, this.newDoc);
-                // } else {
-                // newDoc.text = dataSplit[1];
-                // newDoc['backgroundColor'] = 'lightgray';
                 this.addDoc(newDoc, this.fieldKey + '_0');
-                // this.addDoc()
-                // }
-                // newDoc?.addDocument?.(images);
-
-                // if there is text from the pdf ai cards, put the question on the front side.
-                // eslint-disable-next-line prefer-destructuring
-                // newDoc.text = dataSplit[1];
-                // newDoc['backgroundColor'] = 'lightgray';
-                // this.addDoc(newDoc, this.fieldKey + '_0');
-                // DocCast(this.dataDoc[this.fieldKey + '_0'])[DocData].text = dataSplit[1];
-                // DocCast(this.dataDoc[this.fieldKey + '_0']).text = dataSplit[1];
-                // console.log('HI' + DocCast(this.dataDoc[this.fieldKey + '_0']).text);
-                //console.log('HEREEE' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text));
             }
 
             if (!this.dataDoc[this.fieldKey + '_1'] && !this._isEmpty) {
                 const dataSplit = StrCast(this.dataDoc.data).includes('Keyword: ') ? StrCast(this.dataDoc.data).split('Keyword: ') : StrCast(this.dataDoc.data).split('Answer: ');
                 const newDoc = Docs.Create.TextDocument(dataSplit[0]);
                 this.addDoc(newDoc, this.fieldKey + '_1');
-                // if there is text from the pdf ai cards, put the answer on the alternate side.
-                // eslint-disable-next-line prefer-destructuring
-
-                // newDoc[DocData].text = dataSplit[0];
-                // console.log('HEREEE' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text));
-                // console.log('HI' + DocCast(this.dataDoc[this.fieldKey + '_1']).text);
-                // DocCast(this.dataDoc[this.props.fieldKey + '_1'])[DocData].text = dataSplit[0];
-                // console.log('HEREEE' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text));
-                // DocCast(this.dataDoc[this.fieldKey + '_1'])[DocData].text = dataSplit[0];
+                // if (this.Document.image) {
+                //     console.log('ID: ' + DocCast(this.Document.image)[Id]);
+                //     const rtfiel = new RichTextField(
+                //         JSON.stringify({
+                //             doc: {
+                //                 type: 'doc',
+                //                 content: [
+                //                     {
+                //                         type: 'paragraph',
+                //                         attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
+                //                         content: [
+                //                             { type: 'text', text: dataSplit[0] },
+                //                             { type: 'dashDoc', attrs: { width: '200px', height: '200px', title: 'dashDoc', float: 'unset', hidden: false, docId: DocCast(this.Document.image)[Id] } },
+                //                         ],
+                //                     },
+                //                 ],
+                //             },
+                //             selection: { type: 'text', anchor: 2, head: 2 },
+                //         })
+                //     );
+
+                //     newDoc[DocData].text = rtfiel;
+                // }
             }
 
             // render the QuizCards
@@ -819,7 +864,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             <textarea
                                 value={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this._outputValue : this._inputValue}
                                 onChange={this.handleInputChange}
-                                onScroll={e => e.stopPropagation()}
+                                onScroll={e => {
+                                    e.stopPropagation();
+                                    e.preventDefault();
+                                }}
                                 placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''}
                                 readOnly={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate'}></textarea>
 
@@ -834,19 +882,25 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                                 <div
                                     className="submit-buttonschema-header-button"
                                     onPointerDown={e => this.openContextMenu(e.clientX, e.clientY)}
-                                    style={{ position: 'absolute', top: '1px', left: '11px', zIndex: '100', width: '5px', height: '5px', cursor: 'pointer' }}>
+                                    style={{ position: 'absolute', top: '5px', left: '11px', zIndex: '100', width: '5px', height: '5px', cursor: 'pointer' }}>
                                     <FontAwesomeIcon color={'white'} icon="caret-down" />
                                 </div>
                                 <button className="submit-buttonrecord" onClick={this._listening ? this.stopListening : this.startListening} style={{ background: this._listening ? 'lightgray' : '', borderRadius: '2px' }}>
                                     {<FontAwesomeIcon icon="microphone" size="lg" />}
                                 </button>
+                                <button
+                                    className="submit-buttonpronunciation"
+                                    onClick={this.evaluatePronunciation}
+                                    style={{ display: 'inline-flex', alignItems: 'center', background: this._listening ? 'lightgray' : '', borderRadius: '2px', width: '100%' }}>
+                                    Evaluate Pronunciation
+                                </button>
 
                                 {this.layoutDoc[`_${this._props.fieldKey}_usePath`] !== 'alternate' ? (
-                                    <button className="submit-buttonsubmit" type="button" onClick={this.handleRenderGPTClick} style={{ borderRadius: '2px', marginBottom: '3px', width: '300%' }}>
+                                    <button className="submit-buttonsubmit" type="button" onClick={this.handleRenderGPTClick} style={{ borderRadius: '2px', marginBottom: '3px', width: '100%' }}>
                                         Submit
                                     </button>
                                 ) : (
-                                    <button className="submit-buttonsubmit" type="button" onClick={this.handleRenderClick} style={{ borderRadius: '2px' }}>
+                                    <button className="submit-buttonsubmit" type="button" onClick={this.handleRenderClick} style={{ borderRadius: '2px', marginBottom: '3px', width: '100%' }}>
                                         Redo the Question
                                     </button>
                                 )}
@@ -871,7 +925,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     }}
                     // onPointerUp={() => (this._isAnyChildContentActive = true)}
                 >
-                    {!this.layoutDoc[`_${this._props.fieldKey}_usePath`] && StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '' && !this.childActive ? <p className="explain">Enter text in the flashcard. </p> : null}
+                    {/* {!this.layoutDoc[`_${this._props.fieldKey}_usePath`] && StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '' && !this.childActive ? <p className="explain">Enter text in the flashcard. </p> : null} */}
                     {displayBox(`${this.fieldKey}_${side === 0 ? 1 : 0}`, side, this._props.PanelWidth() - 3)}
                     {this._loading ? (
                         <div className="loading-spinner" style={{ position: 'absolute' }}>
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 138f00492..b9c3528d3 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -50,7 +50,7 @@ export interface FieldViewSharedProps {
     PanelHeight: () => number;
     isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events
     isContentActive: () => boolean | undefined; // whether document contents should handle pointer events
-    dontSelect: () => boolean | undefined;
+    dontSelect?: () => boolean | undefined;
     childFilters: () => string[];
     childFiltersByRanges: () => string[];
     styleProvider: Opt<StyleProviderFuncType>;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 93c07f3a8..ab7605829 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -384,17 +384,93 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         this._loading = false;
     };
 
+    getImageLabels = async () => {
+        this._loading = true;
+        try {
+            const hrefBase64 = await this.createCanvas();
+            // const hw = await gptImageLabel(hrefBase64, 'Find the image dimensions. Return as height and width.');
+            const response = await gptImageLabel(
+                hrefBase64,
+                //'What is the height and width of the image'
+                'For each group of words in the image, find the x-coordinate and ycoordinate of the top left corner. Find the width and height of the group. Return this information in this format with the correct information replacing the underscores: "observed word: _, x: _, y: _, width: _, height: _," No additional text, asterisks and put it all in one line. Divide the x and width by the width of the image. Divide the y and the height by the height of the image.'
+            );
+            // console.log(hw);
+            console.log('RESPONSE: ' + response);
+            this.createTextboxes(response);
+
+            //AnchorMenu.Instance.transferToFlashcard(response, NumCast(this.layoutDoc['x']), NumCast(this.layoutDoc['y']));
+        } catch (error) {
+            console.log('Error');
+        }
+        this._loading = false;
+    };
+
+    createTextboxes = (response: string) => {
+        const groups = response.replace('*', '').toLowerCase().split('observed word: ');
+        groups.shift();
+        for (var i = 0; i < groups.length; i++) {
+            console.log('Group ' + i + ': ' + groups[i]);
+        }
+        // console.log('GROUPS: ' + groups);
+        groups.forEach(
+            group => {
+                const groupArr = group.split(', ');
+                const word = groupArr[0];
+                const x = parseFloat(groupArr[1].substring(3));
+                const y = parseFloat(groupArr[2].substring(3));
+                const width = parseFloat(groupArr[3].substring(7));
+                const height = parseFloat(groupArr[4].substring(8));
+                const scaling = 1 / (this._props.NativeDimScaling?.() || 1);
+                const w = AnchorMenu.Instance.marqueeWidth * scaling;
+                const h = AnchorMenu.Instance.marqueeHeight * scaling;
+
+                const newCol = Docs.Create.TextDocument(word, {
+                    _width: w * width,
+                    //width * NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']),
+                    _height: h * height + 20,
+                    //height * NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']),
+                    _layout_fitWidth: true,
+                    _layout_autoHeight: true,
+                });
+                newCol.x = x * w;
+                newCol.y = y * h;
+                // newCol.x = x * NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
+                // newCol.y = y * NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']);
+                newCol.zIndex = 1000;
+                newCol.forceActive = true;
+                newCol.quiz = true;
+                this.addDocument(newCol);
+            }
+            // console.log(group);
+        );
+    };
+
     @action
     setRef = (iref: HTMLImageElement | null) => {
         this._imageRef = iref;
     };
 
+    pushInfo = async () => {
+        const formData = new FormData();
+
+        const newArticle = {
+            file: '/files/audio/6b412a6222d631a7fff8a8320.mp3',
+        };
+        const response = await axios.post('http://localhost:105/recognize/', newArticle, {
+            headers: {
+                'Content-Type': 'application/json',
+            },
+        });
+        console.log('RESPONSE: ' + response.data['transcription']);
+    };
+
     specificContextMenu = (): void => {
         const field = Cast(this.dataDoc[this.fieldKey], ImageField);
         if (field) {
             const funcs: ContextMenuProps[] = [];
             // funcs.push({ description: 'Create ai flashcards', event: () => this.getImageDesc(), icon: 'id-card' });
-            funcs.push({ description: 'Get Images', event: () => this.handleSelection('Cats'), icon: 'redo-alt' });
+            // funcs.push({ description: 'Push info', event: this.pushInfo, icon: 'redo-alt' });
+            funcs.push({ description: 'Get Labels', event: this.getImageLabels, icon: 'redo-alt' });
             funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' });
             funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' });
             funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' });
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
new file mode 100644
index 000000000..0a4325d8c
--- /dev/null
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -0,0 +1,115 @@
+import { action, computed, makeObservable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Utils, emptyFunction, setupMoveUpEvents } from '../../../Utils';
+import { Doc } from '../../../fields/Doc';
+import { NumCast, StrCast } from '../../../fields/Types';
+import { TraceMobx } from '../../../fields/util';
+import { DragManager, dropActionType } from '../../util/DragManager';
+import { LinkFollower } from '../../util/LinkFollower';
+import { SelectionManager } from '../../util/SelectionManager';
+import { ViewBoxBaseComponent } from '../DocComponent';
+import { StyleProp } from '../StyleProvider';
+import { FieldView, FieldViewProps } from './FieldView';
+import './LinkAnchorBox.scss';
+import { LinkInfo } from './LinkDocPreview';
+const { default: { MEDIUM_GRAY }, } = require('../global/globalCssVariables.module.scss'); // prettier-ignore
+@observer
+export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
+    public static LayoutString(fieldKey: string) {
+        return FieldView.LayoutString(LinkAnchorBox, fieldKey);
+    }
+    _doubleTap = false;
+    _lastTap: number = 0;
+    _ref = React.createRef<HTMLDivElement>();
+    _isOpen = false;
+    _timeout: NodeJS.Timeout | undefined;
+
+    constructor(props: FieldViewProps) {
+        super(props);
+        makeObservable(this);
+    }
+
+    componentDidMount() {
+        this._props.setContentViewBox?.(this);
+    }
+
+    @computed get linkSource() {
+        return this.DocumentView?.().containerViewPath?.().lastElement().Document; // this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.LinkSource);
+    }
+
+    onPointerDown = (e: React.PointerEvent) => {
+        const linkSource = this.linkSource;
+        linkSource &&
+            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, (e, doubleTap) => {
+                if (doubleTap) LinkFollower.FollowLink(this.Document, linkSource, false);
+                else this._props.select(false);
+            });
+    };
+    onPointerMove = action((e: PointerEvent, down: number[], delta: number[]) => {
+        const cdiv = this._ref?.current?.parentElement;
+        if (!this._isOpen && cdiv) {
+            const bounds = cdiv.getBoundingClientRect();
+            const pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY);
+            const separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY));
+            if (separation > 100) {
+                const dragData = new DragManager.DocumentDragData([this.Document]);
+                dragData.dropAction = dropActionType.embed;
+                dragData.dropPropertiesToRemove = ['link_anchor_1_x', 'link_anchor_1_y', 'link_anchor_2_x', 'link_anchor_2_y', 'onClick'];
+                DragManager.StartDocumentDrag([this._ref.current!], dragData, pt[0], pt[1]);
+                return true;
+            } else {
+                this.layoutDoc[this.fieldKey + '_x'] = ((pt[0] - bounds.left) / bounds.width) * 100;
+                this.layoutDoc[this.fieldKey + '_y'] = ((pt[1] - bounds.top) / bounds.height) * 100;
+                this.layoutDoc.link_autoMoveAnchors = false;
+            }
+        }
+        return false;
+    });
+
+    specificContextMenu = (e: React.MouseEvent): void => {};
+
+    render() {
+        TraceMobx();
+        const small = this._props.PanelWidth() <= 1; // this happens when rendered in a treeView
+        const x = NumCast(this.layoutDoc[this.fieldKey + '_x'], 100);
+        const y = NumCast(this.layoutDoc[this.fieldKey + '_y'], 100);
+        const background = this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.BackgroundColor + ':anchor');
+        const anchor = this.fieldKey === 'link_anchor_1' ? 'link_anchor_2' : 'link_anchor_1';
+        const anchorScale = !this.dataDoc[this.fieldKey + '_useSmallAnchor'] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : 0.25;
+        const targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title);
+        const selView = SelectionManager.Views.lastElement()?._props.LayoutTemplateString?.includes('link_anchor_1')
+            ? 'link_anchor_1'
+            : SelectionManager.Views.lastElement()?._props.LayoutTemplateString?.includes('link_anchor_2')
+              ? 'link_anchor_2'
+              : '';
+        return (
+            <div
+                ref={this._ref}
+                title={targetTitle}
+                className={`linkAnchorBox-cont${small ? '-small' : ''}`}
+                onPointerEnter={e =>
+                    LinkInfo.SetLinkInfo({
+                        DocumentView: this.DocumentView,
+                        styleProvider: this._props.styleProvider,
+                        linkSrc: this.linkSource,
+                        linkDoc: this.Document,
+                        showHeader: true,
+                        location: [e.clientX, e.clientY + 20],
+                        noPreview: false,
+                    })
+                }
+                onPointerDown={this.onPointerDown}
+                onContextMenu={this.specificContextMenu}
+                style={{
+                    border: selView && this.dataDoc[selView] === this.dataDoc[this.fieldKey] ? `solid ${MEDIUM_GRAY} 2px` : undefined,
+                    background,
+                    left: `calc(${x}% - ${small ? 2.5 : 7.5}px)`,
+                    top: `calc(${y}% - ${small ? 2.5 : 7.5}px)`,
+                    transform: `scale(${anchorScale})`,
+                    cursor: 'grab',
+                }}
+            />
+        );
+    }
+}
diff --git a/src/client/views/nodes/ae6d-ba67-4ace-93aa-0f9e0bd96b88.wav b/src/client/views/nodes/ae6d-ba67-4ace-93aa-0f9e0bd96b88.wav
new file mode 100644
index 000000000..dc71e7886
Binary files /dev/null and b/src/client/views/nodes/ae6d-ba67-4ace-93aa-0f9e0bd96b88.wav differ
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 3c12db965..274330d31 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -3,7 +3,7 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
-import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction, runInAction, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import { baseKeymap, selectAll } from 'prosemirror-commands';
 import { history } from 'prosemirror-history';
@@ -361,10 +361,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 const rtField = (layoutData !== prevData ? layoutData : undefined) ?? protoData;
                 if (this._applyingChange !== this.fieldKey && (force || textChange || removeSelection(newJson) !== removeSelection(prevData?.Data))) {
                     this._applyingChange = this.fieldKey;
-                    textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
                     if ((!prevData && !protoData && !layoutData) || newText || (!newText && !protoData && !layoutData)) {
                         // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
                         if (force || ((this._finishingLink || this._props.isContentActive() || this._inDrop) && (textChange || removeSelection(newJson) !== removeSelection(prevData?.Data)))) {
+                            textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
                             const numstring = NumCast(dataDoc[this.fieldKey], null);
                             dataDoc[this.fieldKey] =
                                 numstring !== undefined ? Number(newText) : newText || (DocCast(dataDoc.proto)?.[this.fieldKey] === undefined && this.layoutDoc[this.fieldKey] === undefined) ? new RichTextField(newJson, newText) : undefined;
@@ -373,6 +373,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                             unchanged = false;
                         }
                     } else if (rtField) {
+                        textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
                         // if we've deleted all the text in a note driven by a template, then restore the template data
                         dataDoc[this.fieldKey] = undefined;
                         this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(rtField.Data)));
@@ -2206,7 +2207,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                         />
                     </div>
                     {this.noSidebar || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection}
-                    {this.noSidebar || this.Document._layout_noSidebar || this.Document._createDocOnCR || this.layoutDoc._chromeHidden ? null : this.sidebarHandle}
+                    {this.noSidebar || this.Document._layout_noSidebar || this.Document._createDocOnCR || this.layoutDoc._chromeHidden || this.Document.quiz ? null : this.sidebarHandle}
                     {this.audioHandle}
                     {this.layoutDoc._layout_enableAltContentUI && !this.layoutDoc._chromeHidden ? this.overlayAlternateIcon : null}
                 </div>
diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts
index 3f13f7e6d..613bb0fd1 100644
--- a/src/fields/RichTextField.ts
+++ b/src/fields/RichTextField.ts
@@ -13,10 +13,15 @@ export class RichTextField extends ObjectField {
     @serializable(true)
     readonly Text: string;
 
-    constructor(data: string, text: string = '') {
+    /**
+     * NOTE: if 'text' doesn't match the plain text of 'data', this can cause infinite loop problems or other artifacts when rendered.
+     * @param data this is the formatted text representation of the RTF
+     * @param text this is the plain text of whatever text is in the 'data'
+     */
+    constructor(data: string, text: string) {
         super();
         this.Data = data;
-        this.Text = text;
+        this.Text = text; // ideally, we'd compute 'text' from 'data' by doing what Prosemirror does at run-time ... just need to figure out how to write that function accurately
     }
 
     Empty() {
-- 
cgit v1.2.3-70-g09d2


From 80a9bb161cda4a361d9626047869e7acbe845414 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 2 Sep 2024 10:59:58 -0400
Subject: fixed containerViewPath for carouse and comparison boxes

---
 src/client/views/collections/CollectionCarouselView.tsx | 3 ++-
 src/client/views/nodes/ComparisonBox.tsx                | 2 +-
 2 files changed, 3 insertions(+), 2 deletions(-)

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

diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index ba7c944a0..9f59322e8 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -210,7 +210,7 @@ export class CollectionCarouselView extends CollectionSubView() {
 
         return !(curDoc?.layout instanceof Doc) ? null : (
             <>
-                <div className="collectionCarouselView-image" key="image">
+                <div className="collectionCarouselView-image">
                     <DocumentView
                         {...this._props}
                         NativeWidth={returnZero}
@@ -218,6 +218,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                         fitWidth={undefined}
                         setContentViewBox={undefined}
                         childFilters={this.childDocFilters}
+                        containerViewPath={this._props.docViewPath}
                         onDoubleClickScript={this.onContentDoubleClick}
                         onClickScript={this.onContentClick}
                         isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 58af3ad52..9e1c1b7be 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -673,7 +673,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         renderDepth={this.props.renderDepth + 1}
                         LayoutTemplateString={layoutString}
                         Document={layoutString ? this.Document : targetDoc}
-                        containerViewPath={this.DocumentView?.().docViewPath}
+                        containerViewPath={this._props.docViewPath}
                         moveDocument={whichSlot.endsWith('1') ? this.moveDoc1 : this.moveDoc2}
                         removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2}
                         NativeWidth={this.layoutWidth}
-- 
cgit v1.2.3-70-g09d2


From 3fe153d3063c197403502590baaabbab1cec5c74 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 6 Sep 2024 16:53:57 -0400
Subject: added margins for carousel view.  allow fordoc decorations to be
 turned off for carousel items.

---
 src/client/documents/Documents.ts                       |  1 +
 src/client/views/PropertiesView.tsx                     |  4 +++-
 src/client/views/collections/CollectionCarouselView.tsx | 13 +++++++++----
 3 files changed, 13 insertions(+), 5 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index d5a7b0465..4a377a034 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -248,6 +248,7 @@ export class DocumentOptions {
     layout_hideResizeHandles?: BOOLt = new BoolInfo('whether to hide the resize handles when selected');
     layout_hideLinkButton?: BOOLt = new BoolInfo('whether the blue link counter button should be hidden');
     layout_hideDecorationTitle?: BOOLt = new BoolInfo('whether to suppress the document decortations title when selected');
+    layout_hideDecorations?: BOOLt = new BoolInfo('whether to suppress all document decortations when selected');
     _layout_hideContextMenu?: BOOLt = new BoolInfo('whether the context menu can be shown');
     layout_diagramEditor?: STRt = new StrInfo('specify the JSX string for a diagram editor view');
     layout_hideContextMenu?: BOOLt = new BoolInfo('whether the context menu can be shown');
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index daa8e1720..f42838086 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -138,7 +138,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         return this.selectedDoc?.isGroup;
     }
     @computed get isStack() {
-        return [CollectionViewType.Masonry, CollectionViewType.Multicolumn, CollectionViewType.Multirow, CollectionViewType.Stacking, CollectionViewType.NoteTaking].includes(this.selectedDoc?.type_collection as CollectionViewType);
+        return [CollectionViewType.Masonry, CollectionViewType.Multicolumn, CollectionViewType.Multirow, CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Carousel].includes(
+            this.selectedDoc?.type_collection as CollectionViewType
+        );
     }
 
     rtfWidth = () => (!this.selectedLayoutDoc ? 0 : Math.min(NumCast(this.selectedLayoutDoc?._width), this._props.width - 20));
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 9f59322e8..a1c59d238 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -7,7 +7,7 @@ import * as React from 'react';
 import { StopEvent, returnFalse, returnOne, returnTrue, returnZero } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
-import { DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
@@ -167,7 +167,7 @@ export class CollectionCarouselView extends CollectionSubView() {
         const childValue = doc?.['caption_' + property] ? this._props.styleProvider?.(doc, captionProps, property) : undefined;
         return childValue ?? this._props.styleProvider?.(this.layoutDoc, captionProps, property);
     };
-    panelHeight = () => this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0);
+    contentPanelHeight = () => this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0) - 2 * NumCast(this.layoutDoc.yMargin);
     onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
     onContentClick = () => ScriptCast(this.layoutDoc.onChildClick);
     captionWidth = () => this._props.PanelWidth() - 2 * this.marginX;
@@ -197,7 +197,9 @@ export class CollectionCarouselView extends CollectionSubView() {
             // Doc.setDocFilter(this.Document, 'data_star', true, 'remove');
         }
     };
+    contentScreentToLocalXf = () => this._props.ScreenToLocalTransform().translate(-NumCast(this.layoutDoc.xMargin), -NumCast(this.layoutDoc.yMargin));
 
+    contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin);
     @computed get content() {
         trace();
         if (this.layoutDoc._carousel_index === this.carouselItems.length && this.layoutDoc._carousel_index !== 0) {
@@ -210,7 +212,7 @@ export class CollectionCarouselView extends CollectionSubView() {
 
         return !(curDoc?.layout instanceof Doc) ? null : (
             <>
-                <div className="collectionCarouselView-image">
+                <div className="collectionCarouselView-image" style={{ padding: `${NumCast(this.layoutDoc.yMargin)}px ${NumCast(this.layoutDoc.xMargin)}px ${NumCast(this.layoutDoc.yMargin)}px ${NumCast(this.layoutDoc.xMargin)}px` }}>
                     <DocumentView
                         {...this._props}
                         NativeWidth={returnZero}
@@ -221,6 +223,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                         containerViewPath={this._props.docViewPath}
                         onDoubleClickScript={this.onContentDoubleClick}
                         onClickScript={this.onContentClick}
+                        hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)}
                         isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
                         isContentActive={(this._props.childContentsActive ?? this._props.isContentActive() === false) ? returnFalse : emptyFunction}
                         addDocument={this._props.addDocument}
@@ -230,7 +233,9 @@ export class CollectionCarouselView extends CollectionSubView() {
                         LayoutTemplateString={this._props.childLayoutString}
                         Document={curDoc.layout}
                         TemplateDataDocument={DocCast(curDoc.layout.resolvedDataDoc)}
-                        PanelHeight={this.panelHeight}
+                        ScreenToLocalTransform={this.contentScreentToLocalXf}
+                        PanelWidth={this.contentPanelWidth}
+                        PanelHeight={this.contentPanelHeight}
                     />
                 </div>
                 {!carouselShowsCaptions ? null : (
-- 
cgit v1.2.3-70-g09d2


From 0268a524540ff36bf007744eff6709dfcdedec96 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 17 Sep 2024 18:43:26 -0400
Subject: from last

---
 .../views/collections/CollectionCarouselView.tsx   | 42 ++++++++++++----------
 1 file changed, 23 insertions(+), 19 deletions(-)

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

diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index d9a99f47f..91a7c6514 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -89,6 +89,10 @@ export class CollectionCarouselView extends CollectionSubView() {
         .filter(doc => !this.practiceMode || (BoolCast(doc?._layout_isFlashcard) && doc[this.practiceField] !== practiceVal.CORRECT))// show only cards that aren't marked as correct
     } // prettier-ignore
 
+    /**
+     * Move forward or backward the specified number of Docs
+     * @param dir signed number indicating Docs to move forward or backward
+     */
     move = action((dir: number) => {
         this._last_index = this.carouselIndex;
         this.layoutDoc._carousel_index = this.carouselItems.length ? (this.carouselIndex + dir + this.carouselItems.length) % this.carouselItems.length : 0;
@@ -111,9 +115,9 @@ export class CollectionCarouselView extends CollectionSubView() {
     };
 
     /*
-     * Stars the document when the star button is pressed.
+     * Toggles whether the 'star' metadata field is set on the current Doc
      */
-    star = (e: React.MouseEvent) => {
+    toggleStar = (e: React.MouseEvent) => {
         e.stopPropagation();
         const curDoc = this.carouselItems[this.carouselIndex];
         curDoc && (curDoc[this.starField] = curDoc[this.starField] ? undefined : true);
@@ -129,6 +133,16 @@ export class CollectionCarouselView extends CollectionSubView() {
         this.advance(e);
     };
 
+    /**
+     * Sets the practice mode answer style for flashcards
+     * @param mode practiceMode or undefined for no practice
+     */
+    setPracticeMode = (mode: practiceMode | undefined) => {
+        this.layoutDoc.practiceMode = mode;
+        this.carouselItems?.map(doc => (doc[this.practiceField] = undefined));
+        if (mode === practiceMode.QUIZ) this.carouselItems?.map(doc => (doc[this.sideField] = undefined));
+    };
+
     captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string) => {
         // first look for properties on the document in the carousel, then fallback to properties on the container
         const childValue = doc?.['caption_' + property] ? this._props.styleProvider?.(doc, captionProps, property) : undefined;
@@ -138,13 +152,6 @@ export class CollectionCarouselView extends CollectionSubView() {
     onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
     onContentClick = () => ScriptCast(this.layoutDoc.onChildClick);
     captionWidth = () => this._props.PanelWidth() - 2 * this.marginX;
-
-    setPracticeMode = (mode: practiceMode | undefined) => {
-        this.layoutDoc.practiceMode = mode;
-        this.carouselItems?.map(doc => (doc[this.practiceField] = undefined));
-        if (mode === practiceMode.QUIZ) this.carouselItems?.map(doc => (doc[this.sideField] = undefined));
-    };
-
     contentScreentToLocalXf = () => this._props.ScreenToLocalTransform().translate(-NumCast(this.layoutDoc.xMargin), -NumCast(this.layoutDoc.yMargin));
 
     contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin);
@@ -264,7 +271,7 @@ export class CollectionCarouselView extends CollectionSubView() {
             <>
                 <div>
                     <Tooltip title="star">
-                        <div key="star" className="carouselView-star" onClick={this.star}>
+                        <div key="star" className="carouselView-star" onClick={this.toggleStar}>
                             <FontAwesomeIcon icon="star" color={this.carouselItems[this.carouselIndex]?.[this.starField] ? 'yellow' : 'gray'} size="1x" />
                         </div>
                     </Tooltip>
@@ -298,7 +305,7 @@ export class CollectionCarouselView extends CollectionSubView() {
         );
     }
 
-    togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.layoutDoc.practiceMode ? undefined : mode);
+    togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode);
     toggleFilterMode = () => Doc.setDocFilter(this.Document, 'star', true, 'match', true);
     setColor = (mode: practiceMode | cardMode, which: string) => { return which === mode ? 'white' : 'light gray'}; //prettier-ignore
 
@@ -314,12 +321,12 @@ export class CollectionCarouselView extends CollectionSubView() {
                 <div className="carouselView-practiceModes" style={{ display: BoolCast(curDoc?._layout_isFlashcard) ? undefined : 'none' }}>
                     <Tooltip title="Practice flashcards using GPT">
                         <div key="back" className="carouselView-quiz" onClick={e => this.togglePracticeMode(practiceMode.QUIZ)}>
-                            <FontAwesomeIcon icon="file-pen" color={this.setColor(practiceMode.QUIZ, StrCast(this.layoutDoc.practiceMode))} size="1x" />
+                            <FontAwesomeIcon icon="file-pen" color={this.setColor(practiceMode.QUIZ, StrCast(this.practiceMode))} size="1x" />
                         </div>
                     </Tooltip>
-                    <Tooltip title={this.layoutDoc.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}>
+                    <Tooltip title={this.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}>
                         <div key="back" className="carouselView-practice" onClick={e => this.togglePracticeMode(practiceMode.PRACTICE)}>
-                            <FontAwesomeIcon icon="check" color={this.setColor(practiceMode.PRACTICE, StrCast(this.layoutDoc.practiceMode))} size="1x" />
+                            <FontAwesomeIcon icon="check" color={this.setColor(practiceMode.PRACTICE, StrCast(this.practiceMode))} size="1x" />
                         </div>
                     </Tooltip>
                 </div>
@@ -342,13 +349,10 @@ export class CollectionCarouselView extends CollectionSubView() {
                     <p
                         className="message"
                         onClick={e => {
-                            if (this.filterMessage) {
-                                this.layoutDoc.practiceMode = undefined;
+                            if (this.filterMessage || this.practiceMessage) {
+                                this.setPracticeMode(undefined);
                                 Doc.setDocFilter(this.layoutDoc, 'star', undefined, 'remove');
                             }
-                            this.childDocs.forEach(item => {
-                                item[this.practiceField] = undefined;
-                            });
                         }}>
                         {this.filterMessage || this.practiceMessage}
                     </p>
-- 
cgit v1.2.3-70-g09d2


From 14296a5e2b9726d449a9ddf59a3af0a6945e3cb7 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 25 Sep 2024 13:37:36 -0400
Subject: updated carousel to show same filter as in context menu bar.

---
 src/client/util/CurrentUserUtils.ts                |  2 -
 src/client/views/StyleProvider.tsx                 |  2 +-
 .../views/collections/CollectionCarouselView.tsx   | 44 ++++++++++++++++++----
 src/client/views/nodes/DocumentView.tsx            |  1 +
 4 files changed, 39 insertions(+), 10 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 3e7921a08..03975a5e7 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -981,12 +981,10 @@ pie title Minerals in my tap water
         doc.fontColor ?? (doc.fontColor = "black");
         doc.fontHighlight ?? (doc.fontHighlight = "");
         doc.defaultAclPrivate ?? (doc.defaultAclPrivate = false);
-        doc.savedFilters ?? (doc.savedFilters = new List<Doc>());
         doc.userBackgroundColor ?? (doc.userBackgroundColor = Colors.DARK_GRAY);
         doc.userVariantColor ?? (doc.userVariantColor = Colors.MEDIUM_BLUE);
         doc.userColor ?? (doc.userColor = Colors.LIGHT_GRAY);
         doc.userTheme ?? (doc.userTheme = ColorScheme.Dark);
-        doc.filterDocCount = 0;
         doc.treeView_FreezeChildren = "remove|add";
         doc.activePage = doc.activeDashboard === undefined ? 'home': doc.activePage;
 
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 3545afcee..16f6aa40b 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -318,7 +318,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                         : childFilters?.().filter(f => ClientUtils.IsRecursiveFilter(f) && f !== ClientUtils.noDragDocsFilter).length || childFiltersByRanges?.().length
                         ? 'orange' // 'inheritsFilter'
                         : undefined;
-                return !showFilterIcon ? null : (
+                return !showFilterIcon || props?.hideFilterStatus ? null : (
                     <div className="styleProvider-filter">
                         <Dropdown
                             type={Type.TERT}
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 43ab4462b..b6c3744e6 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -4,7 +4,7 @@ import { IReactionDisposer, action, computed, makeObservable, observable, reacti
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
-import { Doc, Opt } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
@@ -16,6 +16,7 @@ import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import './CollectionCarouselView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 import { TagItem } from '../TagsView';
+import { tickStep } from 'd3';
 
 enum cardMode {
     STAR = 'star',
@@ -180,9 +181,9 @@ export class CollectionCarouselView extends CollectionSubView() {
                 NativeHeight={returnZero}
                 fitWidth={undefined}
                 showTags={true}
+                hideFilterStatus={true}
                 containerViewPath={this.childContainerViewPath}
                 setContentViewBox={undefined}
-                ScreenToLocalTransform={this.childScreenToLocalXf}
                 onDoubleClickScript={this.onContentDoubleClick}
                 onClickScript={this.onContentClick}
                 isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
@@ -311,16 +312,45 @@ export class CollectionCarouselView extends CollectionSubView() {
     toggleFilterMode = () => Doc.setDocFilter(this.Document, 'tags', this.starField, 'check', true);
     setColor = (mode: practiceMode | cardMode, which: string) => (which === mode ? 'white' : 'light gray');
 
+    @computed get filterDoc() {
+        return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter');
+    }
+    filterHeight = () => NumCast(this.filterDoc?.height);
+    filterWidth = () => (!this.filterDoc ? 1 : (this.filterHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height));
     @computed get menu() {
         const curDoc = this.carouselItems?.[this.carouselIndex];
         return (
             <div className="carouselView-menu">
-                {/* NOTE: this should really show the same filter as in the workspace View menu, not just something hardwire for 'star' */}
-                <Tooltip title={Doc.hasDocFilter(this.Document, 'tags', this.starField) ? 'Show all cards' : 'Show only starred cards'}>
-                    <div key="back" className="carouselView-starFilter" onClick={e => this.toggleFilterMode()}>
-                        <FontAwesomeIcon icon="filter" color={Doc.hasDocFilter(this.Document, 'tags', this.starField) ? 'white' : 'lightgray'} size="1x" />
+                {!this.filterDoc ? (null) : (
+                    <div style={{ height: this.filterHeight(), width: this.filterHeight() }}>
+                        <DocumentView
+                            {...this._props}
+                            Document={this.filterDoc}
+                            TemplateDataDocument={undefined}
+                            LayoutTemplate={this._props.childLayoutTemplate}
+                            LayoutTemplateString={this._props.childLayoutString}
+                            renderDepth={this._props.renderDepth + 1}
+                            NativeWidth={returnZero}
+                            NativeHeight={returnZero}
+                            fitWidth={undefined}
+                            showTags={false}
+                            hideFilterStatus={true}
+                            containerViewPath={this.childContainerViewPath}
+                            setContentViewBox={undefined}
+                            onDoubleClickScript={this.onContentDoubleClick}
+                            onClickScript={this.onContentClick}
+                            isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
+                            isContentActive={this.isChildContentActive}
+                            hideCaptions={true}
+                            childFilters={this.childDocFilters}
+                            hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)}
+                            addDocument={this._props.addDocument}
+                            ScreenToLocalTransform={this.contentScreentToLocalXf}
+                            PanelWidth={this.filterWidth}
+                            PanelHeight={this.filterHeight}
+                        />
                     </div>
-                </Tooltip>
+                )}
                 <div className="carouselView-practiceModes" style={{ display: BoolCast(curDoc?._layout_isFlashcard) ? undefined : 'none' }}>
                     <Tooltip title="Practice flashcards using GPT">
                         <div key="back" className="carouselView-quiz" onClick={e => this.togglePracticeMode(practiceMode.QUIZ)}>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 884220722..b85cb22bb 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -68,6 +68,7 @@ export interface DocumentViewProps extends FieldViewSharedProps {
     contentPointerEvents?: Property.PointerEvents | undefined; // pointer events allowed for content of a document view.  eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents
     dontCenter?: 'x' | 'y' | 'xy';
     showTags?: boolean;
+    hideFilterStatus?: boolean;
     childHideDecorationTitle?: boolean;
     childHideResizeHandles?: boolean;
     childDragAction?: dropActionType; // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar.
-- 
cgit v1.2.3-70-g09d2


From ef2a1037a418ad9fa35d6c60feba914d828d5b84 Mon Sep 17 00:00:00 2001
From: alyssaf16 <alyssa_feinberg@brown.edu>
Date: Sun, 29 Sep 2024 15:54:49 -0400
Subject: clean up

---
 dash-speech-to-text-101b507c5e39.json              | 13 +++++
 .../views/collections/CollectionCarouselView.tsx   | 16 +------
 src/client/views/nodes/AudioBox.tsx                | 19 ++++++++
 src/client/views/nodes/ComparisonBox.tsx           | 55 +++++++++++++++-------
 src/client/views/nodes/ImageBox.tsx                | 49 ++++++-------------
 5 files changed, 86 insertions(+), 66 deletions(-)
 create mode 100644 dash-speech-to-text-101b507c5e39.json

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

diff --git a/dash-speech-to-text-101b507c5e39.json b/dash-speech-to-text-101b507c5e39.json
new file mode 100644
index 000000000..62a7c7eb0
--- /dev/null
+++ b/dash-speech-to-text-101b507c5e39.json
@@ -0,0 +1,13 @@
+{
+  "type": "service_account",
+  "project_id": "dash-speech-to-text",
+  "private_key_id": "101b507c5e394ec62f075d9416890bef0f597f7f",
+  "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC1BQ4iu7RNbiVF\nuIk/7+Zby4mMy0fde/WaCc4VxEQELK8ys2UtLU92L0npSLOyLACtLcNexdihoPMs\nHcQV7jxj9KRlk7tx3CPuVj8mn52WNIxX5ds5BszQqrfNQGObfdeFsROFzcA2ysbz\nEyfkqoFSQU5OUYANwEaF9oXc5nEkZaz5Zjjy34g8USAlySbB2sKI4o372Xi0slJo\nuPYWn93k/IA7HAmCOeooc2LTi+gu2KCnkXKevXdJCcK9Lntbm9RjoSUQiR/6R0Ls\nCTXY/g3G2FMJiraqxeI4HeAU2Pyw0sl86nHYTHh3/wNRxQkpLXROAc5qlPbMbTzp\nEu7kLI7PAgMBAAECggEADQyQk00SrKOXCOqfl5NUo3i4XATqE6ZJndFny3E63KOC\npwCq93xqW6usWVEbWxRe9seeFik5pOp0Unwm1CnjV+qdJea7aXQkHEdCsCcWIMDS\n/nccZtpS+MEwahm31Hqthv6tLe/CE8ZpscDHbG16Jb19cUSLcEepuHh9TTTzd7Bu\n9iaQGNC4Kpfvnl4zxr5RlooQFBXI/izSnHqlKW6yz/j3ntpabDFp7U3ZDe7cRpJa\nsDXwOXSULrg7LRcV9sPyC4UwfEVNFdJJg7QxWcmJe4QYdDCNnZ7JnKvMcBgZmvyH\nbJZosZGMB3QW/BdPHugk5dmT2Zm7j/0axwNdIflFwQKBgQDddrnGLQMqZwQU+Ag6\nTFjXdSXaGDKRvK4W+PvsNla8kin1TU3NHd+GUGPnBbR+bOF5Y1m0IYEHOOtgVB6E\ni+RzytKPM6ghMDAkzLTrB1R9a1Aj0b2OWQrdPjDgDw2BHcGVmwHsy3i2v97e+1hn\nduEFHZOawg2LALlXqxXb+TyYrwKBgQDRP7k9wcnju1JOcfMFcUH1gcG6CmKoZwVK\nl7cGcKYaTAZZl4W8cZVuC+l/LPH4xXQYg9FqGQ3R6NQQFQV3g83ym2nQeALluuXJ\n7vlFtViu89uiiBtg9DL1wsbYnbUjOd4CYoW9GWv5Jy99SSHp4XIIcy6MNYwuRZaq\nObx5dViz4QKBgBWqBpRPVO7x8uFGPi/NxicVi6VhFplRFsqigqphymxZ9AbOEB1P\nnc0a0hsIYNa5OflQ8baATInzVHsw32dh8/Ar2BswF3NfrmX7LHuNVQOL2uQWIW/q\nX/iym79fWZfW3FgROMf8Q4vFCz1O4yAD7hE9CnJqJz8AzCC6WMITCI17AoGANMv3\nWXXEEwn4woJN51mkYuXSk7Z7HjKgSynsrFtQDeKQ15o7zEh8g7/NsqEMjNCFcooQ\nFM0th4FPwJ8NOhKvrD86e7dMQ9rckA8UJlqTfdFZ4bCl99il4wy6T327bp/zPHbP\nd98qNzuD6ADD5ddUhn1JiWcZb7NvSJQ40gyhzQECgYB7rMpNCq46NNozGktHRhuo\nGJjHiGgSM6vVPkC7DVJbU4jU2RjI7IUL2wfFEWDKOovkts7Ugp/KEFVcff4neeuz\nMomaVWaaT3xIKENUIAzSpqcqshkqDJ/2kAknD30IJXAvGuaK49rql5Vab/Rh6zVW\nWH5hqTQk5IXR02zFSIXwXg==\n-----END PRIVATE KEY-----\n",
+  "client_email": "af010101@dash-speech-to-text.iam.gserviceaccount.com",
+  "client_id": "110674669436677368261",
+  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+  "token_uri": "https://oauth2.googleapis.com/token",
+  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/af010101%40dash-speech-to-text.iam.gserviceaccount.com",
+  "universe_domain": "googleapis.com"
+}
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 91a7c6514..04a0320fc 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -254,15 +254,6 @@ export class CollectionCarouselView extends CollectionSubView() {
     addFlashcard() {
         const newDoc = Docs.Create.ComparisonDocument('', { _layout_isFlashcard: true, _width: 300, _height: 300 });
         this.addDocument?.(newDoc);
-        // DocUtils.copyDragFactory(newDoc);
-        // this._props.addDocument?.();
-        // newDoc.layout = this.layoutDoc;
-        // newDoc.data = this.dataDoc;
-        // Doc.AddDocToList()
-        // this._props.parent._props.addDocument();
-        // this.childLayoutPairs.push({ newDoc.layout, newDoc.data});
-        // this._props.addDocument?.(newDoc);
-        // console.log('HERE');
     }
 
     @computed get buttons() {
@@ -275,11 +266,6 @@ export class CollectionCarouselView extends CollectionSubView() {
                             <FontAwesomeIcon icon="star" color={this.carouselItems[this.carouselIndex]?.[this.starField] ? 'yellow' : 'gray'} size="1x" />
                         </div>
                     </Tooltip>
-                    {/* <Tooltip title="add new flashcard to pile">
-                        <div key="add" className="carouselView-add" onClick={this.addFlashcard}>
-                            <FontAwesomeIcon icon="plus" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" />
-                        </div>
-                    </Tooltip> */}
                 </div>
                 <div key="back" className="carouselView-back" onClick={this.goback}>
                     <FontAwesomeIcon icon="chevron-left" size="2x" />
@@ -287,7 +273,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                 <div key="fwd" className="carouselView-fwd" onClick={this.advance}>
                     <FontAwesomeIcon icon="chevron-right" size="2x" />
                 </div>
-                {this.practiceMode ? (
+                {this.practiceMode == practiceMode.PRACTICE ? (
                     <div>
                         <Tooltip title="Incorrect. View again later.">
                             <div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)}>
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 63a126aec..8056ced1e 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -300,10 +300,29 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         console.log('RESPONSE: ' + response.data['transcription']);
     };
 
+    youtubeUpload = async () => {
+        console.log('Here');
+        const audio = {
+            file: 'Cd2ch4XV84s&t=373s',
+        };
+        const response = await axios.post('http://localhost:105/youtube/', audio, {
+            headers: {
+                'Content-Type': 'application/json',
+            },
+        });
+        console.log('RESPONSE: ' + response.data['transcription']);
+    };
+
     // context menu
     specificContextMenu = (): void => {
         const funcs: ContextMenuProps[] = [];
         // funcs.push({ description: 'Push info', event: this.pushInfo, icon: 'redo-alt' });
+
+        funcs.push({
+            description: 'Youtube',
+            event: this.youtubeUpload, // prettier-ignore
+            icon: 'expand-arrows-alt',
+        });
         funcs.push({
             description: (this.layoutDoc.hideAnchors ? "Don't hide" : 'Hide') + ' anchors',
             event: () => { this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors; }, // prettier-ignore
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index a410f4994..df127f0d5 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -246,8 +246,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     remDoc = (doc: Doc, which: string) => {
         if (this.dataDoc[which] === doc) {
             this._isEmpty = true;
-            // this.dataDoc[which] = 'empty';
-            console.log('HEREEEE');
             this.dataDoc[which] = undefined;
             return true;
         }
@@ -302,7 +300,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     this._animating = 'all 200ms';
                     // on click, animate slider movement to the targetWidth
                     this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth();
-                    // this.layoutDoc[this.clipHeightKey] = (targetWidth * 100) / this._props.PanelHeight();
 
                     setTimeout(
                         action(() => {
@@ -384,6 +381,24 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         console.log('RESPONSE: ' + response.data['transcription']);
     };
 
+    getYouTubeVideoId = (url: string) => {
+        const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=|\?v=)([^#\&\?]*).*/;
+        const match = url.match(regExp);
+        return match && match[2].length === 11 ? match[2] : null;
+    };
+
+    youtubeUpload = async () => {
+        const audio = {
+            file: this.getYouTubeVideoId(StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text)),
+        };
+        const response = await axios.post('http://localhost:105/youtube/', audio, {
+            headers: {
+                'Content-Type': 'application/json',
+            },
+        });
+        return response.data['transcription'];
+    };
+
     createFlashcardPile(collectionArr: Doc[], gpt: boolean) {
         const newCol = Docs.Create.CarouselDocument(collectionArr, {
             _width: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 250) + 50,
@@ -394,6 +409,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         newCol['x'] = this.layoutDoc['x'];
         newCol['y'] = NumCast(this.layoutDoc['y']) + 50;
         newCol.type_collection = 'carousel';
+        for (var i = 0; i < collectionArr.length; i++) {
+            DocCast(collectionArr[i])[DocData].embedContainer = newCol;
+        }
 
         if (gpt) {
             this._props.DocumentView?.()._props.addDocument?.(newCol);
@@ -405,6 +423,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         }
     }
 
+    /**
+     * Transfers the content of flashcards into a flashcard pile.
+     */
     gptFlashcardPile = async () => {
         var text = await this.askGPT(GPTCallType.STACK);
         var senArr = text?.split('Question: ');
@@ -441,17 +462,28 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 DocCast(newDoc)[DocData][this.fieldKey + '_1'] = textDoc1;
                 DocCast(newDoc)[DocData][this.fieldKey + '_0'] = Docs.Create.TextDocument(question[0].includes('Answer: ') ? question[0].split('Answer: ')[1] : question[1]);
             }
+
             collectionArr.push(newDoc);
         }
         this.createFlashcardPile(collectionArr, true);
     };
 
+    /**
+     * Calls GPT for each flashcard type.
+     */
     askGPT = async (callType: GPTCallType): Promise<string | undefined> => {
-        const questionText = 'Question: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text);
+        let 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;
         this._loading = true;
         const doc = DocCast(this.dataDoc[this.props.fieldKey + '_0']);
+
+        // if (true) {
+        //     const s = await this.youtubeUpload();
+
+        //     questionText = 'Question: ' + s;
+        // }
+
         if (callType == GPTCallType.CHATCARD) {
             if (StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '') {
                 this._loading = false;
@@ -461,7 +493,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         }
         try {
             console.log(queryText);
-            const res = await gptAPICall(callType == GPTCallType.QUIZ ? queryText : questionText, callType);
+            const res = await gptAPICall(questionText, GPTCallType.FLASHCARD);
             if (!res) {
                 console.error('GPT call failed');
                 return;
@@ -647,7 +679,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         const clearButton = (which: string) => (
             <Tooltip title={<div className="dash-tooltip">remove</div>}>
                 <div
-                    // style={{ position: 'relative', top: '0px', left: '10px' }}
                     ref={this._closeRef}
                     className={`clear-button ${which}`}
                     onPointerDown={e => this.closeDown(e, which)} // prevent triggering slider movement in registerSliding
@@ -660,18 +691,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             const whichDoc = DocCast(this.dataDoc[whichSlot]);
             const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc);
             const layoutString = targetDoc ? '' : this.testForTextFields(whichSlot);
-            // whichDoc['backgroundColor'] = this.layoutDoc['backgroundColor'];
 
             return targetDoc || layoutString ? (
-                // <MathJaxContext config={this.mathJaxConfig}>
-                //     <MathJax>
                 <>
                     <DocumentView
                         // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
                         fitWidth={undefined}
-                        NativeHeight={returnZero}
-                        NativeWidth={returnZero}
                         ignoreUsePath={layoutString ? true : undefined}
                         renderDepth={this.props.renderDepth + 1}
                         LayoutTemplateString={layoutString}
@@ -679,6 +705,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         containerViewPath={this._props.docViewPath}
                         moveDocument={whichSlot.endsWith('1') ? this.moveDoc1 : this.moveDoc2}
                         removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2}
+                        NativeWidth={this.layoutWidth}
+                        NativeHeight={this.layoutHeight}
                         isContentActive={() => this.childActive}
                         isDocumentActive={returnFalse}
                         dontSelect={returnTrue}
@@ -687,9 +715,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         hideLinkButton
                         pointerEvents={this.childActive ? undefined : returnNone}
                     />
-                    {/* </MathJax> */}
-                    {/* <div style={{ position: 'absolute', top: '-5px', left: '2px' }}>{layoutString ? null : clearButton(whichSlot)}</div> */}
-                    {/* </MathJaxContext> // placeholder image if doc is missingleft: `${NumCast(this.layoutDoc.width, 200) - 33}px` */}
                 </>
             ) : (
                 <div className="placeholder">
@@ -708,7 +733,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 }}
                 ref={ele => this.createDropTarget(ele, which, index)}>
                 {!this._isEmpty ? displayDoc(which) : null}
-                {/* {this.dataDoc[this.fieldKey + '_0'] !== 'empty' ? displayDoc(which) : null} */}
             </div>
         );
 
@@ -716,7 +740,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             const side = this.frontSide ? 1 : 0;
 
             // add text box to each side when comparison box is first created
-            // (!this.dataDoc[this.fieldKey + '_0'] && this.dataDoc[this._props.fieldKey + '_0'] !== 'empty')
             if (!this.dataDoc[this.fieldKey + '_0'] && !this._isEmpty) {
                 const dataSplit = StrCast(this.dataDoc.data).includes('Keyword: ') ? StrCast(this.dataDoc.data).split('Keyword: ') : StrCast(this.dataDoc.data).split('Answer: ');
                 const newDoc = Docs.Create.TextDocument(dataSplit[1]);
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index ed79888ae..45301247b 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -168,11 +168,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         Object.values(this._disposers).forEach(disposer => disposer?.());
     }
 
+    /**
+     * Find images from the unsplash api to add to flashcards.
+     */
     fetchImages = async () => {
         try {
             const { data } = await axios.get(`${API_URL}?query=${this._searchInput}&page=1&per_page=${1}&client_id=${process.env.VITE_API_KEY}`);
-            console.log('data', data);
-            console.log(data.results);
             const imageSnapshot = Docs.Create.ImageDocument(data.results[0].urls.small, {
                 _nativeWidth: Doc.NativeWidth(this.layoutDoc),
                 _nativeHeight: Doc.NativeHeight(this.layoutDoc),
@@ -326,8 +327,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         if (ctx) {
             this._imageRef && ctx.drawImage(this._imageRef, NumCast(this._marqueeref.current?.left) * scaling, NumCast(this._marqueeref.current?.top) * scaling, w, h, 0, 0, w, h);
         }
-        // canvas.style.zIndex = '2000000';
-        // document.body.appendChild(canvas);
         const blob = await ImageUtility.canvasToBlob(canvas);
         return ImageBox.selectUrlToBase64(blob);
     };
@@ -350,7 +349,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         Doc.SetNativeHeight(imageSnapshot[DocData], Doc.NativeHeight(this.layoutDoc));
         this._props.addDocument?.(imageSnapshot);
         DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' });
-        // link && (DocCast(link.link_anchor_2)[DocData].timecodeToHide = NumCast(DocCast(link.link_anchor_2).timecodeToShow) + 3);  // do we need to set an end time? should default to +0.1
         setTimeout(() => downX !== undefined && downY !== undefined && DocumentView.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, dropActionType.move, true));
     };
 
@@ -371,7 +369,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     pushInfo = async (quiz: quizMode, i?: string) => {
         this._quizMode = quiz;
         this._loading = true;
-        console.log('JHSDKFJHKSDJFHKSJDHFKJSDHFKJHSDKF');
 
         const img = {
             file: i ? i : this.paths[0],
@@ -383,10 +380,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 'Content-Type': 'application/json',
             },
         });
-
-        console.log('RESPONSE:');
-        console.log(response.data['boxes']);
-        console.log(response.data['text']);
         if (response.data['boxes'].length != 0) {
             this.createBoxes(response.data['boxes'], response.data['text']);
         } else {
@@ -394,6 +387,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
     };
 
+    /**
+     * Creates label boxes over text on the image to be filled in.
+     * @param boxes
+     * @param texts
+     */
     createBoxes = (boxes: [[[number, number]]], texts: [string]) => {
         const nscale = NumCast(this._props.PanelWidth()) * NumCast(this.layoutDoc._freeform_scale, 1);
         const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
@@ -405,17 +403,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
             const newCol = Docs.Create.LabelDocument({
                 _width: width,
-                //width * NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']),
                 _height: height,
-                //height * NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']),
                 _layout_fitWidth: true,
                 title: '',
-                // _layout_autoHeight: true,
             });
             const scaling = 1 / (this._props.NativeDimScaling?.() || 1);
             newCol.x = coords[0][0] + NumCast(this._marqueeref.current?.left) * scaling;
             newCol.y = coords[0][1] + NumCast(this._marqueeref.current?.top) * scaling;
-            // newCol[DocData].text_fontSize = height + 'px';
 
             newCol.zIndex = 1000;
             newCol.forceActive = true;
@@ -428,12 +422,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
     };
 
+    /**
+     * Create flashcards from an image.
+     */
     getImageDesc = async () => {
         this._loading = true;
         try {
             const hrefBase64 = await this.createCanvas();
             const response = await gptImageLabel(hrefBase64, 'Make flashcards out of this image with each question and answer labeled as "question" and "answer". Do not label each flashcard and do not include asterisks: ');
-            console.log(response);
             AnchorMenu.Instance.transferToFlashcard(response, NumCast(this.layoutDoc['x']), NumCast(this.layoutDoc['y']));
         } catch (error) {
             console.log('Error');
@@ -535,15 +531,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
     };
 
+    /**
+     * Check whether the contents of the label boxes on an image are correct.
+     */
     check = () => {
         this._loading = true;
         this._quizBoxes.forEach(async doc => {
             const input = StrCast(doc[DocData].title);
-            console.log('INP: ' + StrCast(input) + '; DOC: ' + StrCast(doc.quiz));
             if (this._quizMode == quizMode.SMART && input) {
                 const questionText = 'Question: What was labeled in this image?';
                 const rubricText = ' Rubric: ' + StrCast(doc.quiz);
-                // const queryText = 'RealAnswer: ' + StrCast(doc.quiz) + '. UserAnswer: ' + input + '.';
                 const queryText =
                     questionText +
                     ' UserAnswer: ' +
@@ -553,7 +550,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     '. One sentence and evaluate based on meaning, not wording. Provide a hex color at the beginning with a period after it on a scale of green (minor details missed) to red (big error) for how correct the answer is. Example: "#FFFFFF. Pasta is delicious."';
                 const response = await gptAPICall(queryText, GPTCallType.QUIZ);
                 const hexSent = this.extractHexAndSentences(response);
-                console.log(hexSent.hexNumber);
                 doc.quiz = hexSent.sentences?.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
                 doc.backgroundColor = '#' + hexSent.hexNumber;
             } else {
@@ -561,7 +557,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 doc.backgroundColor = match ? '#11c249' : '#eb2d2d';
             }
             doc.showQuiz = true;
-            // console.log(this.compareWords(input, StrCast(doc.quiz)) ? 'Match' : 'No Match');
         });
         this._loading = false;
     };
@@ -577,8 +572,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     exitQuizMode = () => {
         this._quizMode = quizMode.NONE;
         this._quizBoxes.forEach(doc => {
-            // this._props.removeDocument?.(DocCast(doc));
-            // this._props.DocumentView?.()._props.removeDocument?.(doc);
             this.removeDocument?.(doc);
         });
         this._quizBoxes = [];
@@ -595,7 +588,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         if (field) {
             const funcs: ContextMenuProps[] = [];
             const quizes: ContextMenuProps[] = [];
-            // funcs.push({ description: 'Create ai flashcards', event: () => this.getImageDesc(), icon: 'id-card' });
             quizes.push({
                 description: 'Smart Check',
                 event: this._quizMode == quizMode.NONE ? () => this.pushInfo(quizMode.SMART) : this.exitQuizMode,
@@ -606,19 +598,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 event: this._quizMode == quizMode.NONE ? () => this.pushInfo(quizMode.NORMAL) : this.exitQuizMode,
                 icon: 'pencil',
             });
-            // funcs.push({ description: 'Quiz Mode', subitems: optionItems, icon: 'eye' });
-            // funcs.push({
-            //     description: 'Quiz Mode',
-            //     event: !this._quizMode
-            //         ? () => this.pushInfo(false)
-            //         : () => {
-            //               this._quizMode = false;
-            //           },
-            //     icon: 'redo-alt',
-            // });
-            // funcs.push({ description: 'Get Text', event: this.check, icon: 'redo-alt' });
-            // funcs.push({ description: 'Get Labels2', event: this.getImageLabels2, icon: 'redo-alt' });
-            // funcs.push({ description: 'Get Labels', event: this.getImageLabels, icon: 'redo-alt' });
             funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' });
             funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' });
             funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' });
-- 
cgit v1.2.3-70-g09d2


From 39935af9a1204e1afc20659172e5a1a26f65e35c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 2 Oct 2024 12:54:29 -0400
Subject: missing import

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

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

diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index bb757cc43..97d86393b 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -4,7 +4,7 @@ import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
-import { Doc, Opt } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
-- 
cgit v1.2.3-70-g09d2


From 628ab7b5dc1b2d68469d07bc92bc086a44a9704e Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 2 Oct 2024 13:00:36 -0400
Subject: from last

---
 src/client/views/collections/CollectionCarouselView.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

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

diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 97d86393b..7a3f1ae33 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -343,12 +343,12 @@ export class CollectionCarouselView extends CollectionSubView() {
                 )}
                 <div className="carouselView-practiceModes" style={{ display: BoolCast(curDoc?._layout_isFlashcard) ? undefined : 'none' }}>
                     <Tooltip title="Practice flashcards using GPT">
-                        <div key="back" className="carouselView-quiz" onClick={e => this.togglePracticeMode(practiceMode.QUIZ)}>
+                        <div key="back" className="carouselView-quiz" onClick={() => this.togglePracticeMode(practiceMode.QUIZ)}>
                             <FontAwesomeIcon icon="file-pen" color={this.setColor(practiceMode.QUIZ, StrCast(this.practiceMode))} size="1x" />
                         </div>
                     </Tooltip>
                     <Tooltip title={this.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}>
-                        <div key="back" className="carouselView-practice" onClick={e => this.togglePracticeMode(practiceMode.PRACTICE)}>
+                        <div key="back" className="carouselView-practice" onClick={() => this.togglePracticeMode(practiceMode.PRACTICE)}>
                             <FontAwesomeIcon icon="check" color={this.setColor(practiceMode.PRACTICE, StrCast(this.practiceMode))} size="1x" />
                         </div>
                     </Tooltip>
-- 
cgit v1.2.3-70-g09d2


From fc3e2b18b9dad38920e1cec5e58bf9fbd06f4aaf Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 8 Oct 2024 03:30:21 -0400
Subject: changed carousel view to resize ui buttons based on screen scaling
 and document dimensions.  lint errors.

---
 src/client/util/Import & Export/ImageUtils.ts      |   1 -
 .../views/collections/CollectionCarouselView.scss  |  95 ++++++-----
 .../views/collections/CollectionCarouselView.tsx   | 176 +++++++++++----------
 src/client/views/collections/TabDocView.tsx        |   1 -
 src/client/views/nodes/LabelBox.tsx                |   2 +-
 src/client/views/pdf/PDFViewer.tsx                 |   2 -
 6 files changed, 142 insertions(+), 135 deletions(-)

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

diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index 266e05f08..8d4eefa7e 100644
--- a/src/client/util/Import & Export/ImageUtils.ts	
+++ b/src/client/util/Import & Export/ImageUtils.ts	
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/no-namespace */
 import { ClientUtils } from '../../../ClientUtils';
 import { Doc } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss
index af617254a..97952822e 100644
--- a/src/client/views/collections/CollectionCarouselView.scss
+++ b/src/client/views/collections/CollectionCarouselView.scss
@@ -40,7 +40,6 @@
 }
 .carouselView-back,
 .carouselView-fwd,
-.carouselView-star,
 .carouselView-remove,
 .carouselView-check,
 .carouselView-add {
@@ -58,16 +57,14 @@
     }
 }
 .carouselView-fwd {
-    top: 42.5%;
-    right: 20;
+    top: calc(50% - 15px);
+    right: 0;
+    transform-origin: right top;
 }
 .carouselView-back {
-    top: 42.5%;
-    left: 20;
-}
-.carouselView-star {
-    top: 0;
+    top: calc(50% - 15px);
     left: 0;
+    transform-origin: top left;
 }
 .carouselView-add {
     position: absolute;
@@ -75,62 +72,60 @@
     left: 0;
 }
 .carouselView-remove {
-    top: 80%;
     left: 52%;
 }
 .carouselView-check {
-    top: 80%;
     right: 52%;
 }
-.carouselView-quiz {
-    position: relative;
-    display: flex;
-    height: 20px;
-    align-items: center;
-    margin: auto;
-    &:hover {
-        color: white;
-    }
-}
-
-.carouselView-practice {
-    position: relative;
-    display: flex;
-    flex-direction: column;
-    height: 20px;
-    align-items: center;
-    margin: auto;
-    &:hover {
-        color: white;
-    }
-}
-.carouselView-starFilter {
-    position: relative;
-    display: flex;
-    height: 20px;
-    align-items: center;
-    &:hover {
-        color: white;
-    }
-}
-
-.carouselView-practiceModes {
-    width: 100%;
-    height: 40px;
-    display: flex;
-    flex-direction: column;
-}
 .carouselView-menu {
     position: absolute;
     flex-direction: column;
     align-items: center;
     display: flex;
-    top: 2px;
-    left: 2px;
+    top: 0px;
+    left: 0px;
     width: 30;
+    transform-origin: top left;
     border-radius: 5px;
     color: rgba(255, 255, 255, 0.5);
     background: rgba(0, 0, 0, 0.1);
+    .carouselView-practiceModes {
+        width: 100%;
+        display: flex;
+        flex-direction: column;
+        top: 0;
+        position: relative;
+        .carouselView-quiz {
+            position: relative;
+            display: flex;
+            height: 20px;
+            align-items: center;
+            margin: auto;
+            &:hover {
+                color: white;
+            }
+            & > svg {
+                height: 100%;
+                width: 100%;
+            }
+        }
+
+        .carouselView-practice {
+            position: relative;
+            display: flex;
+            flex-direction: column;
+            height: 20px;
+            align-items: center;
+            margin: auto;
+            &:hover {
+                color: white;
+            }
+            & > svg {
+                height: 100%;
+                width: 100%;
+            }
+        }
+    }
 }
 
 .carouselView-back:hover,
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 936226baf..559dcfe2a 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -7,7 +7,6 @@ import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
-import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { StyleProp } from '../StyleProp';
 import { TagItem } from '../TagsView';
@@ -36,6 +35,8 @@ export class CollectionCarouselView extends CollectionSubView() {
     get sideField()     { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore
     get starField()     { return "#star"; } // prettier-ignore
 
+    _sideBtnWidth = 35;
+
     _fadeTimer: NodeJS.Timeout | undefined;
 
     constructor(props: SubCollectionViewProps) {
@@ -155,7 +156,11 @@ export class CollectionCarouselView extends CollectionSubView() {
     onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
     onContentClick = () => ScriptCast(this.layoutDoc.onChildClick);
     captionWidth = () => this._props.PanelWidth() - 2 * this.marginX;
-    contentScreentToLocalXf = () => this._props.ScreenToLocalTransform().translate(-NumCast(this.layoutDoc.xMargin), -NumCast(this.layoutDoc.yMargin));
+    contentScreenToLocalXf = () =>
+        this._props
+            .ScreenToLocalTransform()
+            .translate(-NumCast(this.layoutDoc.xMargin), -NumCast(this.layoutDoc.yMargin))
+            .scale(this._props.NativeDimScaling?.() || 1);
 
     contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin);
 
@@ -168,9 +173,9 @@ export class CollectionCarouselView extends CollectionSubView() {
                 ? false
                 : undefined;
 
-    childScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
-
     renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => {
+        const screenScale = this.ScreenToLocalBoxXf().Scale;
+        const fitWidthScale = (NumCast(this.Document.width, 1) / NumCast(this.carouselItems[this.carouselIndex]?._width)) * (this._props.NativeDimScaling?.() || 1);
         return (
             <DocumentView
                 {...this._props}
@@ -195,10 +200,10 @@ export class CollectionCarouselView extends CollectionSubView() {
                 childFilters={this.childDocFilters}
                 hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)}
                 addDocument={this._props.addDocument}
-                ScreenToLocalTransform={this.contentScreentToLocalXf}
+                ScreenToLocalTransform={this.contentScreenToLocalXf}
                 PanelWidth={this.contentPanelWidth}
                 PanelHeight={this.contentPanelHeight}
-                xPadding={35}
+                xPadding={(this._sideBtnWidth * Math.min(this.maxWidgetScale, screenScale * screenScale)) / fitWidthScale} // padding shrinks based on screenScale to maintain its size, and then again by screenSize to get smaller
             />
         );
     };
@@ -259,46 +264,6 @@ export class CollectionCarouselView extends CollectionSubView() {
         );
     }
 
-    addFlashcard() {
-        const newDoc = Docs.Create.ComparisonDocument('', { _layout_isFlashcard: true, _width: 300, _height: 300 });
-        this.addDocument?.(newDoc);
-    }
-
-    @computed get buttons() {
-        if (!this.carouselItems?.[this.carouselIndex]) return null;
-        return (
-            <>
-                <div>
-                    {/* <Tooltip title="add new flashcard to pile">
-                        <div key="add" className="carouselView-add" onClick={this.addFlashcard}>
-                            <FontAwesomeIcon icon="plus" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" />
-                        </div>
-                    </Tooltip> */}
-                </div>
-                <div key="back" className="carouselView-back" onClick={this.goback}>
-                    <FontAwesomeIcon icon="chevron-left" size="2x" />
-                </div>
-                <div key="fwd" className="carouselView-fwd" onClick={this.advance}>
-                    <FontAwesomeIcon icon="chevron-right" size="2x" />
-                </div>
-                {this.practiceMode == practiceMode.PRACTICE ? (
-                    <div>
-                        <Tooltip title="Incorrect. View again later.">
-                            <div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)}>
-                                <FontAwesomeIcon icon="xmark" color="red" size="1x" />
-                            </div>
-                        </Tooltip>
-                        <Tooltip title="Correct">
-                            <div key="check" className="carouselView-check" onClick={e => this.setPracticeVal(e, practiceVal.CORRECT)}>
-                                <FontAwesomeIcon icon="check" color="green" size="1x" />
-                            </div>
-                        </Tooltip>
-                    </div>
-                ) : null}
-            </>
-        );
-    }
-
     togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode);
     toggleFilterMode = () => Doc.setDocFilter(this.Document, 'tags', this.starField, 'check', true);
     setColor = (mode: practiceMode | cardMode, which: string) => (which === mode ? 'white' : 'light gray');
@@ -306,50 +271,75 @@ export class CollectionCarouselView extends CollectionSubView() {
     @computed get filterDoc() {
         return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter');
     }
-    filterHeight = () => NumCast(this.filterDoc?.height);
+    filterHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this.ScreenToLocalBoxXf().Scale);
     filterWidth = () => (!this.filterDoc ? 1 : (this.filterHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height));
+
+    /**
+     * How much the content of the carousel view is being scaled based on its nesting and its fit-to-width settings
+     */
+    @computed get contentScaling() {
+        return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1);
+    }
+
+    /**
+     * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
+     */
+    @computed get maxWidgetScale() {
+        const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.1 * NumCast(this.Document.width, 1));
+        return Math.max(maxWidgetSize / this._sideBtnWidth, 1);
+    }
+    /**
+     * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
+     */
+    @computed get uiBtnScaleTransform() {
+        return `scale(${this.maxWidgetScale * Math.min(1, this.contentScaling)})`;
+    }
     @computed get menu() {
         const curDoc = this.carouselItems?.[this.carouselIndex];
         return (
-            <div className="carouselView-menu">
+            <div className="carouselView-menu" style={{ height: this.filterHeight(), width: this.filterHeight(), transform: this.uiBtnScaleTransform }}>
                 {!this.filterDoc ? null : (
-                    <div style={{ height: this.filterHeight(), width: this.filterHeight() }}>
-                        <DocumentView
-                            {...this._props}
-                            Document={this.filterDoc}
-                            TemplateDataDocument={undefined}
-                            LayoutTemplate={this._props.childLayoutTemplate}
-                            LayoutTemplateString={this._props.childLayoutString}
-                            renderDepth={this._props.renderDepth + 1}
-                            NativeWidth={returnZero}
-                            NativeHeight={returnZero}
-                            fitWidth={undefined}
-                            showTags={false}
-                            hideFilterStatus={true}
-                            containerViewPath={this.childContainerViewPath}
-                            setContentViewBox={undefined}
-                            onDoubleClickScript={this.onContentDoubleClick}
-                            onClickScript={this.onContentClick}
-                            isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
-                            isContentActive={this.isChildContentActive}
-                            hideCaptions={true}
-                            childFilters={this.childDocFilters}
-                            hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)}
-                            addDocument={this._props.addDocument}
-                            ScreenToLocalTransform={this.contentScreentToLocalXf}
-                            PanelWidth={this.filterWidth}
-                            PanelHeight={this.filterHeight}
-                        />
-                    </div>
+                    <DocumentView
+                        {...this._props}
+                        Document={this.filterDoc}
+                        TemplateDataDocument={undefined}
+                        LayoutTemplate={this._props.childLayoutTemplate}
+                        LayoutTemplateString={this._props.childLayoutString}
+                        renderDepth={this._props.renderDepth + 1}
+                        NativeWidth={returnZero}
+                        NativeHeight={returnZero}
+                        fitWidth={undefined}
+                        showTags={false}
+                        hideFilterStatus={true}
+                        containerViewPath={this.childContainerViewPath}
+                        setContentViewBox={undefined}
+                        onDoubleClickScript={this.onContentDoubleClick}
+                        onClickScript={this.onContentClick}
+                        isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
+                        isContentActive={this.isChildContentActive}
+                        hideCaptions={true}
+                        childFilters={this.childDocFilters}
+                        hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)}
+                        addDocument={this._props.addDocument}
+                        ScreenToLocalTransform={this.contentScreenToLocalXf}
+                        PanelWidth={this.filterWidth}
+                        PanelHeight={this.filterHeight}
+                    />
                 )}
-                <div className="carouselView-practiceModes" style={{ display: BoolCast(curDoc?._layout_isFlashcard) ? undefined : 'none' }}>
+                <div
+                    className="carouselView-practiceModes"
+                    style={{
+                        transformOrigin: `0px ${-this.filterHeight()}px`,
+                        transform: `scale(${Math.max(1, 1 / this.ScreenToLocalBoxXf().Scale / this.maxWidgetScale)})`,
+                        display: BoolCast(curDoc?._layout_isFlashcard) ? undefined : 'none',
+                    }}>
                     <Tooltip title="Practice flashcards using GPT">
-                        <div key="back" className="carouselView-quiz" onClick={() => this.togglePracticeMode(practiceMode.QUIZ)}>
+                        <div key="back" className="carouselView-quiz" style={{ width: this.filterWidth(), height: this.filterHeight() }} onClick={() => this.togglePracticeMode(practiceMode.QUIZ)}>
                             <FontAwesomeIcon icon="file-pen" color={this.setColor(practiceMode.QUIZ, StrCast(this.practiceMode))} size="1x" />
                         </div>
                     </Tooltip>
                     <Tooltip title={this.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}>
-                        <div key="back" className="carouselView-practice" onClick={() => this.togglePracticeMode(practiceMode.PRACTICE)}>
+                        <div key="back" className="carouselView-practice" style={{ width: this.filterWidth(), height: this.filterHeight() }} onClick={() => this.togglePracticeMode(practiceMode.PRACTICE)}>
                             <FontAwesomeIcon icon="check" color={this.setColor(practiceMode.PRACTICE, StrCast(this.practiceMode))} size="1x" />
                         </div>
                     </Tooltip>
@@ -357,6 +347,32 @@ export class CollectionCarouselView extends CollectionSubView() {
             </div>
         );
     }
+    @computed get buttons() {
+        return (
+            <>
+                <div key="back" className="carouselView-back" style={{ transform: this.uiBtnScaleTransform }} onClick={this.goback}>
+                    <FontAwesomeIcon icon="chevron-left" size="2x" />
+                </div>
+                <div key="fwd" className="carouselView-fwd" style={{ transform: this.uiBtnScaleTransform }} onClick={this.advance}>
+                    <FontAwesomeIcon icon="chevron-right" size="2x" />
+                </div>
+                {this.practiceMode == practiceMode.PRACTICE ? (
+                    <div style={{ transform: this.uiBtnScaleTransform, bottom: `${this._sideBtnWidth}px`, height: `${this._sideBtnWidth}px`, position: 'absolute', width: `100%` }}>
+                        <Tooltip title="Incorrect. View again later.">
+                            <div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)}>
+                                <FontAwesomeIcon icon="xmark" color="red" size="1x" />
+                            </div>
+                        </Tooltip>
+                        <Tooltip title="Correct">
+                            <div key="check" className="carouselView-check" onClick={e => this.setPracticeVal(e, practiceVal.CORRECT)}>
+                                <FontAwesomeIcon icon="check" color="green" size="1x" />
+                            </div>
+                        </Tooltip>
+                    </div>
+                ) : null}
+            </>
+        );
+    }
 
     render() {
         return (
@@ -388,7 +404,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                     )}
                 </div>
                 {!this.Document._chromeHidden ? this.menu : null}
-                {!this.Document._chromeHidden ? this.buttons : null}
+                {!this.Document._chromeHidden && this.carouselItems?.[this.carouselIndex] ? this.buttons : null}
             </div>
         );
     }
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index f56ea9d76..5bfdee1f5 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -478,7 +478,6 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     componentDidMount() {
         new ResizeObserver(
             action(entries => {
-                // eslint-disable-next-line no-restricted-syntax
                 for (const entry of entries) {
                     this._panelWidth = entry.contentRect.width;
                     this._panelHeight = entry.contentRect.height;
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 696ba5697..94a9541f2 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -138,7 +138,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         onBlur={() => {
                             this.dataDoc[this.fieldKey] = this._divRef?.innerText ?? '';
                         }}
-                        contentEditable={this._props.onClickScript?.() ? false : true}
+                        contentEditable={this._props.onClickScript?.() ? undefined : true}
                         ref={r => {
                             this._divRef = r;
                             this.fitTextToBox(r);
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index fc74a480e..5743b17c6 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -498,8 +498,6 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
         // }
         this._loading = true;
         try {
-            if (this._selectionText === '') {
-            }
             const res = await gptAPICall(queryText, GPTCallType.FLASHCARD);
 
             console.log(res);
-- 
cgit v1.2.3-70-g09d2


From 972839216c14baa5c9eaf80e1fb2fb2694bbb72c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 8 Oct 2024 22:51:46 -0400
Subject: modified how buttons are laid out on carousel and comparison views so
 that text boxes can reflow around them.  extracted flashcard pratice into its
 own component and applied it to carousel3D and carousel

---
 src/client/views/PropertiesView.tsx                |   7 +-
 .../collections/CollectionCarousel3DView.scss      |   1 +
 .../views/collections/CollectionCarousel3DView.tsx |  70 ++++-
 .../views/collections/CollectionCarouselView.scss  |  86 +-----
 .../views/collections/CollectionCarouselView.tsx   | 316 ++++++---------------
 .../views/collections/FlashcardPracticeUI.scss     |  64 +++++
 .../views/collections/FlashcardPracticeUI.tsx      | 172 +++++++++++
 src/client/views/nodes/ComparisonBox.scss          |  15 +
 src/client/views/nodes/ComparisonBox.tsx           |  92 ++++--
 src/client/views/nodes/FieldView.tsx               |   1 +
 .../nodes/formattedText/FormattedTextBox.scss      |   6 -
 .../views/nodes/formattedText/FormattedTextBox.tsx |  16 +-
 12 files changed, 475 insertions(+), 371 deletions(-)
 create mode 100644 src/client/views/collections/FlashcardPracticeUI.scss
 create mode 100644 src/client/views/collections/FlashcardPracticeUI.tsx

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

diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 371d34173..442dab671 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -139,6 +139,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         Object.values(this._disposers).forEach(disposer => disposer?.());
     }
 
+    @computed get isText() {
+        return this.selectedDoc?.type === DocumentType.RTF;
+    }
     @computed get isInk() {
         return this.selectedDoc?.type === DocumentType.INK;
     }
@@ -1199,8 +1202,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
             // prettier-ignore
             <div className="transform-editor">
                 {!this.isStack ? null : this.getNumber('Gap', ' px', 0, 200, NumCast(this.selectedDoc!.gridGap), this.setVal((doc: Doc, val: number) => { doc.gridGap = val; })) }
-                {!this.isStack ? null : this.getNumber('xMargin', ' px', 0, 500, NumCast(this.selectedDoc!.xMargin), this.setVal((doc: Doc, val: number) => { doc.xMargin = val; })) }
-                {!this.isStack ? null : this.getNumber('yMargin', ' px', 0, 500, NumCast(this.selectedDoc!.yMargin), this.setVal((doc: Doc, val: number) => { doc.yMargin = val; })) }
+                {!this.isStack && !this.isText? null : this.getNumber('xMargin', ' px', 0, 500, NumCast(this.selectedDoc!.xMargin), this.setVal((doc: Doc, val: number) => { doc.xMargin = val; })) }
+                {!this.isStack && !this.isText? null : this.getNumber('yMargin', ' px', 0, 500, NumCast(this.selectedDoc!.yMargin), this.setVal((doc: Doc, val: number) => { doc.yMargin = val; })) }
                 {!this.isGroup ? null : this.getNumber('Padding', ' px', 0, 500, NumCast(this.selectedDoc!.xPadding), this.setVal((doc: Doc, val: number) => { doc.xPadding = doc.yPadding = val; })) }
                 {this.isInk ? this.controlPointsButton : null}
                 {this.getNumber('Width', ' px', 0, Math.max(1000, this.shapeWid), this.shapeWid, this.setVal((doc: Doc, val:number) => {this.shapeWid = val}), 1000, 1)}
diff --git a/src/client/views/collections/CollectionCarousel3DView.scss b/src/client/views/collections/CollectionCarousel3DView.scss
index a556d0fa7..42e112906 100644
--- a/src/client/views/collections/CollectionCarousel3DView.scss
+++ b/src/client/views/collections/CollectionCarousel3DView.scss
@@ -4,6 +4,7 @@
     position: relative;
     background-color: white;
     overflow: hidden;
+    display: flex;
 }
 
 .carousel-wrapper {
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index c5da8e037..1583f0e0c 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { computed, makeObservable } from 'mobx';
+import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { returnZero } from '../../../ClientUtils';
@@ -9,12 +9,13 @@ import { Id } from '../../../fields/FieldSymbols';
 import { DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DragManager } from '../../util/DragManager';
+import { Transform } from '../../util/Transform';
 import { StyleProp } from '../StyleProp';
 import { DocumentView } from '../nodes/DocumentView';
 import { FocusViewOptions } from '../nodes/FocusViewOptions';
 import './CollectionCarousel3DView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
-import { Transform } from '../../util/Transform';
+import { FlashcardPracticeUI } from './FlashcardPracticeUI';
 
 // eslint-disable-next-line @typescript-eslint/no-require-imports
 const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss');
@@ -24,6 +25,9 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     @computed get scrollSpeed() {
         return this.layoutDoc._autoScrollSpeed ? NumCast(this.layoutDoc._autoScrollSpeed) : 1000; // default scroll speed
     }
+    _sideBtnWidth = 35;
+    @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
+
     constructor(props: SubCollectionViewProps) {
         super(props);
         makeObservable(this);
@@ -43,7 +47,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     };
 
     @computed get carouselItems() {
-        return this.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK);
+        return this.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK).filter(pair => !this._filterFunc?.(pair.layout));
     }
 
     centerScale = Number(CAROUSEL3D_CENTER_SCALE);
@@ -53,22 +57,17 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
     isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive();
     isChildContentActive = () => !!this.isContentActive();
+    contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
     childScreenLeftToLocal = () =>
-        this._props
-            .ScreenToLocalTransform()
-            .scale(this._props.NativeDimScaling?.() || 1)
+        this.contentScreenToLocalXf()
             .translate(-(this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight())
             .scale(1 / this.sideScale);
     childScreenRightToLocal = () =>
-        this._props
-            .ScreenToLocalTransform()
-            .scale(this._props.NativeDimScaling?.() || 1)
+        this.contentScreenToLocalXf()
             .translate(-2 * this.panelWidth() - (this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight())
             .scale(1 / this.sideScale);
     childCenterScreenToLocal = () =>
-        this._props
-            .ScreenToLocalTransform()
-            .scale(this._props.NativeDimScaling?.() || 1)
+        this.contentScreenToLocalXf()
             .translate(
                 -this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, // Focused Doc is shifted right by 1/3 panel width then left by increased size percent of center * 1/2 * panel width / 3
                 -((Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2
@@ -119,7 +118,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
 
     changeSlide = (direction: number) => {
         DocumentView.DeselectAll();
-        this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + direction + this.carouselItems.length) % this.carouselItems.length;
+        this.layoutDoc._carousel_index = !this.curDoc() ? 0 : (NumCast(this.layoutDoc._carousel_index) + direction + this.carouselItems.length) % (this.carouselItems.length || 1);
     };
 
     onArrowClick = (direction: number) => {
@@ -192,6 +191,35 @@ export class CollectionCarousel3DView extends CollectionSubView() {
         return this.panelWidth() * (1 - index);
     }
 
+    /**
+     * How much the content of the carousel view is being scaled based on its nesting and its fit-to-width settings
+     */
+    @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore
+
+    /**
+     * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
+     */
+    @computed get maxWidgetScale() {
+        const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.1 * NumCast(this.layoutDoc.width, 1));
+        return Math.max(maxWidgetSize / this._sideBtnWidth, 1);
+    }
+    /**
+     * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
+     */
+    @computed get uiBtnScaleTransform() {  return this.maxWidgetScale * Math.min(1, this.contentScaling); } // prettier-ignore
+    screenXPadding = () => (this.uiBtnScaleTransform * this._sideBtnWidth - NumCast(this.layoutDoc.xMargin)) / this._props.ScreenToLocalTransform().Scale;
+
+    docViewProps = () => ({
+        ...this._props, //
+        isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive,
+        isContentActive: this.isChildContentActive,
+        ScreenToLocalTransform: this.contentScreenToLocalXf,
+    });
+    carouselItemsFunc = () => this.carouselItems.map(pair => pair.layout);
+    @action setFilterFunc = (func?: (doc: Doc) => boolean) => { this._filterFunc = func; }; // prettier-ignore
+    answered = (correct: boolean) => (!correct || !this.curDoc()) && this.changeSlide(1);
+    curDoc = () => this.carouselItems[NumCast(this.layoutDoc._carousel_index)]?.layout;
+
     render() {
         return (
             <div
@@ -206,6 +234,22 @@ export class CollectionCarousel3DView extends CollectionSubView() {
                 </div>
                 {this.buttons}
                 <div className="dot-bar">{this.dots}</div>
+                <FlashcardPracticeUI
+                    setFilterFunc={this.setFilterFunc}
+                    fieldKey={this.fieldKey}
+                    sideBtnWidth={this._sideBtnWidth}
+                    carouselItems={this.carouselItemsFunc}
+                    childDocs={this.childDocs}
+                    advance={this.answered}
+                    curDoc={this.curDoc}
+                    practiceBtnOffset={this._sideBtnWidth * 4}
+                    layoutDoc={this.layoutDoc}
+                    maxWidgetScale={this.maxWidgetScale}
+                    uiBtnScaleTransform={this.uiBtnScaleTransform}
+                    ScreenToLocalBoxXf={this.ScreenToLocalBoxXf}
+                    renderDepth={this._props.renderDepth}
+                    docViewProps={this.docViewProps}
+                />
             </div>
         );
     }
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss
index 97952822e..757072453 100644
--- a/src/client/views/collections/CollectionCarouselView.scss
+++ b/src/client/views/collections/CollectionCarouselView.scss
@@ -12,23 +12,10 @@
         display: inline-block;
         width: 100%;
         user-select: none;
+        position: absolute;
+        top: 0;
+        left: 0;
     }
-    .message {
-        justify-content: center;
-        align-items: center;
-        display: flex;
-        height: 60%;
-        z-index: -1;
-        // margin: 15px;
-    }
-}
-
-.collectionCarouselView-addFlashcards {
-    justify-content: center;
-    align-items: center;
-    height: 100%;
-    z-index: -1;
-    pointer-events: none;
 }
 .collectionCarouselView-recentlyMissed {
     color: red;
@@ -39,10 +26,7 @@
     pointer-events: none;
 }
 .carouselView-back,
-.carouselView-fwd,
-.carouselView-remove,
-.carouselView-check,
-.carouselView-add {
+.carouselView-fwd {
     position: absolute;
     display: flex;
     width: 30;
@@ -66,68 +50,6 @@
     left: 0;
     transform-origin: top left;
 }
-.carouselView-add {
-    position: absolute;
-    bottom: 0;
-    left: 0;
-}
-.carouselView-remove {
-    left: 52%;
-}
-.carouselView-check {
-    right: 52%;
-}
-.carouselView-menu {
-    position: absolute;
-    flex-direction: column;
-    align-items: center;
-    display: flex;
-    top: 0px;
-    left: 0px;
-    width: 30;
-    transform-origin: top left;
-    border-radius: 5px;
-    color: rgba(255, 255, 255, 0.5);
-    background: rgba(0, 0, 0, 0.1);
-    .carouselView-practiceModes {
-        width: 100%;
-        display: flex;
-        flex-direction: column;
-        top: 0;
-        position: relative;
-        .carouselView-quiz {
-            position: relative;
-            display: flex;
-            height: 20px;
-            align-items: center;
-            margin: auto;
-            &:hover {
-                color: white;
-            }
-            & > svg {
-                height: 100%;
-                width: 100%;
-            }
-        }
-
-        .carouselView-practice {
-            position: relative;
-            display: flex;
-            flex-direction: column;
-            height: 20px;
-            align-items: center;
-            margin: auto;
-            &:hover {
-                color: white;
-            }
-            & > svg {
-                height: 100%;
-                width: 100%;
-            }
-        }
-    }
-}
-
 .carouselView-back:hover,
 .carouselView-fwd:hover {
     background: lightgray;
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 559dcfe2a..64ddaac79 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -1,52 +1,35 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@mui/material';
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
-import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { Doc, Opt } from '../../../fields/Doc';
 import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DragManager } from '../../util/DragManager';
 import { StyleProp } from '../StyleProp';
-import { TagItem } from '../TagsView';
 import { DocumentView } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import './CollectionCarouselView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
+import { FlashcardPracticeUI } from './FlashcardPracticeUI';
 
-enum cardMode {
-    STAR = 'star',
-    ALL = 'all',
-}
-enum practiceMode {
-    PRACTICE = 'practice',
-    QUIZ = 'quiz',
-}
-enum practiceVal {
-    MISSED = 'missed',
-    CORRECT = 'correct',
-}
 @observer
 export class CollectionCarouselView extends CollectionSubView() {
     private _dropDisposer?: DragManager.DragDropDisposer;
-    get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore
-    get sideField()     { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore
-    get starField()     { return "#star"; } // prettier-ignore
-
-    _sideBtnWidth = 35;
 
     _fadeTimer: NodeJS.Timeout | undefined;
+    _sideBtnWidth = 35;
+    @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
+    @observable _last_index = this.carouselIndex;
+    @observable _last_opacity = 1;
 
     constructor(props: SubCollectionViewProps) {
         super(props);
         makeObservable(this);
     }
 
-    @observable _last_index = this.carouselIndex;
-    @observable _last_opacity = 1;
-
     componentWillUnmount() {
         this._dropDisposer?.();
     }
@@ -58,38 +41,31 @@ export class CollectionCarouselView extends CollectionSubView() {
         }
     };
 
-    @computed get practiceMode() {
-        return this.childDocs.some(doc => doc._layout_isFlashcard) ? StrCast(this.layoutDoc.practiceMode) : '';
-    }
-    @computed get practiceMessage() {
-        const cardCount = this.carouselItems.length;
-        if (this.practiceMode) {
-            if (!Doc.hasDocFilter(this.layoutDoc, 'tags', Doc.FilterAny) && !cardCount) {
-                return 'Finished! Click here to view all flashcards.';
-            }
-        }
-        return '';
-    }
-
-    @computed get filterMessage() {
-        const cardCount = this.carouselItems.length;
-        if (!this.practiceMessage) {
-            if (Doc.hasDocFilter(this.layoutDoc, 'tags', Doc.FilterAny) && !cardCount) {
-                return 'No tagged items. Click here to view all flash cards.';
-            }
-            if (this.practiceMode) {
-                if (!cardCount) return 'No flashcards to show! Click here to leave practice mode';
-            }
-        }
-        return '';
-    }
-    @computed get marginX()       { return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore
+    @computed get captionMarginX(){ return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore
     @computed get carouselIndex() { return NumCast(this.layoutDoc._carousel_index) % this.carouselItems.length; } // prettier-ignore
     @computed get carouselItems() { return this.childDocs
         .filter(doc => doc.type !== DocumentType.LINK)
-        .filter(doc => !this.practiceMode || (BoolCast(doc?._layout_isFlashcard) && doc[this.practiceField] !== practiceVal.CORRECT))// show only cards that aren't marked as correct
+        .filter(doc => !this._filterFunc?.(doc))
     } // prettier-ignore
 
+    /**
+     * How much the content of the carousel view is being scaled based on its nesting and its fit-to-width settings
+     */
+    @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore
+
+    /**
+     * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
+     */
+    @computed get maxWidgetScale() {
+        const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.1 * NumCast(this.layoutDoc.width, 1));
+        return Math.max(maxWidgetSize / this._sideBtnWidth, 1);
+    }
+    /**
+     * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
+     */
+    @computed get uiBtnScaleTransform() {  return this.maxWidgetScale * Math.min(1, this.contentScaling); } // prettier-ignore
+    screenXPadding = () => (this.uiBtnScaleTransform * this._sideBtnWidth - NumCast(this.layoutDoc.xMargin)) / this._props.ScreenToLocalTransform().Scale;
+
     /**
      * Move forward or backward the specified number of Docs
      * @param dir signed number indicating Docs to move forward or backward
@@ -102,8 +78,8 @@ export class CollectionCarouselView extends CollectionSubView() {
     /**
      * Goes to the next Doc in the stack subject to the currently selected filter option.
      */
-    advance = (e: React.MouseEvent) => {
-        e.stopPropagation();
+    advance = (e?: React.MouseEvent) => {
+        e?.stopPropagation();
         this.move(1);
     };
 
@@ -115,55 +91,23 @@ export class CollectionCarouselView extends CollectionSubView() {
         this.move(-1);
     };
 
-    /*
-     * Toggles whether the 'star' metadata field is set on the current Doc
-     */
-    toggleStar = (e: React.MouseEvent) => {
-        e.stopPropagation();
-        const curDoc = this.carouselItems[this.carouselIndex];
-        if (curDoc) {
-            if (TagItem.docHasTag(curDoc, this.starField)) TagItem.removeTagFromDoc(curDoc, this.starField);
-            else TagItem.addTagToDoc(curDoc, this.starField);
-        }
-    };
-
-    /*
-     * Sets a flashcard to either missed or correct depending on if they got the question right in practice mode.
-     */
-    setPracticeVal = (e: React.MouseEvent, val: string) => {
-        e.stopPropagation();
-        const curDoc = this.carouselItems[this.carouselIndex];
-        curDoc && (curDoc[this.practiceField] = val);
-        this.advance(e);
-    };
-
-    /**
-     * Sets the practice mode answer style for flashcards
-     * @param mode practiceMode or undefined for no practice
-     */
-    setPracticeMode = (mode: practiceMode | undefined) => {
-        this.layoutDoc.practiceMode = mode;
-        this.carouselItems?.map(doc => (doc[this.practiceField] = undefined));
-        if (mode === practiceMode.QUIZ) this.carouselItems?.map(doc => (doc[this.sideField] = undefined));
-    };
+    curDoc = () => this.carouselItems[this.carouselIndex];
 
     captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string) => {
         // first look for properties on the document in the carousel, then fallback to properties on the container
         const childValue = doc?.['caption_' + property] ? this._props.styleProvider?.(doc, captionProps, property) : undefined;
         return childValue ?? this._props.styleProvider?.(this.layoutDoc, captionProps, property);
     };
+    contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin);
     contentPanelHeight = () => this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0) - 2 * NumCast(this.layoutDoc.yMargin);
     onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
     onContentClick = () => ScriptCast(this.layoutDoc.onChildClick);
-    captionWidth = () => this._props.PanelWidth() - 2 * this.marginX;
+    captionWidth = () => this._props.PanelWidth() - 2 * this.captionMarginX;
     contentScreenToLocalXf = () =>
         this._props
             .ScreenToLocalTransform()
             .translate(-NumCast(this.layoutDoc.xMargin), -NumCast(this.layoutDoc.yMargin))
             .scale(this._props.NativeDimScaling?.() || 1);
-
-    contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin);
-
     isChildContentActive = () =>
         this._props.isContentActive?.() === false
             ? false
@@ -172,10 +116,7 @@ export class CollectionCarouselView extends CollectionSubView() {
               : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
                 ? false
                 : undefined;
-
     renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => {
-        const screenScale = this.ScreenToLocalBoxXf().Scale;
-        const fitWidthScale = (NumCast(this.Document.width, 1) / NumCast(this.carouselItems[this.carouselIndex]?._width)) * (this._props.NativeDimScaling?.() || 1);
         return (
             <DocumentView
                 {...this._props}
@@ -203,7 +144,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                 ScreenToLocalTransform={this.contentScreenToLocalXf}
                 PanelWidth={this.contentPanelWidth}
                 PanelHeight={this.contentPanelHeight}
-                xPadding={(this._sideBtnWidth * Math.min(this.maxWidgetScale, screenScale * screenScale)) / fitWidthScale} // padding shrinks based on screenScale to maintain its size, and then again by screenSize to get smaller
+                screenXPadding={this.screenXPadding}
             />
         );
     };
@@ -214,7 +155,7 @@ export class CollectionCarouselView extends CollectionSubView() {
         const fadeTime = 500;
         const lastDoc = this.carouselItems?.[this._last_index];
         return !lastDoc || this.carouselIndex === this._last_index ? null : (
-            <div className="collectionCarouselView-image" style={{ opacity: this._last_opacity, position: 'absolute', top: 0, left: 0, transition: `opacity ${fadeTime}ms` }}>
+            <div className="collectionCarouselView-image" style={{ opacity: this._last_opacity, transition: `opacity ${fadeTime}ms` }}>
                 {this.renderDoc(
                     lastDoc,
                     false, // hide captions if the carousel is configured to show the captions
@@ -235,15 +176,18 @@ export class CollectionCarouselView extends CollectionSubView() {
             </div>
         );
     }
+    @computed get renderedDoc() {
+        const carouselShowsCaptions = StrCast(this.layoutDoc._layout_showCaption);
+        return this.renderDoc(this.curDoc(), !!carouselShowsCaptions);
+    }
+
     @computed get content() {
-        const index = this.carouselIndex;
-        const curDoc = this.carouselItems?.[index];
         const captionProps = { ...this._props, NativeScaling: returnOne, PanelWidth: this.captionWidth, fieldKey: 'caption', setHeight: undefined, setContentView: undefined };
         const carouselShowsCaptions = StrCast(this.layoutDoc._layout_showCaption);
-        return !curDoc ? null : (
+        return !this.curDoc() ? null : (
             <>
                 <div className="collectionCarouselView-image" key="image">
-                    {this.renderDoc(curDoc, !!carouselShowsCaptions)}
+                    {this.renderedDoc}
                     {this.overlay}
                 </div>
                 {!carouselShowsCaptions ? null : (
@@ -253,158 +197,70 @@ export class CollectionCarouselView extends CollectionSubView() {
                         onWheel={StopEvent}
                         style={{
                             borderRadius: this._props.styleProvider?.(this.layoutDoc, captionProps, StyleProp.BorderRounding) as string,
-                            marginRight: this.marginX,
-                            marginLeft: this.marginX,
-                            width: `calc(100% - ${this.marginX * 2}px)`,
+                            marginRight: this.captionMarginX,
+                            marginLeft: this.captionMarginX,
+                            width: `calc(100% - ${this.captionMarginX * 2}px)`,
                         }}>
-                        <FormattedTextBox key={index} xPadding={10} yPadding={10} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={curDoc} TemplateDataDocument={undefined} />
+                        <FormattedTextBox xPadding={10} yPadding={10} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={this.curDoc()} TemplateDataDocument={undefined} />
                     </div>
                 )}
             </>
         );
     }
 
-    togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode);
-    toggleFilterMode = () => Doc.setDocFilter(this.Document, 'tags', this.starField, 'check', true);
-    setColor = (mode: practiceMode | cardMode, which: string) => (which === mode ? 'white' : 'light gray');
-
-    @computed get filterDoc() {
-        return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter');
-    }
-    filterHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this.ScreenToLocalBoxXf().Scale);
-    filterWidth = () => (!this.filterDoc ? 1 : (this.filterHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height));
-
-    /**
-     * How much the content of the carousel view is being scaled based on its nesting and its fit-to-width settings
-     */
-    @computed get contentScaling() {
-        return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1);
-    }
-
-    /**
-     * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
-     */
-    @computed get maxWidgetScale() {
-        const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.1 * NumCast(this.Document.width, 1));
-        return Math.max(maxWidgetSize / this._sideBtnWidth, 1);
-    }
-    /**
-     * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
-     */
-    @computed get uiBtnScaleTransform() {
-        return `scale(${this.maxWidgetScale * Math.min(1, this.contentScaling)})`;
-    }
-    @computed get menu() {
-        const curDoc = this.carouselItems?.[this.carouselIndex];
-        return (
-            <div className="carouselView-menu" style={{ height: this.filterHeight(), width: this.filterHeight(), transform: this.uiBtnScaleTransform }}>
-                {!this.filterDoc ? null : (
-                    <DocumentView
-                        {...this._props}
-                        Document={this.filterDoc}
-                        TemplateDataDocument={undefined}
-                        LayoutTemplate={this._props.childLayoutTemplate}
-                        LayoutTemplateString={this._props.childLayoutString}
-                        renderDepth={this._props.renderDepth + 1}
-                        NativeWidth={returnZero}
-                        NativeHeight={returnZero}
-                        fitWidth={undefined}
-                        showTags={false}
-                        hideFilterStatus={true}
-                        containerViewPath={this.childContainerViewPath}
-                        setContentViewBox={undefined}
-                        onDoubleClickScript={this.onContentDoubleClick}
-                        onClickScript={this.onContentClick}
-                        isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
-                        isContentActive={this.isChildContentActive}
-                        hideCaptions={true}
-                        childFilters={this.childDocFilters}
-                        hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)}
-                        addDocument={this._props.addDocument}
-                        ScreenToLocalTransform={this.contentScreenToLocalXf}
-                        PanelWidth={this.filterWidth}
-                        PanelHeight={this.filterHeight}
-                    />
-                )}
-                <div
-                    className="carouselView-practiceModes"
-                    style={{
-                        transformOrigin: `0px ${-this.filterHeight()}px`,
-                        transform: `scale(${Math.max(1, 1 / this.ScreenToLocalBoxXf().Scale / this.maxWidgetScale)})`,
-                        display: BoolCast(curDoc?._layout_isFlashcard) ? undefined : 'none',
-                    }}>
-                    <Tooltip title="Practice flashcards using GPT">
-                        <div key="back" className="carouselView-quiz" style={{ width: this.filterWidth(), height: this.filterHeight() }} onClick={() => this.togglePracticeMode(practiceMode.QUIZ)}>
-                            <FontAwesomeIcon icon="file-pen" color={this.setColor(practiceMode.QUIZ, StrCast(this.practiceMode))} size="1x" />
-                        </div>
-                    </Tooltip>
-                    <Tooltip title={this.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}>
-                        <div key="back" className="carouselView-practice" style={{ width: this.filterWidth(), height: this.filterHeight() }} onClick={() => this.togglePracticeMode(practiceMode.PRACTICE)}>
-                            <FontAwesomeIcon icon="check" color={this.setColor(practiceMode.PRACTICE, StrCast(this.practiceMode))} size="1x" />
-                        </div>
-                    </Tooltip>
-                </div>
-            </div>
-        );
-    }
-    @computed get buttons() {
-        return (
+    @computed get navButtons() {
+        return this.Document._chromeHidden || !this.curDoc() ? null : (
             <>
-                <div key="back" className="carouselView-back" style={{ transform: this.uiBtnScaleTransform }} onClick={this.goback}>
+                <div key="back" className="carouselView-back" style={{ transform: `scale(${this.uiBtnScaleTransform})` }} onClick={this.goback}>
                     <FontAwesomeIcon icon="chevron-left" size="2x" />
                 </div>
-                <div key="fwd" className="carouselView-fwd" style={{ transform: this.uiBtnScaleTransform }} onClick={this.advance}>
+                <div key="fwd" className="carouselView-fwd" style={{ transform: `scale(${this.uiBtnScaleTransform})` }} onClick={this.advance}>
                     <FontAwesomeIcon icon="chevron-right" size="2x" />
                 </div>
-                {this.practiceMode == practiceMode.PRACTICE ? (
-                    <div style={{ transform: this.uiBtnScaleTransform, bottom: `${this._sideBtnWidth}px`, height: `${this._sideBtnWidth}px`, position: 'absolute', width: `100%` }}>
-                        <Tooltip title="Incorrect. View again later.">
-                            <div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)}>
-                                <FontAwesomeIcon icon="xmark" color="red" size="1x" />
-                            </div>
-                        </Tooltip>
-                        <Tooltip title="Correct">
-                            <div key="check" className="carouselView-check" onClick={e => this.setPracticeVal(e, practiceVal.CORRECT)}>
-                                <FontAwesomeIcon icon="check" color="green" size="1x" />
-                            </div>
-                        </Tooltip>
-                    </div>
-                ) : null}
             </>
         );
     }
 
+    docViewProps = () => ({
+        ...this._props, //
+        isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive,
+        isContentActive: this.isChildContentActive,
+        ScreenToLocalTransform: this.contentScreenToLocalXf,
+    });
+    carouselItemsFunc = () => this.carouselItems;
+    answered = () => this.advance();
+    @action setFilterFunc = (func?: (doc: Doc) => boolean) => { this._filterFunc = func; }; // prettier-ignore
+
     render() {
         return (
-            <div>
-                <div
-                    className="collectionCarouselView-outer"
-                    ref={this.createDashEventsTarget}
-                    style={{
-                        background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string,
-                        color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
-                        width: `calc(100% - ${NumCast(this.layoutDoc._xMargin)}px)`,
-                        height: `calc(100% - ${NumCast(this.layoutDoc._yMargin)}px)`,
-                        left: NumCast(this.layoutDoc._xMargin),
-                        top: NumCast(this.layoutDoc._yMargin),
-                    }}>
-                    {!this.practiceMessage && !this.filterMessage ? (
-                        this.content
-                    ) : (
-                        <p
-                            className="message"
-                            onClick={() => {
-                                if (this.filterMessage || this.practiceMessage) {
-                                    this.setPracticeMode(undefined);
-                                    Doc.setDocFilter(this.layoutDoc, 'tags', Doc.FilterAny, 'remove');
-                                }
-                            }}>
-                            {this.filterMessage || this.practiceMessage}
-                        </p>
-                    )}
-                </div>
-                {!this.Document._chromeHidden ? this.menu : null}
-                {!this.Document._chromeHidden && this.carouselItems?.[this.carouselIndex] ? this.buttons : null}
+            <div
+                className="collectionCarouselView-outer"
+                ref={this.createDashEventsTarget}
+                style={{
+                    background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string,
+                    color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
+                    width: `calc(100% - ${NumCast(this.layoutDoc._xMargin)}px)`,
+                    height: `calc(100% - ${NumCast(this.layoutDoc._yMargin)}px)`,
+                    left: NumCast(this.layoutDoc._xMargin),
+                    top: NumCast(this.layoutDoc._yMargin),
+                }}>
+                {this.content}
+                <FlashcardPracticeUI
+                    setFilterFunc={this.setFilterFunc}
+                    fieldKey={this.fieldKey}
+                    sideBtnWidth={this._sideBtnWidth}
+                    carouselItems={this.carouselItemsFunc}
+                    childDocs={this.childDocs}
+                    advance={this.answered}
+                    curDoc={this.curDoc}
+                    layoutDoc={this.layoutDoc}
+                    maxWidgetScale={this.maxWidgetScale}
+                    uiBtnScaleTransform={this.uiBtnScaleTransform}
+                    ScreenToLocalBoxXf={this.ScreenToLocalBoxXf}
+                    renderDepth={this._props.renderDepth}
+                    docViewProps={this.docViewProps}
+                />
+                {this.navButtons}
             </div>
         );
     }
diff --git a/src/client/views/collections/FlashcardPracticeUI.scss b/src/client/views/collections/FlashcardPracticeUI.scss
new file mode 100644
index 000000000..53c26ad34
--- /dev/null
+++ b/src/client/views/collections/FlashcardPracticeUI.scss
@@ -0,0 +1,64 @@
+.FlashcardPracticeUI-remove,
+.FlashcardPracticeUI-check {
+    position: absolute;
+    display: flex;
+    width: 30;
+    height: 30;
+    align-items: center;
+    border-radius: 5px;
+    justify-content: center;
+    color: rgba(255, 255, 255, 0.5);
+    background: rgba(0, 0, 0, 0.1);
+    &:hover {
+        color: white;
+    }
+}
+.FlashcardPracticeUI-remove {
+    left: 52%;
+}
+.FlashcardPracticeUI-check {
+    right: 52%;
+}
+.FlashcardPracticeUI-menu {
+    position: absolute;
+    flex-direction: column;
+    align-items: center;
+    display: flex;
+    top: 0px;
+    left: 0px;
+    width: 30;
+    transform-origin: top left;
+    border-radius: 5px;
+    color: rgba(255, 255, 255, 0.5);
+    background: rgba(0, 0, 0, 0.1);
+    .FlashcardPracticeUI-practiceModes {
+        width: 100%;
+        display: flex;
+        flex-direction: column;
+        top: 0;
+        position: relative;
+        .FlashcardPracticeUI-quiz,
+        .FlashcardPracticeUI-practice {
+            position: relative;
+            display: flex;
+            height: 20px;
+            align-items: center;
+            margin: auto;
+            padding: 3px;
+            &:hover {
+                color: white;
+            }
+            & > svg {
+                height: 100%;
+                width: 100%;
+            }
+        }
+    }
+}
+.FlashcardPracticeUI-message {
+    z-index: 100;
+    position: relative;
+    margin: auto;
+    align-content: center;
+    width: max-content;
+}
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
new file mode 100644
index 000000000..032a405bf
--- /dev/null
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -0,0 +1,172 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@mui/material';
+import { computed, makeObservable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { returnZero } from '../../../ClientUtils';
+import { Doc, DocListCast } from '../../../fields/Doc';
+import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
+import { Transform } from '../../util/Transform';
+import { ObservableReactComponent } from '../ObservableReactComponent';
+import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
+import './FlashcardPracticeUI.scss';
+
+enum practiceMode {
+    PRACTICE = 'practice',
+    QUIZ = 'quiz',
+}
+enum practiceVal {
+    MISSED = 'missed',
+    CORRECT = 'correct',
+}
+
+interface PracticeUIProps {
+    fieldKey: string;
+    layoutDoc: Doc;
+    carouselItems: () => Doc[];
+    childDocs: Doc[];
+    curDoc: () => Doc;
+    advance: (correct: boolean) => void;
+    renderDepth: number;
+    sideBtnWidth: number;
+    uiBtnScaleTransform: number;
+    ScreenToLocalBoxXf: () => Transform;
+    maxWidgetScale: number;
+    docViewProps: () => DocumentViewProps;
+    setFilterFunc: (func?: (doc: Doc) => boolean) => void;
+    practiceBtnOffset?: number;
+}
+@observer
+export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProps> {
+    constructor(props: PracticeUIProps) {
+        super(props);
+        makeObservable(this);
+        this._props.setFilterFunc(this.tryFilterOut);
+    }
+
+    componentWillUnmount(): void {
+        this._props.setFilterFunc(undefined);
+    }
+
+    get practiceField() { return this._props.fieldKey + "_practice"; } // prettier-ignore
+
+    @computed get filterDoc() { return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter'); } // prettier-ignore
+    @computed get practiceMode() {  return this._props.childDocs.some(doc => doc._layout_isFlashcard) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
+
+    btnHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this._props.ScreenToLocalBoxXf().Scale);
+    btnWidth = () => (!this.filterDoc ? 1 : (this.btnHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height));
+
+    /**
+     * Sets the practice mode answer style for flashcards
+     * @param mode practiceMode or undefined for no practice
+     */
+    setPracticeMode = (mode: practiceMode | undefined) => {
+        this._props.layoutDoc.practiceMode = mode;
+        this._props.carouselItems().map(doc => (doc[this.practiceField] = undefined));
+    };
+
+    @computed get emptyMessage() {
+        const cardCount = this._props.carouselItems().length;
+        const practiceMessage = this.practiceMode && !Doc.hasDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny) && !this._props.carouselItems().length ? 'Finished! Click here to view all flashcards.' : '';
+        const filterMessage = practiceMessage
+            ? ''
+            : Doc.hasDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny) && !cardCount
+              ? 'No tagged items. Click here to view all flash cards.'
+              : this.practiceMode && !cardCount
+                ? 'No flashcards to show! Click here to leave practice mode'
+                : '';
+        return !practiceMessage && !filterMessage ? null : (
+            <p
+                className="FlashcardPracticeUI-message"
+                onClick={() => {
+                    if (filterMessage || practiceMessage) {
+                        this.setPracticeMode(undefined);
+                        Doc.setDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny, 'remove');
+                    }
+                }}>
+                {filterMessage || practiceMessage}
+            </p>
+        );
+    }
+
+    @computed get practiceButtons() {
+        /*
+         * Sets a flashcard to either missed or correct depending on if they got the question right in practice mode.
+         */
+        const setPracticeVal = (e: React.MouseEvent, val: string) => {
+            e.stopPropagation();
+            const curDoc = this._props.curDoc();
+            curDoc && (curDoc[this.practiceField] = val);
+            this._props.advance?.(val === practiceVal.CORRECT);
+        };
+
+        return this.practiceMode == practiceMode.PRACTICE ? (
+            <div style={{ transform: `scale(${this._props.uiBtnScaleTransform})`, bottom: `${this._props.practiceBtnOffset ?? this._props.sideBtnWidth}px`, height: `${this._props.sideBtnWidth}px`, position: 'absolute', width: `100%` }}>
+                <Tooltip title="Incorrect. View again later.">
+                    <div key="remove" className="FlashcardPracticeUI-remove" onClick={e => setPracticeVal(e, practiceVal.MISSED)}>
+                        <FontAwesomeIcon icon="xmark" color="red" size="1x" />
+                    </div>
+                </Tooltip>
+                <Tooltip title="Correct">
+                    <div key="check" className="FlashcardPracticeUI-check" onClick={e => setPracticeVal(e, practiceVal.CORRECT)}>
+                        <FontAwesomeIcon icon="check" color="green" size="1x" />
+                    </div>
+                </Tooltip>
+            </div>
+        ) : null;
+    }
+    @computed get practiceModesMenu() {
+        const setColor = (mode: practiceMode) => (StrCast(this.practiceMode) === mode ? 'white' : 'light gray');
+        const togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode);
+
+        return !this._props.curDoc()?._layout_isFlashcard ? null : (
+            <div
+                className="FlashcardPracticeUI-practiceModes"
+                style={{
+                    transformOrigin: `0px ${-this.btnHeight()}px`,
+                    transform: `scale(${Math.max(1, 1 / this._props.ScreenToLocalBoxXf().Scale / this._props.maxWidgetScale)})`,
+                }}>
+                <Tooltip title="Practice flashcards using GPT">
+                    <div key="back" className="FlashcardPracticeUI-quiz" style={{ width: this.btnWidth(), height: this.btnHeight() }} onClick={() => togglePracticeMode(practiceMode.QUIZ)}>
+                        <FontAwesomeIcon icon="file-pen" color={setColor(practiceMode.QUIZ)} size="sm" />
+                    </div>
+                </Tooltip>
+                <Tooltip title={this.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}>
+                    <div key="back" className="FlashcardPracticeUI-practice" style={{ width: this.btnWidth(), height: this.btnHeight() }} onClick={() => togglePracticeMode(practiceMode.PRACTICE)}>
+                        <FontAwesomeIcon icon="check" color={setColor(practiceMode.PRACTICE)} size="sm" />
+                    </div>
+                </Tooltip>
+            </div>
+        );
+    }
+    tryFilterOut = (doc: Doc) => (this.practiceMode && BoolCast(doc?._layout_isFlashcard) && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct
+    render() {
+        return (
+            <>
+                {this.emptyMessage}
+                {this.practiceButtons}
+                <div className="FlashcardPracticeUI-menu" style={{ height: this.btnHeight(), width: this.btnHeight(), transform: `scale(${this._props.uiBtnScaleTransform})` }}>
+                    {!this.filterDoc || this._props.layoutDoc._chromeHidden ? null : (
+                        <DocumentView
+                            {...this._props.docViewProps()}
+                            Document={this.filterDoc}
+                            TemplateDataDocument={undefined}
+                            PanelWidth={this.btnWidth}
+                            PanelHeight={this.btnHeight}
+                            NativeWidth={returnZero}
+                            NativeHeight={returnZero}
+                            hideDecorations={BoolCast(this._props.layoutDoc.layout_hideDecorations)}
+                            hideCaptions={true}
+                            hideFilterStatus={true}
+                            renderDepth={this._props.renderDepth + 1}
+                            fitWidth={undefined}
+                            showTags={false}
+                            setContentViewBox={undefined}
+                        />
+                    )}
+                    {this.practiceModesMenu}
+                </div>
+            </>
+        );
+    }
+}
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index b7307f3a3..8156c50f6 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -296,3 +296,18 @@
         }
     }
 }
+.comparisonBox-bottomMenu {
+    transform-origin: bottom right;
+    width: max-content;
+    justify-content: space-between;
+    height: max-content;
+    position: absolute;
+    bottom: 0;
+    right: 2;
+    flex-direction: row-reverse;
+    display: flex;
+    cursor: pointer;
+    .comparisonBox-button {
+        padding-right: 8px;
+    }
+}
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index ef66c2b11..81e223028 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -68,7 +68,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         return (
             <Tooltip title={<div className="dash-tooltip">flip</div>}>
                 <div
-                    className="formattedTextBox-alternateButton"
+                    className="comparisonBox-alternateButton ccomparisonBox-button"
                     onPointerDown={e =>
                         setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => {
                             if (!this.revealOp || this.revealOp === 'flip') {
@@ -81,42 +81,74 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         color: this.revealOp === 'hover' ? 'black' : this._frontSide ? 'black' : 'white',
                         display: 'inline-block',
                     }}>
-                    <div key="alternate" className="formattedTextBox-flip">
-                        <FontAwesomeIcon icon="turn-up" size="1x" />
-                    </div>
+                    <FontAwesomeIcon icon="turn-up" size="xl" />
                 </div>
             </Tooltip>
         );
     }
 
+    _sideBtnWidth = 30;
+    /**
+     * How much the content of the view is being scaled based on its nesting and its fit-to-width settings
+     */
+    @computed get contentScaling() {
+        return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1);
+    }
+    /**
+     * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
+     */
+    @computed get maxWidgetScale() {
+        const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.25 * Math.min(NumCast(this.Document.width), NumCast(this.Document.height)));
+        return Math.max(maxWidgetSize / this._sideBtnWidth, 1);
+    }
+    /**
+     * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
+     */
+    @computed get uiBtnScaleTransform() {
+        return this.maxWidgetScale * Math.min(1, this.contentScaling);
+    }
+
     @computed get flashcardMenu() {
         return (
-            <div>
-                <Tooltip
-                    title={this._frontSide ? <div className="dash-tooltip">Flip to front side to use GPT</div> : <div className="dash-tooltip">Ask GPT to create an answer on the back side of the flashcard based on your question on the front</div>}>
-                    <div style={{ position: 'absolute', bottom: '3px', right: '50px', cursor: 'pointer' }} onPointerDown={() => (!this._frontSide ? this.findImageTags() : null)}>
-                        <FontAwesomeIcon icon="lightbulb" size="xl" />
-                    </div>
-                </Tooltip>
-                {DocCast(this.Document.embedContainer)?.type_collection === CollectionViewType.Carousel ? null : (
-                    <div>
-                        <Tooltip title={<div>Create a flashcard pile</div>}>
-                            <div style={{ position: 'absolute', bottom: '3px', right: '74px', cursor: 'pointer' }} onPointerDown={() => this.createFlashcardPile([this.Document], false)}>
-                                <FontAwesomeIcon icon="folder-plus" size="xl" />
-                            </div>
-                        </Tooltip>
-                        <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}>
-                            <div style={{ position: 'absolute', bottom: '3px', right: '104px', cursor: 'pointer' }} onClick={() => this.gptFlashcardPile()}>
-                                <FontAwesomeIcon icon="layer-group" size="xl" />
+            <div className="comparisonBox-bottomMenu" style={{ transform: `scale(${this.uiBtnScaleTransform})` }}>
+                {this.overlayAlternateIcon}
+                {!this._props.isContentActive() ? null : (
+                    <>
+                        {' '}
+                        {!this._frontSide ? null : (
+                            <Tooltip
+                                title={
+                                    <div className="dash-tooltip">{
+                                        !this._frontSide ? "Flip to front side to use GPT":
+                                                "Ask GPT to create an answer on the back side of the flashcard based on your question on the front"}
+                                   </div> // prettier-ignore
+                                }>
+                                <div className="comparisonBox-button" onPointerDown={() => (this._frontSide ? this.findImageTags() : null)}>
+                                    <FontAwesomeIcon icon="lightbulb" size="xl" />
+                                </div>
+                            </Tooltip>
+                        )}
+                        {DocCast(this.Document.embedContainer)?.type_collection === CollectionViewType.Carousel ? null : (
+                            <>
+                                <Tooltip title={<div>Create a flashcard pile</div>}>
+                                    <div className="comparisonBox-button" onPointerDown={() => this.createFlashcardPile([this.Document], false)}>
+                                        <FontAwesomeIcon icon="folder-plus" size="xl" />
+                                    </div>
+                                </Tooltip>
+                                <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}>
+                                    <div className="comparisonBox-button" onClick={this.gptFlashcardPile}>
+                                        <FontAwesomeIcon icon="layer-group" size="xl" />
+                                    </div>
+                                </Tooltip>
+                            </>
+                        )}
+                        <Tooltip title={<div className="dash-tooltip">Hover to reveal</div>}>
+                            <div className="comparisonBox-button" onClick={this.handleHover}>
+                                <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="hand-point-up" size="xl" />
                             </div>
                         </Tooltip>
-                    </div>
+                    </>
                 )}
-                <Tooltip title={<div className="dash-tooltip">Hover to reveal</div>}>
-                    <div style={{ position: 'absolute', bottom: '3px', right: '25px', cursor: 'pointer' }} onClick={() => this.handleHover()}>
-                        <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="hand-point-up" size="xl" />
-                    </div>
-                </Tooltip>
             </div>
         );
     }
@@ -478,7 +510,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 this._loading = false;
                 return;
             }
-            this.flipFlashcard();
         }
         try {
             console.log(queryText);
@@ -655,6 +686,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
 
     childActiveFunc = () => this._childActive;
 
+    contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
     render() {
         const clearButton = (which: string) => (
             <Tooltip title={<div className="dash-tooltip">remove</div>}>
@@ -687,6 +719,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2}
                         NativeWidth={returnZero}
                         NativeHeight={returnZero}
+                        ScreenToLocalTransform={this.contentScreenToLocalXf}
                         isContentActive={this.childActiveFunc}
                         isDocumentActive={returnFalse}
                         dontSelect={returnTrue}
@@ -808,8 +841,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             <ReactLoading type="spin" height={30} width={30} color={'blue'} />
                         </div>
                     ) : null}
-                    {this._props.isContentActive() ? this.flashcardMenu : null}
-                    {this.overlayAlternateIcon}
+                    {this.flashcardMenu}
                 </div>
             );
         }
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 170966471..c81631baa 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -51,6 +51,7 @@ export interface FieldViewSharedProps {
     LayoutTemplate?: () => Opt<Doc>;
     renderDepth: number;
     scriptContext?: unknown; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document
+    screenXPadding?: () => number; // padding in screen space coordinates (used by text box to reflow around UI buttons in carouselView)
     xPadding?: number;
     yPadding?: number;
     dontRegisterView?: boolean;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index d6f13d9ee..f1ae1151f 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -101,12 +101,6 @@ audiotag:hover {
     height: 22;
     cursor: default;
 }
-.formattedTextBox-flip {
-    align-items: center;
-    position: absolute;
-    right: 2px;
-    bottom: 4px;
-}
 
 .formattedTextBox-outer {
     position: relative;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index c89737e1e..93153b453 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -763,10 +763,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         );
     };
     sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => {
-        const localDelta = this._props
-            .ScreenToLocalTransform()
-            .scale(this._props.NativeDimScaling?.() || 1)
-            .transformDirection(delta[0], delta[1]);
+        const localDelta = this.DocumentView?.().screenToViewTransform().transformDirection(delta[0], delta[1]) ?? delta;
         const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.layout_sidebarWidthPercent.replace('%', ''))) / 100;
         const width = NumCast(this.layoutDoc._width) + localDelta[0];
         this.layoutDoc._layout_sidebarWidthPercent = Math.max(0, (sidebarWidth + localDelta[0]) / width) * 100 + '%';
@@ -1264,7 +1261,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         this._cachedLinks = Doc.Links(this.Document);
         this._disposers.breakupDictation = reaction(() => Doc.RecordingEvent, this.breakupDictation);
         this._disposers.layout_autoHeight = reaction(
-            () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize, css: this.Document[DocCss] }),
+            () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize, css: this.Document[DocCss], xMargin: this.Document.xMargin, yMargin: this.Document.yMargin }),
             autoHeight => setTimeout(() => autoHeight && this.tryUpdateScrollHeight())
         );
         this._disposers.highlights = reaction(
@@ -2088,8 +2085,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         const scale = this._props.NativeDimScaling?.() || 1;
         const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : '';
         setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide);
-        const paddingX = Math.max(this._props.xPadding ?? 0, NumCast(this.layoutDoc._xMargin));
-        const paddingY = Math.max(this._props.yPadding ?? 0, NumCast(this.layoutDoc._yMargin));
+
+        const scrSize = (which: number, view = this._props.docViewPath().slice(-which)[0]) => 
+                [view._props.PanelWidth() / view.screenToLocalScale(), view._props.PanelHeight() / view.screenToLocalScale()]; // prettier-ignore
+        const scrMargin = [Math.max(0, (scrSize(2)[0] - scrSize(1)[0]) / 2), Math.max(0, (scrSize(2)[1] - scrSize(1)[1]) / 2)];
+        const paddingX = Math.max(NumCast(this.layoutDoc._xMargin), this._props.xPadding ?? 0, 0, ((this._props.screenXPadding?.() ?? 0) - scrMargin[0]) * this.ScreenToLocalBoxXf().Scale);
+        const paddingY = Math.max(NumCast(this.layoutDoc._yMargin), 0, ((this._props.yPadding ?? 0) - scrMargin[1]) * this.ScreenToLocalBoxXf().Scale);
         const styleFromLayout = styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props.  e.g., <FormattedTextBox height='{this._header_height}px' >
         return styleFromLayout?.height === '0px' ? null : (
             <div
@@ -2176,7 +2177,6 @@ Docs.Prototypes.TemplateMap.set(DocumentType.RTF, {
         _layout_nativeDimEditable: true,
         _layout_reflowVertical: true,
         _layout_reflowHorizontal: true,
-        _layout_noSidebar: true,
         defaultDoubleClick: 'ignore',
         systemIcon: 'BsFileEarmarkTextFill',
     },
-- 
cgit v1.2.3-70-g09d2


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

---
 .../views/collections/CollectionCardDeckView.scss  | 13 +---
 .../views/collections/CollectionCardDeckView.tsx   | 81 ++++++----------------
 .../views/collections/CollectionCarousel3DView.tsx | 72 +++++--------------
 .../views/collections/CollectionCarouselView.scss  |  1 +
 .../views/collections/CollectionCarouselView.tsx   | 53 ++------------
 src/client/views/collections/CollectionSubView.tsx | 51 +++++++++++++-
 .../views/collections/FlashcardPracticeUI.scss     |  2 +-
 .../views/collections/FlashcardPracticeUI.tsx      | 27 ++++----
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 src/client/views/nodes/ComparisonBox.tsx           | 18 ++---
 10 files changed, 113 insertions(+), 207 deletions(-)

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

diff --git a/src/client/views/collections/CollectionCardDeckView.scss b/src/client/views/collections/CollectionCardDeckView.scss
index 0520e38d4..0637cd4e9 100644
--- a/src/client/views/collections/CollectionCardDeckView.scss
+++ b/src/client/views/collections/CollectionCardDeckView.scss
@@ -6,6 +6,7 @@
     position: relative;
     background-color: white;
     overflow: hidden;
+    display: flex;
 
     button {
         border-radius: 50%;
@@ -48,15 +49,3 @@
 .card-item-active {
     z-index: 100;
 }
-
-.collectionCardDeckView-flashcards {
-    width: 100%;
-    height: 100%;
-    position: absolute;
-    top: 0;
-    left: 0;
-    display: flex;
-    transform-origin: top left;
-    pointer-events: none;
-    z-index: 100;
-}
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 7272b22e2..8f351d8a7 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -22,7 +22,6 @@ import { DocumentView } from '../nodes/DocumentView';
 import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
 import './CollectionCardDeckView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
-import { FlashcardPracticeUI } from './FlashcardPracticeUI';
 
 enum cardSortings {
     Time = 'time',
@@ -47,8 +46,6 @@ export class CollectionCardView extends CollectionSubView() {
     private _textToDoc = new Map<string, Doc>();
     private _dropped = false; // set when a card doc has just moved and the drop method has been called - prevents the pointerUp method from hiding doc decorations (which needs to be done when clicking on a card to animate it to front/center)
 
-    _sideBtnWidth = 35;
-    @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
     @observable _forceChildXf = 0;
     @observable _hoveredNodeIndex = -1;
     @observable _docRefs = new ObservableMap<Doc, DocumentView>();
@@ -119,15 +116,15 @@ export class CollectionCardView extends CollectionSubView() {
     /**
      * The child documents to be rendered-- everything other than ink/link docs (which are marks as being svg's)
      */
-    @computed get childDocsWithoutLinks() {
-        return this.childDocs.filter(l => !l.layout_isSvg).filter(doc => !this._filterFunc?.(doc));
+    @computed get childCards() {
+        return this.childLayoutPairs.filter(pair => !pair.layout.layout_isSvg);
     }
 
     /**
      * how much to scale down the contents of the view so that everything will fit
      */
     @computed get fitContentScale() {
-        const length = Math.min(this.childDocsWithoutLinks.length, this._maxRowCount);
+        const length = Math.min(this.childCards.length, this._maxRowCount);
         return (this.childPanelWidth() * length) / this._props.PanelWidth();
     }
 
@@ -280,7 +277,12 @@ export class CollectionCardView extends CollectionSubView() {
     );
 
     @computed get sortedDocs() {
-        return this.sort(this.childDocsWithoutLinks, this.cardSort, BoolCast(this.Document.cardSort_isDesc), this._docDraggedIndex);
+        return this.sort(
+            this.childCards.map(card => card.layout),
+            this.cardSort,
+            BoolCast(this.Document.cardSort_isDesc),
+            this._docDraggedIndex
+        );
     }
 
     /**
@@ -428,12 +430,14 @@ export class CollectionCardView extends CollectionSubView() {
                 default:                return StrCast(doc.title);
             } // prettier-ignore
         };
-        const docTextPromises = this.childDocsWithoutLinks.map(async doc => {
-            const docText = (await docToText(doc)) ?? '';
-            doc.gptInputText = docText;
-            this._textToDoc.set(docText.replace(/\n/g, ' ').trim(), doc);
-            return `======${docText.replace(/\n/g, ' ').trim()}======`;
-        });
+        const docTextPromises = this.childCards
+            .map(pair => pair.layout)
+            .map(async doc => {
+                const docText = (await docToText(doc)) ?? '';
+                doc.gptInputText = docText;
+                this._textToDoc.set(docText.replace(/\n/g, ' ').trim(), doc);
+                return `======${docText.replace(/\n/g, ' ').trim()}======`;
+            });
         return Promise.all<string>(docTextPromises);
     };
 
@@ -567,7 +571,7 @@ export class CollectionCardView extends CollectionSubView() {
      * Actually renders all the cards
      */
     @computed get renderCards() {
-        if (!this.childDocsWithoutLinks.length) {
+        if (!this.childCards.length) {
             return null;
         }
 
@@ -611,36 +615,16 @@ export class CollectionCardView extends CollectionSubView() {
     }
 
     contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
+    curDoc = () => this.childCards.find(card => DocumentView.getDocumentView(card.layout, this.DocumentView?.())?.IsSelected)?.layout;
     docViewProps = () => ({
         ...this._props, //
         isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive,
         isContentActive: this.isChildContentActive,
         ScreenToLocalTransform: this.contentScreenToLocalXf,
     });
-    carouselItemsFunc = () => this.childDocsWithoutLinks;
-    @action setFilterFunc = (func?: (doc: Doc) => boolean) => { this._filterFunc = func; }; // prettier-ignore
-    answered = (correct: boolean) => !correct || !this.curDoc();
-    curDoc = () => this.sortedDocs.find(doc => DocumentView.getDocumentView(doc, this.DocumentView?.())?.IsSelected);
-    /**
-     * How much the content of the carousel view is being scaled based on its nesting and its fit-to-width settings
-     */
-    @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore
-
-    /**
-     * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
-     */
-    @computed get maxWidgetScale() {
-        const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.1 * NumCast(this.layoutDoc.width, 1));
-        return Math.max(maxWidgetSize / this._sideBtnWidth, 1);
-    }
-    /**
-     * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
-     */
-    @computed get uiBtnScaleTransform() {  return this.maxWidgetScale * Math.min(1, this.contentScaling); } // prettier-ignore
 
     render() {
-        const isEmpty = this.childDocsWithoutLinks.length === 0;
-
+        const isEmpty = this.childCards.length === 0;
         return (
             <div
                 className="collectionCardView-outer"
@@ -661,31 +645,8 @@ export class CollectionCardView extends CollectionSubView() {
                         gridAutoRows: `${100 / this.numRows}%`,
                     }}>
                     {this.renderCards}
-                    <div
-                        className="collectionCardDeckView-flashcards"
-                        style={{
-                            transform: `scale(${this.fitContentScale || 1})`,
-                            width: `${100 / (this.fitContentScale || 1)}%`,
-                            height: `${100 / (this.fitContentScale || 1)}%`,
-                            pointerEvents: !this.childDocsWithoutLinks.length ? 'unset' : undefined,
-                        }}>
-                        <FlashcardPracticeUI
-                            setFilterFunc={this.setFilterFunc}
-                            fieldKey={this.fieldKey}
-                            sideBtnWidth={this._sideBtnWidth}
-                            carouselItems={this.carouselItemsFunc}
-                            childDocs={this.childDocs}
-                            advance={this.answered}
-                            curDoc={this.curDoc}
-                            layoutDoc={this.layoutDoc}
-                            maxWidgetScale={this.maxWidgetScale}
-                            uiBtnScaleTransform={this.uiBtnScaleTransform}
-                            ScreenToLocalBoxXf={this.ScreenToLocalBoxXf}
-                            renderDepth={this._props.renderDepth}
-                            docViewProps={this.docViewProps}
-                        />
-                    </div>
                 </div>
+                {this.flashCardUI(this.curDoc, this.docViewProps)}
             </div>
         );
     }
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index 3bcf3450f..9ccac0e0f 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, makeObservable, observable } from 'mobx';
+import { computed, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { returnZero } from '../../../ClientUtils';
@@ -15,26 +15,19 @@ import { DocumentView } from '../nodes/DocumentView';
 import { FocusViewOptions } from '../nodes/FocusViewOptions';
 import './CollectionCarousel3DView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
-import { FlashcardPracticeUI } from './FlashcardPracticeUI';
 
 // eslint-disable-next-line @typescript-eslint/no-require-imports
 const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss');
 
 @observer
 export class CollectionCarousel3DView extends CollectionSubView() {
-    @computed get scrollSpeed() {
-        return this.layoutDoc._autoScrollSpeed ? NumCast(this.layoutDoc._autoScrollSpeed) : 1000; // default scroll speed
-    }
-    _sideBtnWidth = 35;
-    @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
+    private _dropDisposer?: DragManager.DragDropDisposer;
 
     constructor(props: SubCollectionViewProps) {
         super(props);
         makeObservable(this);
     }
 
-    private _dropDisposer?: DragManager.DragDropDisposer;
-
     componentWillUnmount() {
         this._dropDisposer?.();
     }
@@ -46,8 +39,11 @@ export class CollectionCarousel3DView extends CollectionSubView() {
         }
     };
 
+    @computed get scrollSpeed() {
+        return this.layoutDoc._autoScrollSpeed ? NumCast(this.layoutDoc._autoScrollSpeed) : 1000; // default scroll speed
+    }
     @computed get carouselItems() {
-        return this.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK).filter(pair => !this._filterFunc?.(pair.layout));
+        return this.childLayoutPairs.filter(pair => !pair.layout.layout_isSvg);
     }
 
     centerScale = Number(CAROUSEL3D_CENTER_SCALE);
@@ -86,11 +82,11 @@ export class CollectionCarousel3DView extends CollectionSubView() {
 
     @computed get content() {
         const currentIndex = NumCast(this.layoutDoc._carousel_index);
-        const displayDoc = (childPair: { layout: Doc; data: Doc }, dxf: () => Transform) => (
+        const displayDoc = (child: Doc, dxf: () => Transform) => (
             <DocumentView
                 {...this._props}
-                Document={childPair.layout}
-                TemplateDataDocument={childPair.data}
+                Document={child}
+                TemplateDataDocument={undefined}
                 // suppressSetHeight={true}
                 NativeWidth={returnZero}
                 NativeHeight={returnZero}
@@ -109,9 +105,9 @@ export class CollectionCarousel3DView extends CollectionSubView() {
             />
         );
 
-        return this.carouselItems.map((childPair, index) => (
-            <div key={childPair.layout[Id]} className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`} style={{ width: this.panelWidth() }}>
-                {displayDoc(childPair, index < currentIndex ? this.childScreenLeftToLocal : index === currentIndex ? this.childCenterScreenToLocal : this.childScreenRightToLocal)}
+        return this.carouselItems.map((child, index) => (
+            <div key={child.layout[Id]} className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`} style={{ width: this.panelWidth() }}>
+                {displayDoc(child.layout, index < currentIndex ? this.childScreenLeftToLocal : index === currentIndex ? this.childCenterScreenToLocal : this.childScreenRightToLocal)}
             </div>
         ));
     }
@@ -191,35 +187,14 @@ export class CollectionCarousel3DView extends CollectionSubView() {
         return this.panelWidth() * (1 - index);
     }
 
-    /**
-     * How much the content of the carousel view is being scaled based on its nesting and its fit-to-width settings
-     */
-    @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore
-
-    /**
-     * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
-     */
-    @computed get maxWidgetScale() {
-        const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.1 * NumCast(this.layoutDoc.width, 1));
-        return Math.max(maxWidgetSize / this._sideBtnWidth, 1);
-    }
-    /**
-     * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
-     */
-    @computed get uiBtnScaleTransform() {  return this.maxWidgetScale * Math.min(1, this.contentScaling); } // prettier-ignore
-    screenXPadding = () => (this.uiBtnScaleTransform * this._sideBtnWidth - NumCast(this.layoutDoc.xMargin)) / this._props.ScreenToLocalTransform().Scale;
-
+    curDoc = () => this.carouselItems[NumCast(this.layoutDoc._carousel_index)]?.layout;
+    answered = (correct: boolean) => (!correct || !this.curDoc()) && this.changeSlide(1);
     docViewProps = () => ({
         ...this._props, //
         isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive,
         isContentActive: this.isChildContentActive,
         ScreenToLocalTransform: this.contentScreenToLocalXf,
     });
-    carouselItemsFunc = () => this.carouselItems.map(pair => pair.layout);
-    @action setFilterFunc = (func?: (doc: Doc) => boolean) => { this._filterFunc = func; }; // prettier-ignore
-    answered = (correct: boolean) => (!correct || !this.curDoc()) && this.changeSlide(1);
-    curDoc = () => this.carouselItems[NumCast(this.layoutDoc._carousel_index)]?.layout;
-
     render() {
         return (
             <div
@@ -233,25 +208,10 @@ export class CollectionCarousel3DView extends CollectionSubView() {
                     {this.content}
                 </div>
                 {this.buttons}
-                <div className="dot-bar" style={{ transform: `scale(${this.uiBtnScaleTransform})` }}>
+                <div className="dot-bar" style={{ transform: `scale(${this.uiBtnScaling})` }}>
                     {this.dots}
                 </div>
-                <FlashcardPracticeUI
-                    setFilterFunc={this.setFilterFunc}
-                    fieldKey={this.fieldKey}
-                    sideBtnWidth={this._sideBtnWidth}
-                    carouselItems={this.carouselItemsFunc}
-                    childDocs={this.childDocs}
-                    advance={this.answered}
-                    curDoc={this.curDoc}
-                    practiceBtnOffset={this._sideBtnWidth * 4}
-                    layoutDoc={this.layoutDoc}
-                    maxWidgetScale={this.maxWidgetScale}
-                    uiBtnScaleTransform={this.uiBtnScaleTransform}
-                    ScreenToLocalBoxXf={this.ScreenToLocalBoxXf}
-                    renderDepth={this._props.renderDepth}
-                    docViewProps={this.docViewProps}
-                />
+                {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
             </div>
         );
     }
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss
index 757072453..544b3e262 100644
--- a/src/client/views/collections/CollectionCarouselView.scss
+++ b/src/client/views/collections/CollectionCarouselView.scss
@@ -2,6 +2,7 @@
     height: 100%;
     position: relative;
     overflow: hidden;
+    display: flex;
     .collectionCarouselView-caption {
         height: 50;
         display: inline-block;
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 64ddaac79..538eba356 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -5,7 +5,6 @@ import * as React from 'react';
 import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
 import { Doc, Opt } from '../../../fields/Doc';
 import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
-import { DocumentType } from '../../documents/DocumentTypes';
 import { DragManager } from '../../util/DragManager';
 import { StyleProp } from '../StyleProp';
 import { DocumentView } from '../nodes/DocumentView';
@@ -13,15 +12,12 @@ import { FieldViewProps } from '../nodes/FieldView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import './CollectionCarouselView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
-import { FlashcardPracticeUI } from './FlashcardPracticeUI';
 
 @observer
 export class CollectionCarouselView extends CollectionSubView() {
     private _dropDisposer?: DragManager.DragDropDisposer;
 
     _fadeTimer: NodeJS.Timeout | undefined;
-    _sideBtnWidth = 35;
-    @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
     @observable _last_index = this.carouselIndex;
     @observable _last_opacity = 1;
 
@@ -43,28 +39,7 @@ export class CollectionCarouselView extends CollectionSubView() {
 
     @computed get captionMarginX(){ return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore
     @computed get carouselIndex() { return NumCast(this.layoutDoc._carousel_index) % this.carouselItems.length; } // prettier-ignore
-    @computed get carouselItems() { return this.childDocs
-        .filter(doc => doc.type !== DocumentType.LINK)
-        .filter(doc => !this._filterFunc?.(doc))
-    } // prettier-ignore
-
-    /**
-     * How much the content of the carousel view is being scaled based on its nesting and its fit-to-width settings
-     */
-    @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore
-
-    /**
-     * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
-     */
-    @computed get maxWidgetScale() {
-        const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.1 * NumCast(this.layoutDoc.width, 1));
-        return Math.max(maxWidgetSize / this._sideBtnWidth, 1);
-    }
-    /**
-     * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
-     */
-    @computed get uiBtnScaleTransform() {  return this.maxWidgetScale * Math.min(1, this.contentScaling); } // prettier-ignore
-    screenXPadding = () => (this.uiBtnScaleTransform * this._sideBtnWidth - NumCast(this.layoutDoc.xMargin)) / this._props.ScreenToLocalTransform().Scale;
+    @computed get carouselItems() { return this.childLayoutPairs.filter(pair => !pair.layout.layout_isSvg); } // prettier-ignore
 
     /**
      * Move forward or backward the specified number of Docs
@@ -91,7 +66,7 @@ export class CollectionCarouselView extends CollectionSubView() {
         this.move(-1);
     };
 
-    curDoc = () => this.carouselItems[this.carouselIndex];
+    curDoc = () => this.carouselItems[this.carouselIndex]?.layout;
 
     captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string) => {
         // first look for properties on the document in the carousel, then fallback to properties on the container
@@ -153,7 +128,7 @@ export class CollectionCarouselView extends CollectionSubView() {
      */
     @computed get overlay() {
         const fadeTime = 500;
-        const lastDoc = this.carouselItems?.[this._last_index];
+        const lastDoc = this.carouselItems?.[this._last_index]?.layout;
         return !lastDoc || this.carouselIndex === this._last_index ? null : (
             <div className="collectionCarouselView-image" style={{ opacity: this._last_opacity, transition: `opacity ${fadeTime}ms` }}>
                 {this.renderDoc(
@@ -211,10 +186,10 @@ export class CollectionCarouselView extends CollectionSubView() {
     @computed get navButtons() {
         return this.Document._chromeHidden || !this.curDoc() ? null : (
             <>
-                <div key="back" className="carouselView-back" style={{ transform: `scale(${this.uiBtnScaleTransform})` }} onClick={this.goback}>
+                <div key="back" className="carouselView-back" style={{ transform: `scale(${this.uiBtnScaling})` }} onClick={this.goback}>
                     <FontAwesomeIcon icon="chevron-left" size="2x" />
                 </div>
-                <div key="fwd" className="carouselView-fwd" style={{ transform: `scale(${this.uiBtnScaleTransform})` }} onClick={this.advance}>
+                <div key="fwd" className="carouselView-fwd" style={{ transform: `scale(${this.uiBtnScaling})` }} onClick={this.advance}>
                     <FontAwesomeIcon icon="chevron-right" size="2x" />
                 </div>
             </>
@@ -227,9 +202,7 @@ export class CollectionCarouselView extends CollectionSubView() {
         isContentActive: this.isChildContentActive,
         ScreenToLocalTransform: this.contentScreenToLocalXf,
     });
-    carouselItemsFunc = () => this.carouselItems;
     answered = () => this.advance();
-    @action setFilterFunc = (func?: (doc: Doc) => boolean) => { this._filterFunc = func; }; // prettier-ignore
 
     render() {
         return (
@@ -245,21 +218,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                     top: NumCast(this.layoutDoc._yMargin),
                 }}>
                 {this.content}
-                <FlashcardPracticeUI
-                    setFilterFunc={this.setFilterFunc}
-                    fieldKey={this.fieldKey}
-                    sideBtnWidth={this._sideBtnWidth}
-                    carouselItems={this.carouselItemsFunc}
-                    childDocs={this.childDocs}
-                    advance={this.answered}
-                    curDoc={this.curDoc}
-                    layoutDoc={this.layoutDoc}
-                    maxWidgetScale={this.maxWidgetScale}
-                    uiBtnScaleTransform={this.uiBtnScaleTransform}
-                    ScreenToLocalBoxXf={this.ScreenToLocalBoxXf}
-                    renderDepth={this._props.renderDepth}
-                    docViewProps={this.docViewProps}
-                />
+                {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
                 {this.navButtons}
             </div>
         );
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 581201a20..c057d2402 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -9,7 +9,7 @@ import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
 import { GestureUtils } from '../../../pen-gestures/GestureUtils';
@@ -25,7 +25,8 @@ import { SnappingManager } from '../../util/SnappingManager';
 import { UndoManager } from '../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { FieldViewProps } from '../nodes/FieldView';
-import { DocumentView } from '../nodes/DocumentView';
+import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
+import { FlashcardPracticeUI } from './FlashcardPracticeUI';
 
 export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> {
     isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
@@ -119,7 +120,8 @@ export function CollectionSubView<X>() {
                     pair =>
                         // filter out any documents that have a proto that we don't have permissions to
                         !pair.layout?.hidden && pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate))
-                );
+                )
+                .filter(pair => !this._filterFunc?.(pair.layout!));
             return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types
         }
         /**
@@ -515,6 +517,49 @@ export function CollectionSubView<X>() {
                 alert('Document upload failed - possibly an unsupported file type.');
             }
         };
+
+        protected _sideBtnWidth = 35;
+        @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
+        /**
+         * How much the content of the collection is being scaled based on its nesting and its fit-to-width settings
+         */
+        @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore
+        /**
+         * The maximum size a UI widget can be in collection coordinates based on not wanting the widget to visually obscure too much of the collection
+         * This takes the desired screen space size and converts into collection coordinates.  It then returns the smaller of the converted
+         * size or a fraction of the collection view.
+         */
+        @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.contentScaling, 0.25 * NumCast(this.layoutDoc.width, 1));  } // prettier-ignore
+        /**
+         * This computes a scale factor for UI elements so that they shrink and grow as the collection does in screen space.
+         * Note, the scale factor does not allow for elements to grow larger than their native screen space size.
+         */
+        @computed get uiBtnScaling() { return this.maxWidgetSize / this._sideBtnWidth; } // prettier-ignore
+
+        screenXPadding = () => (this.uiBtnScaling * this._sideBtnWidth - NumCast(this.layoutDoc.xMargin)) / this._props.ScreenToLocalTransform().Scale;
+        filteredChildDocs = () => this.childLayoutPairs.map(pair => pair.layout);
+        childDocsFunc = () => this.childDocs;
+        @action setFilterFunc = (func?: (doc: Doc) => boolean) => { this._filterFunc = func; }; // prettier-ignore
+
+        public flashCardUI = (curDoc: () => Doc | undefined, docViewProps: () => DocumentViewProps, answered?: (correct: boolean) => void) => {
+            return (
+                <FlashcardPracticeUI
+                    setFilterFunc={this.setFilterFunc}
+                    fieldKey={this.fieldKey}
+                    sideBtnWidth={this._sideBtnWidth}
+                    allChildDocs={this.childDocsFunc}
+                    filteredChildDocs={this.filteredChildDocs}
+                    advance={answered}
+                    curDoc={curDoc}
+                    practiceBtnOffset={this._sideBtnWidth * 4}
+                    layoutDoc={this.layoutDoc}
+                    uiBtnScaling={this.uiBtnScaling}
+                    ScreenToLocalBoxXf={this.ScreenToLocalBoxXf}
+                    renderDepth={this._props.renderDepth}
+                    docViewProps={docViewProps}
+                />
+            );
+        };
     }
 
     return CollectionSubViewInternal;
diff --git a/src/client/views/collections/FlashcardPracticeUI.scss b/src/client/views/collections/FlashcardPracticeUI.scss
index 2f99500f8..c5252bbfa 100644
--- a/src/client/views/collections/FlashcardPracticeUI.scss
+++ b/src/client/views/collections/FlashcardPracticeUI.scss
@@ -50,7 +50,7 @@
             height: 20px;
             align-items: center;
             margin: auto;
-            padding: 3px;
+            // padding: 3px;
             &:hover {
                 color: white;
             }
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
index a643c95b0..7bf4d86d1 100644
--- a/src/client/views/collections/FlashcardPracticeUI.tsx
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -23,15 +23,14 @@ enum practiceVal {
 interface PracticeUIProps {
     fieldKey: string;
     layoutDoc: Doc;
-    carouselItems: () => Doc[];
-    childDocs: Doc[];
+    filteredChildDocs: () => Doc[];
+    allChildDocs: () => Doc[];
     curDoc: () => Doc | undefined;
-    advance: (correct: boolean) => void;
+    advance?: (correct: boolean) => void;
     renderDepth: number;
     sideBtnWidth: number;
-    uiBtnScaleTransform: number;
+    uiBtnScaling: number;
     ScreenToLocalBoxXf: () => Transform;
-    maxWidgetScale: number;
     docViewProps: () => DocumentViewProps;
     setFilterFunc: (func?: (doc: Doc) => boolean) => void;
     practiceBtnOffset?: number;
@@ -51,7 +50,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
     get practiceField() { return this._props.fieldKey + "_practice"; } // prettier-ignore
 
     @computed get filterDoc() { return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter'); } // prettier-ignore
-    @computed get practiceMode() {  return this._props.childDocs.some(doc => doc._layout_isFlashcard) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
+    @computed get practiceMode() {  return this._props.allChildDocs().some(doc => doc._layout_isFlashcard) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
 
     btnHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this._props.ScreenToLocalBoxXf().Scale);
     btnWidth = () => (!this.filterDoc ? 1 : (this.btnHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height));
@@ -62,12 +61,12 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
      */
     setPracticeMode = (mode: practiceMode | undefined) => {
         this._props.layoutDoc.practiceMode = mode;
-        this._props.carouselItems().map(doc => (doc[this.practiceField] = undefined));
+        this._props.allChildDocs().map(doc => (doc[this.practiceField] = undefined));
     };
 
     @computed get emptyMessage() {
-        const cardCount = this._props.carouselItems().length;
-        const practiceMessage = this.practiceMode && !Doc.hasDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny) && !this._props.carouselItems().length ? 'Finished! Click here to view all flashcards.' : '';
+        const cardCount = this._props.filteredChildDocs().length;
+        const practiceMessage = this.practiceMode && !Doc.hasDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny) && !cardCount ? 'Finished! Click here to view all flashcards.' : '';
         const filterMessage = practiceMessage
             ? ''
             : Doc.hasDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny) && !cardCount
@@ -78,7 +77,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
         return !practiceMessage && !filterMessage ? null : (
             <p
                 className="FlashcardPracticeUI-message"
-                style={{ transform: `scale(${this._props.uiBtnScaleTransform})` }}
+                style={{ transform: `scale(${this._props.uiBtnScaling})` }}
                 onClick={() => {
                     if (filterMessage || practiceMessage) {
                         this.setPracticeMode(undefined);
@@ -102,7 +101,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
         };
 
         return this.practiceMode == practiceMode.PRACTICE && this._props.curDoc() ? (
-            <div className="FlashcardPracticeUI-practice" style={{ transform: `scale(${this._props.uiBtnScaleTransform})`, bottom: `${this._props.practiceBtnOffset ?? this._props.sideBtnWidth}px`, height: `${this._props.sideBtnWidth}px` }}>
+            <div className="FlashcardPracticeUI-practice" style={{ transform: `scale(${this._props.uiBtnScaling})`, bottom: `${this._props.practiceBtnOffset ?? this._props.sideBtnWidth}px`, height: `${this._props.sideBtnWidth}px` }}>
                 <Tooltip title="Incorrect. View again later.">
                     <div key="remove" className="FlashcardPracticeUI-remove" onClick={e => setPracticeVal(e, practiceVal.MISSED)}>
                         <FontAwesomeIcon icon="xmark" color="red" size="1x" />
@@ -120,12 +119,12 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
         const setColor = (mode: practiceMode) => (StrCast(this.practiceMode) === mode ? 'white' : 'light gray');
         const togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode);
 
-        return !this._props.childDocs.some(doc => doc._layout_isFlashcard) ? null : (
+        return !this._props.allChildDocs().some(doc => doc._layout_isFlashcard) ? null : (
             <div
                 className="FlashcardPracticeUI-practiceModes"
                 style={{
                     transformOrigin: `0px ${-this.btnHeight()}px`,
-                    transform: `scale(${Math.max(1, 1 / this._props.ScreenToLocalBoxXf().Scale / this._props.maxWidgetScale)})`,
+                    transform: `scale(${Math.max(1, 1 / this._props.ScreenToLocalBoxXf().Scale)})`,
                 }}>
                 <Tooltip title="Practice flashcards using GPT">
                     <div key="back" className="FlashcardPracticeUI-quiz" style={{ width: this.btnWidth(), height: this.btnHeight() }} onClick={() => togglePracticeMode(practiceMode.QUIZ)}>
@@ -146,7 +145,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
             <>
                 {this.emptyMessage}
                 {this.practiceButtons}
-                <div className="FlashcardPracticeUI-menu" style={{ height: this.btnHeight(), width: this.btnHeight(), transform: `scale(${this._props.uiBtnScaleTransform})` }}>
+                <div className="FlashcardPracticeUI-menu" style={{ height: this.btnHeight(), width: this.btnHeight(), transform: `scale(${this._props.uiBtnScaling})` }}>
                     {!this.filterDoc || this._props.layoutDoc._chromeHidden ? null : (
                         <DocumentView
                             {...this._props.docViewProps()}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d8678eebc..0cc63d632 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -138,7 +138,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine);
     }
     @computed get childPointerEvents() {
-        return falseSnappingManager.IsResizing
+        return SnappingManager.IsResizing
             ? 'none'
             : (this._props.childPointerEvents?.() ??
                   (this._props.viewDefDivClick || //
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 81e223028..a8c91aae8 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -87,34 +87,26 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         );
     }
 
-    _sideBtnWidth = 30;
+    _sideBtnWidth = 35;
     /**
      * How much the content of the view is being scaled based on its nesting and its fit-to-width settings
      */
-    @computed get contentScaling() {
-        return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1);
-    }
+    @computed get viewScaling() { return this.ScreenToLocalBoxXf().Scale; } // prettier-ignore
     /**
      * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
      */
-    @computed get maxWidgetScale() {
-        const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.25 * Math.min(NumCast(this.Document.width), NumCast(this.Document.height)));
-        return Math.max(maxWidgetSize / this._sideBtnWidth, 1);
-    }
+    @computed get maxWidgetSize() { return  Math.min(this._sideBtnWidth * this.viewScaling, 0.25 * Math.min(NumCast(this.Document.width), NumCast(this.Document.height))); } // prettier-ignore
     /**
      * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
      */
-    @computed get uiBtnScaleTransform() {
-        return this.maxWidgetScale * Math.min(1, this.contentScaling);
-    }
+    @computed get uiBtnScaling() { return Math.max(this.maxWidgetSize / this._sideBtnWidth, 1) * Math.min(1, this.viewScaling)* (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore
 
     @computed get flashcardMenu() {
         return (
-            <div className="comparisonBox-bottomMenu" style={{ transform: `scale(${this.uiBtnScaleTransform})` }}>
+            <div className="comparisonBox-bottomMenu" style={{ transform: `scale(${this.uiBtnScaling})` }}>
                 {this.overlayAlternateIcon}
                 {!this._props.isContentActive() ? null : (
                     <>
-                        {' '}
                         {!this._frontSide ? null : (
                             <Tooltip
                                 title={
-- 
cgit v1.2.3-70-g09d2


From f75f15ae58b57644c28d42446c176289bbe55dd4 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 10 Oct 2024 18:27:25 -0400
Subject: adjusted hiding chrome for carousel.  cleaned up some comparisonBox
 quiz code.  removed create flashcard pile button from flashcard - would
 prefer carousel being added to marquee menu.

---
 .../views/collections/CollectionCarouselView.tsx   |   2 +-
 .../views/collections/FlashcardPracticeUI.tsx      |  46 ++++-----
 src/client/views/nodes/ComparisonBox.scss          |  29 +++++-
 src/client/views/nodes/ComparisonBox.tsx           | 109 ++++++++++-----------
 src/client/views/pdf/AnchorMenu.tsx                |   2 +-
 5 files changed, 106 insertions(+), 82 deletions(-)

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

diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 538eba356..aa447c7bf 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -184,7 +184,7 @@ export class CollectionCarouselView extends CollectionSubView() {
     }
 
     @computed get navButtons() {
-        return this.Document._chromeHidden || !this.curDoc() ? null : (
+        return !this.curDoc() ? null : (
             <>
                 <div key="back" className="carouselView-back" style={{ transform: `scale(${this.uiBtnScaling})` }} onClick={this.goback}>
                     <FontAwesomeIcon icon="chevron-left" size="2x" />
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
index 4e424f5cd..7697d308b 100644
--- a/src/client/views/collections/FlashcardPracticeUI.tsx
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -15,7 +15,7 @@ import { SnappingManager } from '../../util/SnappingManager';
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { emptyFunction } from '../../../Utils';
 
-enum practiceMode {
+export enum practiceMode {
     PRACTICE = 'practice',
     QUIZ = 'quiz',
 }
@@ -172,27 +172,29 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
             <>
                 {this.emptyMessage}
                 {this.practiceButtons}
-                <div className="FlashcardPracticeUI-menu" style={{ height: this.btnHeight(), width: this.btnHeight(), transform: `scale(${this._props.uiBtnScaling})` }}>
-                    {!this.filterDoc || this._props.layoutDoc._chromeHidden ? null : (
-                        <DocumentView
-                            {...this._props.docViewProps()}
-                            Document={this.filterDoc}
-                            TemplateDataDocument={undefined}
-                            PanelWidth={this.btnWidth}
-                            PanelHeight={this.btnHeight}
-                            NativeWidth={returnZero}
-                            NativeHeight={returnZero}
-                            hideDecorations={BoolCast(this._props.layoutDoc.layout_hideDecorations)}
-                            hideCaptions={true}
-                            hideFilterStatus={true}
-                            renderDepth={this._props.renderDepth + 1}
-                            fitWidth={undefined}
-                            showTags={false}
-                            setContentViewBox={undefined}
-                        />
-                    )}
-                    {this.practiceModesMenu}
-                </div>
+                {this._props.layoutDoc._chromeHidden ? null : (
+                    <div className="FlashcardPracticeUI-menu" style={{ height: this.btnHeight(), width: this.btnHeight(), transform: `scale(${this._props.uiBtnScaling})` }}>
+                        {!this.filterDoc ? null : (
+                            <DocumentView
+                                {...this._props.docViewProps()}
+                                Document={this.filterDoc}
+                                TemplateDataDocument={undefined}
+                                PanelWidth={this.btnWidth}
+                                PanelHeight={this.btnHeight}
+                                NativeWidth={returnZero}
+                                NativeHeight={returnZero}
+                                hideDecorations={BoolCast(this._props.layoutDoc.layout_hideDecorations)}
+                                hideCaptions={true}
+                                hideFilterStatus={true}
+                                renderDepth={this._props.renderDepth + 1}
+                                fitWidth={undefined}
+                                showTags={false}
+                                setContentViewBox={undefined}
+                            />
+                        )}
+                        {this.practiceModesMenu}
+                    </div>
+                )}
             </>
         );
     }
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index 8156c50f6..c328ef4bf 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -9,6 +9,7 @@
     z-index: 0;
     pointer-events: none;
     display: flex;
+    flex-direction: column;
     p {
         // bcz: what is this styling for?  if text in the comparison box is colored, then this causes it to render with a black outline
         color: rgb(0, 0, 0);
@@ -32,8 +33,10 @@
         padding-right: 5px;
         border-radius: 2px;
         height: 17%;
-        display: inline-block;
         bottom: 0;
+        overflow: hidden;
+        display: flex;
+        width: 100%;
 
         &.schema-header-button {
             color: gray;
@@ -61,6 +64,29 @@
             float: left;
             border-radius: 2px;
         }
+        .submit-buttonrecord {
+            border-radius: 2px;
+        }
+        .submit-buttonpronunciation {
+            display: inline-flex;
+            align-items: center;
+        }
+        .submit-buttonschema-header-button {
+            position: absolute;
+            top: 5px;
+            left: 11px;
+            z-index: 10;
+            width: 5px;
+            height: 5px;
+            cursor: pointer;
+        }
+        .submit-buttonsubmit {
+            border-radius: 2px;
+            margin-bottom: 3px;
+            width: 100%;
+            display: inline-flex;
+            align-items: center;
+        }
     }
 
     .dropbtn {
@@ -194,6 +220,7 @@
 
     .loading-spinner {
         display: flex;
+        position: absolute;
         justify-content: center;
         align-items: center;
         height: 90%;
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 9852228fa..ccbe98257 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -1,7 +1,7 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import axios from 'axios';
-import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
+import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import ReactLoading from 'react-loading';
@@ -30,6 +30,7 @@ import './ComparisonBox.scss';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
+import { practiceMode } from '../collections/FlashcardPracticeUI';
 
 const API_URL = 'https://api.unsplash.com/search/photos';
 
@@ -122,18 +123,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             </Tooltip>
                         )}
                         {DocCast(this.Document.embedContainer)?.type_collection !== CollectionViewType.Freeform ? null : (
-                            <>
-                                <Tooltip title={<div>Create a flashcard pile</div>}>
-                                    <div className="comparisonBox-button" onPointerDown={() => this.createFlashcardPile([this.Document], false)}>
-                                        <FontAwesomeIcon icon="folder-plus" size="xl" />
-                                    </div>
-                                </Tooltip>
-                                <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}>
-                                    <div className="comparisonBox-button" onClick={this.gptFlashcardPile}>
-                                        <FontAwesomeIcon icon="layer-group" size="xl" />
-                                    </div>
-                                </Tooltip>
-                            </>
+                            <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}>
+                                <div className="comparisonBox-button" onClick={this.gptFlashcardPile}>
+                                    <FontAwesomeIcon icon="layer-group" size="xl" />
+                                </div>
+                            </Tooltip>
                         )}
                     </>
                 )}
@@ -153,7 +147,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         this._frontSide = !this._frontSide;
     };
 
-    @action handleRenderGPTClick = async () => {
+    @action handleRenderGPTClick = () => {
         const phonTrans = DocCast(this.Document.audio) ? DocCast(this.Document.audio).phoneticTranscription : undefined;
         if (phonTrans) {
             this._inputValue = StrCast(phonTrans);
@@ -490,20 +484,22 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         }
         try {
             const res = await gptAPICall(questionText, GPTCallType.FLASHCARD);
-            if (!res) {
-                console.error('GPT call failed');
-                return;
-            }
-            if (callType == GPTCallType.CHATCARD) {
-                DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res;
-            } else if (callType == GPTCallType.QUIZ) {
-                this._frontSide = true;
-                this._outputValue = res.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
-            } else if (callType === GPTCallType.FLASHCARD) {
+            runInAction(() => {
+                if (!res) {
+                    console.error('GPT call failed');
+                    return;
+                }
+                if (callType == GPTCallType.CHATCARD) {
+                    DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res;
+                } else if (callType == GPTCallType.QUIZ) {
+                    this._frontSide = true;
+                    this._outputValue = res.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
+                } else if (callType === GPTCallType.FLASHCARD) {
+                    this._loading = false;
+                    return res;
+                }
                 this._loading = false;
-                return res;
-            }
-            this._loading = false;
+            });
             return res;
         } catch (err) {
             console.error('GPT call failed', err);
@@ -725,24 +721,33 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
 
         if (this.Document._layout_isFlashcard) {
             const side = this._frontSide ? 1 : 0;
+            const dataSplit = StrCast(this.dataDoc.data).includes('Keyword: ') ? StrCast(this.dataDoc.data).split('Keyword: ') : StrCast(this.dataDoc.data).split('Answer: ');
+            const textCreator = (which: number, title: string, text: string) => {
+                const newDoc = Docs.Create.TextDocument(text, {
+                    title, //
+                    _layout_autoHeight: true,
+                    _layout_centered: true,
+                    text_align: 'center',
+                    _layout_fitWidth: true,
+                });
+                this.addDoc(newDoc, this.fieldKey + '_' + which);
+                return newDoc;
+            };
 
             // add text box to each side when comparison box is first created
             if (!this.dataDoc[this.fieldKey + '_0'] && !this._isEmpty) {
-                const dataSplit = StrCast(this.dataDoc.data).includes('Keyword: ') ? StrCast(this.dataDoc.data).split('Keyword: ') : StrCast(this.dataDoc.data).split('Answer: ');
-                const newDoc = Docs.Create.TextDocument(dataSplit[1], { title: 'answer', _layout_autoHeight: true, _layout_centered: true, text_align: 'center', _layout_fitWidth: true });
-                this.addDoc(newDoc, this.fieldKey + '_0');
+                textCreator(0, 'answer', dataSplit[1]);
             }
 
             if (!this.dataDoc[this.fieldKey + '_1'] && !this._isEmpty) {
-                const dataSplit = StrCast(this.dataDoc.data).includes('Keyword: ') ? StrCast(this.dataDoc.data).split('Keyword: ') : StrCast(this.dataDoc.data).split('Answer: ');
-                const newDoc = Docs.Create.TextDocument(dataSplit[0], { title: 'question', _layout_autoHeight: true, _layout_centered: true, text_align: 'center', _layout_fitWidth: true });
-                this.addDoc(newDoc, this.fieldKey + '_1');
+                const question = textCreator(1, 'question', dataSplit[0] || 'hint: Enter a topic, select this document and click the stack button to have GPT create a deck of cards');
+                Doc.SelectOnLoad = dataSplit[0] ? undefined : question;
             }
 
-            if (DocCast(this.Document.embedContainer) && DocCast(this.Document.embedContainer).practiceMode === 'quiz') {
+            if (DocCast(this.Document.embedContainer).practiceMode === practiceMode.QUIZ) {
                 const text = StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text);
                 return (
-                    <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} style={{ display: 'flex', flexDirection: 'column' }}>
+                    <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`}>
                         <p style={{ color: 'white', padding: 10 }}>{text}</p>
                         <p style={{ display: text === '' ? 'flex' : 'none', color: 'white', marginLeft: '10px' }}>Return to all flashcards and add text to both sides. </p>
                         <div className="input-box">
@@ -757,38 +762,28 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                                 readOnly={this._frontSide}></textarea>
 
                             {this._loading ? (
-                                <div className="loading-spinner" style={{ position: 'absolute' }}>
+                                <div className="loading-spinner">
                                     <ReactLoading type="spin" height={30} width={30} color={'blue'} />
                                 </div>
                             ) : null}
                         </div>
                         <div>
-                            <div className="submit-button" style={{ overflow: 'hidden', display: 'flex', width: '100%' }}>
-                                <div
-                                    className="submit-buttonschema-header-button"
-                                    onPointerDown={e => this.openContextMenu(e.clientX, e.clientY, false)}
-                                    style={{ position: 'absolute', top: '5px', left: '11px', zIndex: '100', width: '5px', height: '5px', cursor: 'pointer' }}>
-                                    <FontAwesomeIcon color={'white'} icon="caret-down" />
+                            <div className="submit-button">
+                                <div className="submit-buttonschema-header-button" onPointerDown={e => this.openContextMenu(e.clientX, e.clientY, false)}>
+                                    <FontAwesomeIcon color="white" icon="caret-down" />
                                 </div>
-                                <button className="submit-buttonrecord" onClick={this._listening ? this.stopListening : this.startListening} style={{ background: this._listening ? 'lightgray' : '', borderRadius: '2px' }}>
+                                <button className="submit-buttonrecord" onClick={this._listening ? this.stopListening : this.startListening} style={{ background: this._listening ? 'lightgray' : '' }}>
                                     {<FontAwesomeIcon icon="microphone" size="lg" />}
                                 </button>
-                                <div className="submit-buttonschema-header-button" onPointerDown={e => this.openContextMenu(e.clientX, e.clientY, true)} style={{ position: 'absolute', top: '5px', left: '50px', zIndex: '100', cursor: 'pointer' }}>
-                                    <FontAwesomeIcon color={'white'} icon="caret-down" />
+                                <div className="submit-buttonschema-header-button" onPointerDown={e => this.openContextMenu(e.clientX, e.clientY, true)} style={{ left: '50px', zIndex: '100' }}>
+                                    <FontAwesomeIcon color="white" icon="caret-down" />
                                 </div>
-                                <button className="submit-buttonpronunciation" onClick={this.evaluatePronunciation} style={{ display: 'inline-flex', alignItems: 'center' }}>
+                                <button className="submit-buttonpronunciation" onClick={this.evaluatePronunciation}>
                                     Evaluate Pronunciation
                                 </button>
-
-                                {!this._frontSide ? (
-                                    <button className="submit-buttonsubmit" type="button" onClick={this.handleRenderGPTClick} style={{ borderRadius: '2px', marginBottom: '3px', width: '100%' }}>
-                                        Submit
-                                    </button>
-                                ) : (
-                                    <button className="submit-buttonsubmit" type="button" onClick={this.handleRenderClick} style={{ display: 'inline-flex', alignItems: 'center', borderRadius: '2px', marginBottom: '3px', width: '100%' }}>
-                                        Redo the Question
-                                    </button>
-                                )}
+                                <button className="submit-buttonsubmit" type="button" onClick={this._frontSide ? this.handleRenderClick : this.handleRenderGPTClick}>
+                                    {this._frontSide ? 'Redo the Question' : 'Submit'}
+                                </button>
                             </div>
                         </div>
                     </div>
@@ -804,7 +799,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     onMouseLeave={() => this.hoverFlip(false)}>
                     {displayBox(`${this.fieldKey}_${side === 0 ? 1 : 0}`, side, this._props.PanelWidth() - 3)}
                     {this._loading ? (
-                        <div className="loading-spinner" style={{ position: 'absolute' }}>
+                        <div className="loading-spinner">
                             <ReactLoading type="spin" height={30} width={30} color={'blue'} />
                         </div>
                     ) : null}
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index b204d3692..7243473e0 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -254,7 +254,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
                         />
                     )}
                     {/* Adds a create flashcards option to the anchor menu, which calls the gptFlashcard method. */}
-                    <IconButton tooltip="Create flashcards" onPointerDown={this.gptFlashcards} icon={<FontAwesomeIcon icon="id-card" size="lg" />} color={SettingsManager.userColor} />
+                    <IconButton tooltip="Create flashcards" onPointerDown={this.gptFlashcards} icon={<FontAwesomeIcon icon="layer-group" size="lg" />} color={SettingsManager.userColor} />
                     <IconButton tooltip="Create labels" onPointerDown={this.makeLabels} icon={<FontAwesomeIcon icon="tag" size="lg" />} color={SettingsManager.userColor} />
                     {this._selectedText && (
                         <IconButton
-- 
cgit v1.2.3-70-g09d2


From 76abb174684f2cd231a0dd9f6b71484c16e0498a Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 11 Oct 2024 15:55:19 -0400
Subject: fixes for quiz mode - comparisonbox renderSide fixes. scrolling
 doesn't propagate out of carousel or card views.   fix for text with image
 Doc - now gets saved to UPDATE_CACHE working  set.

---
 .../views/collections/CollectionCardDeckView.tsx   |   6 ++
 .../views/collections/CollectionCarousel3DView.tsx |   6 ++
 .../views/collections/CollectionCarouselView.tsx   |   6 ++
 src/client/views/nodes/ComparisonBox.tsx           | 114 +++++++++++----------
 .../nodes/formattedText/DashDocCommentView.tsx     |   4 +-
 src/fields/Doc.ts                                  |  13 +++
 src/server/GarbageCollector.ts                     |   3 -
 7 files changed, 95 insertions(+), 57 deletions(-)

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

diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 286df30aa..14ce9d2af 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -46,6 +46,7 @@ export class CollectionCardView extends CollectionSubView() {
     private _dropDisposer?: DragManager.DragDropDisposer;
     private _disposers: { [key: string]: IReactionDisposer } = {};
     private _textToDoc = new Map<string, Doc>();
+    private _oldWheel: HTMLElement | null = null;
     private _dropped = false; // set when a card doc has just moved and the drop method has been called - prevents the pointerUp method from hiding doc decorations (which needs to be done when clicking on a card to animate it to front/center)
     private _clickScript = () => ScriptField.MakeScript('scriptContext._curDoc=this', { scriptContext: 'any' })!;
 
@@ -66,6 +67,10 @@ export class CollectionCardView extends CollectionSubView() {
         if (ele) {
             this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
         }
+        this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+        this._oldWheel = ele;
+        // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
+        ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
     };
     /**
      * Callback to ensure gpt's text versions of the child docs are updated
@@ -621,6 +626,7 @@ export class CollectionCardView extends CollectionSubView() {
             );
         });
     }
+    onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
 
     contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
     docViewProps = (): DocumentViewProps => ({
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index f2ba90c78..05be376ca 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -22,6 +22,7 @@ const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = requi
 @observer
 export class CollectionCarousel3DView extends CollectionSubView() {
     private _dropDisposer?: DragManager.DragDropDisposer;
+    private _oldWheel: HTMLElement | null = null;
 
     constructor(props: SubCollectionViewProps) {
         super(props);
@@ -37,6 +38,10 @@ export class CollectionCarousel3DView extends CollectionSubView() {
         if (ele) {
             this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
         }
+        this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+        this._oldWheel = ele;
+        // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
+        ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
     };
 
     @computed get scrollSpeed() {
@@ -194,6 +199,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
         return this.panelWidth() * (1 - index);
     }
 
+    onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
     curDoc = () => this.carouselItems[NumCast(this.layoutDoc._carousel_index)]?.layout;
     answered = (correct: boolean) => (!correct || !this.curDoc()) && this.changeSlide(1);
     docViewProps = () => ({
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index aa447c7bf..ef66a2c83 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -17,6 +17,7 @@ import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 export class CollectionCarouselView extends CollectionSubView() {
     private _dropDisposer?: DragManager.DragDropDisposer;
 
+    _oldWheel: HTMLElement | null = null;
     _fadeTimer: NodeJS.Timeout | undefined;
     @observable _last_index = this.carouselIndex;
     @observable _last_opacity = 1;
@@ -35,6 +36,10 @@ export class CollectionCarouselView extends CollectionSubView() {
         if (ele) {
             this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
         }
+        this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+        this._oldWheel = ele;
+        // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
+        ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
     };
 
     @computed get captionMarginX(){ return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore
@@ -91,6 +96,7 @@ export class CollectionCarouselView extends CollectionSubView() {
               : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
                 ? false
                 : undefined;
+    onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
     renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => {
         return (
             <DocumentView
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index a57090e99..111fabca3 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -39,6 +39,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(ComparisonBox, fieldKey);
     }
+    static qtoken = 'Question: ';
+    static ktoken = 'Keyword: ';
+    static atoken = 'Answer: ';
     private SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
     private _closeRef = React.createRef<HTMLDivElement>();
     private _disposers: { [key: string]: DragManager.DragDropDisposer | undefined } = {};
@@ -55,11 +58,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     @observable private _childActive = false;
     @observable private _animating = '';
     @observable private _listening = false;
-    @observable private _renderSide = this.fieldKey;
+    @observable private _renderSide = this.frontKey;
     @observable private _recognition = new this.SpeechRecognition();
 
     @computed get isFlashcard()   { return BoolCast(this.Document.layout_isFlashcard); } // prettier-ignore
-    @computed get frontKey()      { return this._props.fieldKey; } // prettier-ignore
+    @computed get frontKey()      { return this._props.fieldKey + '_front'; } // prettier-ignore
     @computed get backKey()       { return this._props.fieldKey + '_back'; } // prettier-ignore
     @computed get revealOpKey()   { return `_${this._props.fieldKey}_revealOp`; } // prettier-ignore
     @computed get clipHeightKey() { return `_${this._props.fieldKey}_clipHeight`; } // prettier-ignore
@@ -124,7 +127,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                                 </div>
                             </Tooltip>
                         )}
-                        {DocCast(this.Document.embedContainer)?.type_collection !== CollectionViewType.Freeform ? null : (
+                        {DocCast(this.Document.embedContainer)?.type_collection !== CollectionViewType.Freeform || this._renderSide === this.backKey ? null : (
                             <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}>
                                 <div className="comparisonBox-button" onClick={this.gptFlashcardPile}>
                                     <FontAwesomeIcon icon="layer-group" size="xl" />
@@ -150,9 +153,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         if (phonTrans) {
             this._inputValue = StrCast(phonTrans);
             this.askGPTPhonemes(this._inputValue);
+            this._renderSide = this.backKey;
+            this._outputValue = '';
         } else if (this._inputValue) this.askGPT(GPTCallType.QUIZ);
-        this._renderSide = this.backKey;
-        this._outputValue = '';
     };
 
     onPointerMove = ({ movementX }: PointerEvent) => {
@@ -444,38 +447,41 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             }),
             text
         );
-    /**
-     * Transfers the content of flashcards into a flashcard pile.
-     */
-    gptFlashcardPile = async () => {
-        this.askGPT(GPTCallType.STACK).then(text => {
-            const [qtoken, ktoken, atoken] = ['Question: ', 'Keyword: ', 'Answer: '];
-            const collectionArr: Doc[] = [];
-            const promises = text
-                .split(qtoken)
+
+    createFlashcard = (tuple: string, useDoc?: Doc) => {
+        const [ktoken, atoken] = [ComparisonBox.ktoken, ComparisonBox.atoken];
+        const newDoc = useDoc ?? Docs.Create.ComparisonDocument('', { _layout_isFlashcard: true, _width: 300, _height: 300 });
+        const question = (tuple.includes(ktoken) ? tuple.split(ktoken)[0] : tuple).split(atoken)[0];
+        const rest = tuple.replace(question, '');
+        // prettier-ignore
+        const answer = rest.startsWith(ktoken) ?     // if keyword comes first, 
+                            tuple.includes(atoken) ? tuple.split(atoken)[1] : "" : //if tuple includes answer, split at answer and take what's left, otherwise there's no answer
+                        rest.includes(ktoken) ?      // otherwise if keyword is present it must come after answer, 
+                            rest.split(ktoken)[0].split(atoken)[1] :  // split at keyword and take what comes first and split that at answer and take what's left
+                            rest.replace(atoken,""); // finally if there's no keyword, just get rid of answer token and take what's left
+        const keyword = rest.replace(atoken, '').replace(answer, '').replace(ktoken, '').trim();
+        const fillInFlashcard = (img?: Doc) => {
+            newDoc[DocData][this.frontKey] = this.textCreator('question', question, img);
+            newDoc[DocData][this.backKey] = this.textCreator('answer', answer);
+            return newDoc;
+        };
+        return keyword && keyword !== 'none' ? this.fetchImages(keyword).then(img => fillInFlashcard(img)) : fillInFlashcard();
+    };
+
+    createFlashcardDeck = (text: string) => {
+        Promise.all(
+            text
+                .split(ComparisonBox.qtoken)
                 .filter(t => t)
-                .map(tuple => {
-                    const newDoc = Docs.Create.ComparisonDocument('', { _layout_isFlashcard: true, _width: 300, _height: 300 });
-                    const question = (tuple.includes(ktoken) ? tuple.split(ktoken)[0] : tuple).split(atoken)[0];
-                    const rest = tuple.replace(question, '');
-                    // prettier-ignore
-                    const answer = rest.startsWith(ktoken) ?     // if keyword comes first, 
-                                        tuple.includes(atoken) ? tuple.split(atoken)[1] : "" : //if tuple includes answer, split at answer and take what's left, otherwise there's no answer
-                                    rest.includes(ktoken) ?      // otherwise if keyword is present it must come after answer, 
-                                        rest.split(ktoken)[0].split(atoken)[1] :  // split at keyword and take what comes first and split that at answer and take what's left
-                                        rest.replace(atoken,""); // finally if there's no keyword, just get rid of answer token and take what's left
-                    const keyword = rest.replace(atoken, '').replace(answer, '').replace(ktoken, '').trim();
-                    const fillInFlashcard = (img?: Doc) => {
-                        newDoc[DocData][this.frontKey] = this.textCreator('question', question, img);
-                        newDoc[DocData][this.backKey] = this.textCreator('answer', answer);
-                        collectionArr.push(newDoc);
-                    };
-                    return keyword && keyword !== 'none' ? this.fetchImages(keyword).then(img => fillInFlashcard(img)) : fillInFlashcard();
-                });
-            Promise.all(promises).then(() => this.createFlashcardPile(collectionArr, true));
-        });
+                .map(tuple => this.createFlashcard(tuple))
+        ).then(docs => this.createFlashcardPile(docs, true));
     };
 
+    /**
+     * queries GPT about a topic and then creates a flashcard deck from the results.
+     */
+    gptFlashcardPile = () => this.askGPT(GPTCallType.STACK).then(this.createFlashcardDeck);
+
     /**
      * Calls GPT for each flashcard type.
      */
@@ -498,7 +504,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             break;
                         case GPTCallType.QUIZ:
                             runInAction(() => {
-                                this._renderSide = this.frontKey;
+                                this._renderSide = this.backKey;
                                 this._outputValue = res.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
                             });
                             break;
@@ -728,20 +734,24 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         );
 
         if (this.isFlashcard) {
-            const dataSplit = StrCast(this.dataDoc.data).includes('Keyword: ') ? StrCast(this.dataDoc.data).split('Keyword: ') : StrCast(this.dataDoc.data).split('Answer: ');
-
-            // add text box to each side when comparison box is first created
-            if (!this.dataDoc[this.backKey] && !this._isEmpty) {
-                this.dataDoc[this.backKey] = this.textCreator('answer', dataSplit[1]);
+            if (this.dataDoc.data) {
+                if (!this.dataDoc[this.backKey] || !this.dataDoc[this.frontKey]) this.createFlashcard(StrCast(this.dataDoc.data), this.Document);
+            } else {
+                // add text box to each side when comparison box is first created
+                if (!this.dataDoc[this.backKey] && !this._isEmpty) {
+                    const answer = this.textCreator('answer', 'answer here');
+                    this.dataDoc[this.backKey] = answer;
+                    answer[DocData].text_placeholder = true;
+                }
+
+                if (!this.dataDoc[this.frontKey] && !this._isEmpty) {
+                    const question = this.textCreator('question', 'hint: Enter a topic, select this document and click the stack button to have GPT create a deck of cards');
+                    this.dataDoc[this.frontKey] = question;
+                    question[DocData].text_placeholder = true;
+                }
             }
 
-            if (!this.dataDoc[this.frontKey] && !this._isEmpty) {
-                const question = this.textCreator('question', dataSplit[0] || 'hint: Enter a topic, select this document and click the stack button to have GPT create a deck of cards');
-                this.dataDoc[this.frontKey] = question;
-                !dataSplit[0] && (question[DocData].text_placeholder = true);
-            }
-
-            if (DocCast(this.Document.embedContainer).practiceMode === practiceMode.QUIZ) {
+            if (DocCast(this.Document.embedContainer)?.practiceMode === practiceMode.QUIZ) {
                 const text = StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text);
                 return (
                     <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`}>
@@ -749,15 +759,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         <p style={{ display: text === '' ? 'flex' : 'none', color: 'white', marginLeft: '10px' }}>Return to all flashcards and add text to both sides. </p>
                         <div className="input-box">
                             <textarea
-                                value={this._renderSide ? this._outputValue : this._inputValue}
+                                value={this._renderSide === this.backKey ? this._outputValue : this._inputValue}
                                 onChange={this.handleInputChange}
                                 onScroll={e => {
                                     e.stopPropagation();
                                     e.preventDefault();
                                 }}
                                 placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''}
-                                readOnly={this._renderSide === this.frontKey}></textarea>
-
+                                readOnly={this._renderSide === this.backKey}
+                            />
                             {!this.loading ? null : (
                                 <div className="loading-spinner">
                                     <ReactLoading type="spin" height={30} width={30} color={'blue'} />
@@ -778,8 +788,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                                 <button className="submit-buttonpronunciation" onClick={this.evaluatePronunciation}>
                                     Evaluate Pronunciation
                                 </button>
-                                <button className="submit-buttonsubmit" type="button" onClick={this._renderSide === this.frontKey ? this.flipFlashcard : this.handleRenderGPTClick}>
-                                    {this._renderSide === this.frontKey ? 'Redo the Question' : 'Submit'}
+                                <button className="submit-buttonsubmit" type="button" onClick={this._renderSide === this.backKey ? this.flipFlashcard : this.handleRenderGPTClick}>
+                                    {this._renderSide === this.backKey ? 'Redo the Question' : 'Submit'}
                                 </button>
                             </div>
                         </div>
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
index 0304ddc86..967f4aa5b 100644
--- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
@@ -68,7 +68,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
                 expand && this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
                 try {
                     this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, (this.props.getPos() ?? 0) + (expand ? 2 : 1))));
-                } catch (err) {
+                } catch {
                     /* empty */
                 }
             }, 0);
@@ -95,7 +95,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
         setTimeout(() => {
             try {
                 this.props.view.dispatch(state.tr.setSelection(TextSelection.create(state.tr.doc, this.props.getPos() + 2)));
-            } catch (err) {
+            } catch {
                 /* empty */
             }
         }, 0);
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 81241f9fe..45dfe233f 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -960,6 +960,19 @@ export namespace Doc {
                         }
                     } else if (field instanceof PrefetchProxy) {
                         Doc.FindReferences(field.value, references, system);
+                    } else if (field instanceof RichTextField) {
+                        const re = /"docId"\s*:\s*"(.*?)"/g;
+                        let match: string[] | null;
+                        while ((match = re.exec(field.Data)) !== null) {
+                            const urlString = match[1];
+                            if (urlString) {
+                                const rdoc = DocServer.GetCachedRefField(urlString);
+                                if (rdoc) {
+                                    references.add(rdoc);
+                                    Doc.FindReferences(rdoc, references, system);
+                                }
+                            }
+                        }
                     }
                 } else if (field instanceof Promise) {
                     // eslint-disable-next-line no-debugger
diff --git a/src/server/GarbageCollector.ts b/src/server/GarbageCollector.ts
index 041f65592..74e8c288a 100644
--- a/src/server/GarbageCollector.ts
+++ b/src/server/GarbageCollector.ts
@@ -1,7 +1,4 @@
 /* eslint-disable no-await-in-loop */
-/* eslint-disable no-continue */
-/* eslint-disable no-cond-assign */
-/* eslint-disable no-restricted-syntax */
 import * as fs from 'fs';
 import * as path from 'path';
 import { Database } from './database';
-- 
cgit v1.2.3-70-g09d2


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

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

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

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


From 9779182e74e427d3a8a2004d0efd01fac8387d55 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 16 Oct 2024 17:31:12 -0400
Subject: major fixes to cardDeck view to simplify code and to make arch follow
 a true circle arc and to fix doc sizing when fitwidth/lightbox/etc.  fixes to
 flashcard UI for advancing to next Doc in cardView and carousel3D.

---
 src/client/documents/Documents.ts                  |   4 +-
 src/client/views/DocumentDecorations.tsx           |   4 +-
 .../views/collections/CollectionCardDeckView.scss  |  24 +-
 .../views/collections/CollectionCardDeckView.tsx   | 336 +++++++++++----------
 .../views/collections/CollectionCarousel3DView.tsx |   2 +-
 .../views/collections/CollectionCarouselView.tsx   |   2 +-
 .../views/collections/FlashcardPracticeUI.scss     |   6 +
 .../views/collections/FlashcardPracticeUI.tsx      |   6 +-
 src/client/views/global/globalScripts.ts           |  26 +-
 src/client/views/nodes/ComparisonBox.tsx           |  25 +-
 src/client/views/nodes/DocumentContentsView.tsx    |   1 -
 11 files changed, 230 insertions(+), 206 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 99af1f1a9..e539e3c65 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -508,8 +508,8 @@ export class DocumentOptions {
     userBackgroundColor?: STRt = new StrInfo('background color associated with a Dash user (seen in header fields of shared documents)');
     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');
-    cardSort_isDesc?: BOOLt = new BoolInfo('whether the cards are sorted ascending or descending');
+    card_sort?: STRt = new StrInfo('way cards are sorted in deck view');
+    card_sort_isDesc?: BOOLt = new BoolInfo('whether the cards are sorted ascending or descending');
 }
 
 export const DocOptions = new DocumentOptions();
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 62f2de776..5a48b6c62 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -10,7 +10,7 @@ import { lightOrDark, returnFalse, setupMoveUpEvents } from '../../ClientUtils';
 import { Utils, emptyFunction, numberValue } from '../../Utils';
 import { DateField } from '../../fields/DateField';
 import { Doc, DocListCast, Field, FieldType, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc';
-import { AclAdmin, AclAugment, AclEdit, DocData } from '../../fields/DocSymbols';
+import { AclAdmin, AclAugment, AclEdit, Animation, DocData } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
 import { InkField } from '../../fields/InkField';
 import { ScriptField } from '../../fields/ScriptField';
@@ -650,7 +650,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         this._forceRender;
         const { b, r, x, y } = this.Bounds;
         const seldocview = DocumentView.Selected().lastElement();
-        if (SnappingManager.IsDragging || r - x < 1 || x === Number.MAX_VALUE || !seldocview || SnappingManager.HideDecorations || isNaN(r) || isNaN(b) || isNaN(x) || isNaN(y)) {
+        if (SnappingManager.IsDragging || r - x < 1 || x === Number.MAX_VALUE || !seldocview || seldocview?.Document[Animation] || SnappingManager.HideDecorations || isNaN(r) || isNaN(b) || isNaN(x) || isNaN(y)) {
             setTimeout(
                 action(() => {
                     this._editingTitle = false;
diff --git a/src/client/views/collections/CollectionCardDeckView.scss b/src/client/views/collections/CollectionCardDeckView.scss
index 0637cd4e9..5283601bf 100644
--- a/src/client/views/collections/CollectionCardDeckView.scss
+++ b/src/client/views/collections/CollectionCardDeckView.scss
@@ -7,23 +7,31 @@
     background-color: white;
     overflow: hidden;
     display: flex;
+    .collectionCardView-inner {
+        display: flex;
+        transform-origin: top left;
+        align-items: center;
+    }
 
     button {
         border-radius: 50%;
     }
 }
 
-.card-wrapper {
-    display: grid;
-    grid-template-columns: repeat(10, 1fr);
+.collectionCardView-flashcardUI {
+    top: 0;
+    position: absolute;
+    width: 100%;
+    height: 100%;
     transform-origin: top left;
+}
 
-    position: absolute;
+.collectionCardView-cardwrapper {
+    display: grid;
+    grid-template-columns: repeat(10, 1fr);
+    transform-origin: left 50%;
     align-items: center;
-    justify-items: center;
-    justify-content: center;
-
-    transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955);
+    z-index: 0; // so that setting z-index of active card doesn't make it land on top of things outside of the card-wrapper
 }
 
 .no-card-span {
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 14ce9d2af..5faabacf4 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -5,7 +5,7 @@ import * as React from 'react';
 import { ClientUtils, DashColor, imageUrlToBase64, returnFalse, returnNever, returnZero } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
 import { Doc } from '../../../fields/Doc';
-import { DocData } from '../../../fields/DocSymbols';
+import { Animation, DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { ScriptField } from '../../../fields/ScriptField';
@@ -96,7 +96,7 @@ export class CollectionCardView extends CollectionSubView() {
                 if (isVis) {
                     this.openChatPopup();
                 } else {
-                    this.Document.cardSort = this.cardSort === cardSortings.Chat ? '' : this.Document.cardSort;
+                    this.Document.card_sort = this.cardSort === cardSortings.Chat ? '' : this.Document.card_sort;
                 }
             }
         );
@@ -119,7 +119,25 @@ export class CollectionCardView extends CollectionSubView() {
     }
 
     @computed get cardSort() {
-        return StrCast(this.Document.cardSort) as cardSortings;
+        return StrCast(this.Document.card_sort) as cardSortings;
+    }
+    /**
+     * Number of rows of cards to be rendered
+     */
+    @computed get numRows() {
+        return Math.ceil(this.sortedDocs.length / this._maxRowCount);
+    }
+    /**
+     * Circle arc size, in radians, to layout cards
+     */
+    @computed get archAngle() {
+        return NumCast(this.layoutDoc.card_arch, 90) * (Math.PI / 180) * (this.childCards.length < this._maxRowCount ? this.childCards.length / this._maxRowCount : 1);
+    }
+    /**
+     * Spacing card rows as a percent of Doc size.   100 means rows spread out to fill 100% of the Doc vertically.  Default is 60%
+     */
+    @computed get cardSpacing() {
+        return NumCast(this.layoutDoc.card_spacing, 60);
     }
 
     /**
@@ -137,6 +155,10 @@ export class CollectionCardView extends CollectionSubView() {
         return (this.childPanelWidth() * length) / this._props.PanelWidth();
     }
 
+    @computed get nativeScaling() {
+        return this._props.NativeDimScaling?.() || 1;
+    }
+
     /**
      * When in quiz mode, randomly selects a document
      */
@@ -145,70 +167,17 @@ export class CollectionCardView extends CollectionSubView() {
         this._curDoc = this.childDocs[randomIndex];
     });
 
-    /**
-     * Number of rows of cards to be rendered
-     */
-    @computed get numRows() {
-        return Math.ceil(this.sortedDocs.length / this._maxRowCount);
-    }
-
-    @action
-    setHoveredNodeIndex = (index: number) => {
+    setHoveredNodeIndex = action((index: number) => {
         if (!SnappingManager.IsDragging) this._hoveredNodeIndex = index;
-    };
+    });
 
     isSelected = (doc: Doc) => this._docRefs.get(doc)?.IsSelected;
-    childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, this._props.PanelWidth() / 2);
+    childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, Math.max(150, this._props.PanelWidth() / (this.childCards.length > this._maxRowCount ? this._maxRowCount : this.childCards.length) / this.nativeScaling));
     childPanelHeight = () => this._props.PanelHeight() * this.fitContentScale;
     onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
     isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this.isAnyChildContentActive();
     isAnyChildContentActive = this._props.isAnyChildContentActive;
 
-    /**
-     * Returns the degree to rotate a card dependind on the amount of cards in their row and their index in said row
-     * @param amCards
-     * @param index
-     * @returns
-     */
-    rotate = (amCards: number, index: number) => {
-        if (amCards == 1) return 0;
-
-        const possRotate = -30 + index * (30 / ((amCards - (amCards % 2)) / 2));
-        if (amCards % 2 === 0) {
-            if (possRotate === 0) {
-                return possRotate + Math.abs(-30 + (index - 1) * (30 / (amCards / 2)));
-            }
-            if (index > (amCards + 1) / 2) {
-                const stepMag = Math.abs(-30 + (amCards / 2 - 1) * (30 / ((amCards - (amCards % 2)) / 2)));
-                return possRotate + stepMag;
-            }
-        }
-
-        return possRotate;
-    };
-    /**
-     * Returns the degree to which a card should be translated in the y direction for the arch effect
-     */
-    translateY = (amCards: number, index: number, realIndex: number) => {
-        const evenOdd = amCards % 2;
-        const apex = (amCards - evenOdd) / 2;
-        const Magnitude = this.childPanelWidth() / 2; // 400
-        const stepMag = Magnitude / 2 / ((amCards - evenOdd) / 2) + Math.abs((apex - index) * 25);
-
-        let rowOffset = 0;
-        if (realIndex > this._maxRowCount - 1) {
-            rowOffset = Magnitude * ((realIndex - (realIndex % this._maxRowCount)) / this._maxRowCount);
-        }
-        if (evenOdd === 1 || index < apex - 1) {
-            return Math.abs(stepMag * (apex - index)) - rowOffset;
-        }
-        if (index === apex || index === apex - 1) {
-            return 0 - rowOffset;
-        }
-
-        return Math.abs(stepMag * (apex - index - 1)) - rowOffset;
-    };
-
     /**
      * When dragging a card, determines the index the card should be set to if dropped
      * @param mouseX mouse's x location
@@ -216,28 +185,27 @@ export class CollectionCardView extends CollectionSubView() {
      * @returns the card's new index
      */
     findCardDropIndex = (mouseX: number, mouseY: number) => {
-        const amCardsTotal = this.sortedDocs.length;
+        const cardCount = this.sortedDocs.length;
         let index = 0;
-        const cardWidth = amCardsTotal < this._maxRowCount ? this._props.PanelWidth() / amCardsTotal : this._props.PanelWidth() / this._maxRowCount;
+        const cardWidth = cardCount < this._maxRowCount ? this._props.PanelWidth() / cardCount : this._props.PanelWidth() / this._maxRowCount;
 
         // Calculate the adjusted X position accounting for the initial offset
         let adjustedX = mouseX;
 
-        const amRows = Math.ceil(amCardsTotal / this._maxRowCount);
-        const rowHeight = this._props.PanelHeight() / amRows;
+        const rowHeight = this._props.PanelHeight() / this.numRows;
         const currRow = Math.floor((mouseY - 100) / rowHeight); //rows start at 0
 
         if (adjustedX < 0) {
             return 0; // Before the first column
         }
 
-        if (amCardsTotal < this._maxRowCount) {
+        if (cardCount < this._maxRowCount) {
             index = Math.floor(adjustedX / cardWidth);
-        } else if (currRow != amRows - 1) {
+        } else if (currRow != this.numRows - 1) {
             index = Math.floor(adjustedX / cardWidth) + currRow * this._maxRowCount;
         } else {
-            const rowAmCards = amCardsTotal - currRow * this._maxRowCount;
-            const offset = ((this._maxRowCount - rowAmCards) / 2) * cardWidth;
+            const cardsInRow = cardCount - currRow * this._maxRowCount;
+            const offset = ((this._maxRowCount - cardsInRow) / 2) * cardWidth;
             adjustedX = mouseX - offset;
 
             index = Math.floor(adjustedX / cardWidth) + currRow * this._maxRowCount;
@@ -246,11 +214,14 @@ export class CollectionCardView extends CollectionSubView() {
     };
 
     /**
-     * Checks to see if a card is being dragged and calls the appropriate methods if so
+     * if pointer moves over cardDeck while dragging a Doc that is in the Deck or that can be dropped in the deck,
+     * then this sets the card index where the dragged card would be added.
      */
     @action
     onPointerMove = (x: number, y: number) => {
-        this._docDraggedIndex = DragManager.docsBeingDragged.length ? this.findCardDropIndex(x, y) : -1;
+        if (DragManager.docsBeingDragged.some(doc => this.sortedDocs.includes(doc)) || SnappingManager.CanEmbed) {
+            this._docDraggedIndex = this.findCardDropIndex(x, y);
+        }
     };
 
     /**
@@ -269,7 +240,7 @@ export class CollectionCardView extends CollectionSubView() {
                     const sorted = this.sortedDocs;
                     const originalIndex = sorted.findIndex(doc => doc === draggedDoc);
 
-                    this.Document.cardSort = '';
+                    this.Document.card_sort = '';
                     originalIndex !== -1 && sorted.splice(originalIndex, 1);
                     sorted.splice(dragIndex, 0, draggedDoc);
                     if (de.complete.docDragData.removeDocument?.(draggedDoc)) {
@@ -285,15 +256,6 @@ export class CollectionCardView extends CollectionSubView() {
         ''
     );
 
-    @computed get sortedDocs() {
-        return this.sort(
-            this.childCards.map(card => card.layout),
-            this.cardSort,
-            BoolCast(this.Document.cardSort_isDesc),
-            this._docDraggedIndex
-        );
-    }
-
     /**
      * Used to determine how to sort cards based on tags. The leftmost tags are given lower values while cards to the right are
      * given higher values. Decimals are used to determine placement for cards with multiple tags
@@ -341,6 +303,15 @@ export class CollectionCardView extends CollectionSubView() {
         return docs;
     };
 
+    @computed get sortedDocs() {
+        return this.sort(
+            this.childCards.map(card => card.layout),
+            this.cardSort,
+            BoolCast(this.Document.card_sort_isDesc),
+            this._docDraggedIndex
+        );
+    }
+
     isChildContentActive = computedFn(
         (doc: Doc) => () =>
             this._props.isContentActive?.() === false
@@ -359,74 +330,99 @@ export class CollectionCardView extends CollectionSubView() {
             Document={doc}
             NativeWidth={returnZero}
             NativeHeight={returnZero}
-            fitWidth={returnFalse}
-            onDoubleClickScript={this.onChildDoubleClick}
+            PanelWidth={this.childPanelWidth}
+            PanelHeight={this.childPanelHeight}
             renderDepth={this._props.renderDepth + 1}
             LayoutTemplate={this._props.childLayoutTemplate}
             LayoutTemplateString={this._props.childLayoutString}
             containerViewPath={this.childContainerViewPath}
             ScreenToLocalTransform={screenToLocalTransform} // makes sure the box wrapper thing is in the right spot
             isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive}
-            PanelWidth={this.childPanelWidth}
-            PanelHeight={this.childPanelHeight}
+            isContentActive={this.isChildContentActive(doc)}
+            fitWidth={returnFalse}
             waitForDoubleClickToClick={returnNever}
             scriptContext={this}
+            onDoubleClickScript={this.onChildDoubleClick}
             onClickScript={this._curDoc === doc ? undefined : this._clickScript}
             dontCenter="y" // Don't center it vertically, because the grid it's in is already doing that and we don't want to do it twice.
             dragAction={(this.Document.childDragAction ?? this._props.childDragAction) as dropActionType}
             showTags={BoolCast(this.layoutDoc.showChildTags)}
             whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged}
-            isContentActive={this.isChildContentActive(doc)}
             dontHideOnDrag
         />
     );
 
     /**
      * Determines how many cards are in the row of a card at a specific index
-     * @param index
-     * @returns
+     * @param index numerical index of card in total list of all cards
+     * @returns number of cards in row that contains index
      */
-    overflowAmCardsCalc = (index: number) => {
-        if (this.sortedDocs.length < this._maxRowCount) {
-            return this.sortedDocs.length;
+    cardsInRowThatIncludesCardIndex = (index: number) => {
+        if (this.childCards.length < this._maxRowCount) {
+            return this.childCards.length;
         }
-        const totalCards = this.sortedDocs.length;
-        // if 9 or less
+        const totalCards = this.childCards.length;
         if (index < totalCards - (totalCards % this._maxRowCount)) {
             return this._maxRowCount;
         }
         return totalCards % this._maxRowCount;
     };
     /**
-     * Determines the index a card is in in a row
-     * @param realIndex
-     * @returns
+     * Determines the index a card is in in a row.  If the row is not full, then the cards
+     * are centered within the row (as if unrendered cards had been added to the start and end
+     * of the row) and the retuned index is the index the card in this virtual full row.
+     * @param index numerical index of card in total list of all cards
+     * @returns index of card in its row, normalized to a full size row
      */
-    overflowIndexCalc = (realIndex: number) => realIndex % this._maxRowCount;
+    centeredIndexOfCardInRow = (index: number) => {
+        const cardsInRow = this.cardsInRowThatIncludesCardIndex(index);
+        const lineIndex = index % this._maxRowCount;
+        if (cardsInRow === this._maxRowCount) return lineIndex;
+        return lineIndex + (this._maxRowCount - cardsInRow) / 2;
+    };
     /**
-     * Translates the cards in the second rows and beyond over to the right
-     * @param realIndex
-     * @param calcIndex
-     * @param calcRowCards
-     * @returns
+     * Returns the rotation of a card in radians based on its horizontal location (and thus m apping to a circle arc).
+     * The amount of rotation is goverend by the Doc's card_arch field which specifies, in degrees, the range of a circle
+     * arc that cards should cover -- by default, -45 to 45 degrees.
+     * @param index numerical index of card in total list of all cards
+     * @returns angle of rotation in radians
+     */
+    rotate = (index: number) => {
+        const cardsInRow = this.cardsInRowThatIncludesCardIndex(index);
+        const centeredIndexInRow = (cardsInRow < this._maxRowCount ? index + (this._maxRowCount - cardsInRow) / 2 : index) % this._maxRowCount;
+        const rowIndexMax = this._maxRowCount - 1;
+        return ((this.archAngle / 2) * (centeredIndexInRow - rowIndexMax / 2)) / (rowIndexMax / 2);
+    };
+    /**
+     * Provides a vertical adjustment to a card's grid position so that it will lie along an arch.
+     * @param index numerical index of card in total list of all cards
+     */
+    translateY = (index: number) => {
+        const Magnitude = ((this._props.PanelHeight() * this.fitContentScale) / 2) * Math.sqrt(((this.archAngle * (180 / Math.PI)) / 60) * 4);
+        return Magnitude * (1 - Math.sin(this.rotate(index) + Math.PI / 2) - (1 - Math.sin(this.archAngle / 2 + Math.PI / 2)) / 2);
+    };
+    /**
+     * When the card index is for a row (not the first row) that is not full, this returns a horizontal adjustment that centers the row
+     * @param index index of card from start of deck
+     * @param cardsInRow number of cards in the row containing the indexed card
+     * @returns horizontal pixel translation
      */
-    translateOverflowX = (realIndex: number, calcRowCards: number) => (realIndex < this._maxRowCount ? 0 : (this._maxRowCount - calcRowCards) * (this.childPanelWidth() / 2));
+    horizontalAdjustmentForPartialRows = (index: number, cardsInRow: number) => (index < this._maxRowCount ? 0 : (this._maxRowCount - cardsInRow) * (this.childPanelWidth() / 2));
 
     /**
-     * Determines how far to translate a card in the y direction depending on its index and if it's selected
+     * Adjusts the vertical placement of the card from its grid position so that it will either line on a
+     * circular arc if the card isn't active, or so that it will be centered otherwise.
      * @param isActive whether the card is focused for interaction
-     * @param realIndex index of card from start of deck
-     * @param amCards ??
-     * @param calcRowIndex index of card from start of row
-     * @returns Y translation of card
+     * @param index index of card from start of deck
+     * @returns vertical pixel translation
      */
-    calculateTranslateY = (isActive: boolean, realIndex: number, amCards: number, calcRowIndex: number) => {
-        const rowHeight = (this._props.PanelHeight() * this.fitContentScale) / this.numRows;
-        const rowIndex = Math.trunc(realIndex / this._maxRowCount);
+    adjustCardYtoFitArch = (isActive: boolean, index: number) => {
+        const rowHeight = this._props.PanelHeight() / this.numRows;
+        const rowIndex = Math.floor(index / this._maxRowCount);
         const rowToCenterShift = this.numRows / 2 - rowIndex;
-        if (isActive) return rowToCenterShift * rowHeight - rowHeight / 2;
-        if (amCards == 1) return 50 * this.fitContentScale;
-        return this.translateY(amCards, calcRowIndex, realIndex);
+        return isActive
+            ? (rowToCenterShift * rowHeight - rowHeight / 2) * ((this.cardSpacing * this.fitContentScale) / 100) //
+            : this.translateY(index);
     };
 
     /**
@@ -493,7 +489,7 @@ export class CollectionCardView extends CollectionSubView() {
         }
 
         if (questionType === '6') {
-            this.Document.cardSort = 'chat';
+            this.Document.card_sort = 'chat';
         }
 
         listItems.forEach((item, index) => {
@@ -549,7 +545,7 @@ export class CollectionCardView extends CollectionSubView() {
         await this.childPairStringListAndUpdateSortDesc();
     };
 
-    childScreenToLocal = computedFn((doc: Doc, index: number, calcRowIndex: number, isSelected: boolean, amCards: number) => () => {
+    childScreenToLocal = computedFn((doc: Doc, index: number, isSelected: boolean) => () => {
         // need to explicitly trigger an invalidation since we're reading everything from the Dom
         this._forceChildXf;
         this._props.ScreenToLocalTransform();
@@ -560,24 +556,40 @@ export class CollectionCardView extends CollectionSubView() {
 
         return new Transform(-translateX + (dref?.centeringX || 0) * scale, 
                              -translateY + (dref?.centeringY || 0) * scale, 1)
-                  .scale(1 / scale).rotate(!isSelected ? -this.rotate(amCards, calcRowIndex) : 0); // prettier-ignore
+                  .scale(1 / scale).rotate(!isSelected ? -this.rotate(this.centeredIndexOfCardInRow(index)) : 0); // prettier-ignore
     });
 
+    /**
+     * Releases the currently focused Doc by deselecting it and returning it to its location on the arch, and selecting the
+     * cardDeck itself.
+     * This will also force the Doc to recompute its layout transform when the animation completes.
+     * In addition, this sets an animating flag on the Doc so that it will receive no poiner events when animating, such as hover
+     * events that would trigger a flashcard to flip.
+     * @param doc doc that will be animated away from center focus
+     */
+    releaseCurDoc = action(() => {
+        const selDoc = this._curDoc;
+        this._curDoc = undefined;
+        const cardDocView = DocumentView.getDocumentView(selDoc, this.DocumentView?.());
+        if (cardDocView && selDoc) {
+            DocumentView.DeselectView(cardDocView);
+            this._props.select(false);
+            selDoc[Animation] = selDoc; // turns off pointer events & doc decorations while animating - useful for flashcards that reveal back on hover
+            setTimeout(action(() => {
+                selDoc[Animation] = undefined;
+                this._forceChildXf++;
+            }), 350); // prettier-ignore
+        }
+    });
+
+    /**
+     * turns off the _dropped flag at the end of a drag/drop, or releases the focused Doc if a different Doc is clicked
+     */
     cardPointerUp = action((doc: Doc) => {
-        // if a card doc has just moved, or a card is selected and in front, then ignore this event
         if (this._curDoc === doc || this._dropped) {
             this._dropped = false;
         } else {
-            // otherwise, turn off documentDecorations becase we're in a selection transition and want to avoid artifacts.
-            // Turn them back on when the animation has completed and the render and backend structures are in synch
-            SnappingManager.SetHideDecorations(true);
-            setTimeout(
-                action(() => {
-                    SnappingManager.SetHideDecorations(false);
-                    this._forceChildXf++;
-                }),
-                1000
-            );
+            this.releaseCurDoc(); // NOTE: the onClick script for the card will select the new card (ie, 'doc')
         }
     });
 
@@ -585,25 +597,17 @@ export class CollectionCardView extends CollectionSubView() {
      * Actually renders all the cards
      */
     @computed get renderCards() {
-        if (!this.childCards.length) {
-            return null;
-        }
-
+        console.log('CHILDPw = ' + this.childPanelWidth());
         // Map sorted documents to their rendered components
         return this.sortedDocs.map((doc, index) => {
-            const calcRowIndex = this.overflowIndexCalc(index);
-            const amCards = this.overflowAmCardsCalc(index);
+            const cardsInRow = this.cardsInRowThatIncludesCardIndex(index);
+
+            const childScreenToLocal = this.childScreenToLocal(doc, index, doc === this._curDoc);
 
-            const childScreenToLocal = this.childScreenToLocal(doc, index, calcRowIndex, doc === this._curDoc, amCards);
+            const translateToCenterIfActive = () => (doc === this._curDoc ? (cardsInRow / 2 - (index % this._maxRowCount)) * 100 - 50 : 0);
 
-            const translateIfSelected = () => {
-                const indexInRow = index % this._maxRowCount;
-                const rowIndex = Math.trunc(index / this._maxRowCount);
-                const rowCenterIndex = Math.min(this._maxRowCount, this.sortedDocs.length - rowIndex * this._maxRowCount) / 2;
-                return (rowCenterIndex - indexInRow) * 100 - 50;
-            };
             const aspect = NumCast(doc.height) / NumCast(doc.width, 1);
-            const vscale = Math.max(1,Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale) / (aspect * this.childPanelWidth()), 
+            const vscale = Math.max(1,Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale * this.nativeScaling) / (aspect * this.childPanelWidth()), 
                                     (this._props.PanelHeight() - 80) / (aspect * (this._props.PanelWidth() / 10)))); // prettier-ignore
             const hscale = Math.min(this.sortedDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size
             return (
@@ -614,9 +618,9 @@ export class CollectionCardView extends CollectionSubView() {
                     style={{
                         width: this.childPanelWidth(),
                         height: 'max-content',
-                        transform: `translateY(${this.calculateTranslateY(doc === this._curDoc, index, amCards, calcRowIndex)}px)
-                                    translateX(calc(${doc === this._curDoc ? translateIfSelected() : 0}% + ${this.translateOverflowX(index, amCards)}px))
-                                    rotate(${doc !== this._curDoc ? this.rotate(amCards, calcRowIndex) : 0}deg)
+                        transform: `translateY(${this.adjustCardYtoFitArch(doc === this._curDoc, index)}px)
+                                    translateX(calc(${translateToCenterIfActive()}% + ${this.horizontalAdjustmentForPartialRows(index, cardsInRow)}px))
+                                    rotate(${doc !== this._curDoc ? this.rotate(index) : 0}rad)
                                     scale(${doc === this._curDoc ? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.1 : 1})`,
                     }} // prettier-ignore
                     onPointerEnter={() => this.setHoveredNodeIndex(index)}
@@ -636,26 +640,19 @@ export class CollectionCardView extends CollectionSubView() {
         ScreenToLocalTransform: this.contentScreenToLocalXf,
     });
     answered = action(() => {
-        this._curDoc = this.curDoc ? this.filteredChildDocs()[(this.filteredChildDocs().findIndex(this.curDoc) + 1) % (this.filteredChildDocs().length || 1)] : undefined;
+        this._curDoc = this.curDoc ? this.filteredChildDocs()[(this.filteredChildDocs().findIndex(d => d === this.curDoc()) + 1) % (this.filteredChildDocs().length || 1)] : undefined;
     });
     curDoc = () => this._curDoc;
 
     render() {
-        const isEmpty = this.childCards.length === 0;
+        const fitContentScale = this.childCards.length === 0 ? 1 : this.fitContentScale;
         return (
             <div
                 className="collectionCardView-outer"
                 ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)}
-                onPointerDown={action(() => {
-                    this._curDoc = undefined;
-                    SnappingManager.SetHideDecorations(true);
-                    setTimeout(
-                        action(() => {
-                            SnappingManager.SetHideDecorations(false);
-                            this._forceChildXf++;
-                        }),
-                        1000
-                    );
+                onPointerDown={action(e => {
+                    if (e.button === 2 || e.ctrlKey) return;
+                    this.releaseCurDoc();
                 })}
                 onPointerLeave={action(() => (this._docDraggedIndex = -1))}
                 onPointerMove={e => this.onPointerMove(...this._props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY))}
@@ -665,16 +662,31 @@ export class CollectionCardView extends CollectionSubView() {
                     color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
                 }}>
                 <div
-                    className="card-wrapper"
+                    className="collectionCardView-inner"
                     style={{
-                        ...(!isEmpty && { transform: `scale(${1 / this.fitContentScale})` }),
-                        ...{ height: `${100 * (isEmpty ? 1 : this.fitContentScale)}%` },
-                        ...{ width: `${100 * (isEmpty ? 1 : this.fitContentScale)}%` },
-                        gridAutoRows: `${100 / this.numRows}%`,
+                        transform: `scale(${1 / fitContentScale})`,
+                        height: `${100 * fitContentScale}%`,
+                        width: `${100 * fitContentScale}%`,
                     }}>
-                    {this.renderCards}
+                    <div
+                        className="collectionCardView-cardwrapper"
+                        style={{
+                            gridAutoRows: `${100 / this.numRows}%`,
+                            height: `${this.cardSpacing}%`,
+                        }}>
+                        {this.renderCards}
+                    </div>
+                    <div
+                        className="collectionCardView-flashcardUI"
+                        style={{
+                            pointerEvents: this.childCards.length === 0 ? undefined : 'none',
+                            height: `${100 / this.nativeScaling / fitContentScale}%`,
+                            width: `${100 / this.nativeScaling / fitContentScale}%`,
+                            transform: `scale(${this.nativeScaling * fitContentScale})`,
+                        }}>
+                        {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
+                    </div>
                 </div>
-                {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
             </div>
         );
     }
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index e9ace733e..a71cc43ba 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -205,7 +205,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
 
     onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
     curDoc = () => this.carouselItems[NumCast(this.layoutDoc._carousel_index)]?.layout;
-    answered = (correct: boolean) => (!correct || !this.curDoc()) && this.changeSlide(1);
+    answered = (correct: boolean) => (!correct || !this.curDoc() || NumCast(this.layoutDoc._carousel_index) === this.carouselItems.length - 1) && this.changeSlide(1);
     docViewProps = () => ({
         ...this._props, //
         isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive,
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index ff587b199..1f2bc908f 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -203,7 +203,7 @@ export class CollectionCarouselView extends CollectionSubView() {
         isContentActive: this.isChildContentActive,
         ScreenToLocalTransform: this.contentScreenToLocalXf,
     });
-    answered = () => this.advance();
+    answered = (correct: boolean) => (!correct || !this.curDoc() || NumCast(this.layoutDoc._carousel_index) === this.carouselItems.length - 1) && this.advance();
 
     render() {
         return (
diff --git a/src/client/views/collections/FlashcardPracticeUI.scss b/src/client/views/collections/FlashcardPracticeUI.scss
index 4ed27793d..210c6798f 100644
--- a/src/client/views/collections/FlashcardPracticeUI.scss
+++ b/src/client/views/collections/FlashcardPracticeUI.scss
@@ -1,3 +1,9 @@
+.FlashcardPracticeUI {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+}
 .FlashcardPracticeUI-remove,
 .FlashcardPracticeUI-check {
     position: absolute;
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
index 79eb7f107..ec892ee3d 100644
--- a/src/client/views/collections/FlashcardPracticeUI.tsx
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -104,8 +104,8 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
         const setPracticeVal = (e: React.MouseEvent, val: string) => {
             e.stopPropagation();
             const curDoc = this._props.curDoc();
-            curDoc && (curDoc[this.practiceField] = val);
             this._props.advance?.(val === practiceVal.CORRECT);
+            curDoc && (curDoc[this.practiceField] = val);
         };
 
         return this.practiceMode == practiceMode.PRACTICE && this._props.curDoc() ? (
@@ -182,7 +182,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
     tryFilterOut = (doc: Doc) => (this.practiceMode && BoolCast(doc?._layout_isFlashcard) && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct
     render() {
         return (
-            <>
+            <div className="FlashcardPracticeUI">
                 {this.emptyMessage}
                 {this.practiceButtons}
                 {this._props.layoutDoc._chromeHidden ? null : (
@@ -208,7 +208,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
                         {this.practiceModesMenu}
                     </div>
                 )}
-            </>
+            </div>
         );
     }
 }
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 903e04ad7..954c79f7d 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -189,38 +189,38 @@ ScriptingGlobals.add(function showFreeform(
             setDoc: (doc: Doc, dv: DocumentView) => { Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards}, // prettier-ignore
         }],
         ['time', {
-            checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "time",
-            setDoc: (doc: Doc, dv: DocumentView) => { doc.cardSort === "time" ? doc.cardSort = '' : doc.cardSort = 'time'}, // prettier-ignore
+            checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "time",
+            setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "time" ? doc.card_sort = '' : doc.card_sort = 'time'}, // prettier-ignore
         }],
         ['docType', {
-            checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "type",
-            setDoc: (doc: Doc, dv: DocumentView) => { doc.cardSort === "type" ? doc.cardSort = '' : doc.cardSort = 'type'}, // prettier-ignore
+            checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "type",
+            setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "type" ? doc.card_sort = '' : doc.card_sort = 'type'}, // prettier-ignore
         }],
         ['color', {
-            checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "color",
-            setDoc: (doc: Doc, dv: DocumentView) => { doc.cardSort === "color" ? doc.cardSort = '' : doc.cardSort = 'color'}, // prettier-ignore
+            checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "color",
+            setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "color" ? doc.card_sort = '' : doc.card_sort = 'color'}, // prettier-ignore
         }],
         ['tag', {
-            checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "tag",
-            setDoc: (doc: Doc, dv: DocumentView) => { doc.cardSort === "tag" ? doc.cardSort = '' : doc.cardSort = 'tag'}, // prettier-ignore
+            checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "tag",
+            setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "tag" ? doc.card_sort = '' : doc.card_sort = 'tag'}, // prettier-ignore
         }],
         ['up', {
-            checkResult: (doc: Doc) => BoolCast(!doc?.cardSort_isDesc),
+            checkResult: (doc: Doc) => BoolCast(!doc?.card_sort_isDesc),
             setDoc: (doc: Doc, dv: DocumentView) => {
-                doc.cardSort_isDesc = false;
+                doc.card_sort_isDesc = false;
             },
         }],
         ['down', {
-            checkResult: (doc: Doc) => BoolCast(doc?.cardSort_isDesc),
+            checkResult: (doc: Doc) => BoolCast(doc?.card_sort_isDesc),
             setDoc: (doc: Doc, dv: DocumentView) => {
-                doc.cardSort_isDesc = true;
+                doc.card_sort_isDesc = true;
             },
         }],
         ['toggle-chat', {
             checkResult: (doc: Doc) => GPTPopup.Instance.visible,
             setDoc: (doc: Doc, dv: DocumentView) => {
                 if (GPTPopup.Instance.visible){
-                    doc.cardSort = ''
+                    doc.card_sort = ''
                     GPTPopup.Instance.setVisible(false);
    
                 } else {
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 38ce5f2f7..3c126ea4a 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -8,7 +8,7 @@ import ReactLoading from 'react-loading';
 import { imageUrlToBase64, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
-import { DocData } from '../../../fields/DocSymbols';
+import { Animation, DocData } from '../../../fields/DocSymbols';
 import { RichTextField } from '../../../fields/RichTextField';
 import { BoolCast, DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types';
 import { nullAudio } from '../../../fields/URLField';
@@ -18,7 +18,6 @@ import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
-import { SnappingManager } from '../../util/SnappingManager';
 import { undoable } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { ViewBoxAnnotatableComponent } from '../DocComponent';
@@ -235,7 +234,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     @computed get uiBtnScaling() { return Math.max(this.maxWidgetSize / this._sideBtnWidth, 1) * Math.min(1, this.viewScaling)* (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore
 
     @computed get flashcardMenu() {
-        return SnappingManager.HideDecorations ? null : (
+        return (
             <div className="comparisonBox-bottomMenu" style={{ transform: `scale(${this.uiBtnScaling})` }}>
                 {this.revealOpHover || !this._props.isSelected() ? null : this.overlayAlternateIcon}
                 {!this._props.isSelected() || this._renderSide === this.frontKey ? null : (
@@ -666,20 +665,20 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             <>
                 <DocumentView
                     {...this._props}
-                    showTags={undefined}
-                    fitWidth={undefined} // set to returnTrue to make images fill the comparisonBox-- should be a user option
-                    ignoreUsePath={layoutString ? true : undefined}
-                    renderDepth={this.props.renderDepth + 1}
-                    LayoutTemplateString={layoutString}
                     Document={layoutString ? this.Document : targetDoc}
-                    containerViewPath={this._props.docViewPath}
-                    moveDocument={whichSlot === this.frontKey ? this.moveDocFront : this.moveDocBack}
-                    removeDocument={whichSlot === this.frontKey ? this.remDocFront : this.remDocBack}
                     NativeWidth={returnZero}
                     NativeHeight={returnZero}
+                    renderDepth={this.props.renderDepth + 1}
+                    LayoutTemplateString={layoutString}
+                    containerViewPath={this._props.docViewPath}
                     ScreenToLocalTransform={this.contentScreenToLocalXf}
-                    isContentActive={this.childActiveFunc}
                     isDocumentActive={returnFalse}
+                    isContentActive={this.childActiveFunc}
+                    showTags={undefined}
+                    fitWidth={undefined} // set to returnTrue to make images fill the comparisonBox-- should be a user option
+                    ignoreUsePath={layoutString ? true : undefined}
+                    moveDocument={whichSlot === this.frontKey ? this.moveDocFront : this.moveDocBack}
+                    removeDocument={whichSlot === this.frontKey ? this.remDocFront : this.remDocBack}
                     dontSelect={returnTrue}
                     whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                     styleProvider={this._childActive ? this._props.styleProvider : this.docStyleProvider}
@@ -795,7 +794,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             [flashcardRevealOp.SLIDE, this.renderAsBeforeAfter]]); // prettier-ignore
         if (this.isQuizMode) this.renderAsQuiz(this.frontText);
         return (
-            <div className="comparisonBox" style={{ pointerEvents: this._props.isContentActive() ? 'unset' : undefined }} onContextMenu={this.flashcardContextMenu}>
+            <div className="comparisonBox" style={{ pointerEvents: this._props.isContentActive() && !this.Document[Animation] ? 'unset' : undefined }} onContextMenu={this.flashcardContextMenu}>
                 {renderMode.get(this.revealOp)?.() ?? null}
                 {this.loading ? (
                     <div className="loading-spinner">
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 9aa000ba7..aab8a183a 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/require-default-props */
 import { computed, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-- 
cgit v1.2.3-70-g09d2


From 4b231bbaf21939144ed8639d35f022834a406e59 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 17 Oct 2024 22:02:19 -0400
Subject: changed layout_isFlashcard to layout_flashcardType with value of
 'flashcard' to make it easy to search for flashcards. made carousel views
 able to focus() on a Doc.  added ability for carousel and card views to have
 anchors so they can be pinned and linked.  fixed pinning collections to save
 filters

---
 src/client/apis/gpt/GPT.ts                         |  9 +++-
 src/client/documents/Documents.ts                  |  4 +-
 src/client/util/LinkFollower.ts                    |  2 -
 src/client/views/PinFuncs.ts                       | 13 ++++--
 .../views/collections/CollectionCardDeckView.tsx   | 32 +++++++++++++-
 .../views/collections/CollectionCarousel3DView.tsx | 32 +++++++++++++-
 .../views/collections/CollectionCarouselView.tsx   | 51 +++++++++++++++++++++-
 .../collections/CollectionStackedTimeline.tsx      |  5 +--
 .../views/collections/CollectionTimeView.tsx       |  2 -
 .../views/collections/FlashcardPracticeUI.tsx      |  6 +--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 src/client/views/nodes/ComparisonBox.tsx           | 21 +++++----
 src/client/views/nodes/DataVizBox/DataVizBox.tsx   |  2 +-
 src/client/views/nodes/MapBox/MapBox.tsx           |  2 +-
 .../views/nodes/MapboxMapBox/MapboxContainer.tsx   |  5 +--
 src/client/views/nodes/PDFBox.tsx                  |  2 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  2 +-
 17 files changed, 151 insertions(+), 41 deletions(-)

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 8a2c91269..03380e4d6 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -42,7 +42,7 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
         model: 'gpt-4o',
         maxTokens: 2048,
         temp: 0.7,
-        prompt: 'Create a stack of flashcards out of this text with each question and answer labeled as question and answer. For some questions, ask "what is this image of" but tailored to stacks theme and the image and write a keyword that represents the image and label it "keyword". Otherwise, write none. Do not label each flashcard and do not include asterisks.',
+        prompt: 'Create a stack of flashcards out of this text with each question and answer labeled as question and answer. Create a title that represents the question in just a few words and label it "title". For some questions, ask "what is this image of" but tailored to stacks theme and the image and write a keyword that represents the image and label it "keyword". Otherwise, write none. Do not label each flashcard and do not include asterisks.',
     },
 
     completion: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: "You are a helpful assistant. Answer the user's prompt." },
@@ -65,7 +65,12 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
         prompt: "The user is going to give you a list of descriptions. Each one is separated by `======` on either side. Descriptions will vary in length, so make sure to only separate when you see `======`. Sort them by the user's specifications. Make sure each description is only in the list once. Each item should be separated by `======`. Immediately afterward, surrounded by `------` on BOTH SIDES, provide some insight into your reasoning for the way you sorted (and mention nothing about the formatting details given in this description). It is VERY important that you format it exactly as described, ensuring the proper number of `=` and `-` (6 of each) and NO commas",
     },
     describe: { model: 'gpt-4-vision-preview', maxTokens: 2048, temp: 0, prompt: 'Describe these images in 3-5 words' },
-    flashcard: { model: 'gpt-4-turbo', maxTokens: 512, temp: 0.5, prompt: 'Make flashcards out of this text with each question and answer labeled as question and answer. Do not label each flashcard and do not include asterisks: ' },
+    flashcard: {
+        model: 'gpt-4-turbo',
+        maxTokens: 512,
+        temp: 0.5,
+        prompt: 'Make flashcards out of this text with each question and answer labeled as question and answer. Create a title for each question and asnwer that is labeled as "title".  Do not label each flashcard and do not include asterisks: ',
+    },
     chatcard: { model: 'gpt-4-turbo', maxTokens: 512, temp: 0.5, prompt: 'Answer the following question as a short flashcard response. Do not include a label.' },
     quiz: {
         model: 'gpt-4-turbo',
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index e539e3c65..f2e85cf7a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -281,7 +281,7 @@ export class DocumentOptions {
     _layout_fitWidth?: BOOLt = new BoolInfo('whether document should scale its contents to fit its rendered width or not (e.g., for PDFviews)');
     _layout_fieldKey?: STRt = new StrInfo('the field key containing the current layout definition', false);
     _layout_enableAltContentUI?: BOOLt = new BoolInfo('whether to show alternate content button');
-    _layout_isFlashcard?: BOOLt = new BoolInfo('whether comparison node should be displayed as a flashcard');
+    _layout_flashcardType?: STRt = new StrInfo('flashcard style to render in ComparisonBox.  currently just "flashcard".');
     _layout_showTitle?: string; // field name to display in header (:hover is an optional suffix)
     _layout_showSidebar?: BOOLt = new BoolInfo('whether an annotationsidebar should be displayed for text docuemnts');
     _layout_showCaption?: string; // which field to display in the caption area.  leave empty to have no caption
@@ -821,7 +821,7 @@ export namespace Docs {
                 data_front: front ?? CenteredTextCreator('question', 'hint: Enter a topic, select this document and click the stack button to have GPT create a deck of cards', { text_placeholder: true, cloneOnCopy: true }, undefined),
                 data_back: back ?? CenteredTextCreator('answer', 'answer here', { text_placeholder: true, cloneOnCopy: true }, undefined),
                 _layout_fitWidth: true,
-                _layout_isFlashcard: true,
+                _layout_flashcardType: 'flashcard',
                 title,
                 ...options,
             });
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 0a3a0ba49..0e67dcfaa 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -113,12 +113,10 @@ export class LinkFollower {
                 }
                 const moveTo = [NumCast(sourceDoc.x) + NumCast(sourceDoc.followLinkXoffset), NumCast(sourceDoc.y) + NumCast(sourceDoc.followLinkYoffset)];
                 if (srcAnchor.followLinkXoffset !== undefined && moveTo[0] !== target.x) {
-                    // eslint-disable-next-line prefer-destructuring
                     target.x = moveTo[0];
                     movedTarget = true;
                 }
                 if (srcAnchor.followLinkYoffset !== undefined && moveTo[1] !== target.y) {
-                    // eslint-disable-next-line prefer-destructuring
                     target.y = moveTo[1];
                     movedTarget = true;
                 }
diff --git a/src/client/views/PinFuncs.ts b/src/client/views/PinFuncs.ts
index 430455644..5b57cd624 100644
--- a/src/client/views/PinFuncs.ts
+++ b/src/client/views/PinFuncs.ts
@@ -39,9 +39,14 @@ export interface PinProps {
     pinData?: pinDataTypes;
 }
 
-/// copies values from the targetDoc (which is the prototype of the pinDoc) to
-/// reserved fields on the pinDoc so that those values can be restored to the
-/// target doc when navigating to it.
+/**
+ * copies values from the targetDoc (which is the prototype of the pinDoc) to
+ * reserved fields on the pinDoc so that those values can be restored to the
+ * target doc when navigating to it.
+ * @param pinDoc Doc that will store pinned metadata
+ * @param pinProps description of props to pin
+ * @param targetDoc Doc that is being pinned
+ */
 export function PinDocView(pinDocIn: Doc, pinProps: PinProps, targetDoc: Doc) {
     const pinDoc = pinDocIn;
     pinDoc.presentation = true;
@@ -114,7 +119,7 @@ export function PinDocView(pinDocIn: Doc, pinProps: PinProps, targetDoc: Doc) {
                 )
             );
         if (pinProps.pinData.type_collection) pinDoc.config_viewType = targetDoc._type_collection;
-        if (pinProps.pinData.filters) pinDoc.config_docFilters = ObjectField.MakeCopy(targetDoc.childFilters as ObjectField);
+        if (pinProps.pinData.filters) pinDoc.config_docFilters = ObjectField.MakeCopy(targetDoc.childFilters as ObjectField) ?? new List<string>();
         if (pinProps.pinData.pivot) pinDoc.config_pivotField = targetDoc._pivotField;
         if (pinProps.pinData.pannable) {
             pinDoc.config_panX = NumCast(targetDoc._freeform_panX);
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index b86dad9d7..60305244c 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -9,7 +9,7 @@ import { Animation, DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, DateCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, DateCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
 import { URLField } from '../../../fields/URLField';
 import { gptImageLabel } from '../../apis/gpt/GPT';
 import { DocumentType } from '../../documents/DocumentTypes';
@@ -25,6 +25,9 @@ import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
 import './CollectionCardDeckView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 import { FocusViewOptions } from '../nodes/FocusViewOptions';
+import { Docs } from '../../documents/Documents';
+import { PinDocView } from '../PinFuncs';
+import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
 
 enum cardSortings {
     Time = 'time',
@@ -604,6 +607,33 @@ export class CollectionCardView extends CollectionSubView() {
         index !== -1 && (this._curDoc = target);
         return undefined;
     });
+    getAnchor = (addAsAnnotation: boolean) => {
+        const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document });
+        anchor.config_curDoc = this.curDoc();
+        PinDocView(anchor, { pinData: { type_collection: true, filters: true } }, this.Document);
+        if (addAsAnnotation) {
+            // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
+            Doc.AddDocToList(this.dataDoc, this._props.fieldKey + '_annotations', anchor);
+        }
+        return anchor;
+    };
+    // pinned / linked anchor doc includes selected rows, graph titles, and graph colors
+    restoreView = (viewData: Doc) => {
+        if (viewData.config_curDoc !== undefined && this.curDoc() !== viewData.config_curDoc) {
+            this._curDoc = DocCast(viewData.config_curDoc);
+            return true;
+        }
+        return false;
+    };
+    addDocTab = (docsIn: Doc | Doc[], location: OpenWhere) => {
+        const doc = toList(docsIn).lastElement();
+        const where = location.split(':')[0];
+        if (where === OpenWhere.lightbox && (this.childDocList?.includes(doc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(doc))) {
+            if (doc.hidden) doc.hidden = false;
+            if (!location.includes(OpenWhereMod.always)) return true;
+        }
+        return this._props.addDocTab(docsIn, location);
+    };
 
     /**
      * Actually renders all the cards
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index a71cc43ba..2be3ef48f 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -6,7 +6,7 @@ import { returnZero } from '../../../ClientUtils';
 import { Utils } from '../../../Utils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
-import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, DocCast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DragManager } from '../../util/DragManager';
 import { Transform } from '../../util/Transform';
@@ -16,6 +16,9 @@ import { FocusViewOptions } from '../nodes/FocusViewOptions';
 import './CollectionCarousel3DView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 import { computedFn } from 'mobx-utils';
+import { Docs } from '../../documents/Documents';
+import { PinDocView } from '../PinFuncs';
+import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
 
 // eslint-disable-next-line @typescript-eslint/no-require-imports
 const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss');
@@ -96,6 +99,33 @@ export class CollectionCarousel3DView extends CollectionSubView() {
         index !== -1 && (this.layoutDoc._carousel_index = index);
         return undefined;
     };
+    getAnchor = (addAsAnnotation: boolean) => {
+        const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document });
+        anchor.config_carousel_index = this.layoutDoc._carousel_index;
+        PinDocView(anchor, { pinData: { type_collection: true, filters: true } }, this.Document);
+        if (addAsAnnotation) {
+            // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
+            Doc.AddDocToList(this.dataDoc, this._props.fieldKey + '_annotations', anchor);
+        }
+        return anchor;
+    };
+    // pinned / linked anchor doc includes selected rows, graph titles, and graph colors
+    restoreView = (viewData: Doc) => {
+        if (viewData.config_carousel_index !== undefined && this.layoutDoc._carousel_index !== viewData.config_carousel_index) {
+            this.layoutDoc._carousel_index = viewData.config_carousel_index;
+            return true;
+        }
+        return false;
+    };
+    addDocTab = (docsIn: Doc | Doc[], location: OpenWhere) => {
+        const doc = toList(docsIn).lastElement();
+        const where = location.split(':')[0];
+        if (where === OpenWhere.lightbox && (this.childDocList?.includes(doc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(doc))) {
+            if (doc.hidden) doc.hidden = false;
+            if (!location.includes(OpenWhereMod.always)) return true;
+        }
+        return this._props.addDocTab(docsIn, location);
+    };
 
     @computed get content() {
         const currentIndex = NumCast(this.layoutDoc._carousel_index);
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 1f2bc908f..b043706fd 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -3,12 +3,17 @@ import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
-import { Doc, Opt } from '../../../fields/Doc';
-import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { BoolCast, DocCast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
+import { PinDocView } from '../PinFuncs';
 import { StyleProp } from '../StyleProp';
 import { DocumentView } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
+import { FocusViewOptions } from '../nodes/FocusViewOptions';
+import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import './CollectionCarouselView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
@@ -27,6 +32,9 @@ export class CollectionCarouselView extends CollectionSubView() {
         makeObservable(this);
     }
 
+    componentDidMount(): void {
+        this._props.setContentViewBox?.(this);
+    }
     componentWillUnmount() {
         this._dropDisposer?.();
     }
@@ -67,6 +75,44 @@ export class CollectionCarouselView extends CollectionSubView() {
 
     curDoc = () => this.carouselItems[this.carouselIndex]?.layout;
 
+    focus = (anchor: Doc, options: FocusViewOptions): Opt<number> => {
+        const docs = DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]);
+        if (anchor.type !== DocumentType.CONFIG && !docs.includes(anchor)) return undefined;
+        options.didMove = true;
+        const target = DocCast(anchor.annotationOn) ?? anchor;
+        const index = docs.indexOf(target);
+        index !== -1 && (this.layoutDoc._carousel_index = index);
+        return undefined;
+    };
+
+    getAnchor = (addAsAnnotation: boolean) => {
+        const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document });
+        anchor.config_carousel_index = this.carouselIndex;
+        PinDocView(anchor, { pinData: { type_collection: true, filters: true } }, this.Document);
+        if (addAsAnnotation) {
+            // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
+            Doc.AddDocToList(this.dataDoc, this._props.fieldKey + '_annotations', anchor);
+        }
+        return anchor;
+    };
+    // pinned / linked anchor doc includes selected rows, graph titles, and graph colors
+    restoreView = (viewData: Doc) => {
+        if (viewData.config_carousel_index !== undefined && this.layoutDoc._carousel_index !== viewData.config_carousel_index) {
+            this.layoutDoc._carousel_index = viewData.config_carousel_index;
+            return true;
+        }
+        return false;
+    };
+    addDocTab = (docsIn: Doc | Doc[], location: OpenWhere) => {
+        const doc = toList(docsIn).lastElement();
+        const where = location.split(':')[0];
+        if (where === OpenWhere.lightbox && (this.childDocList?.includes(doc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(doc))) {
+            if (doc.hidden) doc.hidden = false;
+            if (!location.includes(OpenWhereMod.always)) return true;
+        }
+        return this._props.addDocTab(docsIn, location);
+    };
+
     captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string) => {
         // first look for properties on the document in the carousel, then fallback to properties on the container
         const childValue = doc?.['caption_' + property] ? this._props.styleProvider?.(doc, captionProps, property) : undefined;
@@ -113,6 +159,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                 LayoutTemplateString={this._props.childLayoutString}
                 TemplateDataDocument={DocCast(Doc.Layout(doc).resolvedDataDoc)}
                 childFilters={this.childDocFilters}
+                focus={this.focus}
                 hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)}
                 addDocument={this._props.addDocument}
                 ScreenToLocalTransform={this.contentScreenToLocalXf}
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 486c826b6..c3047e5fb 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/jsx-props-no-spreading */
 /* eslint-disable no-use-before-define */
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
@@ -59,7 +58,6 @@ export enum TrimScope {
 
 @observer
 export class CollectionStackedTimeline extends CollectionSubView<CollectionStackedTimelineProps>() {
-    // eslint-disable-next-line no-use-before-define
     public static SelectingRegions: Set<CollectionStackedTimeline> = new Set();
     public static StopSelecting() {
         this.SelectingRegions.forEach(
@@ -171,7 +169,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
 
     makeDocUnfiltered = (doc: Doc) => this.childDocList?.some(item => item === doc);
 
-    getView = async (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> =>
+    getView = (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> =>
         new Promise<Opt<DocumentView>>(res => {
             if (doc.hidden) options.didMove = !(doc.hidden = false);
             const findDoc = (finish: (dv: DocumentView) => void) => DocumentView.addViewRenderedCb(doc, dv => finish(dv));
@@ -600,7 +598,6 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                                         pointerEvents: 'none',
                                     }}>
                                     <StackedTimelineAnchor
-                                        // eslint-disable-next-line react/jsx-props-no-spreading
                                         {...this._props}
                                         mark={d.anchor}
                                         containerViewPath={this._props.containerViewPath}
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 8a24db330..5738e7ce3 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -147,7 +147,6 @@ export class CollectionTimeView extends CollectionSubView() {
         return (
             <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this._props.isContentActive() ? undefined : 'none' }} onClick={this.contentsDown}>
                 <CollectionFreeFormView
-                    // eslint-disable-next-line react/jsx-props-no-spreading
                     {...this._props}
                     engineProps={{ pivotField: this.pivotField, childFilters: this.childDocFilters, childFiltersByRanges: this.childDocRangeFilters }}
                     fitContentsToBox={returnTrue}
@@ -257,7 +256,6 @@ ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBou
             const pivotView = DocumentView.getDocumentView(pivotDoc);
             if (pivotDoc && pivotView?.ComponentView instanceof CollectionTimeView && filterVals.length === 1) {
                 if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) {
-                    // eslint-disable-next-line prefer-destructuring
                     pivotDoc._pivotField = filterVals[0];
                 }
             }
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
index 9e9318c0a..c298bff22 100644
--- a/src/client/views/collections/FlashcardPracticeUI.tsx
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -58,7 +58,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
     get practiceField() { return this._props.fieldKey + "_practice"; } // prettier-ignore
 
     @computed get filterDoc() { return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter'); } // prettier-ignore
-    @computed get practiceMode() {  return this._props.allChildDocs().some(doc => doc._layout_isFlashcard) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
+    @computed get practiceMode() {  return this._props.allChildDocs().some(doc => doc._flashcardType) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
 
     btnHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this._props.ScreenToLocalBoxXf().Scale);
     btnWidth = () => (!this.filterDoc ? 1 : (this.btnHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height));
@@ -127,7 +127,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
         const setColor = (mode: practiceMode) => (StrCast(this.practiceMode) === mode ? 'white' : 'lightgray');
         const togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode);
 
-        return !this._props.allChildDocs().some(doc => doc._layout_isFlashcard) ? null : (
+        return !this._props.allChildDocs().some(doc => doc._layout_flashcardType) ? null : (
             <div
                 className="FlashcardPracticeUI-practiceModes"
                 style={{
@@ -179,7 +179,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
             </div>
         );
     }
-    tryFilterOut = (doc: Doc) => (this.practiceMode && BoolCast(doc?._layout_isFlashcard) && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct
+    tryFilterOut = (doc: Doc) => (this.practiceMode && BoolCast(doc?._flashcardType) && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct
     render() {
         return (
             <div className="FlashcardPracticeUI">
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d2bc8f2c2..afd662b22 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -389,7 +389,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return undefined;
     };
 
-    getView = async (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> =>
+    getView = (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> =>
         new Promise<Opt<DocumentView>>(res => {
             if (doc.hidden && this._lightboxDoc !== doc) options.didMove = !(doc.hidden = false);
             if (doc === this.Document) {
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index f6c33d6ba..672c189ba 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -63,8 +63,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
      * @param useDoc doc to fill in instead of creating a Doc
      * @returns the resulting flashcard Doc
      */
-    public static createFlashcard(tuple: string, frontKey: string, backKey: string, useDoc?: Doc) {
-        const [ktoken, atoken] = [ComparisonBox.ktoken, ComparisonBox.atoken];
+    public static createFlashcard(tuple3: string, frontKey: string, backKey: string, useDoc?: Doc) {
+        const [qtoken, ktoken, atoken] = [ComparisonBox.qtoken, ComparisonBox.ktoken, ComparisonBox.atoken];
+        const [title, tuple] = tuple3.split(qtoken);
         const question = (tuple.includes(ktoken) ? tuple.split(ktoken)[0] : tuple).split(atoken)[0];
         const rest = tuple.replace(question, '');
         // prettier-ignore
@@ -82,7 +83,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 useDoc[DocData][backKey] = back;
                 return useDoc;
             }
-            return Docs.Create.FlashcardDocument('flashcard', front, back, { _width: 300, _height: 300 });
+            return Docs.Create.FlashcardDocument(title, front, back, { _width: 300, _height: 300 });
         };
         return keyword && keyword.toLowerCase() !== 'none' ? ComparisonBox.fetchImages(keyword).then(img => fillInFlashcard(img)) : fillInFlashcard();
     }
@@ -95,12 +96,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     public static createFlashcardDeck(text: string, width: number, height: number, front: string, back: string) {
         return Promise.all(
             text
-                .split(ComparisonBox.qtoken)
+                .toLowerCase()
+                .split(ComparisonBox.ttoken)
                 .filter(t => t)
                 .map(tuple => ComparisonBox.createFlashcard(tuple, front, back))
         ).then(docs => {
             return Docs.Create.CarouselDocument(docs, {
-                title: 'flashcard deck',
+                title: text,
                 _width: width,
                 _height: height,
                 _layout_fitWidth: false,
@@ -112,9 +114,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     }
     private SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
 
-    static qtoken = 'Question: ';
-    static ktoken = 'Keyword: ';
-    static atoken = 'Answer: ';
+    static qtoken = 'question: ';
+    static ktoken = 'keyword: ';
+    static atoken = 'answer: ';
+    static ttoken = 'title: ';
     private _slideTiming = 200;
     private _sideBtnWidth = 35;
     private _closeRef = React.createRef<HTMLDivElement>();
@@ -183,7 +186,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
 
     @computed get containerDoc()  { return this._props.docViewPath().slice(-2)[0]?.Document; } // prettier-ignore
     @computed get isQuizMode()    { return this.containerDoc?.practiceMode === practiceMode.QUIZ; } // prettier-ignore
-    @computed get isFlashcard()   { return BoolCast(this.Document.layout_isFlashcard); } // prettier-ignore
+    @computed get isFlashcard()   { return StrCast(this.Document.layout_flashcardType); } // prettier-ignore
     @computed get frontKey()      { return this._props.fieldKey + '_front'; } // prettier-ignore
     @computed get backKey()       { return this._props.fieldKey + '_back'; } // prettier-ignore
     @computed get frontText()     { return RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text; } // prettier-ignore
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 896048ab3..d79a37181 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -323,7 +323,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar')
         );
     };
-    getView = async (doc: Doc, options: FocusViewOptions) => {
+    getView = (doc: Doc, options: FocusViewOptions) => {
         if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) {
             options.didMove = true;
             this.toggleSidebar();
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index c66f7c726..4a436319b 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -428,7 +428,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
     };
 
-    getView = async (doc: Doc, options: FocusViewOptions) => {
+    getView = (doc: Doc, options: FocusViewOptions) => {
         if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) {
             this.toggleSidebar();
             options.didMove = true;
diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
index a4557196e..d5d8f8afa 100644
--- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
+++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
@@ -383,7 +383,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
         }
     };
 
-    getView = async (doc: Doc, options: FocusViewOptions) => {
+    getView = (doc: Doc, options: FocusViewOptions) => {
         if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) {
             this.toggleSidebar();
             options.didMove = true;
@@ -732,7 +732,6 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
                         MapBoxContainer._rerenderDelay = 0;
                     }
                     this._rerenderTimeout = undefined;
-                    // eslint-disable-next-line operator-assignment
                     this.Document[DocCss] = this.Document[DocCss] + 1;
                 }), MapBoxContainer._rerenderDelay);
             return null;
@@ -792,7 +791,6 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
                                   .map(pushpin => (
                                       <DocumentView
                                           key={pushpin[Id]}
-                                          // eslint-disable-next-line react/jsx-props-no-spreading
                                           {...this._props}
                                           renderDepth={this._props.renderDepth + 1}
                                           Document={pushpin}
@@ -830,7 +828,6 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
                 <div className="mapBox-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
                     <SidebarAnnos
                         ref={this._sidebarRef}
-                        // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
                         fieldKey={this.fieldKey}
                         Document={this.Document}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 816d4a3b0..2e4c87d38 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -233,7 +233,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return this._pdfViewer?.scrollFocus(anchor, NumCast(anchor.y, NumCast(anchor.config_scrollTop)), options);
     };
 
-    getView = async (doc: Doc, options: FocusViewOptions) => {
+    getView = (doc: Doc, options: FocusViewOptions) => {
         if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) {
             options.didMove = true;
             this.toggleSidebar(false);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 29be8d285..2aee7b6b3 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1156,7 +1156,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         return anchorDoc ?? this.Document;
     }
 
-    getView = async (doc: Doc, options: FocusViewOptions) => {
+    getView = (doc: Doc, options: FocusViewOptions) => {
         if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
             if (!this.SidebarShown) {
                 this.toggleSidebar(false);
-- 
cgit v1.2.3-70-g09d2


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

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

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

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


From 6d5b80adf8c0d1f93dce30401e1f90d11b838de3 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 18 Oct 2024 10:00:01 -0400
Subject: fixed following anchors to collection card/carousel to set the
 current/selected Doc

---
 src/client/documents/Documents.ts                  |  2 +-
 src/client/views/PinFuncs.ts                       |  6 +--
 src/client/views/ViewBoxInterface.ts               |  2 +-
 .../views/collections/CollectionCardDeckView.tsx   | 55 +++++++++-------------
 .../views/collections/CollectionCarousel3DView.tsx | 10 +---
 .../views/collections/CollectionCarouselView.tsx   | 10 +---
 .../views/collections/CollectionTimeView.tsx       |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 src/client/views/nodes/trails/PresBox.tsx          | 36 +++++++-------
 src/fields/ScriptField.ts                          |  6 +--
 10 files changed, 53 insertions(+), 78 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index be3b9c3bb..98e8bc932 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -373,7 +373,7 @@ export class DocumentOptions {
     config_panY?: NUMt = new NumInfo('panY saved as a view spec', false);
     config_zoom?: NUMt = new NumInfo('zoom saved as a view spec', false);
     config_carousel_index?: NUMt = new NumInfo('saved carousel index', false);
-    config_curDoc?: DOCt = new DocInfo('current doc in a collection view, e.g., cardView');
+    config_card_curDoc?: DOCt = new DocInfo('current doc in a collection view, e.g., cardView');
     config_viewScale?: NUMt = new NumInfo('viewScale saved as a view Spec', false);
     presentation_transition?: NUMt = new NumInfo('the time taken for the transition TO a document', false);
     presentation_duration?: NUMt = new NumInfo('the duration of the slide in presentation view', false);
diff --git a/src/client/views/PinFuncs.ts b/src/client/views/PinFuncs.ts
index 17964c656..bed473eec 100644
--- a/src/client/views/PinFuncs.ts
+++ b/src/client/views/PinFuncs.ts
@@ -16,7 +16,7 @@ export interface pinDataTypes {
     scrollable?: boolean;
     dataviz?: number[];
     pannable?: boolean;
-    type_collection?: boolean;
+    collectionType?: boolean;
     inkable?: boolean;
     filters?: boolean;
     pivot?: boolean;
@@ -65,7 +65,7 @@ export function PinDocView(pinDocIn: Doc, pinProps: PinProps, targetDoc: Doc) {
             pinProps.pinData.scrollable ||
             pinProps.pinData.temporal ||
             pinProps.pinData.pannable ||
-            pinProps.pinData.type_collection ||
+            pinProps.pinData.collectionType ||
             pinProps.pinData.clippable ||
             pinProps.pinData.datarange ||
             pinProps.pinData.dataview ||
@@ -118,7 +118,7 @@ export function PinDocView(pinDocIn: Doc, pinProps: PinProps, targetDoc: Doc) {
                     })
                 )
             );
-        if (pinProps.pinData.type_collection) pinDoc.config_type_collection = targetDoc._type_collection;
+        if (pinProps.pinData.collectionType) pinDoc.config_type_collection = targetDoc._type_collection;
         if (pinProps.pinData.filters) pinDoc.config_docFilters = ObjectField.MakeCopy(targetDoc.childFilters as ObjectField) ?? new List<string>();
         if (pinProps.pinData.pivot) pinDoc.config_pivotField = targetDoc._pivotField;
         if (pinProps.pinData.pannable) {
diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
index f66f6062e..b7980d74e 100644
--- a/src/client/views/ViewBoxInterface.ts
+++ b/src/client/views/ViewBoxInterface.ts
@@ -24,7 +24,7 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
     promoteCollection?: () => void; // moves contents of collection to parent
     updateIcon?: (usePanelDimensions?: boolean) => Promise<void>; // updates the icon representation of the document
     getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
-    restoreView?: (viewSpec: Doc) => boolean;
+    restoreView?: (viewSpec: Doc) => boolean; // DEPRECATED: do not use, it will go away.  see PresBox.restoreTargetDocView
     scrollPreview?: (docView: DocumentView, doc: Doc, focusSpeed: number, options: FocusViewOptions) => Opt<number>; // returns the duration of the focus
     brushView?: (view: { width: number; height: number; panX: number; panY: number }, transTime: number, holdTime: number) => void; // highlight a region of a view (used by freeforms)
     getView?: (doc: Doc, options: FocusViewOptions) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 729a1585a..beb45f326 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -51,14 +51,13 @@ export class CollectionCardView extends CollectionSubView() {
     private _textToDoc = new Map<string, Doc>();
     private _oldWheel: HTMLElement | null = null;
     private _dropped = false; // set when a card doc has just moved and the drop method has been called - prevents the pointerUp method from hiding doc decorations (which needs to be done when clicking on a card to animate it to front/center)
-    private _clickScript = () => ScriptField.MakeScript('scriptContext._curDoc=this', { scriptContext: 'any' })!;
+    private _setCurDocScript = () => ScriptField.MakeScript('scriptContext.layoutDoc._card_curDoc=this', { scriptContext: 'any' })!;
 
     @observable _forceChildXf = 0;
     @observable _hoveredNodeIndex = -1;
     @observable _docRefs = new ObservableMap<Doc, DocumentView>();
     @observable _maxRowCount = 10;
     @observable _docDraggedIndex: number = -1;
-    @observable _curDoc: Doc | undefined = undefined;
 
     constructor(props: SubCollectionViewProps) {
         super(props);
@@ -165,10 +164,10 @@ export class CollectionCardView extends CollectionSubView() {
     /**
      * When in quiz mode, randomly selects a document
      */
-    quizMode = action(() => {
+    quizMode = () => {
         const randomIndex = Math.floor(Math.random() * this.childDocs.length);
-        this._curDoc = this.childDocs[randomIndex];
-    });
+        this.layoutDoc._card_curDoc = this.childDocs[randomIndex];
+    };
 
     setHoveredNodeIndex = action((index: number) => {
         if (!SnappingManager.IsDragging) this._hoveredNodeIndex = index;
@@ -319,7 +318,7 @@ export class CollectionCardView extends CollectionSubView() {
         (doc: Doc) => () =>
             this._props.isContentActive?.() === false
                 ? false
-                : this._props.isDocumentActive?.() && this._curDoc === doc
+                : this._props.isDocumentActive?.() && this.curDoc() === doc
                   ? true
                   : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
                     ? false
@@ -347,7 +346,7 @@ export class CollectionCardView extends CollectionSubView() {
             scriptContext={this}
             focus={this.focus}
             onDoubleClickScript={this.onChildDoubleClick}
-            onClickScript={this._curDoc === doc ? undefined : this._clickScript}
+            onClickScript={this.curDoc() === doc ? undefined : this._setCurDocScript}
             dontCenter="y" // Don't center it vertically, because the grid it's in is already doing that and we don't want to do it twice.
             dragAction={(this.Document.childDragAction ?? this._props.childDragAction) as dropActionType}
             showTags={BoolCast(this.layoutDoc.showChildTags)}
@@ -572,8 +571,8 @@ export class CollectionCardView extends CollectionSubView() {
      * @param doc doc that will be animated away from center focus
      */
     releaseCurDoc = action(() => {
-        const selDoc = this._curDoc;
-        this._curDoc = undefined;
+        const selDoc = this.curDoc();
+        this.layoutDoc._card_curDoc = undefined;
         const cardDocView = DocumentView.getDocumentView(selDoc, this.DocumentView?.());
         if (cardDocView && selDoc) {
             DocumentView.DeselectView(cardDocView);
@@ -590,7 +589,7 @@ export class CollectionCardView extends CollectionSubView() {
      * turns off the _dropped flag at the end of a drag/drop, or releases the focused Doc if a different Doc is clicked
      */
     cardPointerUp = action((doc: Doc) => {
-        if (this._curDoc === doc || this._dropped) {
+        if (this.curDoc() === doc || this._dropped) {
             this._dropped = false;
         } else {
             this.releaseCurDoc(); // NOTE: the onClick script for the card will select the new card (ie, 'doc')
@@ -601,28 +600,20 @@ export class CollectionCardView extends CollectionSubView() {
         const docs = DocListCast(this.Document[this.fieldKey]);
         if (anchor.type === DocumentType.CONFIG || docs.includes(anchor)) {
             const foundDoc = DocCast(
-                anchor.config_curDoc,
+                anchor.config_card_curDoc,
                 docs.find(doc => doc === DocCast(anchor.annotationOn, anchor))
             );
             options.didMove = foundDoc !== this.curDoc() ? true : false;
-            options.didMove && (this._curDoc = foundDoc);
+            options.didMove && (this.layoutDoc._card_curDoc = foundDoc);
         }
         return undefined;
     });
     getAnchor = (addAsAnnotation: boolean) => {
-        const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_curDoc: this.curDoc() });
-        PinDocView(anchor, { pinData: { type_collection: true, filters: true } }, this.Document);
+        const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_card_curDoc: this.curDoc() });
+        PinDocView(anchor, { pinData: { collectionType: true, filters: true } }, this.Document);
         addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
         return anchor;
     };
-    // pinned / linked anchor doc includes selected rows, graph titles, and graph colors
-    restoreView = (viewData: Doc) => {
-        if (viewData.config_curDoc !== undefined && this.curDoc() !== viewData.config_curDoc) {
-            this._curDoc = DocCast(viewData.config_curDoc);
-            return true;
-        }
-        return false;
-    };
     addDocTab = this.addLinkedDocTab;
 
     /**
@@ -634,9 +625,9 @@ export class CollectionCardView extends CollectionSubView() {
         return this.sortedDocs.map((doc, index) => {
             const cardsInRow = this.cardsInRowThatIncludesCardIndex(index);
 
-            const childScreenToLocal = this.childScreenToLocal(doc, index, doc === this._curDoc);
+            const childScreenToLocal = this.childScreenToLocal(doc, index, doc === this.curDoc());
 
-            const translateToCenterIfActive = () => (doc === this._curDoc ? (cardsInRow / 2 - (index % this._maxRowCount)) * 100 - 50 : 0);
+            const translateToCenterIfActive = () => (doc === this.curDoc() ? (cardsInRow / 2 - (index % this._maxRowCount)) * 100 - 50 : 0);
 
             const aspect = NumCast(doc.height) / NumCast(doc.width, 1);
             const vscale = Math.max(1,Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale * this.nativeScaling) / (aspect * this.childPanelWidth()), 
@@ -645,15 +636,15 @@ export class CollectionCardView extends CollectionSubView() {
             return (
                 <div
                     key={doc[Id]}
-                    className={`card-item${doc === this._curDoc ? '-active' : this.isAnyChildContentActive() ? '-inactive' : ''}`}
+                    className={`card-item${doc === this.curDoc() ? '-active' : this.isAnyChildContentActive() ? '-inactive' : ''}`}
                     onPointerUp={() => this.cardPointerUp(doc)}
                     style={{
                         width: this.childPanelWidth(),
                         height: 'max-content',
-                        transform: `translateY(${this.adjustCardYtoFitArch(doc === this._curDoc, index)}px)
+                        transform: `translateY(${this.adjustCardYtoFitArch(doc === this.curDoc(), index)}px)
                                     translateX(calc(${translateToCenterIfActive()}% + ${this.horizontalAdjustmentForPartialRows(index, cardsInRow)}px))
-                                    rotate(${doc !== this._curDoc ? this.rotate(index) : 0}rad)
-                                    scale(${doc === this._curDoc ? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.1 : 1})`,
+                                    rotate(${doc !== this.curDoc()? this.rotate(index) : 0}rad)
+                                    scale(${doc === this.curDoc()? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.1 : 1})`,
                     }} // prettier-ignore
                     onPointerEnter={() => this.setHoveredNodeIndex(index)}
                     onPointerLeave={() => this.setHoveredNodeIndex(-1)}>
@@ -671,10 +662,10 @@ export class CollectionCardView extends CollectionSubView() {
         isContentActive: emptyFunction,
         ScreenToLocalTransform: this.contentScreenToLocalXf,
     });
-    answered = action(() => {
-        this._curDoc = this.curDoc ? this.filteredChildDocs()[(this.filteredChildDocs().findIndex(d => d === this.curDoc()) + 1) % (this.filteredChildDocs().length || 1)] : undefined;
-    });
-    curDoc = () => this._curDoc;
+    answered = () => {
+        this.layoutDoc._card_curDoc = this.curDoc() ? this.filteredChildDocs()[(this.filteredChildDocs().findIndex(d => d === this.curDoc()) + 1) % (this.filteredChildDocs().length || 1)] : undefined;
+    };
+    curDoc = () => DocCast(this.layoutDoc._card_curDoc);
 
     render() {
         const fitContentScale = this.childCards.length === 0 ? 1 : this.fitContentScale;
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index 081e2fe5a..61a74dd7a 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -103,18 +103,10 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     };
     getAnchor = (addAsAnnotation: boolean) => {
         const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_carousel_index: this.layoutDoc._carousel_index as number });
-        PinDocView(anchor, { pinData: { type_collection: true, filters: true } }, this.Document);
+        PinDocView(anchor, { pinData: { collectionType: true, filters: true } }, this.Document);
         addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
         return anchor;
     };
-    // pinned / linked anchor doc includes selected rows, graph titles, and graph colors
-    restoreView = (viewData: Doc) => {
-        if (viewData.config_carousel_index !== undefined && this.layoutDoc._carousel_index !== viewData.config_carousel_index) {
-            this.layoutDoc._carousel_index = viewData.config_carousel_index;
-            return true;
-        }
-        return false;
-    };
     addDocTab = this.addLinkedDocTab;
     @computed get content() {
         const currentIndex = NumCast(this.layoutDoc._carousel_index);
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index bfb58733d..f714e2a00 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -86,18 +86,10 @@ export class CollectionCarouselView extends CollectionSubView() {
 
     getAnchor = (addAsAnnotation: boolean) => {
         const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_carousel_index: this.carouselIndex });
-        PinDocView(anchor, { pinData: { type_collection: true, filters: true } }, this.Document);
+        PinDocView(anchor, { pinData: { collectionType: true, filters: true } }, this.Document);
         addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
         return anchor;
     };
-    // pinned / linked anchor doc includes selected rows, graph titles, and graph colors
-    restoreView = (viewData: Doc) => {
-        if (viewData.config_carousel_index !== undefined && this.layoutDoc._carousel_index !== viewData.config_carousel_index) {
-            this.layoutDoc._carousel_index = viewData.config_carousel_index;
-            return true;
-        }
-        return false;
-    };
     addDocTab = this.addLinkedDocTab;
 
     captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string) => {
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 5738e7ce3..d75c633ac 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -52,7 +52,7 @@ export class CollectionTimeView extends CollectionSubView() {
             title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as unknown as string, // title can take a functiono or a string
             annotationOn: this.Document,
         });
-        PinDocView(anchor, { pinData: { type_collection: true, pivot: true, filters: true } }, this.Document);
+        PinDocView(anchor, { pinData: { collectionType: true, pivot: true, filters: true } }, this.Document);
 
         if (addAsAnnotation) {
             // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 0241fc2dd..ff711314b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1743,7 +1743,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         });
         PinDocView(
             anchor,
-            { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ? { ...pinProps.pinData, poslayoutview: pinProps.pinData.dataview } : {}), pannable: !this.Document.isGroup, type_collection: true, filters: true } },
+            { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ? { ...pinProps.pinData, poslayoutview: pinProps.pinData.dataview } : {}), pannable: !this.Document.isGroup, collectionType: true, filters: true } },
             this.Document
         );
 
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 212dfec08..5d3672aee 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -14,7 +14,7 @@ import { StopEvent, lightOrDark, returnFalse, returnOne, setupMoveUpEvents } fro
 import { emptyFunction, stringHash } from '../../../../Utils';
 import { Doc, DocListCast, Field, FieldResult, FieldType, NumListCast, Opt, StrListCast } from '../../../../fields/Doc';
 import { Animation, DocData, TransitionTimer } from '../../../../fields/DocSymbols';
-import { Copy } from '../../../../fields/FieldSymbols';
+import { Copy, Id } from '../../../../fields/FieldSymbols';
 import { InkField } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
 import { ObjectField } from '../../../../fields/ObjectField';
@@ -382,7 +382,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             if (typeof res === 'string') {
                 const resObj = JSON.parse(res);
                 console.log('Parsed GPT Result ', resObj);
-                // eslint-disable-next-line no-restricted-syntax
                 for (const key in resObj) {
                     if (resObj[key]) {
                         console.log('typeof property', typeof resObj[key]);
@@ -563,11 +562,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const datarange = [DocumentType.FUNCPLOT].includes(targetType);
         const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG, DocumentType.RTF].includes(targetType) && target?.activeFrame === undefined;
         const poslayoutview = [DocumentType.COL].includes(targetType) && target?.activeFrame === undefined;
-        const typeCollection = targetType === DocumentType.COL;
+        const collectionType = targetType === DocumentType.COL;
         const filters = true;
         const pivot = true;
         const dataannos = false;
-        return { scrollable, pannable, inkable, type_collection: typeCollection, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos };
+        return { scrollable, pannable, inkable, collectionType, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos };
     }
 
     @action
@@ -575,7 +574,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         /* empty */
     };
     @action
-    // eslint-disable-next-line default-param-last
     static restoreTargetDocView(bestTargetView: Opt<DocumentView>, activeItem: Doc, transTime: number, pinDocLayout: boolean = BoolCast(activeItem.config_pinLayout), pinDataTypes?: dataTypes, targetDoc?: Doc) {
         const bestTarget = bestTargetView?.Document ?? (targetDoc?.layout_unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc);
         if (!bestTarget) return undefined;
@@ -701,7 +699,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 changed = true;
             }
         }
-        if ((pinDataTypes?.type_collection && activeItem.config_type_collection !== undefined) || (!pinDataTypes && activeItem.config_type_collection !== undefined)) {
+        if ((pinDataTypes?.collectionType && activeItem.config_card_curDoc !== undefined) || (!pinDataTypes && activeItem.config_card_curDoc !== undefined)) {
+            if (bestTarget._card_curDoc !== activeItem.config_card_curDoc) {
+                bestTarget._card_curDoc = activeItem.config_card_curDoc;
+                changed = true;
+            }
+        }
+        if ((pinDataTypes?.collectionType && activeItem.config_carousel_index !== undefined) || (!pinDataTypes && activeItem.config_carousel_index !== undefined)) {
+            if (bestTarget._carousel_index !== activeItem.config_carousel_index) {
+                bestTarget._carousel_index = activeItem.config_carousel_index;
+                changed = true;
+            }
+        }
+        if ((pinDataTypes?.collectionType && activeItem.config_type_collection !== undefined) || (!pinDataTypes && activeItem.config_type_collection !== undefined)) {
             if (bestTarget._type_collection !== activeItem.config_type_collection) {
                 bestTarget._type_collection = activeItem.config_type_collection;
                 changed = true;
@@ -1135,7 +1145,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     return false;
                 }
             } else if (doc.type !== DocumentType.PRES) {
-                // eslint-disable-next-line operator-assignment
                 if (!doc.presentation_targetDoc) doc.title = doc.title + ' - Slide';
                 doc.presentation_targetDoc = doc.createdFrom ?? doc; // dropped document will be a new embedding of an embedded document somewhere else.
                 doc.presentation_movement = PresMovement.Zoom;
@@ -1167,8 +1176,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const tagDoc = Cast(curDoc.presentation_targetDoc, Doc, null);
             if (curDoc && curDoc === this.activeItem)
                 return (
-                    // eslint-disable-next-line react/no-array-index-key
-                    <div key={index} className="selectedList-items">
+                    <div key={doc[Id]} className="selectedList-items">
                         <b>
                             {index + 1}. {StrCast(curDoc.title)})
                         </b>
@@ -1176,15 +1184,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 );
             if (tagDoc)
                 return (
-                    // eslint-disable-next-line react/no-array-index-key
-                    <div key={index} className="selectedList-items">
+                    <div key={doc[Id]} className="selectedList-items">
                         {index + 1}. {StrCast(curDoc.title)}
                     </div>
                 );
             if (curDoc)
                 return (
-                    // eslint-disable-next-line react/no-array-index-key
-                    <div key={index} className="selectedList-items">
+                    <div key={doc[Id]} className="selectedList-items">
                         {index + 1}. {StrCast(curDoc.title)}
                     </div>
                 );
@@ -1369,7 +1375,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const tagDoc = PresBox.targetRenderedDoc(doc);
             const srcContext = Cast(tagDoc.embedContainer, Doc, null);
             const labelCreator = (top: number, left: number, edge: number, fontSize: number) => (
-                // eslint-disable-next-line react/no-array-index-key
                 <div className="pathOrder" key={tagDoc.id + 'pres' + index} style={{ top, left, width: edge, height: edge, fontSize }} onClick={() => this.selectElement(doc)}>
                     <div className="pathOrder-frame">{index + 1}</div>
                 </div>
@@ -1620,7 +1625,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this.updateEffect(this.activeItem.presentation_effect as PresEffect, false, true);
         this.updateEffect(this.activeItem.presBulletEffect as PresEffect, true, true);
         this.updateEffectDirection(this.activeItem.presentation_effectDirection as PresEffectDirection, true);
-        // eslint-disable-next-line camelcase
         const { presentation_transition: pt, presentation_duration: pd, presentation_hideBefore: ph, presentation_hideAfter: pa } = this.activeItem;
         array.forEach(curDoc => {
             curDoc.presentation_transition = pt;
@@ -2018,7 +2022,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 <div className="presBox-effects">
                                     {this.generatedAnimations.map((elem, i) => (
                                         <div
-                                            // eslint-disable-next-line react/no-array-index-key
                                             key={i}
                                             className="presBox-effect-container"
                                             onClick={() => {
@@ -3046,7 +3049,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     <div className="Slide">
                         {mode !== CollectionViewType.Invalid ? (
                             <CollectionView
-                                // eslint-disable-next-line react/jsx-props-no-spreading
                                 {...this._props}
                                 PanelWidth={this._props.PanelWidth}
                                 PanelHeight={this.panelHeight}
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 582c09f29..0252961d3 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -85,6 +85,7 @@ async function deserializeScript(scriptIn: ScriptField) {
 }
 
 @scriptingGlobal
+// eslint-disable-next-line no-use-before-define
 @Deserializable('script', (obj: unknown) => deserializeScript(obj as ScriptField))
 export class ScriptField extends ObjectField {
     @serializable
@@ -137,7 +138,6 @@ export class ScriptField extends ObjectField {
     [ToString]() {
         return this.script.originalScript;
     }
-    // eslint-disable-next-line default-param-last
     public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Doc | string | number | boolean }, transformer?: Transformer) {
         return CompileScript(script, {
             params: {
@@ -156,13 +156,11 @@ export class ScriptField extends ObjectField {
         });
     }
 
-    // eslint-disable-next-line default-param-last
     public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) {
         const compiled = ScriptField.CompileScript(script, params, true, capturedVariables);
         return compiled.compiled ? new ScriptField(compiled) : undefined;
     }
 
-    // eslint-disable-next-line default-param-last
     public static MakeScript(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) {
         const compiled = ScriptField.CompileScript(script, params, false, capturedVariables);
         return compiled.compiled ? new ScriptField(compiled) : undefined;
@@ -186,6 +184,7 @@ export class ScriptField extends ObjectField {
 }
 
 @scriptingGlobal
+// eslint-disable-next-line no-use-before-define
 @Deserializable('computed', (obj: unknown) => deserializeScript(obj as ComputedField))
 export class ComputedField extends ScriptField {
     static undefined = '__undefined';
@@ -229,7 +228,6 @@ export class ComputedField extends ScriptField {
     [ToValue](doc: Doc)   { return ComputedField.useComputed ? { value: this.value(doc) } : undefined; } // prettier-ignore
     [Copy](): ObjectField { return new ComputedField(this.script, this.setterscript, this.rawscript); } // prettier-ignore
 
-    // eslint-disable-next-line default-param-last
     public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }, setterscript?: string) {
         const compiled = ScriptField.CompileScript(script, params, true, { value: '', ...capturedVariables });
         const compiledsetter = setterscript ? ScriptField.CompileScript(setterscript, { ...params, value: 'any' }, false, capturedVariables) : undefined;
-- 
cgit v1.2.3-70-g09d2


From 6b070202843e1e2a11c09c4d29aee4ab8750f31f Mon Sep 17 00:00:00 2001
From: geireann <geireann.lindfield@gmail.com>
Date: Thu, 24 Oct 2024 12:01:21 -0400
Subject: fixed pinning of card and carousel views.

---
 src/client/views/collections/CollectionCardDeckView.tsx   | 6 +++---
 src/client/views/collections/CollectionCarousel3DView.tsx | 6 +++---
 src/client/views/collections/CollectionCarouselView.tsx   | 6 +++---
 3 files changed, 9 insertions(+), 9 deletions(-)

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

diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index beb45f326..3a5cb5f37 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -19,7 +19,7 @@ import { dropActionType } from '../../util/DropActionTypes';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { undoable } from '../../util/UndoManager';
-import { PinDocView } from '../PinFuncs';
+import { PinDocView, PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProp';
 import { TagItem } from '../TagsView';
 import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
@@ -608,9 +608,9 @@ export class CollectionCardView extends CollectionSubView() {
         }
         return undefined;
     });
-    getAnchor = (addAsAnnotation: boolean) => {
+    getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_card_curDoc: this.curDoc() });
-        PinDocView(anchor, { pinData: { collectionType: true, filters: true } }, this.Document);
+        PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { collectionType: true, filters: true } }, this.Document);
         addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
         return anchor;
     };
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index c080ba27e..b7ecf9a2f 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -12,7 +12,7 @@ import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { Transform } from '../../util/Transform';
-import { PinDocView } from '../PinFuncs';
+import { PinDocView, PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProp';
 import { DocumentView } from '../nodes/DocumentView';
 import { FocusViewOptions } from '../nodes/FocusViewOptions';
@@ -107,9 +107,9 @@ export class CollectionCarousel3DView extends CollectionSubView() {
         }
         return undefined;
     };
-    getAnchor = (addAsAnnotation: boolean) => {
+    getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_carousel_index: this.layoutDoc._carousel_index as number });
-        PinDocView(anchor, { pinData: { collectionType: true, filters: true } }, this.Document);
+        PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { collectionType: true, filters: true } }, this.Document);
         addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
         return anchor;
     };
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index f714e2a00..87c6e3e5c 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -8,7 +8,7 @@ import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
-import { PinDocView } from '../PinFuncs';
+import { PinDocView, PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProp';
 import { DocumentView } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
@@ -84,9 +84,9 @@ export class CollectionCarouselView extends CollectionSubView() {
         return undefined;
     };
 
-    getAnchor = (addAsAnnotation: boolean) => {
+    getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_carousel_index: this.carouselIndex });
-        PinDocView(anchor, { pinData: { collectionType: true, filters: true } }, this.Document);
+        PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { collectionType: true, filters: true } }, this.Document);
         addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
         return anchor;
     };
-- 
cgit v1.2.3-70-g09d2


From 28792ad2da9f5dfab96f98f34a11136bd1453e0f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 6 Mar 2025 10:49:20 -0500
Subject: fixed resizing height to be > 10 pixels.  split pivotView out of
 timeView.

---
 src/client/documents/DocumentTypes.ts              |   1 +
 src/client/util/CurrentUserUtils.ts                |   4 +-
 src/client/views/DocumentDecorations.tsx           |   3 +-
 .../views/collections/CollectionCarouselView.tsx   |   2 +-
 .../views/collections/CollectionPivotView.tsx      | 147 +++++++++++++++++++
 .../views/collections/CollectionTimeView.tsx       | 158 ++-------------------
 src/client/views/collections/CollectionView.tsx    |   7 +-
 .../CollectionFreeFormLayoutEngines.tsx            |   6 +-
 8 files changed, 174 insertions(+), 154 deletions(-)
 create mode 100644 src/client/views/collections/CollectionPivotView.tsx

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

diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 8aa844c0b..845aa280c 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -61,6 +61,7 @@ export enum CollectionViewType {
     Multirow = 'multirow',
     NoteTaking = 'notetaking',
     Pile = 'pileup',
+    Pivot = 'pivot',
     Schema = 'schema',
     Stacking = 'stacking',
     StackedTimeline = 'stacked timeline',
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 60acd5b21..8736225d0 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -826,7 +826,7 @@ pie title Minerals in my tap water
         return [
             { btnList: new List<string>([CollectionViewType.Freeform, CollectionViewType.Card, CollectionViewType.Carousel,CollectionViewType.Carousel3D, 
                     CollectionViewType.Masonry, CollectionViewType.Multicolumn, CollectionViewType.Multirow, CollectionViewType.Linear, 
-                    CollectionViewType.Map, CollectionViewType.NoteTaking, CollectionViewType.Schema, CollectionViewType.Stacking, 
+                    CollectionViewType.Map, CollectionViewType.NoteTaking, CollectionViewType.Pivot, CollectionViewType.Schema, CollectionViewType.Stacking, 
                     CollectionViewType.Calendar, CollectionViewType.Grid, CollectionViewType.Tree, CollectionViewType.Time, ]),
               title: "Perspective",                    toolTip: "View",                 btnType: ButtonType.DropdownList,                                                                                                                             ignoreClick: true, width: 100, scripts: { script:  '{ return setView(value, shiftKey, _readOnly_); }'}},
             { title: "Pin",     icon: "map-pin",       toolTip: "Pin View to Trail",    btnType: ButtonType.ClickButton,  expertMode: false,                                                                                                                             width: 30,  scripts: { onClick: 'pinWithView(altKey)'}, funcs: {hidden: "IsNoneSelected()"}},
@@ -911,7 +911,7 @@ pie title Minerals in my tap water
                             CurrentUserUtils.createToolButton(opts), scripts, funcs);
         
         const btnDescs = [// setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet
-            { opts: { title: "Replicate",icon:"camera",toolTip: "Copy dashboard layout",btnType: ButtonType.ClickButton, expertMode: true},                                                                                             scripts: { onClick:  `snapshotDashboard()`}},
+            { opts: { title: "Replicate",icon:"camera",toolTip:"Copy dashboard layout",btnType: ButtonType.ClickButton, expertMode: true},                                                                                             scripts: { onClick:  `snapshotDashboard()`}},
             { opts: { title: "Recordings",            toolTip: "Workspace Recordings", btnType: ButtonType.DropdownList,expertMode: false, ignoreClick: true, width: 100}, funcs: {hidden: `false`, btnList:`getWorkspaceRecordings()`},scripts: { script:  `{ return replayWorkspace(value, _readOnly_); }`, onDragScript: `{ return startRecordingDrag(value); }`}},
             { opts: { title: "Stop Rec",icon: "stop", toolTip: "Stop recording",       btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `!isWorkspaceRecording()`},                                                 scripts: { onClick: `stopWorkspaceRecording()`}},
             { opts: { title: "Play",    icon: "play", toolTip: "Play recording",       btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${mediaState.Paused}"`},                       scripts: { onClick: `resumeWorkspaceReplaying(getCurrentRecording())`}},
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index d9b6bdf1a..54e050f9f 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -36,7 +36,6 @@ import { ImageBox } from './nodes/ImageBox';
 import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { TagsView } from './TagsView';
-import { ImageField } from '../../fields/URLField';
 
 interface DocumentDecorationsProps {
     PanelWidth: number;
@@ -567,7 +566,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             }
 
             doc._width = Math.max(NumCast(doc._width_min, 25), NumCast(doc._width) * scale.x);
-            doc._height = Math.max(NumCast(doc._height_min), NumCast(doc._height) * scale.y);
+            doc._height = Math.max(NumCast(doc._height_min, 10), NumCast(doc._height) * scale.y);
             const { deltaX, deltaY } = this.realignRefPt(doc, refCent, initWidth || 1, initHeight || 1);
             doc.x = NumCast(doc.x) + deltaX;
             doc.y = NumCast(doc.y) + deltaY;
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 87c6e3e5c..a7d217076 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -249,9 +249,9 @@ export class CollectionCarouselView extends CollectionSubView() {
                         position: 'relative',
                     }}>
                     {this.content}
-                    {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
                     {this.navButtons}
                 </div>
+                {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
             </div>
         );
     }
diff --git a/src/client/views/collections/CollectionPivotView.tsx b/src/client/views/collections/CollectionPivotView.tsx
new file mode 100644
index 000000000..2600c0f57
--- /dev/null
+++ b/src/client/views/collections/CollectionPivotView.tsx
@@ -0,0 +1,147 @@
+import { action, computed, makeObservable, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { returnTrue } from '../../../ClientUtils';
+import { Doc, Opt, StrListCast } from '../../../fields/Doc';
+import { List } from '../../../fields/List';
+import { ObjectField } from '../../../fields/ObjectField';
+import { ComputedField, ScriptField } from '../../../fields/ScriptField';
+import { NumCast, StrCast } from '../../../fields/Types';
+import { Docs } from '../../documents/Documents';
+import { ScriptingGlobals } from '../../util/ScriptingGlobals';
+import { FieldsDropdown } from '../FieldsDropdown';
+import { PinDocView } from '../PinFuncs';
+import { DocumentView } from '../nodes/DocumentView';
+import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
+import './CollectionTimeView.scss';
+import { ViewDefBounds, computePivotLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
+import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
+
+@observer
+export class CollectionPivotView extends CollectionSubView() {
+    _changing = false;
+    @observable _collapsed: boolean = false;
+    @observable _childClickedScript: Opt<ScriptField> = undefined;
+    @observable _viewDefDivClick: Opt<ScriptField> = undefined;
+    @observable _focusPivotField: Opt<string> = undefined;
+
+    constructor(props: SubCollectionViewProps) {
+        super(props);
+        makeObservable(this);
+    }
+
+    componentDidMount() {
+        this._props.setContentViewBox?.(this);
+        runInAction(() => {
+            this._childClickedScript = ScriptField.MakeScript('openInLightbox(this)', { this: Doc.name });
+            this._viewDefDivClick = ScriptField.MakeScript('pivotColumnClick(this,payload)', { payload: 'any' });
+        });
+    }
+
+    get pivotField() {
+        return this._focusPivotField || StrCast(this.layoutDoc._pivotField);
+    }
+
+    getAnchor = (addAsAnnotation: boolean) => {
+        const anchor = Docs.Create.ConfigDocument({
+            title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as unknown as string, // title can take a functiono or a string
+            annotationOn: this.Document,
+        });
+        PinDocView(anchor, { pinData: { collectionType: true, pivot: true, filters: true } }, this.Document);
+        addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
+        return anchor;
+    };
+
+    @action
+    scrollPreview = (docView: DocumentView, anchor: Doc /* , focusSpeed: number, options: FocusViewOptions */) => {
+        // if in preview, then override document's fields with view spec
+        this._focusFilters = StrListCast(anchor.config_docFilters);
+        this._focusRangeFilters = StrListCast(anchor.config_docRangeFilters);
+        this._focusPivotField = StrCast(anchor.config_pivotField);
+        return undefined;
+    };
+
+    toggleVisibility = action(() => {
+        this._collapsed = !this._collapsed;
+    });
+
+    goTo = (prevFilterIndex: number) => {
+        this.layoutDoc._pivotField = this.layoutDoc['_prevPivotFields' + prevFilterIndex];
+        this.layoutDoc._childFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocFilter' + prevFilterIndex] as ObjectField);
+        this.layoutDoc._childFiltersByRanges = ObjectField.MakeCopy(this.layoutDoc['_prevDocRangeFilters' + prevFilterIndex] as ObjectField);
+        this.layoutDoc._prevFilterIndex = prevFilterIndex;
+    };
+
+    @action
+    contentsDown = () => {
+        const prevFilterIndex = NumCast(this.layoutDoc._prevFilterIndex);
+        if (prevFilterIndex > 0) {
+            this.goTo(prevFilterIndex - 1);
+        } else {
+            this.layoutDoc._childFilters = new List([]);
+        }
+    };
+    layoutEngine = () => computePivotLayout.name;
+    @computed get contents() {
+        return (
+            <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this._props.isContentActive() ? undefined : 'none' }} onClick={this.contentsDown}>
+                <CollectionFreeFormView
+                    {...this._props}
+                    engineProps={{ pivotField: this.pivotField, childFilters: this.childDocFilters, childFiltersByRanges: this.childDocRangeFilters }}
+                    fitContentsToBox={returnTrue}
+                    childClickScript={this._childClickedScript}
+                    viewDefDivClick={this._viewDefDivClick}
+                    layoutEngine={this.layoutEngine}
+                />
+            </div>
+        );
+    }
+
+    render() {
+        return (
+            <div className="collectionTimeView-pivot" style={{ width: this._props.PanelWidth(), height: '100%' }}>
+                {this.contents}
+                <div style={{ right: 0, top: 0, position: 'absolute' }}>
+                    <FieldsDropdown
+                        Document={this.Document}
+                        selectFunc={fieldKey => {
+                            this.layoutDoc._pivotField = fieldKey;
+                        }}
+                        placeholder={StrCast(this.layoutDoc._pivotField)}
+                    />
+                </div>
+            </div>
+        );
+    }
+}
+
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBounds) {
+    const pivotField = StrCast(pivotDoc._pivotField, 'author');
+    let prevFilterIndex = NumCast(pivotDoc._prevFilterIndex);
+    const originalFilter = StrListCast(ObjectField.MakeCopy(pivotDoc._childFilters as ObjectField));
+    pivotDoc['_prevDocFilter' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._childFilters as ObjectField);
+    pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._childFiltersByRanges as ObjectField);
+    pivotDoc['_prevPivotFields' + prevFilterIndex] = pivotField;
+    pivotDoc._prevFilterIndex = ++prevFilterIndex;
+    pivotDoc._childFilters = new List();
+    setTimeout(
+        action(() => {
+            const filterVals = bounds.payload as string[];
+            filterVals.map(filterVal => Doc.setDocFilter(pivotDoc, pivotField, filterVal, 'check'));
+            const pivotView = DocumentView.getDocumentView(pivotDoc);
+            if (pivotDoc && pivotView?.ComponentView instanceof CollectionPivotView && filterVals.length === 1) {
+                if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) {
+                    pivotDoc._pivotField = filterVals[0];
+                }
+            }
+            const newFilters = StrListCast(pivotDoc._childFilters);
+            if (newFilters.length && originalFilter.length && newFilters.lastElement() === originalFilter.lastElement()) {
+                pivotDoc._prevFilterIndex = --prevFilterIndex;
+                pivotDoc['_prevDocFilter' + prevFilterIndex] = undefined;
+                pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = undefined;
+                pivotDoc['_prevPivotFields' + prevFilterIndex] = undefined;
+            }
+        })
+    );
+});
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index d75c633ac..98bd06221 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -1,33 +1,22 @@
-import { action, computed, makeObservable, observable, runInAction } from 'mobx';
+import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
 import { Doc, Opt, StrListCast } from '../../../fields/Doc';
-import { List } from '../../../fields/List';
-import { ObjectField } from '../../../fields/ObjectField';
-import { listSpec } from '../../../fields/Schema';
-import { ComputedField, ScriptField } from '../../../fields/ScriptField';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { NumCast, StrCast } from '../../../fields/Types';
 import { Docs } from '../../documents/Documents';
-import { ScriptingGlobals } from '../../util/ScriptingGlobals';
-import { ContextMenu } from '../ContextMenu';
-import { ContextMenuProps } from '../ContextMenuItem';
 import { FieldsDropdown } from '../FieldsDropdown';
 import { PinDocView } from '../PinFuncs';
 import { DocumentView } from '../nodes/DocumentView';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 import './CollectionTimeView.scss';
-import { ViewDefBounds, computePivotLayout, computeTimelineLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
+import { computeTimelineLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
 import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
 
 @observer
 export class CollectionTimeView extends CollectionSubView() {
-    _changing = false;
-    @observable _layoutEngine = computePivotLayout.name;
     @observable _collapsed: boolean = false;
-    @observable _childClickedScript: Opt<ScriptField> = undefined;
-    @observable _viewDefDivClick: Opt<ScriptField> = undefined;
     @observable _focusPivotField: Opt<string> = undefined;
 
     constructor(props: SubCollectionViewProps) {
@@ -37,10 +26,6 @@ export class CollectionTimeView extends CollectionSubView() {
 
     componentDidMount() {
         this._props.setContentViewBox?.(this);
-        runInAction(() => {
-            this._childClickedScript = ScriptField.MakeScript('openInLightbox(this)', { this: Doc.name });
-            this._viewDefDivClick = ScriptField.MakeScript('pivotColumnClick(this,payload)', { payload: 'any' });
-        });
     }
 
     get pivotField() {
@@ -49,19 +34,10 @@ export class CollectionTimeView extends CollectionSubView() {
 
     getAnchor = (addAsAnnotation: boolean) => {
         const anchor = Docs.Create.ConfigDocument({
-            title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as unknown as string, // title can take a functiono or a string
             annotationOn: this.Document,
         });
         PinDocView(anchor, { pinData: { collectionType: true, pivot: true, filters: true } }, this.Document);
-
-        if (addAsAnnotation) {
-            // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
-            if (Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), null) !== undefined) {
-                Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), []).push(anchor);
-            } else {
-                this.dataDoc[this._props.fieldKey + '_annotations'] = new List<Doc>([anchor]);
-            }
-        }
+        addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
         return anchor;
     };
 
@@ -74,7 +50,6 @@ export class CollectionTimeView extends CollectionSubView() {
         return undefined;
     };
 
-    layoutEngine = () => this._layoutEngine;
     toggleVisibility = action(() => {
         this._collapsed = !this._collapsed;
     });
@@ -126,105 +101,24 @@ export class CollectionTimeView extends CollectionSubView() {
         );
     };
 
-    goTo = (prevFilterIndex: number) => {
-        this.layoutDoc._pivotField = this.layoutDoc['_prevPivotFields' + prevFilterIndex];
-        this.layoutDoc._childFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocFilter' + prevFilterIndex] as ObjectField);
-        this.layoutDoc._childFiltersByRanges = ObjectField.MakeCopy(this.layoutDoc['_prevDocRangeFilters' + prevFilterIndex] as ObjectField);
-        this.layoutDoc._prevFilterIndex = prevFilterIndex;
-    };
-
-    @action
-    contentsDown = () => {
-        const prevFilterIndex = NumCast(this.layoutDoc._prevFilterIndex);
-        if (prevFilterIndex > 0) {
-            this.goTo(prevFilterIndex - 1);
-        } else {
-            this.layoutDoc._childFilters = new List([]);
-        }
-    };
-
+    layoutEngine = () => computeTimelineLayout.name;
     @computed get contents() {
         return (
-            <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this._props.isContentActive() ? undefined : 'none' }} onClick={this.contentsDown}>
+            <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this._props.isContentActive() ? undefined : 'none' }}>
                 <CollectionFreeFormView
                     {...this._props}
                     engineProps={{ pivotField: this.pivotField, childFilters: this.childDocFilters, childFiltersByRanges: this.childDocRangeFilters }}
                     fitContentsToBox={returnTrue}
-                    childClickScript={this._childClickedScript}
-                    viewDefDivClick={this.layoutEngine() === computeTimelineLayout.name ? undefined : this._viewDefDivClick}
                     layoutEngine={this.layoutEngine}
                 />
             </div>
         );
     }
 
-    public static SyncTimelineToPresentation(doc: Doc) {
-        const fieldKey = Doc.LayoutFieldKey(doc);
-        doc[fieldKey + '-timelineCur'] = ComputedField.MakeFunction("(activePresentationItem()[this._pivotField || 'year'] || 0)");
-    }
-    specificMenu = () => {
-        const layoutItems: ContextMenuProps[] = [];
-        const doc = this.layoutDoc;
-
-        layoutItems.push({
-            description: 'Force Timeline',
-            event: () => {
-                doc._forceRenderEngine = computeTimelineLayout.name;
-            },
-            icon: 'compress-arrows-alt',
-        });
-        layoutItems.push({
-            description: 'Force Pivot',
-            event: () => {
-                doc._forceRenderEngine = computePivotLayout.name;
-            },
-            icon: 'compress-arrows-alt',
-        });
-        layoutItems.push({
-            description: 'Auto Time/Pivot layout',
-            event: () => {
-                doc._forceRenderEngine = undefined;
-            },
-            icon: 'compress-arrows-alt',
-        });
-        layoutItems.push({ description: 'Sync with presentation', event: () => CollectionTimeView.SyncTimelineToPresentation(doc), icon: 'compress-arrows-alt' });
-
-        ContextMenu.Instance.addItem({ description: 'Options...', subitems: layoutItems, icon: 'eye' });
-    };
-
     render() {
-        let nonNumbers = 0;
-        this.childDocs.forEach(doc => {
-            const num = NumCast(doc[this.pivotField], Number(StrCast(doc[this.pivotField])));
-            if (isNaN(num)) {
-                nonNumbers++;
-            }
-        });
-        const forceLayout = StrCast(this.layoutDoc._forceRenderEngine);
-        const doTimeline = forceLayout ? forceLayout === computeTimelineLayout.name : nonNumbers / this.childDocs.length < 0.1 && this._props.PanelWidth() / this._props.PanelHeight() > 6;
-        if (doTimeline !== (this._layoutEngine === computeTimelineLayout.name)) {
-            if (!this._changing) {
-                this._changing = true;
-                setTimeout(
-                    action(() => {
-                        this._layoutEngine = doTimeline ? computeTimelineLayout.name : computePivotLayout.name;
-                        this._changing = false;
-                    }),
-                    0
-                );
-            }
-        }
-
         return (
-            <div className={'collectionTimeView' + (doTimeline ? '' : '-pivot')} onContextMenu={this.specificMenu} style={{ width: this._props.PanelWidth(), height: '100%' }}>
+            <div className="collectionTimeView" style={{ width: this._props.PanelWidth(), height: '100%' }}>
                 {this.contents}
-                {!this._props.isSelected() || !doTimeline ? null : (
-                    <>
-                        <div className="collectionTimeView-thumb-min collectionTimeView-thumb" key="min" onPointerDown={this.onMinDown} />
-                        <div className="collectionTimeView-thumb-max collectionTimeView-thumb" key="mid" onPointerDown={this.onMaxDown} />
-                        <div className="collectionTimeView-thumb-mid collectionTimeView-thumb" key="max" onPointerDown={this.onMidDown} />
-                    </>
-                )}
                 <div style={{ right: 0, top: 0, position: 'absolute' }}>
                     <FieldsDropdown
                         Document={this.Document}
@@ -234,38 +128,14 @@ export class CollectionTimeView extends CollectionSubView() {
                         placeholder={StrCast(this.layoutDoc._pivotField)}
                     />
                 </div>
+                {!this._props.isSelected() ? null : (
+                    <>
+                        <div className="collectionTimeView-thumb-min collectionTimeView-thumb" key="min" onPointerDown={this.onMinDown} />
+                        <div className="collectionTimeView-thumb-max collectionTimeView-thumb" key="mid" onPointerDown={this.onMaxDown} />
+                        <div className="collectionTimeView-thumb-mid collectionTimeView-thumb" key="max" onPointerDown={this.onMidDown} />
+                    </>
+                )}
             </div>
         );
     }
 }
-
-// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBounds) {
-    const pivotField = StrCast(pivotDoc._pivotField, 'author');
-    let prevFilterIndex = NumCast(pivotDoc._prevFilterIndex);
-    const originalFilter = StrListCast(ObjectField.MakeCopy(pivotDoc._childFilters as ObjectField));
-    pivotDoc['_prevDocFilter' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._childFilters as ObjectField);
-    pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._childFiltersByRanges as ObjectField);
-    pivotDoc['_prevPivotFields' + prevFilterIndex] = pivotField;
-    pivotDoc._prevFilterIndex = ++prevFilterIndex;
-    pivotDoc._childFilters = new List();
-    setTimeout(
-        action(() => {
-            const filterVals = bounds.payload as string[];
-            filterVals.map(filterVal => Doc.setDocFilter(pivotDoc, pivotField, filterVal, 'check'));
-            const pivotView = DocumentView.getDocumentView(pivotDoc);
-            if (pivotDoc && pivotView?.ComponentView instanceof CollectionTimeView && filterVals.length === 1) {
-                if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) {
-                    pivotDoc._pivotField = filterVals[0];
-                }
-            }
-            const newFilters = StrListCast(pivotDoc._childFilters);
-            if (newFilters.length && originalFilter.length && newFilters.lastElement() === originalFilter.lastElement()) {
-                pivotDoc._prevFilterIndex = --prevFilterIndex;
-                pivotDoc['_prevDocFilter' + prevFilterIndex] = undefined;
-                pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = undefined;
-                pivotDoc['_prevPivotFields' + prevFilterIndex] = undefined;
-            }
-        })
-    );
-});
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index a4900e9d7..7ba8989ce 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -15,12 +15,14 @@ import { ContextMenuProps } from '../ContextMenuItem';
 import { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { FieldView } from '../nodes/FieldView';
 import { OpenWhere } from '../nodes/OpenWhere';
+import { CalendarBox } from '../nodes/calendarBox/CalendarBox';
 import { CollectionCardView } from './CollectionCardDeckView';
 import { CollectionCarousel3DView } from './CollectionCarousel3DView';
 import { CollectionCarouselView } from './CollectionCarouselView';
 import { CollectionDockingView } from './CollectionDockingView';
 import { CollectionNoteTakingView } from './CollectionNoteTakingView';
 import { CollectionPileView } from './CollectionPileView';
+import { CollectionPivotView } from './CollectionPivotView';
 import { CollectionStackingView } from './CollectionStackingView';
 import { CollectionViewProps, SubCollectionViewProps } from './CollectionSubView';
 import { CollectionTimeView } from './CollectionTimeView';
@@ -32,7 +34,6 @@ import { CollectionLinearView } from './collectionLinear';
 import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView';
 import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView';
 import { CollectionSchemaView } from './collectionSchema/CollectionSchemaView';
-import { CalendarBox } from '../nodes/calendarBox/CalendarBox';
 
 @observer
 export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewProps>() {
@@ -105,6 +106,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
             case CollectionViewType.NoteTaking:   return <CollectionNoteTakingView  key="collview" {...props} />;
             case CollectionViewType.Masonry:      return <CollectionStackingView    key="collview" {...props} />;
             case CollectionViewType.Time:         return <CollectionTimeView        key="collview" {...props} />;
+            case CollectionViewType.Pivot:        return <CollectionPivotView       key="collview" {...props} />;
             case CollectionViewType.Grid:         return <CollectionGridView        key="collview" {...props} />;
             case CollectionViewType.Card:         return <CollectionCardView        key="collview" {...props} />;
         }
@@ -126,7 +128,8 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
                 { description: 'Carousel',    event: () => func(CollectionViewType.Carousel), icon: 'columns' },
                 { description: '3D Carousel', event: () => func(CollectionViewType.Carousel3D), icon: 'columns' },
                 { description: 'Calendar',    event: () => func(CollectionViewType.Calendar), icon: 'columns' },
-                { description: 'Pivot/Time',  event: () => func(CollectionViewType.Time), icon: 'columns' },
+                { description: 'Pivot',       event: () => func(CollectionViewType.Pivot), icon: 'columns' },
+                { description: 'Time',        event: () => func(CollectionViewType.Time), icon: 'columns' },
                 { description: 'Map',         event: () => func(CollectionViewType.Map), icon: 'globe-americas' },
                 { description: 'Grid',        event: () => func(CollectionViewType.Grid), icon: 'th-list' },
             ];
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index bebdbd731..241a56a88 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -4,7 +4,7 @@ import { Id, ToString } from '../../../../fields/FieldSymbols';
 import { ObjectField } from '../../../../fields/ObjectField';
 import { RefField } from '../../../../fields/RefField';
 import { listSpec } from '../../../../fields/Schema';
-import { Cast, NumCast, StrCast } from '../../../../fields/Types';
+import { Cast, DateCast, NumCast, StrCast } from '../../../../fields/Types';
 import { aggregateBounds } from '../../../../Utils';
 
 export interface ViewDefBounds {
@@ -107,7 +107,7 @@ export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc
 }
 
 function toNumber(val: FieldResult<FieldType>) {
-    return val === undefined ? undefined : NumCast(val, Number(StrCast(val)));
+    return val === undefined ? undefined : DateCast(val) ? DateCast(val).date.getMilliseconds() : NumCast(val, Number(StrCast(val)));
 }
 
 export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[] /* , engineProps: any */) {
@@ -295,7 +295,7 @@ export function computeTimelineLayout(poolData: Map<string, PoolData>, pivotDoc:
     let minTime = minTimeReq === undefined ? Number.MAX_VALUE : minTimeReq;
     let maxTime = maxTimeReq === undefined ? -Number.MAX_VALUE : maxTimeReq;
     childPairs.forEach(pair => {
-        const num = NumCast(pair.layout[timelineFieldKey], Number(StrCast(pair.layout[timelineFieldKey])));
+        const num = toNumber(pair.layout[timelineFieldKey]) ?? 0;
         if (!isNaN(num) && (!minTimeReq || num >= minTimeReq) && (!maxTimeReq || num <= maxTimeReq)) {
             !pivotDateGroups.get(num) && pivotDateGroups.set(num, []);
             pivotDateGroups.get(num)!.push(pair.layout);
-- 
cgit v1.2.3-70-g09d2

