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

---
 src/client/views/pdf/GPTPopup/GPTPopup.tsx | 629 +++++++++++------------------
 1 file changed, 236 insertions(+), 393 deletions(-)

(limited to 'src/client/views/pdf/GPTPopup/GPTPopup.tsx')

diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 2d95ac2eb..691c24792 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -1,6 +1,6 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Button, IconButton, Toggle, ToggleType, Type } from '@dash/components';
-import { action, makeObservable, observable, runInAction } from 'mobx';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { CgClose, CgCornerUpLeft } from 'react-icons/cg';
@@ -10,7 +10,7 @@ import { ClientUtils } from '../../../../ClientUtils';
 import { Doc } from '../../../../fields/Doc';
 import { NumCast, StrCast } from '../../../../fields/Types';
 import { Networking } from '../../../Network';
-import { GPTCallType, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
+import { GPTCallType, GPTTypeStyle, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
 import { DocUtils } from '../../../documents/DocUtils';
 import { Docs } from '../../../documents/Documents';
 import { SettingsManager } from '../../../util/SettingsManager';
@@ -19,7 +19,6 @@ import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { DocumentView } from '../../nodes/DocumentView';
 import { AnchorMenu } from '../AnchorMenu';
 import './GPTPopup.scss';
-import { isJSDocProtectedTag } from 'typescript';
 
 export enum GPTPopupMode {
     SUMMARY,
@@ -42,133 +41,79 @@ export enum GPTQuizType {
 export class GPTPopup extends ObservableReactComponent<object> {
     // eslint-disable-next-line no-use-before-define
     static Instance: GPTPopup;
-    private messagesEndRef: React.RefObject<HTMLDivElement>;
-
-    @observable private chatMode: boolean = false;
-    private correlatedColumns: string[] = [];
-
-    @observable public Visible: boolean = false;
-    @action public setVisible = (vis: boolean) => {
-        this.Visible = vis;
-    };
-    @observable public loading: boolean = false;
-    @action public setLoading = (loading: boolean) => {
-        this.loading = loading;
-    };
-    @observable public text: string = '';
-    @action public setText = (text: string) => {
-        this.text = text;
-    };
-    @observable public selectedText: string = '';
-    @action public setSelectedText = (text: string) => {
-        this.selectedText = text;
-    };
-    @observable public dataJson: string = '';
-    public dataChatPrompt: string | undefined = undefined;
-    @action public setDataJson = (text: string) => {
-        if (text === '') this.dataChatPrompt = '';
-        this.dataJson = text;
-    };
-
-    @observable public imgDesc: string = '';
-    @action public setImgDesc = (text: string) => {
-        this.imgDesc = text;
-    };
-
-    @observable public imgUrls: string[][] = [];
-    @action public setImgUrls = (imgs: string[][]) => {
-        this.imgUrls = imgs;
-    };
-
-    @observable public mode: GPTPopupMode = GPTPopupMode.SUMMARY;
-    @action public setMode = (mode: GPTPopupMode) => {
-        this.mode = mode;
-    };
-
-    @observable public highlightRange: number[] = [];
-    @action callSummaryApi = () => {};
-
-    @observable private done: boolean = false;
-    @action public setDone = (done: boolean) => {
-        this.done = done;
-        this.chatMode = false;
-    };
-
-    // change what can be a ref into a ref
-    @observable private sidebarId: string = '';
-    @action public setSidebarId = (id: string) => {
-        this.sidebarId = id;
-    };
-
-    @observable private imgTargetDoc: Doc | undefined;
-    @action public setImgTargetDoc = (anchor: Doc) => {
-        this.imgTargetDoc = anchor;
-    };
-
-    @observable private textAnchor: Doc | undefined;
-    @action public setTextAnchor = (anchor: Doc) => {
-        this.textAnchor = anchor;
+    private _regenerateCallback: (() => Promise<void>) | null = null;
+    private _messagesEndRef: React.RefObject<HTMLDivElement>;
+    private _correlatedColumns: string[] = [];
+    private _dataChatPrompt: string | undefined = undefined;
+    private _imgTargetDoc: Doc | undefined;
+    private _textAnchor: Doc | undefined;
+    private _dataJson: string = '';
+    private _sortDesc: string = '';
+    private _sidebarFieldKey: string = '';
+    private _imgDesc: string = '';
+    private _selectedText: string = '';
+
+    public setImgDesc = (text: string) => (this._imgDesc = text);
+    public setSidebarFieldKey = (id: string) => (this._sidebarFieldKey = id);
+    public setSortDesc = (t: string) => (this._sortDesc = t);
+    public setImgTargetDoc = (anchor: Doc) => (this._imgTargetDoc = anchor);
+    public setTextAnchor = (anchor: Doc) => (this._textAnchor = anchor);
+    public onGptResponse?: (sortResult: string, questionType: GPTTypeStyle, tag?: string) => void;
+    public onQuizRandom?: () => void;
+    public setDataJson = (text: string) => {
+        if (text === '') this._dataChatPrompt = '';
+        this._dataJson = text;
     };
 
-    @observable public sortDesc: string = '';
-    @action public setSortDesc = (t: string) => {
-        this.sortDesc = t;
-    };
-
-    onSortComplete?: (sortResult: string, questionType: string, tag?: string) => void;
-    onQuizRandom?: () => void;
-
-    @observable collectionDoc: Doc | undefined = undefined;
-    @action setCollectionDoc(doc: Doc | undefined) {
-        this.collectionDoc = doc;
-    }
-
-    @observable sortRespText: string = '';
-
-    @action setSortRespText(resp: string) {
-        this.sortRespText = resp;
+    constructor(props: object) {
+        super(props);
+        makeObservable(this);
+        GPTPopup.Instance = this;
+        this._messagesEndRef = React.createRef();
     }
 
-    @observable chatSortPrompt: string = '';
-
-    sortPromptChanged = action((e: React.ChangeEvent<HTMLInputElement>) => {
-        this.chatSortPrompt = e.target.value;
-    });
+    componentDidUpdate = () => this._gptProcessing && this.setStopAnimatingResponse(false);
 
-    @observable quizAnswer: string = '';
-
-    quizAnswerChanged = action((e: React.ChangeEvent<HTMLInputElement>) => {
-        this.quizAnswer = e.target.value;
-    });
-
-    @observable conversationArray: string[] = ['Hi! In this pop up, you can ask ChatGPT questions about your documents and filter / sort them. '];
+    @observable private _conversationArray: string[] = ['Hi! In this pop up, you can ask ChatGPT questions about your documents and filter / sort them. '];
+    @observable private _chatEnabled: boolean = false;
+    @action private setChatEnabled = (start: boolean) => (this._chatEnabled = start);
+    @observable public Visible: boolean = false;
+    @action public setVisible = (vis: boolean) => (this.Visible = vis);
+    @observable private _gptProcessing: boolean = false;
+    @action public setGptProcessing = (loading: boolean) => (this._gptProcessing = loading);
+    @observable private _responseText: string = '';
+    @action public setResponseText = (text: string) => (this._responseText = text);
+    @observable private _imgUrls: string[][] = [];
+    @action public setImgUrls = (imgs: string[][]) => (this._imgUrls = imgs);
+    @observable private _mode: GPTPopupMode = GPTPopupMode.SUMMARY;
+    @action public setMode = (mode: GPTPopupMode) => (this._mode = mode);
+    @observable private _collectionContext: Doc | undefined = undefined;
+    @action setCollectionContext = (doc: Doc | undefined) => (this._collectionContext = doc);
+    @observable private _sortPrompt: string = '';
+    @action setSortPrompt = (e: React.ChangeEvent<HTMLInputElement>) => (this._sortPrompt = e.target.value);
+    @observable private _quizAnswer: string = '';
+    @action setQuizAnswer = (e: React.ChangeEvent<HTMLInputElement>) => (this._quizAnswer = e.target.value);
+    @observable private _stopAnimatingResponse: boolean = false;
+    @action public setStopAnimatingResponse = (done: boolean) => (this._stopAnimatingResponse = done);
 
     /**
-     * When the cards are in quiz mode in the card view, allows gpt to determine whether the user's answer was correct
-     * @returns
+     * Callback function that causes the card view to update the childpair string list
+     * @param callback
      */
-    generateQuiz = (selected: Doc) =>
-        (this.regenerateCallback?.() ?? Promise.resolve()).then(() =>
-            this.generateRubric(selected).then(() =>
-                gptAPICall('Question: ' + StrCast(selected.gptInputText) + ' UserAnswer: ' + this.quizAnswer + '.  Rubric: ' + StrCast(selected.gptRubric), GPTCallType.QUIZ)
-                    .then(res => {
-                        if (res) {
-                            this.setQuizResp(res);
-                            this.conversationArray.push(res);
-                            this.onQuizRandom?.();
-                        } else {
-                            console.error('GPT provided no response');
-                        }
-                    })
-                    .catch(err => console.error('GPT call failed', err))
-            )
-        );
+    public setRegenerateCallback(collectionDoc: Doc | undefined, callback: null | (() => Promise<void>)) {
+        this.setCollectionContext(collectionDoc);
+        this._regenerateCallback = callback;
+    }
+
+    public addDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean = () => false;
+    public createFilteredDoc: (axes?: string[]) => boolean = () => false;
+    public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
+    public questionTypeNumberToStyle = (questionType: string) => +questionType.split(' ')[0][0];
 
     /**
-     * Generates a rubric by which to compare the user's answer to
-     * @param inputText user's answer
+     * Generates a rubric for evaluating the user's description of the document's text
      * @param doc the doc the user is providing info about
-     * @returns gpt's response
+     * @returns gpt's response rubric
      */
     generateRubric = (doc: Doc) =>
         StrCast(doc.gptRubric)
@@ -177,148 +122,129 @@ export class GPTPopup extends ObservableReactComponent<object> {
                   .then(res => (doc.gptRubric = res))
                   .catch(err => console.error('GPT call failed', err));
 
-    @observable private regenerateCallback: (() => Promise<void>) | null = null;
-
     /**
-     * Callback function that causes the card view to update the childpair string list
-     * @param callback
+     * When the cards are in quiz mode in the card view, allows gpt to determine whether the user's answer was correct
+     * @param doc the doc the user is providing info about
+     * @param quizAnswer the user's answer/description for the document
+     * @returns
      */
-    @action public setRegenerateCallback(collectionDoc: Doc | undefined, callback: null | (() => Promise<void>)) {
-        this.setCollectionDoc(collectionDoc);
-        this.regenerateCallback = callback;
-    }
-
-    public addDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean = () => false;
-    public createFilteredDoc: (axes?: string[]) => boolean = () => false;
-    public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
-
-    @observable quizRespText: string = '';
-
-    @action setQuizResp(resp: string) {
-        this.quizRespText = resp;
-    }
+    generateQuizAnswerAnalysis = (doc: Doc, quizAnswer: string) =>
+        (this._regenerateCallback?.() ?? Promise.resolve()).then(
+            () =>
+            this.generateRubric(doc).then(() =>
+                gptAPICall(
+                    `Question: ${StrCast(doc.gptInputText)}; 
+                     UserAnswer: ${quizAnswer};
+                     Rubric: ${StrCast(doc.gptRubric)}`,
+                    GPTCallType.QUIZ
+                ).then(res => {
+                    this._conversationArray.push(res || 'GPT provided no answer');
+                    this.onQuizRandom?.();
+                })
+                .catch(err => console.error('GPT call failed', err))
+            ) // prettier-ignore
+        );
 
     /**
      * Generates a response to the user's question depending on the type of their question
+     * @param userPrompt the user's input that chat will respond to
      */
-    generateCard = async () => {
-        this.setLoading(true);
-
-        await this.regenerateCallback?.();
-
-        try {
-            const questionType = await gptAPICall(this.chatSortPrompt, GPTCallType.TYPE);
-            const questionNumber = questionType.split(' ')[0][0];
-            const res = await (() => {
-                switch (questionNumber) {
-                    case '1':
-                    case '2':
-                    case '4': return gptAPICall(this.sortDesc, GPTCallType.SUBSET, this.chatSortPrompt);
-                    case '6': return gptAPICall(this.sortDesc, GPTCallType.SORT, this.chatSortPrompt);
-                    default: return gptAPICall(StrCast(DocumentView.SelectedDocs().lastElement()?.gptInputText), GPTCallType.INFO, this.chatSortPrompt);
-                }})(); // prettier-ignore
-
-            // Trigger the callback with the result
-            if (this.onSortComplete) {
-                this.onSortComplete(res || 'Something went wrong :(', questionNumber, questionType.split(' ').slice(1).join(' '));
-
-                let explanation = res;
-
-                if (questionType != '5' && questionType != '3') {
-                    // Extract explanation surrounded by ------ at the top or both at the top and bottom
-                    const explanationMatch = res.match(/------\s*([\s\S]*?)\s*(?:------|$)/) || [];
-                    explanation = explanationMatch[1] ? explanationMatch[1].trim() : 'No explanation found';
-                }
-
-                // Set the extracted explanation to sortRespText
-                this.setSortRespText(explanation);
-                runInAction(() => this.conversationArray.push(this.sortRespText));
-                this.scrollToBottom();
-
-                console.log(res);
-            }
-        } catch (err) {
-            console.error(err);
-        }
-
-        this.setLoading(false);
-    };
+    generateQueryResponse = (userPrompt: string) =>
+        (this._regenerateCallback ?? Promise.resolve)().then(() =>
+            gptAPICall(userPrompt, GPTCallType.TYPE).then(questionType =>
+                (() => {
+                    switch (this.questionTypeNumberToStyle(questionType)) {
+                        case GPTTypeStyle.AssignTags:
+                        case GPTTypeStyle.Filter:
+                        case GPTTypeStyle.ChooseDoc: return gptAPICall(this._sortDesc, GPTCallType.SUBSET, userPrompt);
+                        case GPTTypeStyle.SortDocs:  return gptAPICall(this._sortDesc, GPTCallType.SORT, userPrompt);
+                        default:                     return gptAPICall(StrCast(DocumentView.SelectedDocs().lastElement()?.gptInputText), GPTCallType.INFO, userPrompt);
+                    } // prettier-ignore
+                })().then(
+                    action(res => {
+                        // Trigger the callback with the result
+                        this.onGptResponse?.(res || 'Something went wrong :(', this.questionTypeNumberToStyle(questionType), questionType.split(' ').slice(1).join(' '));
+                        this._conversationArray.push(
+                            ![GPTTypeStyle.GeneralInfo, GPTTypeStyle.DocInfo].includes(this.questionTypeNumberToStyle(questionType))? 
+                            // Extract explanation surrounded by ------ at the top or both at the top and bottom
+                            (res.match(/------\s*([\s\S]*?)\s*(?:------|$)/) ?? [])[1]?.trim() ?? 'No explanation found' : res);
+                    })
+                ).catch(err => console.log(err))
+            ).catch(err => console.log(err))
+        ); // prettier-ignore
 
     /**
      * Generates a Dalle image and uploads it to the server.
      */
-    generateImage = async () => {
-        if (this.imgDesc === '') return undefined;
-        this.setImgUrls([]);
-        this.setMode(GPTPopupMode.IMAGE);
-        this.setVisible(true);
-        this.setLoading(true);
-
-        try {
-            const imageUrls = await gptImageCall(this.imgDesc);
-            if (imageUrls && imageUrls[0]) {
-                const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [imageUrls[0]] });
-                const source = ClientUtils.prepend(result.accessPaths.agnostic.client);
-                this.setImgUrls([[imageUrls[0], source]]);
-            }
-        } catch (err) {
-            console.error(err);
+    generateImage = () => {
+        if (this._imgDesc !== '') {
+            this.setImgUrls([]);
+            this.setMode(GPTPopupMode.IMAGE);
+            this.setVisible(true);
+            this.setGptProcessing(true);
+
+            return gptImageCall(this._imgDesc)
+                .then(imageUrls =>
+                    imageUrls?.[0]
+                        ? Networking.PostToServer('/uploadRemoteImage', { sources: [imageUrls[0]] }).then(res => {
+                              const source = ClientUtils.prepend(res[0].accessPaths.agnostic.client);
+                              return this.setImgUrls([[imageUrls[0]!, source]]);
+                          })
+                        : undefined
+                )
+                .catch(err => console.error(err))
+                .finally(() => this.setGptProcessing(false));
         }
-        this.setLoading(false);
         return undefined;
     };
 
     /**
-     * Completes an API call to generate a summary of
-     * this.selectedText in the popup.
+     * Completes an API call to generate a summary of the specified text
+     *
+     * @param text the text to summarizz
      */
-    generateSummary = async () => {
-        GPTPopup.Instance.setVisible(true);
-        GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY);
-        GPTPopup.Instance.setLoading(true);
-
-        try {
-            const res = await gptAPICall(this.selectedText, GPTCallType.SUMMARY);
-            GPTPopup.Instance.setText(res || 'Something went wrong.');
-        } catch (err) {
-            console.error(err);
-        }
-        GPTPopup.Instance.setLoading(false);
+    generateSummary = (text?: string) => {
+        this._selectedText = text ?? this._selectedText;
+        this.setVisible(true);
+        this.setMode(GPTPopupMode.SUMMARY);
+        this.setGptProcessing(true);
+        return gptAPICall(this._selectedText, GPTCallType.SUMMARY)
+            .then(res => this.setResponseText(res || 'Something went wrong.'))
+            .catch(err => console.error(err))
+            .finally(() => this.setGptProcessing(false));
     };
 
     /**
      * Completes an API call to generate an analysis of
      * this.dataJson in the popup.
      */
-    generateDataAnalysis = async () => {
-        GPTPopup.Instance.setVisible(true);
-        GPTPopup.Instance.setLoading(true);
-        try {
-            const res = await gptAPICall(this.dataJson, GPTCallType.DATA, this.dataChatPrompt);
-            const json = JSON.parse(res! as string);
-            const keys = Object.keys(json);
-            this.correlatedColumns = [];
-            this.correlatedColumns.push(json[keys[0]]);
-            this.correlatedColumns.push(json[keys[1]]);
-            GPTPopup.Instance.setText(json[keys[2]] || 'Something went wrong.');
-        } catch (err) {
-            console.error(err);
-        }
-        GPTPopup.Instance.setLoading(false);
+    generateDataAnalysis = () => {
+        this.setVisible(true);
+        this.setGptProcessing(true);
+        return gptAPICall(this._dataJson, GPTCallType.DATA, this._dataChatPrompt)
+            .then(res => {
+                const json = JSON.parse(res! as string);
+                const keys = Object.keys(json);
+                this._correlatedColumns = [];
+                this._correlatedColumns.push(json[keys[0]]);
+                this._correlatedColumns.push(json[keys[1]]);
+                this.setResponseText(json[keys[2]] || 'Something went wrong.');
+            })
+            .catch(err => console.error(err))
+            .finally(() => this.setGptProcessing(false));
     };
 
     /**
      * Transfers the summarization text to a sidebar annotation text document.
      */
     private transferToText = () => {
-        const newDoc = Docs.Create.TextDocument(this.text.trim(), {
+        const newDoc = Docs.Create.TextDocument(this._responseText.trim(), {
             _width: 200,
             _height: 50,
             _layout_fitWidth: true,
             _layout_autoHeight: true,
         });
-        this.addDoc(newDoc, this.sidebarId);
-        // newDoc.data = 'Hello world';
+        this.addDoc(newDoc, this._sidebarFieldKey);
         const anchor = AnchorMenu.Instance?.GetAnchor(undefined, false);
         if (anchor) {
             DocUtils.MakeLink(newDoc, anchor, {
@@ -330,73 +256,35 @@ export class GPTPopup extends ObservableReactComponent<object> {
     /**
      * Creates a histogram to show the correlation relationship that was found
      */
-    private createVisualization = () => {
-        this.createFilteredDoc(this.correlatedColumns);
-    };
+    private createVisualization = () => this.createFilteredDoc(this._correlatedColumns);
 
     /**
      * Transfers the image urls to actual image docs
      */
     private transferToImage = (source: string) => {
-        const textAnchor = this.textAnchor ?? this.imgTargetDoc;
-        if (!textAnchor) return;
-        const newDoc = Docs.Create.ImageDocument(source, {
-            x: NumCast(textAnchor.x) + NumCast(textAnchor._width) + 10,
-            y: NumCast(textAnchor.y),
-            _height: 200,
-            _width: 200,
-            data_nativeWidth: 1024,
-            data_nativeHeight: 1024,
-        });
-        if (Doc.IsInMyOverlay(textAnchor)) {
-            newDoc.overlayX = textAnchor.x;
-            newDoc.overlayY = NumCast(textAnchor.y) + NumCast(textAnchor._height);
-            Doc.AddToMyOverlay(newDoc);
-        } else {
-            this.addToCollection?.(newDoc);
-        }
-        // Create link between prompt and image
-        DocUtils.MakeLink(textAnchor, newDoc, { link_relationship: 'Image Prompt' });
-    };
-
-    /**
-     * Creates a chatbox for analyzing data so that users can ask specific questions.
-     */
-    private chatWithAI = () => {
-        this.chatMode = true;
-    };
-    dataPromptChanged = action((e: React.ChangeEvent<HTMLInputElement>) => {
-        this.dataChatPrompt = e.target.value;
-    });
-
-    private getPreviewUrl = (source: string) => source.split('.').join('_m.');
-
-    constructor(props: object) {
-        super(props);
-        makeObservable(this);
-        GPTPopup.Instance = this;
-        this.messagesEndRef = React.createRef();
-    }
-
-    scrollToBottom = () => {
-        setTimeout(() => {
-            // Code to execute after 1 second (1000 ms)
-            if (this.messagesEndRef.current) {
-                this.messagesEndRef.current.scrollIntoView({ behavior: 'smooth', block: 'end' });
+        const textAnchor = this._textAnchor ?? this._imgTargetDoc;
+        if (textAnchor) {
+            const newDoc = Docs.Create.ImageDocument(source, {
+                x: NumCast(textAnchor.x) + NumCast(textAnchor._width) + 10,
+                y: NumCast(textAnchor.y),
+                _height: 200,
+                _width: 200,
+                data_nativeWidth: 1024,
+                data_nativeHeight: 1024,
+            });
+            if (Doc.IsInMyOverlay(textAnchor)) {
+                newDoc.overlayX = textAnchor.x;
+                newDoc.overlayY = NumCast(textAnchor.y) + NumCast(textAnchor._height);
+                Doc.AddToMyOverlay(newDoc);
+            } else {
+                this.addToCollection?.(newDoc);
             }
-        }, 50);
-    };
-
-    componentDidUpdate = () => {
-        if (this.loading) {
-            this.setDone(false);
+            // Create link between prompt and image
+            DocUtils.MakeLink(textAnchor, newDoc, { link_relationship: 'Image Prompt' });
         }
     };
 
-    @observable quizMode: GPTQuizType = GPTQuizType.CURRENT;
-    @action setQuizMode(g: GPTQuizType) {
-        this.quizMode = g;
-    }
+    scrollToBottom = () => setTimeout(() => this._messagesEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }), 50);
 
     cardMenu = () => (
         <div className="btns-wrapper-gpt">
@@ -419,7 +307,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
                 tooltip="Test your knowledge with ChatGPT!"
                 text="Quiz Cards!"
                 onClick={() => {
-                    this.conversationArray = ['Define the selected card!'];
+                    this._conversationArray = ['Define the selected card!'];
                     this.setMode(GPTPopupMode.QUIZ);
                     this.onQuizRandom?.();
                 }}
@@ -437,27 +325,19 @@ export class GPTPopup extends ObservableReactComponent<object> {
     );
 
     @action
-    handleKeyPress = (e: React.KeyboardEvent, isSort: boolean) => {
+    handleKeyPress = async (e: React.KeyboardEvent, isSort: boolean) => {
         if (e.key === 'Enter') {
             e.stopPropagation();
 
+            this.setGptProcessing(true);
             if (isSort) {
-                this.conversationArray.push(this.chatSortPrompt);
-                this.generateCard().then(
-                    action(() => {
-                        this.chatSortPrompt = '';
-                    })
-                );
+                this._conversationArray.push(this._sortPrompt);
+                await this.generateQueryResponse(this._sortPrompt).then(action(() => (this._sortPrompt = '')));
             } else {
-                this.conversationArray.push(this.quizAnswer);
-                this.setLoading(true);
-                this.generateQuiz(DocumentView.SelectedDocs().lastElement()).then(
-                    action(() => {
-                        this.quizAnswer = '';
-                        this.setLoading(false);
-                    })
-                );
+                this._conversationArray.push(this._quizAnswer);
+                await this.generateQuizAnswerAnalysis(DocumentView.SelectedDocs().lastElement(), this._quizAnswer).then(action(() => (this._quizAnswer = '')));
             }
+            this.setGptProcessing(false);
 
             this.scrollToBottom();
         }
@@ -467,26 +347,24 @@ export class GPTPopup extends ObservableReactComponent<object> {
         <div className="btns-wrapper-gpt">
             <div className="chat-wrapper">
                 <div className="chat-bubbles">
-                    {this.conversationArray.map((message, index) => (
+                    {this._conversationArray.map((message, index) => (
                         <div key={index} className={`chat-bubble ${index % 2 === 1 ? 'user-message' : 'chat-message'}`}>
                             {message}
                         </div>
                     ))}
-                    {this.loading && <div className={`chat-bubble chat-message`}>...</div>}
+                    {this._gptProcessing && <div className={`chat-bubble chat-message`}>...</div>}
                 </div>
 
-                <div ref={this.messagesEndRef} style={{ height: '100px' }} />
+                <div ref={this._messagesEndRef} style={{ height: '100px' }} />
             </div>
             <div className="inputWrapper">
                 <input
                     className="searchBox-input"
                     defaultValue=""
-                    value={isSort ? this.chatSortPrompt : this.quizAnswer} // Controlled input
+                    value={isSort ? this._sortPrompt : this._quizAnswer} // Controlled input
                     autoComplete="off"
-                    onChange={isSort ? this.sortPromptChanged : this.quizAnswerChanged}
-                    onKeyDown={e => {
-                        this.handleKeyPress(e, isSort);
-                    }}
+                    onChange={isSort ? this.setSortPrompt : this.setQuizAnswer}
+                    onKeyDown={e => this.handleKeyPress(e, isSort)}
                     type="text"
                     placeholder={`${isSort ? 'Have ChatGPT sort, tag, define, or filter your cards for you!' : 'Define the selected card!'}`}
                 />
@@ -497,7 +375,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
     sortBox = (isSort: boolean) => (
         <div className="gptPopup-sortBox" style={{ height: '80%' }}>
             {this.heading(isSort ? 'SORTING' : 'QUIZ')}
-            {this.mode === GPTPopupMode.CARD ? this.cardMenu() : this.cardActual(isSort)}
+            {this._mode === GPTPopupMode.CARD ? this.cardMenu() : this.cardActual(isSort)}
         </div>
     );
 
@@ -505,7 +383,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
         <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
             {this.heading('GENERATED IMAGE')}
             <div className="image-content-wrapper">
-                {this.imgUrls.map((rawSrc, i) => (
+                {this._imgUrls.map((rawSrc, i) => (
                     <div key={rawSrc[0] + i} className="img-wrapper">
                         <div className="img-container">
                             <img key={rawSrc[0]} src={rawSrc[0]} width={150} height={150} alt="dalle generation" />
@@ -516,7 +394,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
                     </div>
                 ))}
             </div>
-            {!this.loading && <IconButton tooltip="Generate Again" onClick={this.generateImage} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />}
+            {this._gptProcessing ? null : <IconButton tooltip="Generate Again" onClick={this.generateImage} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />}
         </div>
     );
 
@@ -524,44 +402,35 @@ export class GPTPopup extends ObservableReactComponent<object> {
         <>
             <div>
                 {this.heading('SUMMARY')}
-                <div className="content-wrapper">
-                    {!this.loading &&
-                        (!this.done ? (
+                <div className="gptPopup-content-wrapper">
+                    {!this._gptProcessing &&
+                        (!this._stopAnimatingResponse ? (
                             <TypeAnimation
                                 speed={50}
                                 sequence={[
-                                    this.text,
+                                    this._responseText,
                                     () => {
-                                        setTimeout(() => {
-                                            this.setDone(true);
-                                        }, 500);
+                                        setTimeout(() => this.setStopAnimatingResponse(true), 500);
                                     },
                                 ]}
                             />
                         ) : (
-                            this.text
+                            this._responseText
                         ))}
                 </div>
             </div>
-            {!this.loading && (
+            {!this._gptProcessing && (
                 <div className="btns-wrapper">
-                    {this.done ? (
+                    {this._stopAnimatingResponse ? (
                         <>
-                            <IconButton tooltip="Generate Again" onClick={this.generateSummary /* this.callSummaryApi */} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(SettingsManager.userVariantColor)} />
+                            <IconButton tooltip="Generate Again" onClick={() => this.generateSummary(this._selectedText + ' ')} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(SettingsManager.userVariantColor)} />
                             <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(SettingsManager.userVariantColor)} type={Type.TERT} />
                         </>
                     ) : (
                         <div className="summarizing">
                             <span>Summarizing</span>
                             <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} />
-                            <Button
-                                text="Stop Animation"
-                                onClick={() => {
-                                    this.setDone(true);
-                                }}
-                                color={StrCast(SettingsManager.userVariantColor)}
-                                type={Type.TERT}
-                            />
+                            <Button text="Stop Animation" onClick={() => this.setStopAnimatingResponse(true)} color={StrCast(SettingsManager.userVariantColor)} type={Type.TERT} />
                         </div>
                     )}
                 </div>
@@ -573,33 +442,31 @@ export class GPTPopup extends ObservableReactComponent<object> {
         <>
             <div>
                 {this.heading('ANALYSIS')}
-                <div className="content-wrapper">
-                    {!this.loading &&
-                        (!this.done ? (
+                <div className="gptPopup-content-wrapper">
+                    {!this._gptProcessing &&
+                        (!this._stopAnimatingResponse ? (
                             <TypeAnimation
                                 speed={50}
                                 sequence={[
-                                    this.text,
+                                    this._responseText,
                                     () => {
-                                        setTimeout(() => {
-                                            this.setDone(true);
-                                        }, 500);
+                                        setTimeout(() => this.setStopAnimatingResponse(true), 500);
                                     },
                                 ]}
                             />
                         ) : (
-                            this.text
+                            this._responseText
                         ))}
                 </div>
             </div>
-            {!this.loading && (
+            {!this._gptProcessing && (
                 <div className="btns-wrapper">
-                    {this.done ? (
-                        this.chatMode ? (
+                    {this._stopAnimatingResponse ? (
+                        this._chatEnabled ? (
                             <input
                                 defaultValue=""
                                 autoComplete="off"
-                                onChange={this.dataPromptChanged}
+                                onChange={e => (this._dataChatPrompt = e.target.value)}
                                 onKeyDown={e => {
                                     e.key === 'Enter' ? this.generateDataAnalysis() : null;
                                     e.stopPropagation();
@@ -613,21 +480,14 @@ export class GPTPopup extends ObservableReactComponent<object> {
                         ) : (
                             <>
                                 <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} />
-                                <Button tooltip="Chat with AI" text="Chat with AI" onClick={this.chatWithAI} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} />
+                                <Button tooltip="Chat with AI" text="Chat with AI" onClick={() => this.setChatEnabled(true)} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} />
                             </>
                         )
                     ) : (
                         <div className="summarizing">
                             <span>Summarizing</span>
                             <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} />
-                            <Button
-                                text="Stop Animation"
-                                onClick={() => {
-                                    this.setDone(true);
-                                }}
-                                color={StrCast(SnappingManager.userVariantColor)}
-                                type={Type.TERT}
-                            />
+                            <Button text="Stop Animation" onClick={() => this.setStopAnimatingResponse(true)} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} />
                         </div>
                     )}
                 </div>
@@ -636,70 +496,53 @@ export class GPTPopup extends ObservableReactComponent<object> {
     );
 
     aiWarning = () =>
-        this.done ? (
+        !this._stopAnimatingResponse ? null : (
             <div className="ai-warning">
                 <FontAwesomeIcon icon="exclamation-circle" size="sm" style={{ paddingRight: '5px' }} />
                 AI generated responses can contain inaccurate or misleading content.
             </div>
-        ) : null;
+        );
 
     heading = (headingText: string) => (
         <div className="summary-heading">
             <label className="summary-text">{headingText}</label>
-            {this.loading ? (
+            {this._gptProcessing ? (
                 <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} />
             ) : (
                 <>
-                    {(this.mode === GPTPopupMode.SORT || this.mode === GPTPopupMode.QUIZ) && (
-                        <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="back" icon={<CgCornerUpLeft size="16px" />} onClick={() => (this.mode = GPTPopupMode.CARD)} style={{ right: '50px', position: 'absolute' }} />
+                    {(this._mode === GPTPopupMode.SORT || this._mode === GPTPopupMode.QUIZ) && (
+                        <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="back" icon={<CgCornerUpLeft size="16px" />} onClick={() => (this._mode = GPTPopupMode.CARD)} style={{ right: '50px', position: 'absolute' }} />
                     )}
                     <Toggle
                         tooltip="Clear Chat filter"
                         toggleType={ToggleType.BUTTON}
                         type={Type.PRIM}
-                        toggleStatus={Doc.hasDocFilter(this.collectionDoc, 'tags', '#chat')}
-                        text={Doc.hasDocFilter(this.collectionDoc, 'tags', '#chat') ? 'filtered' : ''}
+                        toggleStatus={Doc.hasDocFilter(this._collectionContext, 'tags', '#chat')}
+                        text={Doc.hasDocFilter(this._collectionContext, 'tags', '#chat') ? 'filtered' : ''}
                         color="red"
-                        onClick={() => this.collectionDoc && Doc.setDocFilter(this.collectionDoc, 'tags', '#chat', 'remove')}
-                    />
-                    <IconButton
-                        color={StrCast(SettingsManager.userVariantColor)}
-                        tooltip="close"
-                        icon={<CgClose size="16px" />}
-                        onClick={() => {
-                            this.setVisible(false);
-                        }}
+                        onClick={() => this._collectionContext && Doc.setDocFilter(this._collectionContext, 'tags', '#chat', 'remove')}
                     />
+                    <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="close" icon={<CgClose size="16px" />} onClick={() => this.setVisible(false)} />
                 </>
             )}
         </div>
     );
 
     render() {
-        let content;
-
-        switch (this.mode) {
-            case GPTPopupMode.SUMMARY:
-                content = this.summaryBox();
-                break;
-            case GPTPopupMode.DATA:
-                content = this.dataAnalysisBox();
-                break;
-            case GPTPopupMode.IMAGE:
-                content = this.imageBox();
-                break;
-            case GPTPopupMode.SORT:
-            case GPTPopupMode.CARD:
-            case GPTPopupMode.QUIZ:
-                content = this.sortBox(this.mode === GPTPopupMode.SORT);
-                break;
-            default:
-                content = null;
-        }
-
         return (
-            <div className="summary-box" style={{ display: this.Visible ? 'flex' : 'none' }}>
-                {content}
+            <div className="gptPopup-summary-box" style={{ display: this.Visible ? 'flex' : 'none' }}>
+                {(() => {
+                    //prettier-ignore
+                    switch (this._mode) {
+                        case GPTPopupMode.SORT:
+                        case GPTPopupMode.CARD:
+                        case GPTPopupMode.QUIZ:    return this.sortBox(this._mode === GPTPopupMode.SORT);
+                        case GPTPopupMode.SUMMARY: return this.summaryBox();
+                        case GPTPopupMode.DATA:    return this.dataAnalysisBox();
+                        case GPTPopupMode.IMAGE:   return this.imageBox();
+                        default:                   return null;
+                }
+                })()}
                 <div className="resize-handle" />
             </div>
         );
-- 
cgit v1.2.3-70-g09d2

