From c079b7aa916d7eb35ce186806ca312c84e910cb2 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 14 Feb 2025 14:07:18 -0500
Subject: major restructure of GPTPopup so that it works with all collections.

---
 .../views/collections/CollectionCardDeckView.tsx   | 157 +++------------------
 1 file changed, 17 insertions(+), 140 deletions(-)

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

diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index e00aa65d7..d7f4251f3 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -1,22 +1,22 @@
-import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import * as CSS from 'csstype';
+import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
 import * as React from 'react';
-import * as CSS from 'csstype';
-import { ClientUtils, imageUrlToBase64, returnFalse, returnNever, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
+import { ClientUtils, returnFalse, returnNever, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
-import { Animation, DocData } from '../../../fields/DocSymbols';
+import { Animation } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
-import { URLField } from '../../../fields/URLField';
-import { gptImageLabel, GPTTypeStyle } from '../../apis/gpt/GPT';
+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 { dropActionType } from '../../util/DropActionTypes';
+import { SettingsManager } from '../../util/SettingsManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { undoable, UndoManager } from '../../util/UndoManager';
@@ -25,11 +25,8 @@ import { StyleProp } from '../StyleProp';
 import { TagItem } from '../TagsView';
 import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
 import { FocusViewOptions } from '../nodes/FocusViewOptions';
-import { GPTPopup } from '../pdf/GPTPopup/GPTPopup';
 import './CollectionCardDeckView.scss';
-import { CollectionSubView, docSortings, SubCollectionViewProps } from './CollectionSubView';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { SettingsManager } from '../../util/SettingsManager';
+import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 
 /**
  * New view type specifically for studying more dynamically. Allows you to reorder docs however you see fit, easily
@@ -42,7 +39,6 @@ import { SettingsManager } from '../../util/SettingsManager';
 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 _setCurDocScript = () => ScriptField.MakeScript('scriptContext.layoutDoc._card_curDoc=this', { scriptContext: 'any' })!;
@@ -74,22 +70,7 @@ export class CollectionCardView extends CollectionSubView() {
         return Math.ceil(this.cardDeckWidth / this.cardWidth);
     }
 
-    /**
-     * update's gpt's doc-text list and initialize callbacks to respond to GPT output
-     */
-    updateDocumentDescriptions = () =>
-        this.childPairStringList().then(docDescriptions => {
-            GPTPopup.Instance.setDocumentDescriptions(docDescriptions.join());
-            GPTPopup.Instance.onGptResponse = this.processGptResponse;
-            GPTPopup.Instance.onQuizRandom = this.randomlyChooseDoc;
-        });
-
     componentDidMount() {
-        this._disposers.chatVis = reaction(
-            () => GPTPopup.Instance.Visible,
-            vis => !vis && this.onGptHide()
-        );
-        GPTPopup.Instance.setRetrieveDocDescriptionsCallback(this.Document, this.updateDocumentDescriptions);
         this._props.setContentViewBox?.(this);
         // if card deck moves, then the child doc views are hidden so their screen to local transforms will return empty rectangles
         // when inquired from the dom (below in childScreenToLocal).  When the doc is actually rendered, we need to act like the
@@ -110,10 +91,7 @@ export class CollectionCardView extends CollectionSubView() {
         );
     }
 
-    onGptHide = () => Doc.setDocFilter(this.Document, 'tags', '#chat', 'remove');
     componentWillUnmount() {
-        GPTPopup.Instance.onGptResponse = undefined;
-        GPTPopup.Instance.onQuizRandom = undefined;
         Object.keys(this._disposers).forEach(key => this._disposers[key]?.());
         this._dropDisposer?.();
     }
@@ -128,7 +106,7 @@ export class CollectionCardView extends CollectionSubView() {
      * 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);
+        return NumCast(this.layoutDoc.card_arch, 90) * (Math.PI / 180) * (this.childDocsNoInk.length < this._maxRowCount ? this.childDocsNoInk.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%
@@ -140,7 +118,7 @@ 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 childCards() {
+    @computed get childDocsNoInk() {
         return this.childLayoutPairs.filter(pair => !pair.layout.layout_isSvg);
     }
 
@@ -148,7 +126,7 @@ export class CollectionCardView extends CollectionSubView() {
      * how much to scale down the contents of the view so that everything will fit
      */
     @computed get fitContentScale() {
-        const length = Math.min(this.childCards.length, this._maxRowCount);
+        const length = Math.min(this.childDocsNoInk.length, this._maxRowCount);
         return (this.childPanelWidth() * length) / this._props.PanelWidth();
     }
 
@@ -164,17 +142,12 @@ export class CollectionCardView extends CollectionSubView() {
         return this._props.PanelWidth() - 2 * this.xMargin;
     }
 
-    /**
-     * When in quiz mode, randomly selects a document
-     */
-    randomlyChooseDoc = () => (this.layoutDoc._card_curDoc = this.childDocs[Math.floor(Math.random() * this.childDocs.length)]);
-
     setHoveredNodeIndex = action((index: number) => {
         if (!SnappingManager.IsDragging) this._hoveredNodeIndex = index;
     });
 
     isSelected = (doc: Doc) => this._docRefs.get(doc)?.IsSelected;
-    childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, Math.max(150, this._props.PanelWidth() / (this.childCards.length > this._maxRowCount ? this._maxRowCount : this.childCards.length) / this.nativeScaling));
+    childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, Math.max(150, this._props.PanelWidth() / (this.childDocsNoInk.length > this._maxRowCount ? this._maxRowCount : this.childDocsNoInk.length) / this.nativeScaling));
     childPanelHeight = () => this._props.PanelHeight() * this.fitContentScale;
     onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
     isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this.isAnyChildContentActive();
@@ -318,10 +291,10 @@ export class CollectionCardView extends CollectionSubView() {
      * @returns number of cards in row that contains index
      */
     cardsInRowThatIncludesCardIndex = (index: number) => {
-        if (this.childCards.length < this._maxRowCount) {
-            return this.childCards.length;
+        if (this.childDocsNoInk.length < this._maxRowCount) {
+            return this.childDocsNoInk.length;
         }
-        const totalCards = this.childCards.length;
+        const totalCards = this.childDocsNoInk.length;
         if (index < totalCards - (totalCards % this._maxRowCount)) {
             return this._maxRowCount;
         }
@@ -385,102 +358,6 @@ export class CollectionCardView extends CollectionSubView() {
             : this.translateY(index);
     };
 
-    /**
-     * A list of the text content of all the child docs. RTF documents will have just their text and pdf documents will have the first 50 words.
-     * Image documents are converted to bse64 and gpt generates a description for them. all other documents use their title. This string is
-     * inputted into the gpt prompt to sort everything together
-     * @returns
-     */
-    childPairStringList = () => {
-        const docToText = (doc: Doc) => {
-            switch (doc.type) {
-                case DocumentType.PDF:  return StrCast(doc.text).split(/\s+/).slice(0, 50).join(' '); // first 50 words of pdf text
-                case DocumentType.IMG:  return this.getImageDesc(doc);
-                case DocumentType.RTF:  return StrCast(RTFCast(doc.text).Text);
-                default:                return StrCast(doc.title);
-            } // prettier-ignore
-        };
-        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);
-    };
-
-    /**
-     * Calls the gpt API to generate descriptions for the images in the view
-     * @param image
-     * @returns
-     */
-    getImageDesc = async (image: Doc) => {
-        if (StrCast(image.description)) return StrCast(image.description); // Return existing description
-        const { href } = (image.data as URLField).url;
-        const hrefParts = href.split('.');
-        const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
-        try {
-            const hrefBase64 = await imageUrlToBase64(hrefComplete);
-            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) {
-            console.log(error);
-        }
-        return '';
-    };
-
-    /**
-     * Processes gpt's output depending on the type of question the user asked. Converts gpt's string output to
-     * usable code
-     * @param gptOutput
-     * @param questionType
-     * @param tag
-     */
-    processGptResponse = (gptOutput: string, questionType: GPTTypeStyle, tag?: string) =>
-        undoable(() => {
-            // Split the string into individual list items
-            const listItems = gptOutput.split('======').filter(item => item.trim() !== '');
-
-            if (questionType === GPTTypeStyle.Filter) {
-                this.childDocs.forEach(d => {
-                    TagItem.removeTagFromDoc(d, '#chat');
-                });
-            }
-
-            if (questionType === GPTTypeStyle.SortDocs) {
-                this.Document[this._props.fieldKey + '_sort'] = docSortings.Chat;
-            }
-
-            listItems.forEach((item, index) => {
-                const normalizedItem = item.trim();
-                // find the corresponding Doc in the textToDoc map
-                const doc = this._textToDoc.get(normalizedItem);
-                if (doc) {
-                    switch (questionType) {
-                        case GPTTypeStyle.SortDocs:
-                            doc.chatIndex = index;
-                            break;
-                        case GPTTypeStyle.AssignTags:
-                            if (tag) {
-                                const hashTag = tag.startsWith('#') ? tag : '#' + tag[0].toLowerCase() + tag.slice(1);
-                                const filterTag = Doc.MyFilterHotKeys.map(key => StrCast(key.toolType)).find(key => key.includes(tag)) ?? hashTag;
-                                TagItem.addTagToDoc(doc, filterTag);
-                            }
-                            break;
-                        case GPTTypeStyle.Filter:
-                            TagItem.addTagToDoc(doc, '#chat');
-                            Doc.setDocFilter(this.Document, 'tags', '#chat', 'check');
-                            break;
-                    }
-                } else {
-                    console.warn(`No matching document found for item: ${normalizedItem}`);
-                }
-            });
-        }, '')();
-
     childScreenToLocal = computedFn((doc: Doc, index: number, isSelected: boolean) => () => {
         // need to explicitly trigger an invalidation since we're reading everything from the Dom
         this._forceChildXf;
@@ -620,7 +497,7 @@ export class CollectionCardView extends CollectionSubView() {
     curDoc = () => DocCast(this.layoutDoc._card_curDoc);
 
     render() {
-        const fitContentScale = this.childCards.length === 0 ? 1 : this.fitContentScale;
+        const fitContentScale = this.childDocsNoInk.length === 0 ? 1 : this.fitContentScale;
         return (
             <div
                 className="collectionCardView-outer"
@@ -652,7 +529,7 @@ export class CollectionCardView extends CollectionSubView() {
                     <div
                         className="collectionCardView-flashcardUI"
                         style={{
-                            pointerEvents: this.childCards.length === 0 ? undefined : 'none',
+                            pointerEvents: this.childDocsNoInk.length === 0 ? undefined : 'none',
                             height: `${100 / this.nativeScaling / fitContentScale}%`,
                             width: `${100 / this.nativeScaling / fitContentScale}%`,
                             transform: `scale(${this.nativeScaling * fitContentScale})`,
-- 
cgit v1.2.3-70-g09d2

