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.

---
 src/client/views/nodes/ComparisonBox.tsx | 114 +++++++++++++++++--------------
 1 file changed, 62 insertions(+), 52 deletions(-)

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

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>
-- 
cgit v1.2.3-70-g09d2

