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.tsx526
1 files changed, 252 insertions, 274 deletions
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 0582bc996..80ef126dc 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,21 +25,22 @@ 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
+ * This view serves three 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)
+ * 3) it provides a quiz view that displays a question and a user answer that can be "scored" by GPT
*
- * In either case, the two docs are stored in the <fieldKey>_front and <fieldKey>_back fields
+ * In each case, the two docs are stored in the <fieldKey>_front and <fieldKey>_back fields
*
* 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.
@@ -49,6 +49,9 @@ const API_URL = 'https://api.unsplash.com/search/photos';
* 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.
*
+ * Quiz mode is activated when the parent collection has its 'quiz' field set when it renders a flashcard.
+ * NOTE: this should probably be changed to passing down a prop to the flashcard telling it to render as a quiz.
+ *
*/
@observer
@@ -56,6 +59,54 @@ 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 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][frontKey] = FormattedTextBox.centeredTextCreator('question', question, img);
+ newDoc[DocData][backKey] = FormattedTextBox.centeredTextCreator('answer', answer);
+ return newDoc;
+ };
+ 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, {
+ _width: width,
+ _height: height,
+ _layout_fitWidth: false,
+ _layout_autoHeight: true,
+ _xMargin: 5,
+ _yMargin: 5,
+ });
+ });
+ }
private SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
static qtoken = 'Question: ';
@@ -118,9 +169,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
return undefined;
}, 'internal drop');
+ @computed get isQuizMode() { return DocCast(this.Document.embedContainer)?.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
@@ -177,9 +231,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>
@@ -397,124 +461,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 +558,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 {
@@ -610,9 +597,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
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,181 +626,174 @@ 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);
+ this.isFlashcard && this.activateContent();
+ }}
+ ref={ele => this.createDropTarget(ele, which)}>
+ {!this._isEmpty ? this.displayDoc(which) : null}
+ </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>
+ )}
+ </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>
+ </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);
+ } else {
+ // add text box to each side when comparison box is first created
+ if (!this.dataDoc[this.backKey] && !this._isEmpty) {
+ this.dataDoc[this.backKey] = FormattedTextBox.centeredTextCreator('answer', 'answer here', undefined, true);
}
- // 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>
- );
+ if (!this.dataDoc[this.frontKey] && !this._isEmpty) {
+ this.dataDoc[this.frontKey] = FormattedTextBox.centeredTextCreator('question', 'hint: Enter a topic, select this document and click the stack button to have GPT create a deck of cards', undefined, true);
+ }
}
- // 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
- 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" />
+ renderAsFlashcard = () => (
+ <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)}>
+ {this.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
+ renderAsBeforeAfter = () => (
+ <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}>
+ {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>
+ );
+
+ render() {
+ this.isFlashcard && this.addPlaceholdersForEmptyFlashcard();
+ return this.isFlashcard ?
+ this.isQuizMode ? this.renderAsQuiz(this.frontText) :
+ this.renderAsFlashcard() :
+ this.renderAsBeforeAfter(); // prettier-ignore
}
}