diff options
author | bobzel <zzzman@gmail.com> | 2024-10-11 15:55:19 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2024-10-11 15:55:19 -0400 |
commit | 76abb174684f2cd231a0dd9f6b71484c16e0498a (patch) | |
tree | 8d824b7218440948008281b8d63784ad38e75bcb | |
parent | 66f2b03283a1e42c48b1c16b4344b730c0a2e9f3 (diff) |
fixes for quiz mode - comparisonbox renderSide fixes. scrolling doesn't propagate out of carousel or card views. fix for text with image Doc - now gets saved to UPDATE_CACHE working set.
-rw-r--r-- | src/client/views/collections/CollectionCardDeckView.tsx | 6 | ||||
-rw-r--r-- | src/client/views/collections/CollectionCarousel3DView.tsx | 6 | ||||
-rw-r--r-- | src/client/views/collections/CollectionCarouselView.tsx | 6 | ||||
-rw-r--r-- | src/client/views/nodes/ComparisonBox.tsx | 114 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/DashDocCommentView.tsx | 4 | ||||
-rw-r--r-- | src/fields/Doc.ts | 13 | ||||
-rw-r--r-- | src/server/GarbageCollector.ts | 3 |
7 files changed, 95 insertions, 57 deletions
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx index 286df30aa..14ce9d2af 100644 --- a/src/client/views/collections/CollectionCardDeckView.tsx +++ b/src/client/views/collections/CollectionCardDeckView.tsx @@ -46,6 +46,7 @@ export class CollectionCardView extends CollectionSubView() { private _dropDisposer?: DragManager.DragDropDisposer; private _disposers: { [key: string]: IReactionDisposer } = {}; private _textToDoc = new Map<string, Doc>(); + private _oldWheel: HTMLElement | null = null; private _dropped = false; // set when a card doc has just moved and the drop method has been called - prevents the pointerUp method from hiding doc decorations (which needs to be done when clicking on a card to animate it to front/center) private _clickScript = () => ScriptField.MakeScript('scriptContext._curDoc=this', { scriptContext: 'any' })!; @@ -66,6 +67,10 @@ export class CollectionCardView extends CollectionSubView() { if (ele) { this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc); } + this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); + this._oldWheel = ele; + // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling + ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); }; /** * Callback to ensure gpt's text versions of the child docs are updated @@ -621,6 +626,7 @@ export class CollectionCardView extends CollectionSubView() { ); }); } + onPassiveWheel = (e: WheelEvent) => e.stopPropagation(); contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1); docViewProps = (): DocumentViewProps => ({ diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index f2ba90c78..05be376ca 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -22,6 +22,7 @@ const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = requi @observer export class CollectionCarousel3DView extends CollectionSubView() { private _dropDisposer?: DragManager.DragDropDisposer; + private _oldWheel: HTMLElement | null = null; constructor(props: SubCollectionViewProps) { super(props); @@ -37,6 +38,10 @@ export class CollectionCarousel3DView extends CollectionSubView() { if (ele) { this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc); } + this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); + this._oldWheel = ele; + // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling + ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); }; @computed get scrollSpeed() { @@ -194,6 +199,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { return this.panelWidth() * (1 - index); } + onPassiveWheel = (e: WheelEvent) => e.stopPropagation(); curDoc = () => this.carouselItems[NumCast(this.layoutDoc._carousel_index)]?.layout; answered = (correct: boolean) => (!correct || !this.curDoc()) && this.changeSlide(1); docViewProps = () => ({ diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index aa447c7bf..ef66a2c83 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -17,6 +17,7 @@ import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; export class CollectionCarouselView extends CollectionSubView() { private _dropDisposer?: DragManager.DragDropDisposer; + _oldWheel: HTMLElement | null = null; _fadeTimer: NodeJS.Timeout | undefined; @observable _last_index = this.carouselIndex; @observable _last_opacity = 1; @@ -35,6 +36,10 @@ export class CollectionCarouselView extends CollectionSubView() { if (ele) { this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc); } + this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); + this._oldWheel = ele; + // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling + ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); }; @computed get captionMarginX(){ return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore @@ -91,6 +96,7 @@ export class CollectionCarouselView extends CollectionSubView() { : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false ? false : undefined; + onPassiveWheel = (e: WheelEvent) => e.stopPropagation(); renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => { return ( <DocumentView diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index a57090e99..111fabca3 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -39,6 +39,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ComparisonBox, fieldKey); } + static qtoken = 'Question: '; + static ktoken = 'Keyword: '; + static atoken = 'Answer: '; private SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; private _closeRef = React.createRef<HTMLDivElement>(); private _disposers: { [key: string]: DragManager.DragDropDisposer | undefined } = {}; @@ -55,11 +58,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() @observable private _childActive = false; @observable private _animating = ''; @observable private _listening = false; - @observable private _renderSide = this.fieldKey; + @observable private _renderSide = this.frontKey; @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 frontKey() { return this._props.fieldKey + '_front'; } // 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 @@ -124,7 +127,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() </div> </Tooltip> )} - {DocCast(this.Document.embedContainer)?.type_collection !== CollectionViewType.Freeform ? null : ( + {DocCast(this.Document.embedContainer)?.type_collection !== CollectionViewType.Freeform || this._renderSide === this.backKey ? null : ( <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}> <div className="comparisonBox-button" onClick={this.gptFlashcardPile}> <FontAwesomeIcon icon="layer-group" size="xl" /> @@ -150,9 +153,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() if (phonTrans) { this._inputValue = StrCast(phonTrans); this.askGPTPhonemes(this._inputValue); + this._renderSide = this.backKey; + this._outputValue = ''; } else if (this._inputValue) this.askGPT(GPTCallType.QUIZ); - this._renderSide = this.backKey; - this._outputValue = ''; }; onPointerMove = ({ movementX }: PointerEvent) => { @@ -444,39 +447,42 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() }), text ); - /** - * Transfers the content of flashcards into a flashcard pile. - */ - gptFlashcardPile = async () => { - this.askGPT(GPTCallType.STACK).then(text => { - const [qtoken, ktoken, atoken] = ['Question: ', 'Keyword: ', 'Answer: ']; - const collectionArr: Doc[] = []; - const promises = text - .split(qtoken) + + 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 !== 'none' ? this.fetchImages(keyword).then(img => fillInFlashcard(img)) : fillInFlashcard(); + }; + + createFlashcardDeck = (text: string) => { + Promise.all( + text + .split(ComparisonBox.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(); - }); - Promise.all(promises).then(() => this.createFlashcardPile(collectionArr, true)); - }); + .map(tuple => this.createFlashcard(tuple)) + ).then(docs => this.createFlashcardPile(docs, true)); }; /** + * queries GPT about a topic and then creates a flashcard deck from the results. + */ + gptFlashcardPile = () => this.askGPT(GPTCallType.STACK).then(this.createFlashcardDeck); + + /** * Calls GPT for each flashcard type. */ askGPT = async (callType: GPTCallType) => { @@ -498,7 +504,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() break; case GPTCallType.QUIZ: runInAction(() => { - this._renderSide = this.frontKey; + this._renderSide = this.backKey; this._outputValue = res.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric'); }); break; @@ -728,20 +734,24 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() ); if (this.isFlashcard) { - const dataSplit = StrCast(this.dataDoc.data).includes('Keyword: ') ? StrCast(this.dataDoc.data).split('Keyword: ') : StrCast(this.dataDoc.data).split('Answer: '); - - // add text box to each side when comparison box is first created - if (!this.dataDoc[this.backKey] && !this._isEmpty) { - this.dataDoc[this.backKey] = this.textCreator('answer', dataSplit[1]); + 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 (!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) { + 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' : ''}`}> @@ -749,15 +759,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() <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._outputValue : this._inputValue} + value={this._renderSide === this.backKey ? 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._renderSide === this.frontKey}></textarea> - + readOnly={this._renderSide === this.backKey} + /> {!this.loading ? null : ( <div className="loading-spinner"> <ReactLoading type="spin" height={30} width={30} color={'blue'} /> @@ -778,8 +788,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._renderSide === this.frontKey ? this.flipFlashcard : this.handleRenderGPTClick}> - {this._renderSide === this.frontKey ? 'Redo the Question' : 'Submit'} + <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> diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx index 0304ddc86..967f4aa5b 100644 --- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx +++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx @@ -68,7 +68,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV expand && this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc)); try { this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, (this.props.getPos() ?? 0) + (expand ? 2 : 1)))); - } catch (err) { + } catch { /* empty */ } }, 0); @@ -95,7 +95,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV setTimeout(() => { try { this.props.view.dispatch(state.tr.setSelection(TextSelection.create(state.tr.doc, this.props.getPos() + 2))); - } catch (err) { + } catch { /* empty */ } }, 0); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 81241f9fe..45dfe233f 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -960,6 +960,19 @@ export namespace Doc { } } else if (field instanceof PrefetchProxy) { Doc.FindReferences(field.value, references, system); + } else if (field instanceof RichTextField) { + const re = /"docId"\s*:\s*"(.*?)"/g; + let match: string[] | null; + while ((match = re.exec(field.Data)) !== null) { + const urlString = match[1]; + if (urlString) { + const rdoc = DocServer.GetCachedRefField(urlString); + if (rdoc) { + references.add(rdoc); + Doc.FindReferences(rdoc, references, system); + } + } + } } } else if (field instanceof Promise) { // eslint-disable-next-line no-debugger diff --git a/src/server/GarbageCollector.ts b/src/server/GarbageCollector.ts index 041f65592..74e8c288a 100644 --- a/src/server/GarbageCollector.ts +++ b/src/server/GarbageCollector.ts @@ -1,7 +1,4 @@ /* eslint-disable no-await-in-loop */ -/* eslint-disable no-continue */ -/* eslint-disable no-cond-assign */ -/* eslint-disable no-restricted-syntax */ import * as fs from 'fs'; import * as path from 'path'; import { Database } from './database'; |