From 585f03bf45df4ac7ed61d22c9dbe10d8e453199d Mon Sep 17 00:00:00 2001 From: alyssaf16 Date: Wed, 5 Jun 2024 15:06:15 -0400 Subject: Flashcards changes --- src/client/views/nodes/DocumentView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/client/views/nodes/DocumentView.tsx') diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 7a1f94948..8bf7b094f 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -575,7 +575,7 @@ export class DocumentViewInternal extends DocComponent this._props.pinToPres(this.Document, {}), icon: 'eye' }); if (this.Document._layout_isFlashcard) { - appearanceItems.push({ description: 'Create ChatCard', event: () => this.askGPT(), icon: 'id-card' }); + appearanceItems.push({ description: 'Create an answer on the back', event: () => this.askGPT(), icon: 'id-card' }); } !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' }); -- cgit v1.2.3-70-g09d2 From c76505f56a83625e3993427838aaee0c54c5fb81 Mon Sep 17 00:00:00 2001 From: alyssaf16 Date: Thu, 6 Jun 2024 09:53:12 -0400 Subject: Fixes to chatcard and quizcard --- src/client/apis/gpt/GPT.ts | 2 +- .../views/collections/CollectionCarouselView.tsx | 7 ++-- src/client/views/nodes/ComparisonBox.tsx | 43 ++++++++++++++++++---- src/client/views/nodes/DocumentView.tsx | 19 +--------- 4 files changed, 40 insertions(+), 31 deletions(-) (limited to 'src/client/views/nodes/DocumentView.tsx') diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 05007960d..b036349dc 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -51,7 +51,7 @@ const callTypeMap: { [type: string]: GPTCallOpts } = { model: 'gpt-4-turbo', maxTokens: 1024, temp: 0, - prompt: 'List unique differences between the content of the UserAnswer and Rubric. Before each difference, label it and provide any additional information the UserAnswer missed and explain it in second person without separating it into UserAnswer and Rubric content and additional information. If there are no differences, say correct', + prompt: 'List unique differences between the content of the UserAnswer and Rubric. Before each difference, label it and provide any additional information the UserAnswer missed and explain it in second person without separating it into UserAnswer and Rubric content and additional information. If the Rubric is incorrect, explain why. If there are no differences, say correct. If it is empty, say there is nothing for me to evaluate.', }, }; diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 2893de762..53d14e6e0 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -35,6 +35,7 @@ export class CollectionCarouselView extends CollectionSubView() { private _dropDisposer?: DragManager.DragDropDisposer; @observable private _message = 'Drag a document'; get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore + get sideField() { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore get starField() { return this.fieldKey + "_star"; } // prettier-ignore constructor(props: any) { @@ -131,9 +132,6 @@ export class CollectionCarouselView extends CollectionSubView() { curDoc.layout[this.practiceField] = val; this.advance(e); }; - clearContent = () => { - this.carouselItems?.map(doc => (doc.layout[this.practiceField] = undefined)); - }; captionStyleProvider = (doc: Doc | undefined, captionProps: Opt, property: string): any => { // first look for properties on the document in the carousel, then fallback to properties on the container const childValue = doc?.['caption_' + property] ? this._props.styleProvider?.(doc, captionProps, property) : undefined; @@ -146,7 +144,8 @@ export class CollectionCarouselView extends CollectionSubView() { setFilterMode = (mode: cardMode) => { this.layoutDoc.filterOp = mode; if (mode == cardMode.STAR) this.move(1); - this.clearContent(); + if (mode == cardMode.QUIZ) this.carouselItems?.map(doc => (doc.layout[this.sideField] = undefined)); + this.carouselItems?.map(doc => (doc.layout[this.practiceField] = undefined)); }; specificMenu = (): void => { const cm = ContextMenu.Instance; diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index c0c173d9a..6d4af2f3e 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -25,6 +25,8 @@ import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import ReactLoading from 'react-loading'; +import { ContextMenu } from '../ContextMenu'; +import { ContextMenuProps } from '../ContextMenuItem'; @observer export class ComparisonBox extends ViewBoxAnnotatableComponent() { @@ -282,7 +284,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() // Call the GPT model and get the output this.layoutDoc[`_${this._props.fieldKey}_usePath`] = 'alternate'; this._outputValue = ''; - if (this._inputValue) this.askGPT(); + if (this._inputValue) this.askGPT(GPTCallType.QUIZ); }; @action handleRenderClick = () => { @@ -290,12 +292,22 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() this.layoutDoc[`_${this._props.fieldKey}_usePath`] = undefined; }; - animateRes = (resIndex: number, newText: string) => { + animateRes = (resIndex: number, newText: string, callType: GPTCallType) => { if (resIndex < newText.length) { // const marks = this._editorView?.state.storedMarks ?? []; - this._outputValue += newText[resIndex]; + switch (callType) { + case GPTCallType.CHATCARD: + DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text += newText[resIndex]; + break; + case GPTCallType.QUIZ: + this._outputValue += newText[resIndex]; + break; + default: + return; + } + // this._editorView?.dispatch(this._editorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(this._outputValue)); - setTimeout(() => this.animateRes(resIndex + 1, newText), 20); + setTimeout(() => this.animateRes(resIndex + 1, newText, callType), 20); } }; @@ -303,18 +315,22 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() * Calls the GPT model to create QuizCards. Evaluates how similar the user's response is to the alternate * side of the flashcard. */ - askGPT = async (): Promise => { + 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; this._loading = true; + if (callType == GPTCallType.CHATCARD) { + DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = ''; + this.flipFlashcard(); + } try { - const res = await gptAPICall(queryText, GPTCallType.QUIZ); + const res = await gptAPICall(callType == GPTCallType.QUIZ ? queryText : questionText, callType); if (!res) { console.error('GPT call failed'); return; } - this.animateRes(0, '\n\n' + res); + this.animateRes(0, res, callType); // this._outputValue = res; console.log(res); } catch (err) { @@ -325,6 +341,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() layoutWidth = () => NumCast(this.layoutDoc.width, 200); layoutHeight = () => NumCast(this.layoutDoc.height, 200); + specificMenu = (): void => { + const cm = ContextMenu.Instance; + cm.addItem({ description: 'Create an Answer on the Back', event: () => this.askGPT(GPTCallType.CHATCARD), icon: 'pencil' }); + }; + render() { const clearButton = (which: string) => ( remove}> @@ -425,7 +446,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent()
@@ -436,6 +457,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() return (
{ this.hoverFlip('alternate'); @@ -446,6 +468,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() // onPointerUp={() => (this._isAnyChildContentActive = true)} > {displayBox(`${this.fieldKey}_${side === 0 ? 1 : 0}`, side, this._props.PanelWidth() - 3)} + {this._loading ? ( +
+ +
+ ) : null} {this.overlayAlternateIcon}
); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 8bf7b094f..a25249eac 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -111,6 +111,7 @@ export class DocumentViewInternal extends DocComponent(); private _titleRef = React.createRef(); private _dropDisposer?: DragManager.DragDropDisposer; @@ -501,21 +502,6 @@ export class DocumentViewInternal extends DocComponent => { - const queryText = RTFCast(DocCast(this.dataDoc[this.props.fieldKey + '_1']).text)?.Text; - try { - const res = await gptAPICall('Question: ' + StrCast(queryText), GPTCallType.CHATCARD); - if (!res) { - console.error('GPT call failed'); - return; - } - DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res; - console.log(res); - } catch (err) { - console.error('GPT call failed'); - } - }; - onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => { if (e && this.layoutDoc.layout_hideContextMenu && Doc.noviceMode) { e.preventDefault(); @@ -574,9 +560,6 @@ export class DocumentViewInternal extends DocComponent DocumentView.SetLightboxDoc(this.Document), icon: 'external-link-alt' }); } appearanceItems.push({ description: 'Pin', event: () => this._props.pinToPres(this.Document, {}), icon: 'eye' }); - if (this.Document._layout_isFlashcard) { - appearanceItems.push({ description: 'Create an answer on the back', event: () => this.askGPT(), icon: 'id-card' }); - } !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' }); !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'compass' }); -- cgit v1.2.3-70-g09d2 From 7594f649890d27d558be35c9d40a0aa8211aec67 Mon Sep 17 00:00:00 2001 From: alyssaf16 Date: Thu, 6 Jun 2024 22:20:24 -0400 Subject: Flashcard changes - menu added --- src/client/views/nodes/ComparisonBox.scss | 20 +---- src/client/views/nodes/ComparisonBox.tsx | 99 +++++++++++++++++----- src/client/views/nodes/DocumentView.tsx | 3 +- src/client/views/nodes/FieldView.tsx | 1 + .../nodes/formattedText/FormattedTextBox.scss | 6 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 1 + 6 files changed, 87 insertions(+), 43 deletions(-) (limited to 'src/client/views/nodes/DocumentView.tsx') diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss index f3dc0f68c..dc107b576 100644 --- a/src/client/views/nodes/ComparisonBox.scss +++ b/src/client/views/nodes/ComparisonBox.scss @@ -47,6 +47,7 @@ width: '91%'; height: '80%'; z-index: '-1'; + overscroll-behavior: contain; } .clip-div { @@ -241,24 +242,5 @@ } } } - - // .loading-circle { - // position: relative; - // width: 50px; - // height: 50px; - // border-radius: 50%; - // border: 3px solid #ccc; - // border-top-color: #333; - // animation: spin 1s infinite linear; - // } - - // @keyframes spin { - // 0% { - // transform: rotate(0deg); - // } - // 100% { - // transform: rotate(360deg); - // } - // } } } diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 6d4af2f3e..f844892c5 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -1,9 +1,9 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, makeObservable, observable } from 'mobx'; +import { action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { returnFalse, returnNone, setupMoveUpEvents } from '../../../ClientUtils'; +import { returnFalse, returnNone, returnTrue, setupMoveUpEvents } from '../../../ClientUtils'; import { emptyFunction } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; @@ -27,6 +27,7 @@ import { FormattedTextBox } from './formattedText/FormattedTextBox'; import ReactLoading from 'react-loading'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; +import { tickStep } from 'd3'; @observer export class ComparisonBox extends ViewBoxAnnotatableComponent() { @@ -37,7 +38,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() constructor(props: FieldViewProps) { super(props); makeObservable(this); - // this._isAnyChildContentActive = true; } @observable private _inputValue = ''; @@ -63,12 +63,19 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() @computed get clipHeight() { return NumCast(this.layoutDoc[this.clipHeightKey], 200); } + get revealOp() { + return this.layoutDoc[`_${this._props.fieldKey}_revealOp`]; + } get clipHeightKey() { return '_' + this._props.fieldKey + '_clipHeight'; } componentDidMount() { this._props.setContentViewBox?.(this); + reaction( + () => this._props.isSelected(), + selected => !selected && (this.childActive = false) + ); } protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => { this._disposers[disposerId]?.(); @@ -98,7 +105,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() emptyFunction, action((moveEv, doubleTap) => { if (doubleTap) { - this._isAnyChildContentActive = true; + 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); } @@ -106,7 +113,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() false, undefined, action(() => { - if (this._isAnyChildContentActive) return; + 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(); @@ -247,7 +254,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() hoverFlip = (side: string | undefined) => { if (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] === 'hover') this.layoutDoc[`_${this._props.fieldKey}_usePath`] = side; }; - /** * Creates the button used to flip the flashcards. */ @@ -259,7 +265,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() className="formattedTextBox-alternateButton" onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => { - console.log(this.layoutDoc[`_${this._props.fieldKey}_revealOp`]); if (!this.layoutDoc[`_${this._props.fieldKey}_revealOp`] || this.layoutDoc[`_${this._props.fieldKey}_revealOp`] === 'flip') { this.flipFlashcard(); @@ -271,6 +276,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() style={{ background: usepath === 'alternate' ? 'white' : 'black', color: usepath === 'alternate' ? 'black' : 'white', + display: 'inline-block', }}>
@@ -280,6 +286,37 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() ); } + @computed get flashcardMenu() { + return ( +
+ Flip to front side to use GPT
+ ) : ( +
Ask GPT to create an answer on the back side of the flashcard
+ ) + }> +
(!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? this.askGPT(GPTCallType.CHATCARD) : null)}> + +
+ + Hover to reveal
}> +
(this.revealOp === 'hover' ? (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip') : (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'hover'))}> + +
+
+ {this.overlayAlternateIcon} + + ); + } + + @action activateContent = () => { + this.childActive = true; + }; + @action handleRenderGPTClick = () => { // Call the GPT model and get the output this.layoutDoc[`_${this._props.fieldKey}_usePath`] = 'alternate'; @@ -320,8 +357,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() const rubricText = ' Rubric: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text); const queryText = questionText + ' UserAnswer: ' + this._inputValue + '. ' + rubricText; this._loading = true; + const doc = DocCast(this.dataDoc[this.props.fieldKey + '_0']); if (callType == GPTCallType.CHATCARD) { - DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = ''; + if (StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '') { + this._loading = false; + return; + } this.flipFlashcard(); } try { @@ -330,7 +371,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() console.error('GPT call failed'); return; } - this.animateRes(0, res, callType); + // this.animateRes(0, res, callType); + if (callType == GPTCallType.CHATCARD) { + DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res; + // this.flipFlashcard(); + } + if (callType == GPTCallType.QUIZ) this._outputValue = res; + // DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res; // this._outputValue = res; console.log(res); } catch (err) { @@ -341,10 +388,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() layoutWidth = () => NumCast(this.layoutDoc.width, 200); layoutHeight = () => NumCast(this.layoutDoc.height, 200); - specificMenu = (): void => { - const cm = ContextMenu.Instance; - cm.addItem({ description: 'Create an Answer on the Back', event: () => this.askGPT(GPTCallType.CHATCARD), icon: 'pencil' }); - }; + // specificMenu = (): void => { + // const cm = ContextMenu.Instance; + // cm.addItem({ description: 'Create an Answer on the Back', event: () => this.askGPT(GPTCallType.CHATCARD), icon: 'pencil' }); + // }; + @observable childActive = false; render() { const clearButton = (which: string) => ( @@ -378,12 +426,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2} NativeWidth={this.layoutWidth} NativeHeight={this.layoutHeight} - isContentActive={emptyFunction} + isContentActive={() => this.childActive} isDocumentActive={returnFalse} + dontSelect={returnTrue} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - styleProvider={this._isAnyChildContentActive ? this._props.styleProvider : this.docStyleProvider} + styleProvider={this.childActive ? this._props.styleProvider : this.docStyleProvider} hideLinkButton - pointerEvents={this._isAnyChildContentActive ? undefined : returnNone} + pointerEvents={this.childActive ? undefined : returnNone} />
{layoutString ? null : clearButton(whichSlot)}
// placeholder image if doc is missingleft: `${NumCast(this.layoutDoc.width, 200) - 33}px` @@ -394,7 +443,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() ); }; const displayBox = (which: string, index: number, cover: number) => ( -
this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which, index)}> +
{ + this.registerSliding(e, cover); + this.activateContent(); + }} + ref={ele => this.createDropTarget(ele, which, index)}> {displayDoc(which)}
); @@ -431,6 +488,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() {this._loading ? ( @@ -457,7 +515,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() return (
{ this.hoverFlip('alternate'); @@ -473,7 +531,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent()
) : null} - {this.overlayAlternateIcon} + {this.flashcardMenu} + {/* {this.overlayAlternateIcon} */}
); } @@ -491,7 +550,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() left: `calc(${this.clipWidth + '%'} - 0.5px)`, cursor: this.clipWidth < 5 ? 'e-resize' : this.clipWidth / 100 > (this._props.PanelWidth() - 5) / this._props.PanelWidth() ? 'w-resize' : undefined, }} - onPointerDown={e => !this._isAnyChildContentActive && this.registerSliding(e, this._props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ + onPointerDown={e => !this.childActive && this.registerSliding(e, this._props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ >
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index a25249eac..2f3357791 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -503,6 +503,7 @@ export class DocumentViewInternal extends DocComponent { + if (this._props.dontSelect?.()) return; if (e && this.layoutDoc.layout_hideContextMenu && Doc.noviceMode) { e.preventDefault(); e.stopPropagation(); @@ -1378,7 +1379,7 @@ export class DocumentView extends DocComponent() { screenToLocalScale = () => this._props.ScreenToLocalTransform().Scale; isSelected = () => this.IsSelected; select = (extendSelection: boolean, focusSelection?: boolean) => { - DocumentView.SelectView(this, extendSelection); + if (!this._props.dontSelect?.()) DocumentView.SelectView(this, extendSelection); if (focusSelection) { DocumentView.showDocument(this.Document, { willZoomCentered: true, diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 818d26956..138f00492 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -50,6 +50,7 @@ export interface FieldViewSharedProps { PanelHeight: () => number; isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events isContentActive: () => boolean | undefined; // whether document contents should handle pointer events + dontSelect: () => boolean | undefined; childFilters: () => string[]; childFiltersByRanges: () => string[]; styleProvider: Opt; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 8ac8c2c5f..54643b4a5 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -89,14 +89,14 @@ audiotag:hover { right: 0; bottom: 0; width: 15; - height: 15; + height: 22; cursor: default; } .formattedTextBox-flip { align-items: center; position: absolute; - right: 3px; - bottom: 1px; + right: 2px; + bottom: 4px; } .formattedTextBox-outer { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 82133a06e..3e2befb5f 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -816,6 +816,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent BoolCast(anchor.followLinkToggle); specificContextMenu = (e: React.MouseEvent): void => { + if (this._props.dontSelect?.()) return; const cm = ContextMenu.Instance; let target = e.target as any; // hrefs are stored on the database of the node that wraps the hyerlink -- cgit v1.2.3-70-g09d2 From d1b7e29761fa0395263bff3521f148170659ff62 Mon Sep 17 00:00:00 2001 From: alyssaf16 Date: Fri, 7 Jun 2024 21:48:34 -0400 Subject: Flashcard menus --- src/client/views/MainView.tsx | 1 + .../views/collections/CollectionCarouselView.scss | 42 ++++++++++++- .../views/collections/CollectionCarouselView.tsx | 53 ++++++++++++---- src/client/views/nodes/ComparisonBox.tsx | 73 +++++++++++++++++----- src/client/views/nodes/DocumentView.tsx | 14 ++--- 5 files changed, 147 insertions(+), 36 deletions(-) (limited to 'src/client/views/nodes/DocumentView.tsx') diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 31d88fb87..541b15006 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -548,6 +548,7 @@ export class MainView extends ObservableReactComponent<{}> { fa.faRobot, fa.faSatellite, fa.faStar, + fa.faFilePen, ] ); } diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss index 975b352cf..c4679e888 100644 --- a/src/client/views/collections/CollectionCarouselView.scss +++ b/src/client/views/collections/CollectionCarouselView.scss @@ -50,7 +50,7 @@ } .carouselView-star { top: 0; - right: 20; + left: 0; } .carouselView-remove { top: 80%; @@ -60,6 +60,46 @@ top: 80%; right: 52%; } +.carouselView-quiz { + position: absolute; + display: flex; + top: 5px; + right: 8px; + &:hover { + color: white; + } +} + +.carouselView-practice { + position: absolute; + display: flex; + top: 22px; + right: 8px; + &:hover { + color: white; + } +} +.carouselView-starFilter { + position: absolute; + display: flex; + top: 40px; + right: 7px; + &:hover { + color: white; + } +} + +.carouselView-menu { + position: absolute; + display: flex; + top: 2px; + right: 2px; + width: 30; + height: 60; + border-radius: 5px; + color: rgba(255, 255, 255, 0.5); + background: rgba(0, 0, 0, 0.1); +} .carouselView-back:hover, .carouselView-fwd:hover { diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 53d14e6e0..8da808065 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -19,6 +19,7 @@ import { FieldViewProps } from '../nodes/FieldView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import './CollectionCarouselView.scss'; import { CollectionSubView } from './CollectionSubView'; +import { Tooltip } from '@mui/material'; enum cardMode { PRACTICE = 'practice', @@ -143,22 +144,26 @@ export class CollectionCarouselView extends CollectionSubView() { captionWidth = () => this._props.PanelWidth() - 2 * this.marginX; setFilterMode = (mode: cardMode) => { this.layoutDoc.filterOp = mode; + console.log('MODE' + mode); + console.log('FILT' + this.layoutDoc.filterOp + ';'); if (mode == cardMode.STAR) this.move(1); - if (mode == cardMode.QUIZ) this.carouselItems?.map(doc => (doc.layout[this.sideField] = undefined)); + if (mode == cardMode.QUIZ) { + this.carouselItems?.map(doc => (doc.layout[this.sideField] = undefined)); + } this.carouselItems?.map(doc => (doc.layout[this.practiceField] = undefined)); }; - specificMenu = (): void => { - const cm = ContextMenu.Instance; + // specificMenu = (): void => { + // const cm = ContextMenu.Instance; - const revealOptions = cm.findByDescription('Filter Flashcards'); - const revealItems: ContextMenuProps[] = revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : []; - revealItems.push({description: 'All', event: () => {this.setFilterMode(cardMode.ALL);}, icon: 'layer-group',}); // prettier-ignore - revealItems.push({description: 'Star', event: () => {this.setFilterMode(cardMode.STAR);}, icon: 'star',}); // prettier-ignore - revealItems.push({description: 'Practice Mode', event: () => {this.setFilterMode(cardMode.PRACTICE);}, icon: 'check',}); // prettier-ignore - cm.addItem({description: 'Quiz Cards', event: () => {this.setFilterMode(cardMode.QUIZ);}, icon: 'pencil',}); // prettier-ignore - !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' }); - //cm.addItem({description: 'Quiz Cards', event: () => {this.layoutDoc.filterOp = cardMode.QUIZ; this.clearContent()}); - }; + // const revealOptions = cm.findByDescription('Filter Flashcards'); + // const revealItems: ContextMenuProps[] = revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : []; + // revealItems.push({description: 'All', event: () => {this.setFilterMode(cardMode.ALL);}, icon: 'layer-group',}); // prettier-ignore + // revealItems.push({description: 'Star', event: () => {this.setFilterMode(cardMode.STAR);}, icon: 'star',}); // prettier-ignore + // revealItems.push({description: 'Practice Mode', event: () => {this.setFilterMode(cardMode.PRACTICE);}, icon: 'check',}); // prettier-ignore + // cm.addItem({description: 'Quiz Cards', event: () => {this.setFilterMode(cardMode.QUIZ);}, icon: 'pencil',}); // prettier-ignore + // !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' }); + // //cm.addItem({description: 'Quiz Cards', event: () => {this.layoutDoc.filterOp = cardMode.QUIZ; this.clearContent()}); + // }; @computed get content() { const index = NumCast(this.layoutDoc._carousel_index); const curDoc = this.carouselItems?.[index]; @@ -226,6 +231,28 @@ export class CollectionCarouselView extends CollectionSubView() { ); } + @computed get menu() { + return ( +
+ +
(this.layoutDoc.filterOp === cardMode.QUIZ ? this.setFilterMode(cardMode.ALL) : this.setFilterMode(cardMode.QUIZ))}> + +
+
+ +
(this.layoutDoc.filterOp === cardMode.PRACTICE ? this.setFilterMode(cardMode.ALL) : this.setFilterMode(cardMode.PRACTICE))}> + +
+
+ +
(!this.layoutDoc.filterOp ? this.setFilterMode(cardMode.ALL) : this.setFilterMode(cardMode.STAR))}> + +
+
+
+ ); + } + render() { return (
Recently missed!

+ {this.menu} {this.Document._chromeHidden || !this.layoutDoc.filterOp ? null : this.buttons}
); diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index f844892c5..084723d56 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -45,6 +45,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() @observable private _loading = false; @observable private _errorMessage = ''; @observable private _outputMessage = ''; + @observable private _isEmpty = false; + + public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined; @action handleInputChange = (e: React.ChangeEvent) => { this._inputValue = e.target.value; @@ -162,23 +165,29 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() }; clearDoc = undoable((fieldKey: string) => { - delete this.dataDoc[fieldKey]; - this.dataDoc[fieldKey] = 'empty'; + // delete this.dataDoc[fieldKey]; + this.dataDoc[fieldKey] = undefined; + this._isEmpty = true; + // this.dataDoc[fieldKey] = 'empty'; + console.log('HERE' + fieldKey + ';'); }, 'clear doc'); // clearDoc = (fieldKey: string) => delete this.dataDoc[fieldKey]; moveDoc = (doc: Doc, addDocument: (document: Doc | Doc[]) => boolean, which: string) => this.remDoc(doc, which) && addDocument(doc); addDoc = (doc: Doc, which: string) => { - if (this.dataDoc[which] && this.dataDoc[which] !== 'empty') return false; + if (this.dataDoc[which] && !this._isEmpty) return false; this.dataDoc[which] = doc; return true; }; remDoc = (doc: Doc, which: string) => { if (this.dataDoc[which] === doc) { - this.dataDoc[which] = 'empty'; + this._isEmpty = true; + // this.dataDoc[which] = 'empty'; + console.log('HEREEEE'); this.dataDoc[which] = undefined; return true; } + console.log('FALSE'); return false; }; @@ -268,8 +277,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() if (!this.layoutDoc[`_${this._props.fieldKey}_revealOp`] || this.layoutDoc[`_${this._props.fieldKey}_revealOp`] === 'flip') { this.flipFlashcard(); - console.log('Print Front of cards: ' + (RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text ?? '')); - console.log('Print Back of cards: ' + (RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text ?? '')); + // console.log('Print Front of cards: ' + (RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text ?? '')); + // console.log('Print Back of cards: ' + (RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text ?? '')); } }) } @@ -297,10 +306,31 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent()
Ask GPT to create an answer on the back side of the flashcard
) }> -
(!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? this.askGPT(GPTCallType.CHATCARD) : null)}> +
(!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? this.askGPT(GPTCallType.CHATCARD) : null)}>
+ Create a flashcard pile
}> +
{ + const collectionArr: Doc[] = []; + collectionArr.push(this.Document); + const newCol = Docs.Create.CarouselDocument(collectionArr, { + _width: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 250), + _height: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 200), + _layout_fitWidth: false, + _layout_autoHeight: true, + }); + newCol['x'] = e.clientX - 820; + newCol['y'] = e.clientY - 640; + this._props.addDocument?.(newCol); + this._props.removeDocument?.(this.Document); + this.Document.embedContainer = newCol; + }}> + +
+ Hover to reveal}>
()
- {this.overlayAlternateIcon} + {/* Remove this side of the flashcard}> +
this.closeDown(e, this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this._props.fieldKey + '_1' : this._props.fieldKey + '_0')}> + +
+
*/} + {/* {this.overlayAlternateIcon} */} ); } - + // this.closeDown(e, this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this.fieldKey + '_0' : this.fieldKey + '_1')} @action activateContent = () => { this.childActive = true; }; @@ -434,7 +471,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() hideLinkButton pointerEvents={this.childActive ? undefined : returnNone} /> -
{layoutString ? null : clearButton(whichSlot)}
+ {/*
{layoutString ? null : clearButton(whichSlot)}
*/} // placeholder image if doc is missingleft: `${NumCast(this.layoutDoc.width, 200) - 33}px` ) : (
@@ -452,7 +489,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() this.activateContent(); }} ref={ele => this.createDropTarget(ele, which, index)}> - {displayDoc(which)} + {!this._isEmpty ? displayDoc(which) : null} + {/* {this.dataDoc[this.fieldKey + '_0'] !== 'empty' ? displayDoc(which) : null} */}
); @@ -460,7 +498,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() const side = this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? 1 : 0; // add text box to each side when comparison box is first created - if (!(this.dataDoc[this.fieldKey + '_0'] || this.dataDoc[this.fieldKey + '_0'] === 'empty')) { + // (!this.dataDoc[this.fieldKey + '_0'] && this.dataDoc[this._props.fieldKey + '_0'] !== 'empty') + if (!this.dataDoc[this.fieldKey + '_0'] && !this._isEmpty) { const dataSplit = StrCast(this.dataDoc.data).split('Answer'); const newDoc = Docs.Create.TextDocument(dataSplit[1]); // if there is text from the pdf ai cards, put the question on the front side. @@ -468,7 +507,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() newDoc[DocData].text = dataSplit[1]; this.addDoc(newDoc, this.fieldKey + '_0'); } - if (!(this.dataDoc[this.fieldKey + '_1'] || this.dataDoc[this.fieldKey + '_1'] === 'empty')) { + if (!this.dataDoc[this.fieldKey + '_1'] && !this._isEmpty) { const dataSplit = StrCast(this.dataDoc.data).split('Answer'); const newDoc = Docs.Create.TextDocument(dataSplit[0]); // if there is text from the pdf ai cards, put the answer on the alternate side. @@ -478,6 +517,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() } // render the QuizCards + console.log('GERE' + DocCast(this.Document.embedContainer).filterOp); if (DocCast(this.Document.embedContainer) && DocCast(this.Document.embedContainer).filterOp === 'quiz') { const text = StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text); return ( @@ -489,6 +529,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() value={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this._outputValue : this._inputValue} onChange={this.handleInputChange} onScroll={e => e.stopPropagation()} + placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''} readOnly={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate'}> {this._loading ? ( @@ -516,7 +557,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent()
{ this.hoverFlip('alternate'); }} @@ -531,8 +572,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent()
) : null} - {this.flashcardMenu} - {/* {this.overlayAlternateIcon} */} + {this._props.isContentActive() ? this.flashcardMenu : null} + {this.overlayAlternateIcon} ); } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 2f3357791..d4c31a5b3 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -566,13 +566,13 @@ export class DocumentViewInternal extends DocComponent { 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.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...'); -- cgit v1.2.3-70-g09d2 From 14296a5e2b9726d449a9ddf59a3af0a6945e3cb7 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 25 Sep 2024 13:37:36 -0400 Subject: updated carousel to show same filter as in context menu bar. --- src/client/util/CurrentUserUtils.ts | 2 - src/client/views/StyleProvider.tsx | 2 +- .../views/collections/CollectionCarouselView.tsx | 44 ++++++++++++++++++---- src/client/views/nodes/DocumentView.tsx | 1 + 4 files changed, 39 insertions(+), 10 deletions(-) (limited to 'src/client/views/nodes/DocumentView.tsx') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 3e7921a08..03975a5e7 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -981,12 +981,10 @@ pie title Minerals in my tap water doc.fontColor ?? (doc.fontColor = "black"); doc.fontHighlight ?? (doc.fontHighlight = ""); doc.defaultAclPrivate ?? (doc.defaultAclPrivate = false); - doc.savedFilters ?? (doc.savedFilters = new List()); doc.userBackgroundColor ?? (doc.userBackgroundColor = Colors.DARK_GRAY); doc.userVariantColor ?? (doc.userVariantColor = Colors.MEDIUM_BLUE); doc.userColor ?? (doc.userColor = Colors.LIGHT_GRAY); doc.userTheme ?? (doc.userTheme = ColorScheme.Dark); - doc.filterDocCount = 0; doc.treeView_FreezeChildren = "remove|add"; doc.activePage = doc.activeDashboard === undefined ? 'home': doc.activePage; diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 3545afcee..16f6aa40b 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -318,7 +318,7 @@ export function DefaultStyleProvider(doc: Opt, props: Opt ClientUtils.IsRecursiveFilter(f) && f !== ClientUtils.noDragDocsFilter).length || childFiltersByRanges?.().length ? 'orange' // 'inheritsFilter' : undefined; - return !showFilterIcon ? null : ( + return !showFilterIcon || props?.hideFilterStatus ? null : (
Doc.setDocFilter(this.Document, 'tags', this.starField, 'check', true); setColor = (mode: practiceMode | cardMode, which: string) => (which === mode ? 'white' : 'light gray'); + @computed get filterDoc() { + return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter'); + } + filterHeight = () => NumCast(this.filterDoc?.height); + filterWidth = () => (!this.filterDoc ? 1 : (this.filterHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height)); @computed get menu() { const curDoc = this.carouselItems?.[this.carouselIndex]; return (
- {/* NOTE: this should really show the same filter as in the workspace View menu, not just something hardwire for 'star' */} - -
this.toggleFilterMode()}> - + {!this.filterDoc ? (null) : ( +
+
- + )}
this.togglePracticeMode(practiceMode.QUIZ)}> diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 884220722..b85cb22bb 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -68,6 +68,7 @@ export interface DocumentViewProps extends FieldViewSharedProps { contentPointerEvents?: Property.PointerEvents | undefined; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents dontCenter?: 'x' | 'y' | 'xy'; showTags?: boolean; + hideFilterStatus?: boolean; childHideDecorationTitle?: boolean; childHideResizeHandles?: boolean; childDragAction?: dropActionType; // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar. -- cgit v1.2.3-70-g09d2 From 11244a698bce594982ee5dca55b9695bb774451c Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 7 Oct 2024 18:13:38 -0400 Subject: moved all quiz code out of LabelBox and ImageBox and into StyleProviderQuiz. changed quizBoxes and quizMode to be stored as Doc metadata. Extended styles to cover contextMenuItems. remove this.setListening() from comparisonBox until contextMenu selectedVal is fixed. --- src/client/views/StyleProp.ts | 2 + src/client/views/StyleProvider.tsx | 10 +- src/client/views/StyleProviderQuiz.scss | 40 ++++ src/client/views/StyleProviderQuiz.tsx | 391 +++++++++++++++++++++++++++++++ src/client/views/nodes/ComparisonBox.tsx | 1 - src/client/views/nodes/DocumentView.tsx | 3 + src/client/views/nodes/FieldView.tsx | 2 + src/client/views/nodes/ImageBox.scss | 41 ---- src/client/views/nodes/ImageBox.tsx | 365 ++--------------------------- src/client/views/nodes/LabelBox.tsx | 52 +--- 10 files changed, 466 insertions(+), 441 deletions(-) create mode 100644 src/client/views/StyleProviderQuiz.scss create mode 100644 src/client/views/StyleProviderQuiz.tsx (limited to 'src/client/views/nodes/DocumentView.tsx') diff --git a/src/client/views/StyleProp.ts b/src/client/views/StyleProp.ts index dd5b98cfe..44d3bf757 100644 --- a/src/client/views/StyleProp.ts +++ b/src/client/views/StyleProp.ts @@ -21,4 +21,6 @@ export enum StyleProp { FontFamily = 'fontFamily', // font family of text FontWeight = 'fontWeight', // font weight of text Highlighting = 'highlighting', // border highlighting + ContextMenuItems = 'contextMenuItems', // menu items to add to context menu + AnchorMenuItems = 'anchorMenuItems', } diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 16f6aa40b..44bea57eb 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -21,6 +21,7 @@ import { TreeSort } from './collections/TreeSort'; import { Colors } from './global/globalEnums'; import { DocumentView, DocumentViewProps } from './nodes/DocumentView'; import { FieldViewProps } from './nodes/FieldView'; +import { styleProviderQuiz } from './StyleProviderQuiz'; import { StyleProp } from './StyleProp'; import './StyleProvider.scss'; import { TagsView } from './TagsView'; @@ -89,6 +90,7 @@ export function DefaultStyleProvider(doc: Opt, props: Opt, props: Opt styleProvider?.(doc, props, StyleProp.Color) as string; const opacity = () => styleProvider?.(doc, props, StyleProp.Opacity); const layoutShowTitle = () => styleProvider?.(doc, props, StyleProp.ShowTitle) as string; + + // bcz: For now, this is how to add custom-stylings (like a Quiz styling) for app-specific purposes. The quiz styling will short-circuit + // the regular stylings for items that it controls (eg., things with a quiz field, or images) + const quizProp = styleProviderQuiz.quizStyleProvider(doc, props, property); + if (quizProp !== undefined) return quizProp; + // prettier-ignore switch (property.split(':')[0]) { case StyleProp.TreeViewIcon: { @@ -318,7 +326,7 @@ export function DefaultStyleProvider(doc: Opt, props: Opt ClientUtils.IsRecursiveFilter(f) && f !== ClientUtils.noDragDocsFilter).length || childFiltersByRanges?.().length ? 'orange' // 'inheritsFilter' : undefined; - return !showFilterIcon || props?.hideFilterStatus ? null : ( + return !showFilterIcon || hideFilterStatus ? null : (
{ + try { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = () => resolve(reader.result as string); + reader.onerror = error => reject(error); + }); + } catch (error) { + console.error('Error:', error); + throw error; + } + } + /** + * Creates label boxes over text on the image to be filled in. + * @param boxes + * @param texts + */ + async function createBoxes(img: ImageBox, boxes: [[[number, number]]], texts: [string]) { + img.Document._quizBoxes = new List([]); + for (let i = 0; i < boxes.length; i++) { + const coords = boxes[i] ? boxes[i] : []; + const width = coords[1][0] - coords[0][0]; + const height = coords[2][1] - coords[0][1]; + const text = texts[i]; + + const newCol = Docs.Create.LabelDocument({ + _width: width, + _height: height, + _layout_fitWidth: true, + title: '', + }); + const scaling = 1 / (img._props.NativeDimScaling?.() || 1); + newCol.x = coords[0][0] + NumCast(img.marqueeref.current?.left) * scaling; + newCol.y = coords[0][1] + NumCast(img.marqueeref.current?.top) * scaling; + + newCol.zIndex = 1000; + newCol.forceActive = true; + newCol.quiz = text; + newCol[DocData].textTransform = 'none'; + Doc.AddDocToList(img.Document, '_quizBoxes', newCol); + img.addDocument(newCol); + // img._loading = false; + } + } + + /** + * Calls backend to find any text on an image. Gets the text and the + * coordinates of the text and creates label boxes at those locations. + * @param quiz + * @param i + */ + async function pushInfo(imgBox: ImageBox, quiz: quizMode, i?: string) { + imgBox.Document._quizMode = quiz; + const quizBoxes = DocListCast(imgBox.Document.quizBoxes); + if (!quizBoxes.length) { + // this._loading = true; + + const img = { + file: i ? i : imgBox.paths[0], + drag: i ? 'drag' : 'full', + smart: quiz, + }; + const response = await axios.post('http://localhost:105/labels/', img, { + headers: { + 'Content-Type': 'application/json', + }, + }); + if (response.data['boxes'].length != 0) { + createBoxes(imgBox, response.data['boxes'], response.data['text']); + } else { + // this._loading = false; + } + } else quizBoxes.forEach(box => (box.hidden = false)); + } + + async function createCanvas(img: ImageBox) { + const canvas = document.createElement('canvas'); + const scaling = 1 / (img._props.NativeDimScaling?.() || 1); + const w = AnchorMenu.Instance.marqueeWidth * scaling; + const h = AnchorMenu.Instance.marqueeHeight * scaling; + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext('2d'); // draw image to canvas. scale to target dimensions + if (ctx) { + img.imageRef && ctx.drawImage(img.imageRef, NumCast(img.marqueeref.current?.left) * scaling, NumCast(img.marqueeref.current?.top) * scaling, w, h, 0, 0, w, h); + } + const blob = await ImageUtility.canvasToBlob(canvas); + return selectUrlToBase64(blob); + } + /** + * Create flashcards from an image. + */ + async function getImageDesc(img: ImageBox) { + // this._loading = true; + try { + const hrefBase64 = await createCanvas(img); + const response = await gptImageLabel(hrefBase64, 'Make flashcards out of this image with each question and answer labeled as "question" and "answer". Do not label each flashcard and do not include asterisks: '); + AnchorMenu.Instance.transferToFlashcard(response, NumCast(img.layoutDoc['x']), NumCast(img.layoutDoc['y'])); + } catch (error) { + console.log('Error', error); + } + // this._loading = false; + } + + /** + * Calls the createCanvas and pushInfo methods to convert the + * image to a form that can be passed to GPT and find the locations + * of the text. + */ + async function makeLabels(img: ImageBox) { + try { + const hrefBase64 = await createCanvas(img); + pushInfo(img, quizMode.NORMAL, hrefBase64); + } catch (error) { + console.log('Error', error); + } + } + + /** + * Determines whether two words should be considered + * the same, allowing minor typos. + * @param str1 + * @param str2 + * @returns + */ + function levenshteinDistance(str1: string, str2: string) { + const len1 = str1.length; + const len2 = str2.length; + const dp = Array.from(Array(len1 + 1), () => Array(len2 + 1).fill(0)); + + if (len1 === 0) return len2; + if (len2 === 0) return len1; + + for (let i = 0; i <= len1; i++) dp[i][0] = i; + for (let j = 0; j <= len2; j++) dp[0][j] = j; + + for (let i = 1; i <= len1; i++) { + for (let j = 1; j <= len2; j++) { + const cost = str1[i - 1] === str2[j - 1] ? 0 : 1; + dp[i][j] = Math.min( + dp[i - 1][j] + 1, // deletion + dp[i][j - 1] + 1, // insertion + dp[i - 1][j - 1] + cost // substitution + ); + } + } + + return dp[len1][len2]; + } + + /** + * Different algorithm for determining string similarity. + * @param str1 + * @param str2 + * @returns + */ + function jaccardSimilarity(str1: string, str2: string) { + const set1 = new Set(str1.split(' ')); + const set2 = new Set(str2.split(' ')); + + const intersection = new Set([...set1].filter(x => set2.has(x))); + const union = new Set([...set1, ...set2]); + + return intersection.size / union.size; + } + + /** + * Averages the jaccardSimilarity and levenshteinDistance scores + * to determine string similarity for the labelboxes answers and + * the users response. + * @param str1 + * @param str2 + * @returns + */ + function stringSimilarity(str1: string, str2: string) { + const levenshteinDist = levenshteinDistance(str1, str2); + const levenshteinScore = 1 - levenshteinDist / Math.max(str1.length, str2.length); + + const jaccardScore = jaccardSimilarity(str1, str2); + + // Combine the scores with a higher weight on Jaccard similarity + return 0.5 * levenshteinScore + 0.5 * jaccardScore; + } + /** + * Returns whether two strings are similar + * @param input + * @param target + * @returns + */ + function compareWords(input: string, target: string) { + const distance = stringSimilarity(input.toLowerCase(), target.toLowerCase()); + return distance >= 0.7; + } + + /** + * GPT returns a hex color for what color the label box should be based on + * the correctness of the users answer. + * @param inputString + * @returns + */ + function extractHexAndSentences(inputString: string) { + // Regular expression to match a hexadecimal number at the beginning followed by a period and sentences + const regex = /^#([0-9A-Fa-f]+)\.\s*(.+)$/s; + const match = inputString.match(regex); + + if (match) { + const hexNumber = match[1]; + const sentences = match[2].trim(); + return { hexNumber, sentences }; + } else { + return { error: 'The input string does not match the expected format.' }; + } + } + /** + * Check whether the contents of the label boxes on an image are correct. + */ + function check(img: ImageBox) { + //this._loading = true; + img.quizBoxes.forEach(async doc => { + const input = StrCast(doc[DocData].title); + if (img.quizMode == quizMode.SMART && input) { + const questionText = 'Question: What was labeled in this image?'; + const rubricText = ' Rubric: ' + StrCast(doc.quiz); + const queryText = + questionText + + ' UserAnswer: ' + + input + + '. ' + + rubricText + + '. One sentence and evaluate based on meaning, not wording. Provide a hex color at the beginning with a period after it on a scale of green (minor details missed) to red (big error) for how correct the answer is. Example: "#FFFFFF. Pasta is delicious."'; + const response = await gptAPICall(queryText, GPTCallType.QUIZ); + const hexSent = extractHexAndSentences(response); + doc.quiz = hexSent.sentences?.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric'); + doc.backgroundColor = '#' + hexSent.hexNumber; + } else { + const match = compareWords(input, StrCast(doc.quiz).trim()); + doc.backgroundColor = match ? '#11c249' : '#eb2d2d'; + } + }); + //this._loading = false; + } + + function redo(img: ImageBox) { + img.quizBoxes.forEach(doc => { + doc[DocData].title = ''; + doc.backgroundColor = '#e4e4e4'; + }); + } + + /** + * Get rid of all the label boxes on the images. + */ + function exitQuizMode(img: ImageBox) { + img.Document._quizMode = quizMode.NONE; + DocListCast(img.Document._quizBoxes).forEach(box => { + box.hidden = true; + }); + } + + export function quizStyleProvider(doc: Opt, props: Opt, property: string) { + const editLabelAnswer = (qdoc: Doc) => { + // when click the pencil, set the text to the quiz content. when click off, set the quiz text to that and set textbox to nothing. + if (!qdoc._editLabel) { + qdoc.title = StrCast(qdoc.quiz); + } else { + qdoc.quiz = StrCast(qdoc.title); + qdoc.title = ''; + } + qdoc._editLabel = !qdoc._editLabel; + }; + const editAnswer = (qdoc: Opt) => { + return ( + + {qdoc?._editLabel ? 'save' : 'edit correct answer'} +
+ }> +
setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => qdoc && editLabelAnswer(qdoc))}> + +
+ + ); + }; + const answerIcon = (qdoc: Opt) => { + return ( + + {StrCast(qdoc?.quiz ?? '')} +
+ }> +
+ + +
+
+ ); + }; + const checkIcon = (img: ImageBox) => ( + Check
}> +
check(img)}> + +
+ + ); + const redoIcon = (img: ImageBox) => ( + Redo
}> +
redo(img)}> + +
+
+ ); + + const imgBox = props?.DocumentView?.().ComponentView as ImageBox; + switch (property) { + case StyleProp.Decorations: + { + if (doc?.quiz) { + // this should only be set on Labels that are part of an image quiz + return ( + <> + {editAnswer(doc?.[DocData])} + {answerIcon(doc)} + + ); + } else if (imgBox?.Document._quizMode && imgBox.Document._quizMode !== quizMode.NONE) { + return ( + <> + {checkIcon(imgBox)} + {redoIcon(imgBox)} + + ); + } + } + break; + case StyleProp.ContextMenuItems: + if (imgBox) { + const quizes: ContextMenuProps[] = []; + quizes.push({ + description: 'Smart Check', + event: doc?.quizMode == quizMode.NONE ? () => pushInfo(imgBox, quizMode.SMART) : () => exitQuizMode(imgBox), + icon: 'pen-to-square', + }); + quizes.push({ + description: 'Normal', + event: doc?.quizMode == quizMode.NONE ? () => pushInfo(imgBox, quizMode.NORMAL) : () => exitQuizMode(imgBox), + icon: 'pencil', + }); + ContextMenu.Instance?.addItem({ description: 'Quiz Mode', subitems: quizes, icon: 'file-pen' }); + } + break; + case StyleProp.AnchorMenuItems: + if (imgBox) { + AnchorMenu.Instance.gptFlashcards = () => getImageDesc(imgBox); + AnchorMenu.Instance.makeLabels = () => makeLabels(props?.DocumentView?.().ComponentView as ImageBox); + } + } + return undefined; + } +} diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 9fb8bc4d6..c32bbc803 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -44,7 +44,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() constructor(props: FieldViewProps) { super(props); makeObservable(this); - this.setListening(); } @observable private _inputValue = ''; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 5054432a9..04a31fd83 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -545,6 +545,9 @@ export class DocumentViewInternal extends DocComponent ContextMenu.Instance.addItem(item)); + const customScripts = Cast(this.Document.contextMenuScripts, listSpec(ScriptField), []); StrListCast(this.Document.contextMenuLabels).forEach((label, i) => cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.Document, scriptContext: this._props.scriptContext }), icon: 'sticky-note' }) diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index ca783d034..170966471 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -14,6 +14,7 @@ import { DocumentView } from './DocumentView'; import { FocusViewOptions } from './FocusViewOptions'; import { OpenWhere } from './OpenWhere'; import { WebField } from '../../../fields/URLField'; +import { ContextMenuProps } from '../ContextMenuItem'; export type FocusFuncType = (doc: Doc, options: FocusViewOptions) => Opt; export type StyleProviderFuncType = ( @@ -23,6 +24,7 @@ export type StyleProviderFuncType = ( property: string ) => | Opt + | ContextMenuProps[] | { clipPath: string; jsx: JSX.Element } | JSX.Element | JSX.IntrinsicElements diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 4d199b360..3ffda5a35 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -139,44 +139,3 @@ .imageBox-fadeBlocker-hover { opacity: 0; } - -.loading-spinner { - position: absolute; - display: flex; - justify-content: center; - align-items: center; - height: 100%; - width: 100%; - // left: 50%; - // top: 50%; - z-index: 200; - font-size: 20px; - font-weight: bold; - color: #17175e; -} - -.check-icon { - position: absolute; - right: 40; - bottom: 10; - color: green; - display: inline-block; - font-size: 20px; - overflow: hidden; -} - -.redo-icon { - position: absolute; - right: 10; - bottom: 10; - color: black; - display: inline-block; - font-size: 20px; - overflow: hidden; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 31f6df2ea..0b474076b 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -17,13 +17,11 @@ import { Cast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Type import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction } from '../../../Utils'; -import { gptAPICall, GPTCallType, gptImageLabel } from '../../apis/gpt/GPT'; import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocUtils, FollowLinkScript } from '../../documents/DocUtils'; import { Networking } from '../../Network'; import { DragManager } from '../../util/DragManager'; -import { dropActionType } from '../../util/DropActionTypes'; import { SnappingManager } from '../../util/SnappingManager'; import { undoable, undoBatch } from '../../util/UndoManager'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; @@ -38,16 +36,8 @@ import { StyleProp } from '../StyleProp'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; -import { ImageUtility } from './generativeFill/generativeFillUtils/ImageHandler'; import './ImageBox.scss'; import { OpenWhere } from './OpenWhere'; -// import stringSimilarity from 'string-similarity'; - -enum quizMode { - SMART = 'smart', - NORMAL = 'normal', - NONE = 'none', -} export class ImageEditorData { // eslint-disable-next-line no-use-before-define @@ -79,25 +69,24 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); } + _ffref = React.createRef(); private _ignoreScroll = false; private _forcedScroll = false; private _dropDisposer?: DragManager.DragDropDisposer; private _disposers: { [name: string]: IReactionDisposer } = {}; private _getAnchor: (savedAnnotations: Opt>, addAsAnnotation: boolean) => Opt = () => undefined; private _overlayIconRef = React.createRef(); - private _marqueeref = React.createRef(); private _mainCont: React.RefObject = React.createRef(); private _annotationLayer: React.RefObject = React.createRef(); - private _imageRef: HTMLImageElement | null = null; //
}> -
- -
- - ); - } - - @computed get redoIcon() { - return ( - Redo
}> -
- -
- - ); - } - - /** - * Returns whether two strings are similar - * @param input - * @param target - * @returns - */ - compareWords = (input: string, target: string) => { - const distance = this.stringSimilarity(input.toLowerCase(), target.toLowerCase()); - return distance >= 0.7; - }; - - /** - * GPT returns a hex color for what color the label box should be based on - * the correctness of the users answer. - * @param inputString - * @returns - */ - extractHexAndSentences = (inputString: string) => { - // Regular expression to match a hexadecimal number at the beginning followed by a period and sentences - const regex = /^#([0-9A-Fa-f]+)\.\s*(.+)$/s; - const match = inputString.match(regex); - - if (match) { - const hexNumber = match[1]; - const sentences = match[2].trim(); - return { hexNumber, sentences }; - } else { - return { error: 'The input string does not match the expected format.' }; - } - }; - - /** - * Check whether the contents of the label boxes on an image are correct. - */ - check = () => { - this._loading = true; - this._quizBoxes.forEach(async doc => { - const input = StrCast(doc[DocData].title); - if (this._quizMode == quizMode.SMART && input) { - const questionText = 'Question: What was labeled in this image?'; - const rubricText = ' Rubric: ' + StrCast(doc.quiz); - const queryText = - questionText + - ' UserAnswer: ' + - input + - '. ' + - rubricText + - '. One sentence and evaluate based on meaning, not wording. Provide a hex color at the beginning with a period after it on a scale of green (minor details missed) to red (big error) for how correct the answer is. Example: "#FFFFFF. Pasta is delicious."'; - const response = await gptAPICall(queryText, GPTCallType.QUIZ); - const hexSent = this.extractHexAndSentences(response); - doc.quiz = hexSent.sentences?.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric'); - doc.backgroundColor = '#' + hexSent.hexNumber; - } else { - const match = this.compareWords(input, StrCast(doc.quiz)); - doc.backgroundColor = match ? '#11c249' : '#eb2d2d'; - } - doc.showQuiz = true; - }); - this._loading = false; - }; - - redo = () => { - this._quizBoxes.forEach(doc => { - doc[DocData].title = ''; - doc.backgroundColor = '#e4e4e4'; - doc.showQuiz = false; - }); - }; - - /** - * Get rid of all the label boxes on the images. - */ - exitQuizMode = () => { - this._quizMode = quizMode.NONE; - this._quizBoxes.forEach(doc => { - this.removeDocument?.(doc); - }); - this._quizBoxes = []; - }; - - @action - setRef = (iref: HTMLImageElement | null) => { - this._imageRef = iref; - }; - specificContextMenu = (): void => { const field = Cast(this.dataDoc[this.fieldKey], ImageField); if (field) { const funcs: ContextMenuProps[] = []; - const quizes: ContextMenuProps[] = []; - quizes.push({ - description: 'Smart Check', - event: this._quizMode == quizMode.NONE ? () => this.pushInfo(quizMode.SMART) : this.exitQuizMode, - icon: 'pen-to-square', - }); - quizes.push({ - description: 'Normal', - event: this._quizMode == quizMode.NONE ? () => this.pushInfo(quizMode.NORMAL) : this.exitQuizMode, - icon: 'pencil', - }); funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' }); funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' }); funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' }); @@ -654,7 +329,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { }), icon: 'pencil-alt', }); - ContextMenu.Instance?.addItem({ description: 'Quiz Mode', subitems: quizes, icon: 'file-pen' }); ContextMenu.Instance?.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' }); } }; @@ -770,7 +444,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() {
(this.imageRef = r))} key="paths" src={srcpath} style={{ transform, transformOrigin }} @@ -811,7 +485,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { e, action(moveEv => { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this._marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]); + this.marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]); return true; }), returnFalse, @@ -823,12 +497,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { @action finishMarquee = () => { this._getAnchor = AnchorMenu.Instance?.GetAnchor; - AnchorMenu.Instance.gptFlashcards = this.getImageDesc; + this._props.styleProvider?.(this.Document, this._props, StyleProp.AnchorMenuItems); AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument; - AnchorMenu.Instance.makeLabels = this.makeLabels; - AnchorMenu.Instance.marqueeWidth = this._marqueeref.current?.Width ?? 0; - AnchorMenu.Instance.marqueeHeight = this._marqueeref.current?.Height ?? 0; - this._marqueeref.current?.onTerminateSelection(); + AnchorMenu.Instance.marqueeWidth = this.marqueeref.current?.Width ?? 0; + AnchorMenu.Instance.marqueeHeight = this.marqueeref.current?.Height ?? 0; + this.marqueeref.current?.onTerminateSelection(); this._props.select(false); }; focus = (anchor: Doc, options: FocusViewOptions) => (anchor.type === DocumentType.CONFIG ? undefined : this._ffref.current?.focus(anchor, options)); @@ -898,7 +571,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { {!this._mainCont.current || !this.DocumentView || !this._annotationLayer.current ? null : ( () { // anchorMenuFlashcard={() => this.getImageDesc()} /> )} - {this._quizMode != quizMode.NONE ? this.checkIcon : null} - {this._quizMode != quizMode.NONE ? this.redoIcon : null}
); } diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 058932457..07c0a114a 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -1,12 +1,8 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@mui/material'; import { Property } from 'csstype'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import * as textfit from 'textfit'; -import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils'; -import { emptyFunction } from '../../../Utils'; import { Field, FieldType } from '../../../fields/Doc'; import { BoolCast, NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; @@ -49,48 +45,6 @@ export class LabelBox extends ViewBoxBaseComponent() { return this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string; } - @computed get answerIcon() { - return ( - - {StrCast(this.Document.quiz)} - - }> -
- - -
-
- ); - } - - @computed get editAnswer() { - return ( - - {this._editLabel ? 'save' : 'edit correct answer'} - - }> -
setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => this.editLabelAnswer())}> - -
-
- ); - } - - editLabelAnswer = () => { - // when click the pencil, set the text to the quiz content. when click off, set the quiz text to that and set textbox to nothing. - if (!this._editLabel) { - this.dataDoc.title = StrCast(this.Document.quiz); - } else { - this.Document.quiz = this.Title; - this.dataDoc.title = ''; - } - this._editLabel = !this._editLabel; - }; - componentDidMount() { this._props.setContentViewBox?.(this); } @@ -98,8 +52,6 @@ export class LabelBox extends ViewBoxBaseComponent() { this._timeout && clearTimeout(this._timeout); } - specificContextMenu = (): void => {}; - drop = (/* e: Event, de: DragManager.DropEvent */) => { return false; }; @@ -152,7 +104,7 @@ export class LabelBox extends ViewBoxBaseComponent() { const boxParams = this.fitTextToBox(undefined); // this causes mobx to trigger re-render when data changes const label = this.Title.startsWith('#') ? null : this.Title; return ( -
+
() { {label}
- {this.Document.showQuiz ? this.answerIcon : null} - {this.Document.showQuiz ? this.editAnswer : null}
); } -- 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/client/views/nodes/DocumentView.tsx') 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 bb8fe2933154c6db70cfe5da1e890535bc9012d4 Mon Sep 17 00:00:00 2001 From: bobzel 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/client/views/nodes/DocumentView.tsx') 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()