diff options
| author | bobzel <zzzman@gmail.com> | 2024-10-11 22:01:50 -0400 |
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2024-10-11 22:01:50 -0400 |
| commit | 737efc3e6cada537536c7bc5e46f5b57970da276 (patch) | |
| tree | 2be4f30196f829fea4c9579875bef4e0fe5fa393 /src/client/views/nodes/ComparisonBox.tsx | |
| parent | bb8fe2933154c6db70cfe5da1e890535bc9012d4 (diff) | |
Lots of cleanup in comparisonBox to get rid of duplicated code and siimplify long functions. Utility functions were added to ComparisonBox to simplify menu functions, and to FormattedTextBox to simplify creating centered text views.
Diffstat (limited to 'src/client/views/nodes/ComparisonBox.tsx')
| -rw-r--r-- | src/client/views/nodes/ComparisonBox.tsx | 526 |
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 } } |
