diff options
-rw-r--r-- | src/client/views/collections/FlashcardPracticeUI.tsx | 17 | ||||
-rw-r--r-- | src/client/views/nodes/ComparisonBox.tsx | 308 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 9 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 9 |
4 files changed, 170 insertions, 173 deletions
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx index 7697d308b..45e040653 100644 --- a/src/client/views/collections/FlashcardPracticeUI.tsx +++ b/src/client/views/collections/FlashcardPracticeUI.tsx @@ -1,19 +1,19 @@ +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; +import { IconButton, MultiToggle, Type } from 'browndash-components'; import { computed, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { returnFalse, returnZero, setupMoveUpEvents } from '../../../ClientUtils'; +import { emptyFunction } from '../../../Utils'; import { Doc, DocListCast } from '../../../fields/Doc'; import { BoolCast, NumCast, StrCast } from '../../../fields/Types'; +import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { DocumentView, DocumentViewProps } from '../nodes/DocumentView'; import './FlashcardPracticeUI.scss'; -import { IconButton, MultiToggle, Type } from 'browndash-components'; -import { SnappingManager } from '../../util/SnappingManager'; -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { emptyFunction } from '../../../Utils'; export enum practiceMode { PRACTICE = 'practice', @@ -24,6 +24,11 @@ enum practiceVal { CORRECT = 'correct', } +export enum flashcardRevealOp { + HOVER = 'hover', + FLIP = 'flip', +} + interface PracticeUIProps { fieldKey: string; layoutDoc: Doc; @@ -154,11 +159,11 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp text={StrCast(this._props.layoutDoc.revealOp)} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} - icon={<FontAwesomeIcon color={SnappingManager.userColor} icon={this._props.layoutDoc.revealOp === 'hover' ? 'hand-point-up' : 'question'} size="sm" />} + icon={<FontAwesomeIcon color={SnappingManager.userColor} icon={this._props.layoutDoc.revealOp === flashcardRevealOp.HOVER ? 'hand-point-up' : 'question'} size="sm" />} label={StrCast(this._props.layoutDoc.revealOp)} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => { - this._props.layoutDoc.revealOp = this._props.layoutDoc.revealOp === 'hover' ? 'flip' : 'hover'; + this._props.layoutDoc.revealOp = this._props.layoutDoc.revealOp === flashcardRevealOp.HOVER ? flashcardRevealOp.FLIP : flashcardRevealOp.HOVER; this._props.layoutDoc.childDocumentsActive = this._props.layoutDoc.revealOp === 'hover' ? true : undefined; }) } diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index b2a717c3c..a57090e99 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -11,7 +11,7 @@ import { Doc, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { RichTextField } from '../../../fields/RichTextField'; -import { DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types'; +import { BoolCast, DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types'; import { nullAudio } from '../../../fields/URLField'; import { GPTCallType, gptAPICall, gptImageLabel } from '../../apis/gpt/GPT'; import { DocUtils, FollowLinkScript } from '../../documents/DocUtils'; @@ -25,12 +25,12 @@ import { ContextMenu } from '../ContextMenu'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { PinDocView, PinProps } from '../PinFuncs'; import { StyleProp } from '../StyleProp'; +import { flashcardRevealOp, practiceMode } from '../collections/FlashcardPracticeUI'; import '../pdf/GPTPopup/GPTPopup.scss'; import './ComparisonBox.scss'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; -import { practiceMode } from '../collections/FlashcardPracticeUI'; const API_URL = 'https://api.unsplash.com/search/photos'; @@ -41,7 +41,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() } private SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; private _closeRef = React.createRef<HTMLDivElement>(); - private _disposers: (DragManager.DragDropDisposer | undefined)[] = [undefined, undefined]; + private _disposers: { [key: string]: DragManager.DragDropDisposer | undefined } = {}; private _reactDisposer: IReactionDisposer | undefined; constructor(props: FieldViewProps) { super(props); @@ -55,9 +55,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() @observable private _childActive = false; @observable private _animating = ''; @observable private _listening = false; - @observable private _frontSide = false; - @observable recognition = new this.SpeechRecognition(); + @observable private _renderSide = this.fieldKey; + @observable private _recognition = new this.SpeechRecognition(); + @computed get isFlashcard() { return BoolCast(this.Document.layout_isFlashcard); } // prettier-ignore + @computed get frontKey() { return this._props.fieldKey; } // prettier-ignore + @computed get backKey() { return this._props.fieldKey + '_back'; } // 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 @@ -65,6 +68,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() @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 loading() { return this._loading; } // prettier-ignore + set loading(value) { runInAction(() => { this._loading = value; })} // prettier-ignore @computed get overlayAlternateIcon() { return ( @@ -73,14 +78,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() className="comparisonBox-alternateButton ccomparisonBox-button" onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => { - if (!this.revealOp || this.revealOp === 'flip') { + if (!this.revealOp || this.revealOp === flashcardRevealOp.FLIP) { this.flipFlashcard(); } }) } style={{ - background: this.revealOp === 'hover' ? 'gray' : this._frontSide ? 'white' : 'black', - color: this.revealOp === 'hover' ? 'black' : this._frontSide ? 'black' : 'white', + background: this.revealOp === flashcardRevealOp.HOVER ? 'gray' : this._renderSide === this.backKey ? 'white' : 'black', + color: this.revealOp === flashcardRevealOp.HOVER ? 'black' : this._renderSide === this.backKey ? 'black' : 'white', display: 'inline-block', }}> <FontAwesomeIcon icon="turn-up" size="xl" /> @@ -106,18 +111,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() @computed get flashcardMenu() { return SnappingManager.HideDecorations ? null : ( <div className="comparisonBox-bottomMenu" style={{ transform: `scale(${this.uiBtnScaling})` }}> - {this.revealOp === 'hover' || !this._props.isSelected() ? null : this.overlayAlternateIcon} + {this.revealOp === flashcardRevealOp.HOVER || !this._props.isSelected() ? null : this.overlayAlternateIcon} {!this._props.isSelected() ? null : ( <> - {!this._frontSide ? null : ( + {this._renderSide === this.frontKey ? null : ( <Tooltip title={ - <div className="dash-tooltip">{ - !this._frontSide ? "Flip to front side to use GPT": - "Ask GPT to create an answer on the back side of the flashcard based on your question on the front"} - </div> // prettier-ignore + <div className="dash-tooltip">Ask GPT to create an answer for the question on the front</div> // prettier-ignore }> - <div className="comparisonBox-button" onPointerDown={() => (this._frontSide ? this.askGPT(GPTCallType.CHATCARD) : null)}> + <div className="comparisonBox-button" onPointerDown={() => this.askGPT(GPTCallType.CHATCARD)}> <FontAwesomeIcon icon="lightbulb" size="xl" /> </div> </Tooltip> @@ -143,17 +145,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() this._childActive = true; }; - @action handleRenderClick = () => { - this._frontSide = !this._frontSide; - }; - @action handleRenderGPTClick = () => { const phonTrans = DocCast(this.Document.audio) ? DocCast(this.Document.audio).phoneticTranscription : undefined; if (phonTrans) { this._inputValue = StrCast(phonTrans); this.askGPTPhonemes(this._inputValue); } else if (this._inputValue) this.askGPT(GPTCallType.QUIZ); - this._frontSide = false; + this._renderSide = this.backKey; this._outputValue = ''; }; @@ -169,7 +167,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() this._props.setContentViewBox?.(this); this._reactDisposer = reaction( () => this._props.isSelected(), // when this reaction should update - selected => !selected && (this._childActive = false) // what it should update to + selected => { + if (selected && this.isFlashcard) this.activateContent(); + !selected && (this._childActive = false); + }, // what it should update to + { fireImmediately: true } ); } @@ -177,10 +179,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() this._reactDisposer?.(); } - protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => { - this._disposers[disposerId]?.(); + protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string) => { + this._disposers[fieldKey]?.(); if (ele) { - this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.internalDrop(e, dropEvent, fieldKey), this.layoutDoc); + this._disposers[fieldKey] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.internalDrop(e, dropEvent, fieldKey), this.layoutDoc); } }; @@ -253,10 +255,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() default: return this._props.styleProvider?.(doc, props, property); } // prettier-ignore }; - moveDoc1 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_1'), true); - moveDoc2 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true); - remDoc1 = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true); - remDoc2 = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true); + moveDocFront = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.frontKey), true); + 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); registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => { if (e.button !== 2) { @@ -268,8 +270,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() action((moveEv, doubleTap) => { if (doubleTap) { this._childActive = true; - if (!this.dataDoc[this.fieldKey + '_1'] && !this.dataDoc[this.fieldKey]) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); - if (!this.dataDoc[this.fieldKey + '_2'] && !this.dataDoc[this.fieldKey + '_alternate']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); + if (!this.dataDoc[this.frontKey] && !this.dataDoc[this.fieldKey]) this.dataDoc[this.frontKey] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); + if (!this.dataDoc[this.backKey] && !this.dataDoc[this.fieldKey + '_alternate']) this.dataDoc[this.backKey] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); } }), false, @@ -296,26 +298,26 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() */ setListening = () => { if (this.SpeechRecognition) { - this.recognition.continuous = true; - this.recognition.interimResults = true; - this.recognition.lang = 'en-US'; - this.recognition.onresult = this.handleResult.bind(this); + this._recognition.continuous = true; + this._recognition.interimResults = true; + this._recognition.lang = 'en-US'; + this._recognition.onresult = this.handleResult.bind(this); } ContextMenu.Instance.setLangIndex(0); }; startListening = () => { - this.recognition.start(); + this._recognition.start(); this._listening = true; }; stopListening = () => { - this.recognition.stop(); + this._recognition.stop(); this._listening = false; }; setLanguage = (language: string, ind: number) => { - this.recognition.lang = language; + this._recognition.lang = language; ContextMenu.Instance.setLangIndex(ind); }; @@ -324,7 +326,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() * @returns */ convertAbr = () => { - switch (this.recognition.lang) { + switch (this._recognition.lang) { case 'en-US': return 'English'; //prettier-ignore case 'es-ES': return 'Spanish'; //prettier-ignore case 'fr-FR': return 'French'; //prettier-ignore @@ -389,7 +391,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() */ youtubeUpload = async () => { const audio = { - file: this.getYouTubeVideoId(StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text)), + file: this.getYouTubeVideoId(StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text)), }; const response = await axios.post('http://localhost:105/youtube/', audio, { headers: { @@ -407,13 +409,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() _layout_autoHeight: true, _xMargin: 5, _yMargin: 5, + x: NumCast(this.layoutDoc.x), + y: NumCast(this.layoutDoc.y) + 50, }); - newCol.x = this.layoutDoc.x; - newCol.y = NumCast(this.layoutDoc.y) + 50; - newCol.type_collection = CollectionViewType.Carousel as string; - for (let i = 0; i < collectionArr.length; i++) { - DocCast(collectionArr[i])[DocData].embedContainer = newCol; - } if (gpt) { this._props.DocumentView?.()._props.addDocument?.(newCol); @@ -421,7 +419,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() } else { this._props.addDocument?.(newCol); this._props.removeDocument?.(this.Document); - this.Document.embedContainer = newCol; + Doc.SetContainer(this.Document, newCol); } } @@ -450,65 +448,69 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() * Transfers the content of flashcards into a flashcard pile. */ gptFlashcardPile = async () => { - const text = await this.askGPT(GPTCallType.STACK); - const senArr = text?.split('Question: ') ?? []; - const collectionArr: Doc[] = []; - for (let i = 1; i < senArr.length; i++) { - const newDoc = Docs.Create.ComparisonDocument(senArr[i], { _layout_isFlashcard: true, _width: 300, _height: 300 }); - - if (senArr[i].includes('Keyword: ')) { - const question = StrCast(senArr![i]).split('Keyword: '); - const questionTxt = question[0].includes('Answer: ') ? question[0].split('Answer: ')[0] : question[0]; - const answerTxt = question[0].includes('Answer: ') ? question[0].split('Answer: ')[1] : question[1]; - this.fetchImages(question[1]).then(img => { - newDoc[DocData][this.fieldKey + '_1'] = Docs.Create.TextDocument(this.textToRtf(questionTxt)); - newDoc[DocData][this.fieldKey + '_0'] = Docs.Create.TextDocument(this.textToRtf(answerTxt, img)); + this.askGPT(GPTCallType.STACK).then(text => { + const [qtoken, ktoken, atoken] = ['Question: ', 'Keyword: ', 'Answer: ']; + const collectionArr: Doc[] = []; + const promises = text + .split(qtoken) + .filter(t => t) + .map(tuple => { + const newDoc = 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); + collectionArr.push(newDoc); + }; + return keyword && keyword !== 'none' ? this.fetchImages(keyword).then(img => fillInFlashcard(img)) : fillInFlashcard(); }); - } - - collectionArr.push(newDoc); - } - this.createFlashcardPile(collectionArr, true); + Promise.all(promises).then(() => this.createFlashcardPile(collectionArr, true)); + }); }; /** * Calls GPT for each flashcard type. */ - askGPT = async (callType: GPTCallType): Promise<string | undefined> => { - const questionText = 'Question: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text); - const rubricText = ' Rubric: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text); + askGPT = async (callType: GPTCallType) => { + const questionText = 'Question: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text); + const rubricText = ' Rubric: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.backKey]).text)?.Text); const queryText = questionText + ' UserAnswer: ' + this._inputValue + '. ' + rubricText; - this._loading = true; + this.loading = true; + let res = ''; - if (callType == GPTCallType.CHATCARD) { - if (StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '') { - this._loading = false; - return; - } - } - try { - const res = await gptAPICall(callType == GPTCallType.QUIZ ? queryText : questionText, callType); - runInAction(() => { + if (callType !== GPTCallType.CHATCARD || StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text) !== '') { + try { + res = await gptAPICall(callType == GPTCallType.QUIZ ? queryText : questionText, callType); if (!res) { console.error('GPT call failed'); - return; - } - if (callType == GPTCallType.CHATCARD) { - DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res; - } else if (callType == GPTCallType.QUIZ) { - this._frontSide = true; - this._outputValue = res.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric'); - } else if (callType === GPTCallType.FLASHCARD) { - this._loading = false; - return res; - } - this._loading = false; - }); - return res; - } catch (err) { - console.error('GPT call failed', err); + } else + switch (callType) { + case GPTCallType.CHATCARD: + DocCast(this.dataDoc[this.backKey])[DocData].text = res; + break; + case GPTCallType.QUIZ: + runInAction(() => { + this._renderSide = this.frontKey; + 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); + } } - this._loading = false; + this.loading = false; + return res; }; layoutWidth = () => NumCast(this.layoutDoc.width, 200); layoutHeight = () => NumCast(this.layoutDoc.height, 200); @@ -517,11 +519,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() const c = this.DocumentView?.().ContentDiv?.getElementsByTagName('img'); if (c?.length === 0) this.askGPT(GPTCallType.CHATCARD); if (c) { - this._loading = true; + this.loading = true; for (const i of c) { if (i.className !== 'ProseMirror-separator') this.getImageDesc(i.src); } - this._loading = false; + this.loading = false; } }; @@ -531,7 +533,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() * @param phonemes */ askGPTPhonemes = async (phonemes: string) => { - const sentence = StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text); + const sentence = StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text); const phon6 = 'huː ɑɹ juː tədeɪ'; const phon4 = 'kamo estas hɔi'; const promptEng = @@ -557,17 +559,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() this.convertAbr() + ' speech and they are not allophones of the same phoneme and they are far away from each on the ipa vowel chat and that pronunciation is not normal for the meaning of the word, note this difference and explain how it is supposed to sound. Just so you know, "i" sounds like "ee" as in "bee", not "ih" as an "lick". Interpret "ɹ" as the same as "r". Interpret "ʌ" as the same as "ə". Do not make "θ" and "f" interchangable. Do not make "n" and "ɲ" interchangable. Do not make "e" and "i" interchangable. If "ɚ", "ɔː", and "ɔ" are options for pronunciation, do not choose "ɚ". Ignore differences with colons. Ignore redundant letters and words and sounds and the splitting of words; do not mention this since there could be repeated words in the sentence. Provide a response like this: "Lets work on improving the pronunciation of "coffee." You said "cawffee," which is close, but we need to adjust the vowel sound. In American English, "coffee" is pronounced /ˈkɔːfi/, with a long "aw" sound. Try saying "kah-fee." Your intonation is good, but try putting a bit more stress on "like" in the sentence "I would like a coffee with milk." This will make your speech sound more natural. Keep practicing, and lets try saying the whole sentence again!"'; - switch (this.recognition.lang) { - case 'en-US': - this._outputValue = await gptAPICall(promptEng, GPTCallType.PRONUNCIATION); - break; - case 'es-ES': - this._outputValue = await gptAPICall(promptSpa, GPTCallType.PRONUNCIATION); - break; - default: - this._outputValue = await gptAPICall(promptAll, GPTCallType.PRONUNCIATION); - break; - } + switch (this._recognition.lang) { + case 'en-US': this._outputValue = await gptAPICall(promptEng, GPTCallType.PRONUNCIATION); break; + case 'es-ES': this._outputValue = await gptAPICall(promptSpa, GPTCallType.PRONUNCIATION); break; + default: this._outputValue = await gptAPICall(promptAll, GPTCallType.PRONUNCIATION); break; + } // prettier-ignore }; /** @@ -603,8 +599,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() _height: 150, title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-', }); - imageSnapshot.x = this.layoutDoc.x; - imageSnapshot.y = this.layoutDoc.y; return imageSnapshot; } catch (error) { console.log(error); @@ -616,7 +610,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() const hrefBase64 = await imageUrlToBase64(u); const response = await gptImageLabel(hrefBase64, 'Answer the following question as a short flashcard response. Do not include a label.' + (this.dataDoc.text as RichTextField)?.Text); - DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = response; + DocCast(this.dataDoc[this.backKey])[DocData].text = response; } catch (error) { console.log('Error', error); } @@ -624,11 +618,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() @action flipFlashcard = () => { - this._frontSide = !this._frontSide; + this._renderSide = this._renderSide === this.frontKey ? this.backKey : this.frontKey; }; - hoverFlip = (side: boolean) => { - if (this.revealOp === 'hover') this._frontSide = side; + @action + hoverFlip = (side: string) => { + if (this.revealOp === flashcardRevealOp.HOVER) this._renderSide = side; }; testForTextFields = (whichSlot: string) => { @@ -638,8 +633,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() const altText = RTFCast(this.Document[this.fieldKey + '_alternate'])?.Text.trim(); const layoutTemplateString = slotHasText ? FormattedTextBox.LayoutString(whichSlot): - whichSlot.endsWith('1') ? (subjectText !== undefined ? FormattedTextBox.LayoutString(this.fieldKey) : undefined) : - altText !== undefined ? FormattedTextBox.LayoutString(this.fieldKey + '_alternate'): undefined; // prettier-ignore + whichSlot === this.frontKey ? (subjectText !== undefined ? FormattedTextBox.LayoutString(this.fieldKey) : undefined) : + altText !== undefined ? FormattedTextBox.LayoutString(this.fieldKey + '_alternate'): undefined; // prettier-ignore // A bit hacky to try out the concept of using GPT to fill in flashcards // If the second slot doesn't have anything in it, but the fieldKey slot has text (e.g., this.text is a string) @@ -647,8 +642,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() // eg., this.text_alternate is // "((Provide a one sentence definition for (this) that doesn't use any word in (this.excludeWords) ))" // where (this) is replaced by the text in the fieldKey slot abd this.excludeWords is repalced by the conetnts of the excludeWords field - // The GPT call will put the "answer" in the second slot of the comparison (eg., text_2) - if (whichSlot.endsWith('2') && !layoutTemplateString?.includes(whichSlot)) { + // The GPT call will put the "answer" in the second slot of the comparison (eg., text_0) + if (whichSlot === this.backKey && !layoutTemplateString?.includes(whichSlot)) { const queryText = altText?.replace('(this)', subjectText); // TODO: this should be done in Doc.setField but it doesn't know about the fieldKey ... if (queryText?.match(/\(\(.*\)\)/)) { Doc.SetField(this.Document, whichSlot, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt @@ -656,7 +651,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() } return layoutTemplateString; }; - + textCreator = (title: string, text: string, img?: Doc) => { + const newDoc = Docs.Create.TextDocument(this.textToRtf(text, img), { + 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); @@ -688,8 +692,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() LayoutTemplateString={layoutString} Document={layoutString ? this.Document : targetDoc} containerViewPath={this._props.docViewPath} - moveDocument={whichSlot.endsWith('1') ? this.moveDoc1 : this.moveDoc2} - removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2} + moveDocument={whichSlot === this.frontKey ? this.moveDocFront : this.moveDocBack} + removeDocument={whichSlot === this.frontKey ? this.remDocFront : this.remDocBack} NativeWidth={returnZero} NativeHeight={returnZero} ScreenToLocalTransform={this.contentScreenToLocalXf} @@ -701,7 +705,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() hideLinkButton pointerEvents={this._childActive ? undefined : returnNone} /> - {!this.Document._layout_isFlashcard ? clearButton(whichSlot) : null} + {!this.isFlashcard ? clearButton(whichSlot) : null} </> ) : ( <div className="placeholder"> @@ -709,67 +713,56 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() </div> ); }; - const displayBox = (which: string, index: number, cover: number) => ( + const displayBox = (which: string, cover: number) => ( <div - className={`${index === 0 ? 'before' : 'after'}Box-cont`} + className={`${which === this.frontKey ? 'before' : 'after'}Box-cont`} key={which} style={{ width: this._props.PanelWidth() }} onPointerDown={e => { this.registerSliding(e, cover); - this.Document._layout_isFlashcard && this.activateContent(); + this.isFlashcard && this.activateContent(); }} - ref={ele => this.createDropTarget(ele, which, index)}> + ref={ele => this.createDropTarget(ele, which)}> {!this._isEmpty ? displayDoc(which) : null} </div> ); - if (this.Document._layout_isFlashcard) { - const side = this._frontSide ? 1 : 0; + if (this.isFlashcard) { const dataSplit = StrCast(this.dataDoc.data).includes('Keyword: ') ? StrCast(this.dataDoc.data).split('Keyword: ') : StrCast(this.dataDoc.data).split('Answer: '); - const textCreator = (which: number, title: string, text: string) => { - const newDoc = Docs.Create.TextDocument(this.textToRtf(text), { - title, // - _layout_autoHeight: true, - _layout_centered: true, - text_align: 'center', - _layout_fitWidth: true, - }); - this.addDoc(newDoc, this.fieldKey + '_' + which); - return newDoc; - }; // add text box to each side when comparison box is first created - if (!this.dataDoc[this.fieldKey + '_0'] && !this._isEmpty) { - textCreator(0, 'answer', dataSplit[1]); + if (!this.dataDoc[this.backKey] && !this._isEmpty) { + this.dataDoc[this.backKey] = this.textCreator('answer', dataSplit[1]); } - if (!this.dataDoc[this.fieldKey + '_1'] && !this._isEmpty) { - const question = textCreator(1, 'question', dataSplit[0] || 'hint: Enter a topic, select this document and click the stack button to have GPT create a deck of cards'); - Doc.SelectOnLoad = dataSplit[0] ? undefined : question; + if (!this.dataDoc[this.frontKey] && !this._isEmpty) { + const question = this.textCreator('question', dataSplit[0] || '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; + !dataSplit[0] && (question[DocData].text_placeholder = true); } if (DocCast(this.Document.embedContainer).practiceMode === practiceMode.QUIZ) { - const text = StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text); + 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._frontSide ? this._outputValue : this._inputValue} + value={this._renderSide ? this._outputValue : this._inputValue} onChange={this.handleInputChange} onScroll={e => { e.stopPropagation(); e.preventDefault(); }} placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''} - readOnly={this._frontSide}></textarea> + readOnly={this._renderSide === this.frontKey}></textarea> - {this._loading ? ( + {!this.loading ? null : ( <div className="loading-spinner"> <ReactLoading type="spin" height={30} width={30} color={'blue'} /> </div> - ) : null} + )} </div> <div> <div className="submit-button"> @@ -785,8 +778,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() <button className="submit-buttonpronunciation" onClick={this.evaluatePronunciation}> Evaluate Pronunciation </button> - <button className="submit-buttonsubmit" type="button" onClick={this._frontSide ? this.handleRenderClick : this.handleRenderGPTClick}> - {this._frontSide ? 'Redo the Question' : 'Submit'} + <button className="submit-buttonsubmit" type="button" onClick={this._renderSide === this.frontKey ? this.flipFlashcard : this.handleRenderGPTClick}> + {this._renderSide === this.frontKey ? 'Redo the Question' : 'Submit'} </button> </div> </div> @@ -798,11 +791,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() return ( <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */ - style={{ display: 'flex', flexDirection: 'column' }} - onMouseEnter={() => this.hoverFlip(true)} - onMouseLeave={() => this.hoverFlip(false)}> - {displayBox(`${this.fieldKey}_${side === 0 ? 1 : 0}`, side, this._props.PanelWidth() - 3)} - {this._loading ? ( + 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> @@ -814,9 +806,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() // 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.fieldKey}_2`, 1, this._props.PanelWidth() - 3)} + {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.fieldKey}_1`, 0, 0)} + {displayBox(this.frontKey, 0)} </div> <div diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index f7aba7542..c052a2823 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -576,15 +576,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' }); !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'compass' }); - // creates menu for the user to select how to reveal the flashcards - // if (this.Document._layout_isFlashcard) { - // const revealOptions = cm.findByDescription('Reveal Options'); - // const revealItems: ContextMenuProps[] = revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : []; - // revealItems.push({ description: 'Hover', event: () => { this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'hover'; }, icon: 'hand-point-up' }); // prettier-ignore - // revealItems.push({ description: 'Flip', event: () => { this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip'; }, icon: 'rotate' }); // prettier-ignore - // !revealOptions && cm.addItem({ description: 'Reveal Options', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' }); - // } - if (this._props.bringToFront) { const zorders = cm.findByDescription('ZOrder...'); const zorderItems = zorders?.subitems ?? []; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index c57307974..bf19a2f82 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -360,6 +360,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) if (force || ((this._finishingLink || this._props.isContentActive() || this._inDrop) && (textChange || removeSelection(newJson) !== removeSelection(prevData?.Data)))) { textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now()))); + textChange && (dataDoc[this.fieldKey + '_placeholder'] = undefined); const numstring = NumCast(dataDoc[this.fieldKey], null); dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : newText || (DocCast(dataDoc.proto)?.[this.fieldKey] === undefined && this.layoutDoc[this.fieldKey] === undefined) ? new RichTextField(newJson, newText) : undefined; @@ -1330,6 +1331,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._disposers.selected = reaction( () => this._props.rootSelected?.() || this._props.isContentActive(), action(selected => { + if (selected && this.dataDoc[this.fieldKey + '_placeholder']) { + setTimeout(() => { + selectAll(this._editorView!.state, (tx: Transaction) => { + this._editorView?.dispatch(tx); + this._editorView!.focus(); + }); + }); + } this.prepareForTyping(); if (FormattedTextBox._globalHighlights.has('Bold Text')) { this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed |