diff options
Diffstat (limited to 'src/client/views')
-rw-r--r-- | src/client/views/TagsView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/CollectionCarousel3DView.tsx | 33 | ||||
-rw-r--r-- | src/client/views/collections/CollectionCarouselView.tsx | 58 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSubView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/CollectionView.tsx | 6 | ||||
-rw-r--r-- | src/client/views/collections/FlashcardPracticeUI.tsx | 36 | ||||
-rw-r--r-- | src/client/views/nodes/ComparisonBox.scss | 3 | ||||
-rw-r--r-- | src/client/views/nodes/ComparisonBox.tsx | 620 | ||||
-rw-r--r-- | src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx | 3 | ||||
-rw-r--r-- | src/client/views/nodes/KeyValueBox.tsx | 2 | ||||
-rw-r--r-- | src/client/views/pdf/AnchorMenu.tsx | 34 |
11 files changed, 394 insertions, 405 deletions
diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx index 072cae3af..2615bc5fb 100644 --- a/src/client/views/TagsView.tsx +++ b/src/client/views/TagsView.tsx @@ -361,7 +361,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> { display: SnappingManager.IsResizing === this.View.Document[Id] ? 'none' : undefined, transformOrigin: 'top left', maxWidth: `${100 * this.currentScale}%`, - width: 'max-content', + width: `${100 * this.currentScale}%`, transform: `scale(${1 / this.currentScale})`, backgroundColor: this.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT, borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT, 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 0582bc996..38ce5f2f7 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -9,7 +9,6 @@ import { imageUrlToBase64, returnFalse, returnNone, returnTrue, returnZero, setu import { emptyFunction } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; -import { Id } from '../../../fields/FieldSymbols'; import { RichTextField } from '../../../fields/RichTextField'; import { BoolCast, DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types'; import { nullAudio } from '../../../fields/URLField'; @@ -26,28 +25,31 @@ import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { PinDocView, PinProps } from '../PinFuncs'; import { StyleProp } from '../StyleProp'; import { flashcardRevealOp, practiceMode } from '../collections/FlashcardPracticeUI'; +import { CollectionFreeFormView } from '../collections/collectionFreeForm'; import '../pdf/GPTPopup/GPTPopup.scss'; import './ComparisonBox.scss'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; -import { CollectionFreeFormView } from '../collections/collectionFreeForm'; const API_URL = 'https://api.unsplash.com/search/photos'; /** - * This view serves two 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) + * 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 either case, the two docs are stored in the <fieldKey>_front and <fieldKey>_back fields + * 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. * - * 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. + * 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. * */ @@ -56,6 +58,59 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ComparisonBox, fieldKey); } + /** + * Creates a flashcard (or fills in flashcard data to a specified Doc) from a control string containing a question and answer + * @param tuple string containing Question:, Answer: and optionally a Keyword: + * @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]; + 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) => { + 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(); + } + + /** + * Create a carousel of flashcards from a GPT response string where questions and answers are given in a format loosely defined by: + * Question: ... Answer: ... Keyword: ... + * Note that Keyword or Answer may not be present, or their orders may be reversed. + */ + public static createFlashcardDeck(text: string, width: number, height: number, front: string, back: string) { + return Promise.all( + text + .split(ComparisonBox.qtoken) + .filter(t => t) + .map(tuple => ComparisonBox.createFlashcard(tuple, front, back)) + ).then(docs => { + return Docs.Create.CarouselDocument(docs, { + title: 'flashcard deck', + _width: width, + _height: height, + _layout_fitWidth: false, + _layout_autoHeight: true, + _xMargin: 5, + _yMargin: 5, + }); + }); + } private SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; static qtoken = 'Question: '; @@ -65,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; @@ -84,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) => { @@ -118,16 +182,20 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() return undefined; }, 'internal drop'); + @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 + @computed get frontText() { return RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text; } // prettier-ignore + @computed get backText() { return RTFCast(DocCast(this.dataDoc[this.backKey]).text)?.Text; } // prettier-ignore @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 @@ -139,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" /> @@ -169,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)}> @@ -177,9 +245,19 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() </div> </Tooltip> )} - {!this._props.isSelected() || this._renderSide === this.backKey || CollectionFreeFormView.from(this.DocumentView?.()) ? null : ( + {!this._props.isSelected() || this._renderSide === this.backKey || !CollectionFreeFormView.from(this.DocumentView?.()) || (this.dataDoc[this.backKey] && !DocCast(this.dataDoc[this.backKey])?.text_placeholder) ? null : ( <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}> - <div className="comparisonBox-button" onClick={() => this.askGPT(GPTCallType.STACK).then(this.createFlashcardDeck)}> + <div + className="comparisonBox-button" + onClick={() => + this.askGPT(GPTCallType.STACK).then(async text => { + const newCol = await ComparisonBox.createFlashcardDeck(text, NumCast(this.layoutDoc._width, 250) + 50, NumCast(this.layoutDoc._height, 200), this.frontKey, this.backKey); + newCol.x = NumCast(this.layoutDoc.x); + newCol.y = NumCast(this.layoutDoc.y); + this._props.DocumentView?.()._props.addDocument?.(newCol); + this._props.removeDocument?.(this.Document); + }) + }> <FontAwesomeIcon icon="layer-group" size="xl" /> </div> </Tooltip> @@ -232,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; } @@ -270,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) { @@ -287,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)) ); } }; @@ -397,124 +487,51 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() axios .post( 'http://localhost:105/youtube/', // - { file: this.getYouTubeVideoId(StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text)) }, + { file: this.getYouTubeVideoId(this.frontText) }, { headers: { 'Content-Type': 'application/json' } } ) .then(response => response.data.transcription); /** - * Creates a flashcard (or fills in flashcard data to a specified Doc) from a control string containing a question and answer - * @param tuple string containing Question:, Answer: and optionally a Keyword: - * @param useDoc doc to fill in instead of creating a Doc - * @returns the resulting flashcard Doc - */ - 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.toLowerCase() !== 'none' ? this.fetchImages(keyword).then(img => fillInFlashcard(img)) : fillInFlashcard(); - }; - - /** - * Create a carousel of flashcards from a GPT response string where questions and answers are given in a format loosely defined by: - * Question: ... Answer: ... Keyword: ... - * Note that Keyword or Answer may not be present, or their orders may be reversed. - */ - createFlashcardDeck = (text: string) => { - Promise.all( - text - .split(ComparisonBox.qtoken) - .filter(t => t) - .map(tuple => this.createFlashcard(tuple)) - ).then(docs => { - const newCol = Docs.Create.CarouselDocument(docs, { - _width: NumCast(this.layoutDoc._width, 250) + 50, - _height: NumCast(this.layoutDoc._height, 200) + 50, - _layout_fitWidth: false, - _layout_autoHeight: true, - _xMargin: 5, - _yMargin: 5, - x: NumCast(this.layoutDoc.x), - y: NumCast(this.layoutDoc.y), - }); - - this._props.DocumentView?.()._props.addDocument?.(newCol); - this._props.removeDocument?.(this.Document); - }); - }; - - /** * Calls GPT for each flashcard type. */ askGPT = async (callType: GPTCallType) => { - const frontText = RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text; - const backText = RTFCast(DocCast(this.dataDoc[this.backKey]).text)?.Text; - const questionText = 'Question: ' + frontText; - const queryText = questionText + (callType == GPTCallType.QUIZ ? ' UserAnswer: ' + this._inputValue + '. ' + ' Rubric: ' + backText : ''); + const questionText = 'Question: ' + this.frontText; + const queryText = questionText + (callType == GPTCallType.QUIZ ? ' UserAnswer: ' + this._inputValue + '. ' + ' Rubric: ' + this.backText : ''); + this.loading = true; - let res = ''; - - if (callType !== GPTCallType.CHATCARD || frontText) { - try { - res = await gptAPICall(queryText, callType); - if (!res) { - console.error('GPT call failed'); - } else - switch (callType) { - case GPTCallType.CHATCARD: - DocCast(this.dataDoc[this.backKey])[DocData].text = res; - break; - case GPTCallType.QUIZ: - runInAction(() => { - this._renderSide = this.backKey; - this._outputValue = res.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric'); - }); - break; - case GPTCallType.FLASHCARD: - default: - } - } catch (err) { - console.error('GPT call failed', err); - } - } + const res = !this.frontText + ? '' + : await gptAPICall(queryText, callType).then( + action(resp => { + switch (resp && callType) { + case GPTCallType.CHATCARD: + DocCast(this.dataDoc[this.backKey])[DocData].text = resp; + break; + case GPTCallType.QUIZ: + this._renderSide = this.backKey; + this._outputValue = resp.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric'); + break; + case GPTCallType.FLASHCARD: + default: + } + return resp; + }) + ); this.loading = false; + if (!res) console.error('GPT call failed'); return res; }; layoutWidth = () => NumCast(this.layoutDoc.width, 200); layoutHeight = () => NumCast(this.layoutDoc.height, 200); - findImageTags = async () => { - const c = this.DocumentView?.().ContentDiv?.getElementsByTagName('img'); - if (c?.length === 0) this.askGPT(GPTCallType.CHATCARD); - if (c) { - this.loading = true; - for (const i of c) { - if (i.className !== 'ProseMirror-separator') this.getImageDesc(i.src); - } - this.loading = false; - } - }; - /** * Ask GPT for advice on how to improve speech by comparing the phonetic transcription of * a users audio recording with the phonetic transcription of their intended sentence. * @param phonemes */ askGPTPhonemes = async (phonemes: string) => { - const sentence = StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text); + const sentence = this.frontText; const phon6 = 'huː ɑɹ juː tədeɪ'; const phon4 = 'kamo estas hɔi'; const promptEng = @@ -567,24 +584,20 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() * @param selection * @returns Image Document */ - fetchImages = async (selection: string) => { + public static async fetchImages(selection: string) { try { const { data } = await axios.get(`${API_URL}?query=${selection}&page=1&per_page=${1}&client_id=Q4zruu6k6lum2kExiGhLNBJIgXDxD6NNj0SRHH_XXU0`); 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-', + title: selection, }); return imageSnapshot; } catch (error) { console.log(error); } - }; + } getImageDesc = async (u: string) => { try { @@ -597,22 +610,10 @@ 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 ?? []; - if (this.Document._layout_isFlashcard) { - appearanceItems.push({ description: 'Create ChatCard', event: () => this.askGPT(GPTCallType.CHATCARD), icon: 'id-card' }); - } + appearanceItems.push({ description: 'Create ChatCard', event: () => this.askGPT(GPTCallType.CHATCARD), icon: 'id-card' }); !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); }; @@ -641,179 +642,166 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() } return layoutTemplateString; }; - textCreator = (title: string, text: string, img?: Doc) => { - const newDoc = Docs.Create.TextDocument(RichTextField.textToRtf(text, img?.[Id]), { - title, // - _layout_autoHeight: true, - _layout_centered: true, - text_align: 'center', - _layout_fitWidth: true, - }); - return newDoc; - }; 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>}> - <div - ref={this._closeRef} - className={`clear-button ${which}`} - onPointerDown={e => this.closeDown(e, which)} // prevent triggering slider movement in registerSliding - > - <FontAwesomeIcon className={`clear-button ${which}`} icon="times" size="xs" /> - </div> - </Tooltip> - ); - const displayDoc = (whichSlot: string) => { - const whichDoc = DocCast(this.dataDoc[whichSlot]); - const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc); - const layoutString = targetDoc ? '' : this.testForTextFields(whichSlot); - - return targetDoc || layoutString ? ( - <> - <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} - ScreenToLocalTransform={this.contentScreenToLocalXf} - isContentActive={this.childActiveFunc} - isDocumentActive={returnFalse} - dontSelect={returnTrue} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - styleProvider={this._childActive ? this._props.styleProvider : this.docStyleProvider} - hideLinkButton - pointerEvents={this._childActive ? undefined : returnNone} - /> - {!this.isFlashcard ? clearButton(whichSlot) : null} - </> - ) : ( - <div className="placeholder"> - <FontAwesomeIcon className="upload-icon" icon="cloud-upload-alt" size="lg" /> - </div> - ); - }; - const displayBox = (which: string, cover: number) => ( + + clearButton = (which: string) => ( + <Tooltip title={<div className="dash-tooltip">remove</div>}> <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 ? displayDoc(which) : null} + ref={this._closeRef} + className={`clear-button ${which}`} + onPointerDown={e => this.closeDown(e, which)} // prevent triggering slider movement in registerSliding + > + <FontAwesomeIcon className={`clear-button ${which}`} icon="times" size="xs" /> + </div> + </Tooltip> + ); + displayDoc = (whichSlot: string) => { + const whichDoc = DocCast(this.dataDoc[whichSlot]); + const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc); + const layoutString = targetDoc ? '' : this.testForTextFields(whichSlot); + + return targetDoc || layoutString ? ( + <> + <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} + ScreenToLocalTransform={this.contentScreenToLocalXf} + isContentActive={this.childActiveFunc} + isDocumentActive={returnFalse} + dontSelect={returnTrue} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + styleProvider={this._childActive ? this._props.styleProvider : this.docStyleProvider} + hideLinkButton + pointerEvents={this._childActive ? undefined : returnNone} + /> + {!this.isFlashcard ? this.clearButton(whichSlot) : null} + </> + ) : ( + <div className="placeholder"> + <FontAwesomeIcon className="upload-icon" icon="cloud-upload-alt" size="lg" /> </div> ); + }; - if (this.isFlashcard) { - 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 (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' : ''}`}> - <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"> - <textarea - value={this._renderSide === this.backKey ? this._outputValue : this._inputValue} - onChange={action(e => { - this._inputValue = e.target.value; - })} - placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''} - readOnly={this._renderSide === this.backKey} - /> - {!this.loading ? null : ( - <div className="loading-spinner"> - <ReactLoading type="spin" height={30} width={30} color='blue' /> - </div> - )} - </div> - <div> - <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' : '' }}> - {<FontAwesomeIcon icon="microphone" size="lg" />} - </button> - <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}> - Evaluate Pronunciation - </button> - <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> + 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)} ref={ele => this.createDropTarget(ele, which)}> + {this.displayDoc(which)} + </div> + ); + + /* renders front(qustion) and back(answer) at the same time, then on user input replaces the answer with a GPT analysis of the answer */ + renderAsQuiz = (text: string) => ( + <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"> + <textarea + value={this._renderSide === this.backKey ? this._outputValue : this._inputValue} + onChange={action(e => { + this._inputValue = e.target.value; + })} + placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''} + readOnly={this._renderSide === this.backKey} + /> + {!this.loading ? null : ( + <div className="loading-spinner"> + <ReactLoading type="spin" height={30} width={30} color="blue" /> </div> - ); - } - - // render a normal flashcard when not a QuizCard - return ( - <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)}> - {displayBox(this._renderSide, this._props.PanelWidth() - 3)} - {this.loading ? ( - <div className="loading-spinner"> - <ReactLoading type="spin" height={30} width={30} color="blue" /> - </div> - ) : null} - {this.flashcardMenu} - </div> - ); - } - // render a comparison box that compares items side by side - return ( - <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}> - {displayBox(this.backKey, this._props.PanelWidth() - 3)} - <div className="clip-div" style={{ width: this.clipWidth + '%', transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}> - {displayBox(this.frontKey, 0)} + )} + </div> + <div> + <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' : '' }}> + {<FontAwesomeIcon icon="microphone" size="lg" />} + </button> + <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}> + Evaluate Pronunciation + </button> + <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> + </div> + </div> + ); + + // 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); + // } + // }; + + // render a button that flips between front and back + renderAsFlip = () => ( + <div + 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 slider that reveals front and back as slider is dragged horizonally + renderAsBeforeAfter = () => ( + <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)} + </div> - <div - className="slide-bar" - style={{ - left: `calc(${this.clipWidth + '%'} - 0.5px)`, - cursor: this.clipWidth < 5 ? 'e-resize' : this.clipWidth / 100 > (this._props.PanelWidth() - 5) / this._props.PanelWidth() ? 'w-resize' : undefined, - }} - onPointerDown={e => !this._isAnyChildContentActive && this.registerSliding(e, this._props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ - > - <div className="slide-handle" /> - </div> + <div + className="slide-bar" + style={{ + left: `calc(${this.clipWidth + '%'} - 0.5px)`, + cursor: this.clipWidth < 5 ? 'e-resize' : this.clipWidth / 100 > (this._props.PanelWidth() - 5) / this._props.PanelWidth() ? 'w-resize' : undefined, + }} + onPointerDown={e => !this._isAnyChildContentActive && this.registerSliding(e, this._props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ + > + <div className="slide-handle" /> + </div> + </div> + ); + + render() { + 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/DataVizBox/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx index 6c649bde3..16c016d6c 100644 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx @@ -231,7 +231,8 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> { this._disposers.lightbox = reaction( () => LightboxView.LightboxDoc(), doc => { - doc ? this._shouldDisplay && this.closeMenu() : !this._shouldDisplay && this.openMenu(); + // NOTE: bcz; commented this out because the doc creator would appear everytime I close out of the lightbox + // doc ? this._shouldDisplay && this.closeMenu() : !this._shouldDisplay && this.openMenu(); } ); //this._disposers.fields = reaction(() => this._dataViz?.axes, cols => this._selectedCols = cols?.map(col => { return {title: col, type: '', desc: ''}})) diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 3daacc9bb..40c687b7e 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -114,7 +114,7 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() { if (key) target[key] = script.originalScript; return false; } - field === undefined && (field = res.result instanceof Array ? new List<FieldType>(res.result) : (res.result as FieldType)); + field === undefined && (field = res.result instanceof Array ? new List<FieldType>(res.result) : (typeof res.result === 'function' ? res.result.name : res.result as FieldType)); } } if (!key) return false; diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index 7243473e0..5ab9b556c 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -10,7 +10,6 @@ import { emptyFunction, unimplementedFunction } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT'; -import { Docs } from '../../documents/Documents'; import { SettingsManager } from '../../util/SettingsManager'; import { undoBatch } from '../../util/UndoManager'; import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu'; @@ -19,6 +18,7 @@ import { DocumentView } from '../nodes/DocumentView'; import { DrawingOptions, SmartDrawHandler } from '../smartdraw/SmartDrawHandler'; import './AnchorMenu.scss'; import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup'; +import { ComparisonBox } from '../nodes/ComparisonBox'; @observer export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @@ -117,29 +117,15 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { */ transferToFlashcard = (text: string, x: number, y: number) => { - // put each question generated by GPT on the front of the flashcard - const senArr = text.trim().split('Question:'); - const 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]; - - 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 = x; - newCol.y = y; - newCol.zIndex = 1000; - - this.addToCollection?.(newCol); - this._loading = false; + ComparisonBox.createFlashcardDeck(text, 250, 200, 'data_front', 'data_back').then( + action(newCol => { + newCol.x = x; + newCol.y = y; + newCol.zIndex = 1000; + this.addToCollection?.(newCol); + this._loading = false; + }) + ); }; /** |