aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/ComparisonBox.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/ComparisonBox.tsx')
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx620
1 files changed, 304 insertions, 316 deletions
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>
);
}