diff options
| author | bobzel <zzzman@gmail.com> | 2025-03-06 16:17:47 -0500 |
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2025-03-06 16:17:47 -0500 |
| commit | 5ad858090f3006631062877d90120e3cc505fada (patch) | |
| tree | 9f87a8e1e7098a1025f6f4aac332dbc854db5be3 /src/client/views/collections/CollectionCardDeckView.tsx | |
| parent | 9c2a7c14fd9d0e44609aab30c6323583162009db (diff) | |
| parent | adaa107aac8558fa6f46e6ba1263c650c212d506 (diff) | |
Merge branch 'master' into aarav_edit
Diffstat (limited to 'src/client/views/collections/CollectionCardDeckView.tsx')
| -rw-r--r-- | src/client/views/collections/CollectionCardDeckView.tsx | 180 |
1 files changed, 29 insertions, 151 deletions
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx index 43464e50c..756b37f99 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 } 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 initializes callbacks - */ - childPairStringListAndUpdateSortDesc = () => - this.childPairStringList().then(sortDesc => { - GPTPopup.Instance.setSortDesc(sortDesc.join()); - GPTPopup.Instance.onSortComplete = this.processGptOutput; - GPTPopup.Instance.onQuizRandom = this.quizMode; - }); - componentDidMount() { - this._disposers.chatVis = reaction( - () => GPTPopup.Instance.Visible, - vis => !vis && this.onGptHide() - ); - GPTPopup.Instance.setRegenerateCallback(this.Document, this.childPairStringListAndUpdateSortDesc); 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,12 +91,7 @@ export class CollectionCardView extends CollectionSubView() { ); } - onGptHide = () => Doc.setDocFilter(this.Document, 'tags', '#chat', 'remove'); componentWillUnmount() { - GPTPopup.Instance.setSortDesc(''); - GPTPopup.Instance.onSortComplete = undefined; - GPTPopup.Instance.onQuizRandom = undefined; - GPTPopup.Instance.setRegenerateCallback(undefined, null); Object.keys(this._disposers).forEach(key => this._disposers[key]?.()); this._dropDisposer?.(); } @@ -130,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% @@ -142,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); } @@ -150,8 +126,8 @@ 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); - return (this.childPanelWidth() * length) / this._props.PanelWidth(); + const length = Math.min(this.childDocsNoInk.length, this._maxRowCount); + return (this.childPanelWidth() * length) / (this._props.PanelWidth() - 2 * this.xMargin); } @computed get nativeScaling() { @@ -162,23 +138,20 @@ export class CollectionCardView extends CollectionSubView() { return NumCast(this.layoutDoc._xMargin, Math.max(3, 0.05 * this._props.PanelWidth())); } + @computed get yMargin() { + return this._props.yPadding || NumCast(this.layoutDoc._yMargin, Math.min(5, 0.05 * this._props.PanelWidth())); + } + @computed get cardDeckWidth() { return this._props.PanelWidth() - 2 * this.xMargin; } - /** - * When in quiz mode, randomly selects a document - */ - quizMode = () => { - 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(); @@ -322,10 +295,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; } @@ -389,103 +362,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 - */ - processGptOutput = (gptOutput: string, questionType: string, tag?: string) => - undoable(() => { - // Split the string into individual list items - const listItems = gptOutput.split('======').filter(item => item.trim() !== ''); - - if (questionType === '2' || questionType === '4') { - this.childDocs.forEach(d => { - TagItem.removeTagFromDoc(d, '#chat'); - }); - } - - if (questionType === '6') { - 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 '6': - doc.chatIndex = index; - break; - case '1': - 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 '2': - case '4': - 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; @@ -531,8 +407,8 @@ export class CollectionCardView extends CollectionSubView() { setupMoveUpEvents( this, e, - (emove: PointerEvent, down: number[], delta: number[]) => { - this.layoutDoc._cardWidth = Math.max(10, delta[0] < 0 ? Math.floor(this.cardWidth + delta[0]) : Math.ceil(this.cardWidth + delta[0])); + (emove: PointerEvent) => { + this.layoutDoc._cardWidth = Math.max(10, this.ScreenToLocalBoxXf().transformPoint(emove.clientX, 0)[0] - this.xMargin); return false; }, action(() => { @@ -625,7 +501,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" @@ -637,6 +513,8 @@ export class CollectionCardView 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, + paddingLeft: this.xMargin, + paddingRight: this.xMargin, }}> <div className="collectionCardView-inner" @@ -644,6 +522,7 @@ export class CollectionCardView extends CollectionSubView() { transform: `scale(${1 / fitContentScale})`, height: `${100 * fitContentScale}%`, width: `${100 * fitContentScale}%`, + top: this.yMargin, }}> <div className="collectionCardView-cardwrapper" @@ -657,15 +536,14 @@ 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})`, - }}> - {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)} - </div> + }}></div> </div> + {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)} <div className="collectionCardView-cardSizeDragger" onPointerDown={this.cardSizerDown} |
