aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-rw-r--r--src/client/views/TagsView.tsx2
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.tsx33
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx58
-rw-r--r--src/client/views/collections/CollectionSubView.tsx2
-rw-r--r--src/client/views/collections/CollectionView.tsx6
-rw-r--r--src/client/views/collections/FlashcardPracticeUI.tsx36
-rw-r--r--src/client/views/nodes/ComparisonBox.scss3
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx620
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx3
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx2
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx34
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;
+ })
+ );
};
/**