From de4cc95e406b10bd92975abd5eef8f708cbf8f02 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 10 Oct 2024 22:32:52 -0400 Subject: fixed being able to use text menu on text in comparison box. Allow TextDocuments to be created with a RichText field. Changed comparisonBox to horizontally center flashcard text. --- src/client/documents/Documents.ts | 7 ++-- src/client/views/nodes/ComparisonBox.tsx | 44 ++++++++++++---------- .../views/nodes/formattedText/FormattedTextBox.tsx | 6 +-- .../views/nodes/formattedText/RichTextMenu.tsx | 12 ++++-- 4 files changed, 38 insertions(+), 31 deletions(-) (limited to 'src') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 5f2a592ae..0d7e0b20e 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -240,7 +240,6 @@ export class DocumentOptions { borderWidth?: STRt = new StrInfo('Width of user-added border', false); borderColor?: STRt = new StrInfo('Color of user-added border', false); text_fontColor?: STRt = new StrInfo('Color of text', false); - text_align?: STRt = new StrInfo('alignment'); hCentering?: 'h-left' | 'h-center' | 'h-right'; isDefaultTemplateDoc?: BOOLt = new BoolInfo(''); contentBold?: BOOLt = new BoolInfo(''); @@ -697,7 +696,7 @@ export namespace Docs { dataProps.author_date = new DateField(); if (fieldKey) { dataProps[`${fieldKey}_modificationDate`] = new DateField(); - dataProps[fieldKey] = options.data ?? data; + dataProps[fieldKey] = (options as unknown as { [key: string]: FieldType | undefined })[fieldKey] ?? data; // so that the list of annotations is already initialised, prevents issues in addonly. // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do. @@ -827,7 +826,7 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.MESSAGE), field, options, undefined, fieldKey); } - export function TextDocument(text: string, options: DocumentOptions = {}, fieldKey: string = 'text') { + export function TextDocument(text: string | RichTextField, options: DocumentOptions = {}, fieldKey: string = 'text') { const rtf = { doc: { type: 'doc', @@ -846,7 +845,7 @@ export namespace Docs { selection: { type: 'text', anchor: 1, head: 1 }, storedMarks: [], }; - const field = text ? new RichTextField(JSON.stringify(rtf), text) : undefined; + const field = text instanceof RichTextField ? text : text ? new RichTextField(JSON.stringify(rtf), text) : options.text instanceof RichTextField ? options.text : undefined; return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey); } diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index ccbe98257..f8cf0f464 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -425,6 +425,27 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() } } + textToRtf = (text: string, img?: Doc) => + new RichTextField( + JSON.stringify({ + // this is a RichText json that has the question text placed above a related image + doc: { + type: 'doc', + content: [ + { + type: 'paragraph', + attrs: { align: 'center', color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null }, + content: [ + ...(text ? [{ type: 'text', text }] : []), // + ...(img ? [{ type: 'dashDoc', attrs: { width: '200px', height: '200px', title: 'dashDoc', float: 'unset', hidden: false, docId: img[Id] } }] : []), + ], + }, + ], + }, + selection: { type: 'text', anchor: 2, head: 2 }, + }), + text + ); /** * Transfers the content of flashcards into a flashcard pile. */ @@ -440,25 +461,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() 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 => { - const rtfiel = new RichTextField( - JSON.stringify({ - // this is a RichText json that has the question text placed above a related image - doc: { - type: 'doc', - content: [ - { - type: 'paragraph', - attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null }, - content: [{ type: 'text', text: questionTxt }, img ? { type: 'dashDoc', attrs: { width: '200px', height: '200px', title: 'dashDoc', float: 'unset', hidden: false, docId: img[Id] } } : {}], - }, - ], - }, - selection: { type: 'text', anchor: 2, head: 2 }, - }), - questionTxt - ); - newDoc[DocData][this.fieldKey + '_1'] = Docs.Create.TextDocument(questionTxt, { text: rtfiel }); - newDoc[DocData][this.fieldKey + '_0'] = Docs.Create.TextDocument(answerTxt); + newDoc[DocData][this.fieldKey + '_1'] = Docs.Create.TextDocument(this.textToRtf(questionTxt)); + newDoc[DocData][this.fieldKey + '_0'] = Docs.Create.TextDocument(this.textToRtf(answerTxt, img)); }); } @@ -723,7 +727,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() const side = this._frontSide ? 1 : 0; 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(text, { + const newDoc = Docs.Create.TextDocument(this.textToRtf(text), { title, // _layout_autoHeight: true, _layout_centered: true, diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 18b8c9d34..c57307974 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1328,7 +1328,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent this._props.rootSelected?.(), + () => this._props.rootSelected?.() || this._props.isContentActive(), action(selected => { this.prepareForTyping(); if (FormattedTextBox._globalHighlights.has('Bold Text')) { @@ -1514,7 +1514,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { this._editorView!.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: textAlign }))); }); @@ -1775,7 +1775,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { }); } + @computed get RootSelected() { + return this.TextView?._props.rootSelected?.() || this.TextView?._props.isContentActive(); + } + @computed get noAutoLink() { return this._noLinkActive; } @@ -183,7 +187,7 @@ export class RichTextMenu extends AntimodeMenu { // finds font sizes and families in selection getActiveAlignment = () => { - if (this.view && this.TextView?._props.rootSelected?.()) { + if (this.view && this.RootSelected) { const from = this.view.state.selection.$from; for (let i = from.depth; i >= 0; i--) { const node = from.node(i); @@ -216,7 +220,7 @@ export class RichTextMenu extends AntimodeMenu { const activeSizes = new Set(); const activeColors = new Set(); const activeHighlights = new Set(); - if (this.view && this.TextView?._props.rootSelected?.()) { + if (this.view && this.RootSelected) { const { state } = this.view; const pos = this.view.state.selection.$from; let marks: Mark[] = [...(state.storedMarks ?? [])]; @@ -252,7 +256,7 @@ export class RichTextMenu extends AntimodeMenu { // finds all active marks on selection in given group getActiveMarksOnSelection() { - if (!this.view || !this.TextView?._props.rootSelected?.()) return [] as MarkType[]; + if (!this.view || !this.RootSelected) return [] as MarkType[]; const { state } = this.view; let marks: Mark[] = [...(state.storedMarks ?? [])]; @@ -409,7 +413,7 @@ export class RichTextMenu extends AntimodeMenu { this.layoutDoc && (this.layoutDoc._layout_centered = !this.layoutDoc._layout_centered); }; align = (view: EditorView, dispatch: (tr: Transaction) => void, alignment: 'left' | 'right' | 'center') => { - if (this.TextView?._props.rootSelected?.()) { + if (this.RootSelected) { let { tr } = view.state; view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos) => { if ([schema.nodes.paragraph, schema.nodes.heading].includes(node.type)) { -- cgit v1.2.3-70-g09d2 From 04fb4e772c6b7cdc6e266b2c661b2eb5f075f954 Mon Sep 17 00:00:00 2001 From: alyssaf16 Date: Fri, 11 Oct 2024 00:42:08 -0400 Subject: quiz changes --- src/client/views/nodes/ComparisonBox.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index f8cf0f464..b2a717c3c 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -117,7 +117,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() "Ask GPT to create an answer on the back side of the flashcard based on your question on the front"} // prettier-ignore }> -
(this._frontSide ? this.findImageTags() : null)}> +
(this._frontSide ? this.askGPT(GPTCallType.CHATCARD) : null)}>
@@ -476,8 +476,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() */ askGPT = async (callType: GPTCallType): Promise => { 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); - // const queryText = questionText + ' UserAnswer: ' + this._inputValue + '. ' + rubricText; + const rubricText = ' Rubric: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text); + const queryText = questionText + ' UserAnswer: ' + this._inputValue + '. ' + rubricText; this._loading = true; if (callType == GPTCallType.CHATCARD) { @@ -487,7 +487,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() } } try { - const res = await gptAPICall(questionText, GPTCallType.FLASHCARD); + const res = await gptAPICall(callType == GPTCallType.QUIZ ? queryText : questionText, callType); runInAction(() => { if (!res) { console.error('GPT call failed'); -- cgit v1.2.3-70-g09d2 From 66f2b03283a1e42c48b1c16b4344b730c0a2e9f3 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 11 Oct 2024 14:19:35 -0400 Subject: cleaned up comparisonBox a bit - fixed text doc placeholder text and fixed parsing keyword/question/answer from flashcard creator. also fixed text boxes within in comparisonBox --- .../views/collections/FlashcardPracticeUI.tsx | 17 +- src/client/views/nodes/ComparisonBox.tsx | 308 ++++++++++----------- src/client/views/nodes/DocumentView.tsx | 9 - .../views/nodes/formattedText/FormattedTextBox.tsx | 9 + 4 files changed, 170 insertions(+), 173 deletions(-) (limited to 'src') 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} + icon={} 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() } private SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; private _closeRef = React.createRef(); - 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() @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() @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() 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', }}> @@ -106,18 +111,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() @computed get flashcardMenu() { return SnappingManager.HideDecorations ? null : (
- {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 : ( { - !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"} -
// prettier-ignore +
Ask GPT to create an answer for the question on the front
// prettier-ignore }> -
(this._frontSide ? this.askGPT(GPTCallType.CHATCARD) : null)}> +
this.askGPT(GPTCallType.CHATCARD)}>
@@ -143,17 +145,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() 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() 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() 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() 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, targetWidth: number) => { if (e.button !== 2) { @@ -268,8 +270,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() 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() */ 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() * @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() */ 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() _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() } 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() * 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 => { - 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() 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() * @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() 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() _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() 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() @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() 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() // 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() } 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() 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() hideLinkButton pointerEvents={this._childActive ? undefined : returnNone} /> - {!this.Document._layout_isFlashcard ? clearButton(whichSlot) : null} + {!this.isFlashcard ? clearButton(whichSlot) : null} ) : (
@@ -709,67 +713,56 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent()
); }; - const displayBox = (which: string, index: number, cover: number) => ( + const displayBox = (which: string, cover: number) => (
{ 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}
); - 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 (

{text}

Return to all flashcards and add text to both sides.

+ readOnly={this._renderSide === this.frontKey}> - {this._loading ? ( + {!this.loading ? null : (
- ) : null} + )}
@@ -785,8 +778,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() -
@@ -798,11 +791,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() return (
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 ? (
@@ -814,9 +806,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() // render a comparison box that compares items side by side return (
- {displayBox(`${this.fieldKey}_2`, 1, this._props.PanelWidth() - 3)} + {displayBox(this.backKey, this._props.PanelWidth() - 3)}
- {displayBox(`${this.fieldKey}_1`, 0, 0)} + {displayBox(this.frontKey, 0)}
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 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 -- cgit v1.2.3-70-g09d2 From 76abb174684f2cd231a0dd9f6b71484c16e0498a Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 11 Oct 2024 15:55:19 -0400 Subject: 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. --- .../views/collections/CollectionCardDeckView.tsx | 6 ++ .../views/collections/CollectionCarousel3DView.tsx | 6 ++ .../views/collections/CollectionCarouselView.tsx | 6 ++ src/client/views/nodes/ComparisonBox.tsx | 114 +++++++++++---------- .../nodes/formattedText/DashDocCommentView.tsx | 4 +- src/fields/Doc.ts | 13 +++ src/server/GarbageCollector.ts | 3 - 7 files changed, 95 insertions(+), 57 deletions(-) (limited to 'src') 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(); + 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 ( () 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(); private _disposers: { [key: string]: DragManager.DragDropDisposer | undefined } = {}; @@ -55,11 +58,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() @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()
)} - {DocCast(this.Document.embedContainer)?.type_collection !== CollectionViewType.Freeform ? null : ( + {DocCast(this.Document.embedContainer)?.type_collection !== CollectionViewType.Freeform || this._renderSide === this.backKey ? null : ( Create new flashcard stack based on text
}>
@@ -150,9 +153,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() 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,38 +447,41 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() }), 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. */ @@ -498,7 +504,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() 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() ); 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 (
@@ -749,15 +759,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent()

Return to all flashcards and add text to both sides.

- + readOnly={this._renderSide === this.backKey} + /> {!this.loading ? null : (
@@ -778,8 +788,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() -
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 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 { 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'; -- cgit v1.2.3-70-g09d2 From d7770e94876540a81bae06d233b6196a91650325 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 11 Oct 2024 16:20:09 -0400 Subject: added some commenting and code tweaks to ComparisonBox --- src/client/views/nodes/ComparisonBox.tsx | 98 +++++++++++++++----------------- src/fields/RichTextField.ts | 23 ++++++++ 2 files changed, 68 insertions(+), 53 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 111fabca3..672008968 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -34,6 +34,22 @@ import { FormattedTextBox } from './formattedText/FormattedTextBox'; const API_URL = 'https://api.unsplash.com/search/photos'; +/** + * This view serves two distinct functions depending on the metadata field layout_isFlashcard + * 1) it provides a before/after animated sliding transition between two Docs + * 2) it provides a question/answer switch between two Docs (flashcard) + * + * In either case, the two docs are stored in the _front and _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. + * In addition, if a flashcard is created without data in the front/back fields, this will + * create Text documents with placeholder text indicating to the user how to fill in the cards. + * One option is to allow the user to enter a topic and, by clicking on the flashcard stack button, + * convert the comparision box into a stack of comparison boxes filled in by GPT about the topic. + * + */ + @observer export class ComparisonBox extends ViewBoxAnnotatableComponent() { public static LayoutString(fieldKey: string) { @@ -129,7 +145,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() )} {DocCast(this.Document.embedContainer)?.type_collection !== CollectionViewType.Freeform || this._renderSide === this.backKey ? null : ( Create new flashcard stack based on text
}> -
+
this.askGPT(GPTCallType.STACK).then(this.createFlashcardDeck)}>
@@ -404,50 +420,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() return response.data.transcription; }; - createFlashcardPile(collectionArr: Doc[], gpt: boolean) { - const newCol = Docs.Create.CarouselDocument(collectionArr, { - _width: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 250) + 50, - _height: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 200) + 50, - _layout_fitWidth: false, - _layout_autoHeight: true, - _xMargin: 5, - _yMargin: 5, - x: NumCast(this.layoutDoc.x), - y: NumCast(this.layoutDoc.y) + 50, - }); - - if (gpt) { - this._props.DocumentView?.()._props.addDocument?.(newCol); - this._props.removeDocument?.(this.Document); - } else { - this._props.addDocument?.(newCol); - this._props.removeDocument?.(this.Document); - Doc.SetContainer(this.Document, newCol); - } - } - - textToRtf = (text: string, img?: Doc) => - new RichTextField( - JSON.stringify({ - // this is a RichText json that has the question text placed above a related image - doc: { - type: 'doc', - content: [ - { - type: 'paragraph', - attrs: { align: 'center', color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null }, - content: [ - ...(text ? [{ type: 'text', text }] : []), // - ...(img ? [{ type: 'dashDoc', attrs: { width: '200px', height: '200px', title: 'dashDoc', float: 'unset', hidden: false, docId: img[Id] } }] : []), - ], - }, - ], - }, - selection: { type: 'text', anchor: 2, head: 2 }, - }), - text - ); - + /** + * 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 }); @@ -465,22 +443,36 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() newDoc[DocData][this.backKey] = this.textCreator('answer', answer); return newDoc; }; - return keyword && keyword !== 'none' ? this.fetchImages(keyword).then(img => fillInFlashcard(img)) : fillInFlashcard(); + 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 => this.createFlashcardPile(docs, true)); - }; + ).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), + }); - /** - * queries GPT about a topic and then creates a flashcard deck from the results. - */ - gptFlashcardPile = () => this.askGPT(GPTCallType.STACK).then(this.createFlashcardDeck); + this._props.DocumentView?.()._props.addDocument?.(newCol); + this._props.removeDocument?.(this.Document); + }); + }; /** * Calls GPT for each flashcard type. @@ -658,7 +650,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() return layoutTemplateString; }; textCreator = (title: string, text: string, img?: Doc) => { - const newDoc = Docs.Create.TextDocument(this.textToRtf(text, img), { + const newDoc = Docs.Create.TextDocument(RichTextField.textToRtf(text, img?.[Id]), { title, // _layout_autoHeight: true, _layout_centered: true, diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts index 613bb0fd1..dc636031a 100644 --- a/src/fields/RichTextField.ts +++ b/src/fields/RichTextField.ts @@ -48,4 +48,27 @@ export class RichTextField extends ObjectField { '' ); } + + public static textToRtf(text: string, imgDocId?: string) { + return new RichTextField( + JSON.stringify({ + // this is a RichText json that has the question text placed above a related image + doc: { + type: 'doc', + content: [ + { + type: 'paragraph', + attrs: { align: 'center', color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null }, + content: [ + ...(text ? [{ type: 'text', text }] : []), // + ...(imgDocId ? [{ type: 'dashDoc', attrs: { width: '200px', height: '200px', title: 'dashDoc', float: 'unset', hidden: false, docId: imgDocId } }] : []), + ], + }, + ], + }, + selection: { type: 'text', anchor: 2, head: 2 }, + }), + text + ); + } } -- cgit v1.2.3-70-g09d2 From 0f83debd8d2cca04d9fac959c7ed450312ef8d7d Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 11 Oct 2024 17:14:33 -0400 Subject: fix for vertically centered textbox that overflows to switch to top so that scrolling works. --- src/client/views/nodes/formattedText/FormattedTextBox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index bf19a2f82..36f06aaf2 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -2147,7 +2147,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent
Date: Fri, 11 Oct 2024 20:21:33 -0400 Subject: fixed verticalalign of text boxes on load when existing text was there. fixe scrolling of vertical align textboxes when fitwidth is set. added flashcard contextmenu to comparisonbox and --- src/client/views/StyleProvider.tsx | 3 +- src/client/views/nodes/ComparisonBox.tsx | 217 ++++++++++----------- src/client/views/nodes/DocumentView.tsx | 21 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 18 +- 4 files changed, 115 insertions(+), 144 deletions(-) (limited to 'src') diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 02e0a34d8..8859f6464 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -54,7 +54,6 @@ export function styleFromLayoutString(doc: Doc, props: FieldViewProps, scale: nu } export function border(doc: Doc, pw: number, ph: number, rad: number = 0, inset: number = 0) { - if (!rad) rad = 0; const width = pw * inset; const height = ph * inset; @@ -218,7 +217,7 @@ export function DefaultStyleProvider(doc: Opt, props: Opt() public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ComparisonBox, fieldKey); } + private SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + static qtoken = 'Question: '; static ktoken = 'Keyword: '; static atoken = 'Answer: '; - private SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + private _slideTiming = 200; + private _sideBtnWidth = 35; private _closeRef = React.createRef(); private _disposers: { [key: string]: DragManager.DragDropDisposer | undefined } = {}; private _reactDisposer: IReactionDisposer | undefined; - constructor(props: FieldViewProps) { - super(props); - makeObservable(this); - } @observable private _inputValue = ''; @observable private _outputValue = ''; @@ -77,6 +77,47 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() @observable private _renderSide = this.frontKey; @observable private _recognition = new this.SpeechRecognition(); + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); + } + + componentDidMount() { + this._props.setContentViewBox?.(this); + this._reactDisposer = reaction( + () => this._props.isSelected(), // when this reaction should update + selected => { + if (selected && this.isFlashcard) this.activateContent(); + !selected && (this._childActive = false); + }, // what it should update to + { fireImmediately: true } + ); + } + + componentWillUnmount() { + this._reactDisposer?.(); + } + + protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string) => { + this._disposers[fieldKey]?.(); + if (ele) { + this._disposers[fieldKey] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.internalDrop(e, dropEvent, fieldKey), this.layoutDoc); + } + }; + + private internalDrop = undoable((e: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { + if (dropEvent.complete.docDragData) { + const { droppedDocuments } = dropEvent.complete.docDragData; + const added = dropEvent.complete.docDragData.moveDocument?.(droppedDocuments, this.Document, (doc: Doc | Doc[]) => this.addDoc(toList(doc).lastElement(), fieldKey)); + Doc.SetContainer(droppedDocuments.lastElement(), this.dataDoc); + !added && e.preventDefault(); + e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place + // this.childActive = false; + return added; + } + return undefined; + }, 'internal drop'); + @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 @@ -112,8 +153,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() ); } - - _sideBtnWidth = 35; /** * How much the content of the view is being scaled based on its nesting and its fit-to-width settings */ @@ -131,35 +170,24 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() return SnappingManager.HideDecorations ? null : (
{this.revealOp === flashcardRevealOp.HOVER || !this._props.isSelected() ? null : this.overlayAlternateIcon} - {!this._props.isSelected() ? null : ( - <> - {this._renderSide === this.frontKey ? null : ( - Ask GPT to create an answer for the question on the front
// prettier-ignore - }> -
this.askGPT(GPTCallType.CHATCARD)}> - -
- - )} - {DocCast(this.Document.embedContainer)?.type_collection !== CollectionViewType.Freeform || this._renderSide === this.backKey ? null : ( - Create new flashcard stack based on text
}> -
this.askGPT(GPTCallType.STACK).then(this.createFlashcardDeck)}> - -
- - )} - + {!this._props.isSelected() || this._renderSide === this.frontKey ? null : ( + Ask GPT to create an answer for the question on the front
}> +
this.askGPT(GPTCallType.CHATCARD)}> + +
+ + )} + {!this._props.isSelected() || this._renderSide === this.backKey || CollectionFreeFormView.from(this.DocumentView?.()) ? null : ( + Create new flashcard stack based on text
}> +
this.askGPT(GPTCallType.STACK).then(this.createFlashcardDeck)}> + +
+ )}
); } - @action handleInputChange = (e: React.ChangeEvent) => { - this._inputValue = e.target.value; - }; - @action activateContent = () => { this._childActive = true; }; @@ -182,42 +210,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() return false; }; - componentDidMount() { - this._props.setContentViewBox?.(this); - this._reactDisposer = reaction( - () => this._props.isSelected(), // when this reaction should update - selected => { - if (selected && this.isFlashcard) this.activateContent(); - !selected && (this._childActive = false); - }, // what it should update to - { fireImmediately: true } - ); - } - - componentWillUnmount() { - this._reactDisposer?.(); - } - - protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string) => { - this._disposers[fieldKey]?.(); - if (ele) { - this._disposers[fieldKey] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.internalDrop(e, dropEvent, fieldKey), this.layoutDoc); - } - }; - - private internalDrop = undoable((e: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { - if (dropEvent.complete.docDragData) { - const { droppedDocuments } = dropEvent.complete.docDragData; - const added = dropEvent.complete.docDragData.moveDocument?.(droppedDocuments, this.Document, (doc: Doc | Doc[]) => this.addDoc(toList(doc).lastElement(), fieldKey)); - Doc.SetContainer(droppedDocuments.lastElement(), this.dataDoc); - !added && e.preventDefault(); - e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place - // this.childActive = false; - return added; - } - return undefined; - }, 'internal drop'); - getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { const anchor = Docs.Create.ConfigDocument({ title: 'CompareAnchor:' + this.Document.title, @@ -296,17 +288,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() false, undefined, action(() => { - if (this._childActive) return; - this._animating = 'all 200ms'; - // on click, animate slider movement to the targetWidth - this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth(); - - setTimeout( - action(() => { - this._animating = ''; - }), - 200 - ); + if (!this._childActive) { + this._animating = `all ${this._slideTiming}ms`; // on click, animate slider movement to the targetWidth + this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth(); + setTimeout( action(() => {this._animating = ''; }), this._slideTiming); // prettier-ignore + } }) ); } @@ -381,17 +367,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() * Gets the transcription of an audio recording by sending the * recording to backend. */ - pushInfo = async () => { - const audio = { - file: DocCast(this.Document.audio)[DocData].url, - }; - const response = await axios.post('http://localhost:105/recognize/', audio, { - headers: { - 'Content-Type': 'application/json', - }, - }); - this.Document.phoneticTranscription = response.data.transcription; - }; + pushInfo = () => + axios + .post( + 'http://localhost:105/recognize/', // + { file: DocCast(this.Document.audio)[DocData].url }, + { headers: { 'Content-Type': 'application/json' } } + ) + .then(response => { + this.Document.phoneticTranscription = response.data.transcription; + }); /** * Extracts the id of the youtube video url. @@ -408,17 +393,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() * Gets the transcript of a youtube video by sending the video url to the backend. * @returns transcription of youtube recording */ - youtubeUpload = async () => { - const audio = { - file: this.getYouTubeVideoId(StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text)), - }; - const response = await axios.post('http://localhost:105/youtube/', audio, { - headers: { - 'Content-Type': 'application/json', - }, - }); - return response.data.transcription; - }; + youtubeUpload = async () => + axios + .post( + 'http://localhost:105/youtube/', // + { file: this.getYouTubeVideoId(StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text)) }, + { 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 @@ -478,15 +460,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() * Calls GPT for each flashcard type. */ 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; + 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 : ''); this.loading = true; let res = ''; - if (callType !== GPTCallType.CHATCARD || StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text) !== '') { + if (callType !== GPTCallType.CHATCARD || frontText) { try { - res = await gptAPICall(callType == GPTCallType.QUIZ ? queryText : questionText, callType); + res = await gptAPICall(queryText, callType); if (!res) { console.error('GPT call failed'); } else @@ -624,6 +607,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() if (this.revealOp === flashcardRevealOp.HOVER) this._renderSide = side; }; + flashcardContextMenu = () => { + const appearance = ContextMenu.Instance.findByDescription('Appearance...'); + const appearanceItems = appearance?.subitems ?? []; + if (this.Document._layout_isFlashcard) { + appearanceItems.push({ description: 'Create ChatCard', event: () => this.askGPT(GPTCallType.CHATCARD), icon: 'id-card' }); + } + !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); + }; + testForTextFields = (whichSlot: string) => { const slotData = Doc.Get(this.dataDoc, whichSlot, true); const slotHasText = slotData instanceof RichTextField || typeof slotData === 'string'; @@ -752,17 +744,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent()