diff options
Diffstat (limited to 'src/client/views/nodes')
| -rw-r--r-- | src/client/views/nodes/ComparisonBox.scss | 68 | ||||
| -rw-r--r-- | src/client/views/nodes/ComparisonBox.tsx | 301 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 36 | ||||
| -rw-r--r-- | src/client/views/nodes/FieldView.tsx | 1 | ||||
| -rw-r--r-- | src/client/views/nodes/ImageBox.scss | 21 | ||||
| -rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 232 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.scss | 12 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 5 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBoxComment.scss | 8 |
9 files changed, 578 insertions, 106 deletions
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss index 08d9e6010..7848add24 100644 --- a/src/client/views/nodes/ComparisonBox.scss +++ b/src/client/views/nodes/ComparisonBox.scss @@ -5,31 +5,35 @@ width: 100%; height: 100%; position: relative; + background: gray; z-index: 0; pointer-events: none; display: flex; p { + // bcz: what is this styling for? if text in the comparison box is colored, then this causes it to render with a black outline color: rgb(0, 0, 0); -webkit-text-stroke-color: black; -webkit-text-stroke-width: 0.2px; } - .input-box { - position: relative; + position: absolute; + top: 50; padding: 10px; width: 100%; - height: 100%; + height: 70%; display: flex; } .submit-button { - position: relative; + position: absolute; padding-bottom: 10px; padding-left: 5px; padding-right: 5px; width: 100%; + border-radius: 2px; height: 15%; display: flex; + bottom: 0; button { flex: 1; @@ -39,8 +43,13 @@ textarea { flex: 1; padding: 10px; - position: relative; + // position: relative; resize: none; + position: 'absolute'; + width: '91%'; + height: '80%'; + z-index: '-1'; + overscroll-behavior: contain; } .clip-div { @@ -117,6 +126,33 @@ opacity: 0.5; } } + + .loading-spinner { + display: flex; + justify-content: center; + align-items: center; + height: 90%; + width: 93%; + font-size: 20px; + font-weight: bold; + color: #0b0a0a; + } + + @keyframes spin { + to { + transform: rotate(360deg); + } + } +} + +.comparisonBox-explain { + position: absolute; + top: 10px; + left: 10px; + z-index: 200; + // padding: 5px; + background: #dfdfdf; + pointer-events: none; } .comparisonBox-interactive { @@ -128,8 +164,9 @@ display: flex; } } + // .input-box { - // position: relative; + // position: absolute; // padding: 10px; // } // input[type='text'] { @@ -216,24 +253,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 efaf6807a..3e1c415d6 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -1,15 +1,16 @@ 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'; import { RichTextField } from '../../../fields/RichTextField'; import { DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types'; import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT'; +import '../pdf/GPTPopup/GPTPopup.scss'; import { DocUtils } from '../../documents/DocUtils'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; @@ -23,7 +24,15 @@ import './ComparisonBox.scss'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; +import ReactLoading from 'react-loading'; +enum RevealOp { + Hover = 'hover', + Flip = 'flip', +} +enum UsePath { + Alternate = 'alternate', +} @observer export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { @@ -37,14 +46,24 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() @observable _errorMessage = ''; @observable _outputMessage = ''; @observable _animating = ''; + @observable private _isEmpty = false; + @observable _yRelativeToTop: boolean = true; constructor(props: FieldViewProps) { super(props); makeObservable(this); } + @action handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { + this._inputValue = e.target.value; + console.log(this._inputValue); + }; componentDidMount() { this._props.setContentViewBox?.(this); + reaction( + () => this._props.isSelected(), // when this reaction should update + selected => !selected && (this.childActive = false) // what it should update to + ); } protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => { @@ -54,11 +73,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() } }; - @computed get revealOp() { return this.layoutDoc[`_${this.fieldKey}_revealOp`] as ('flip'|'hover'|undefined); } // prettier-ignore + @computed get useAlternate() { return this.layoutDoc[`_${this.fieldKey}_usePath`] === UsePath.Alternate; } // prettier-ignore + @computed get revealOp() { return this.layoutDoc[`_${this.fieldKey}_revealOp`] as Opt<RevealOp>; } // prettier-ignore @computed get clipWidth() { return NumCast(this.layoutDoc[`_${this.fieldKey}_clipWidth`], 50); } // prettier-ignore + set useAlternate(alt: boolean) { this.layoutDoc[`_${this.fieldKey}_usePath`] = alt ? UsePath.Alternate : undefined; } // prettier-ignore + set revealOp(op: Opt<RevealOp>){ this.layoutDoc[`_${this.fieldKey}_revealOp`] = op; } // prettier-ignore set clipWidth(width: number) { this.layoutDoc[`_${this.fieldKey}_clipWidth`] = width; } // prettier-ignore - @computed get useAlternate() { return this.layoutDoc[`_${this.fieldKey}_usePath`] === 'alternate'; } // prettier-ignore - set useAlternate(alt: boolean) { this.layoutDoc[`_${this.fieldKey}_usePath`] = alt ? 'alternate' : undefined; } // prettier-ignore animateClipWidth = action((clipWidth: number, duration = 200 /* ms */) => { this._animating = `all ${duration}ms`; // turn on clip animation transition, then turn it off at end of animation @@ -73,6 +93,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() 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; @@ -87,7 +108,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() emptyFunction, action((clickEv, 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); // DocumentView.addViewRenderedCb(DocCast(this.dataDoc[this.fieldKey + '_1']), dv => { @@ -127,23 +148,29 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() }; 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._isEmpty = true; // this.dataDoc[which] = 'empty'; + console.log('HEREEEE'); this.dataDoc[which] = undefined; return true; } + console.log('FALSE'); return false; }; @@ -205,6 +232,41 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() return layoutTemplateString; }; + createFlashcardPile(collectionArr: Doc[], gpt: boolean) { + const newCol = Docs.Create.CarouselDocument(collectionArr, { + _width: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 250) + 50, + _height: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 200) + 50, + _layout_fitWidth: false, + _layout_autoHeight: true, + }); + newCol['x'] = this.layoutDoc['x']; + newCol['y'] = NumCast(this.layoutDoc['y']) + 50; + newCol.type_collection = 'carousel'; + // console.log(newCol.data); + + if (gpt) { + this._props.DocumentView?.()._props.addDocument?.(newCol); + this._props.removeDocument?.(this.Document); + } else { + this._props.addDocument?.(newCol); + this._props.removeDocument?.(this.Document); + this.Document.embedContainer = newCol; + } + } + + gptFlashcardPile = async () => { + var text = await this.askGPT(GPTCallType.FLASHCARD); + + var senArr = text?.split('Question: '); + var collectionArr: Doc[] = []; + for (let i = 1; i < senArr?.length!; i++) { + const newDoc = Docs.Create.ComparisonDocument(senArr![i], { _layout_isFlashcard: true, _width: 300, _height: 300 }); + newDoc.text = senArr![i]; + collectionArr.push(newDoc); + } + this.createFlashcardPile(collectionArr, true); + }; + /** * Flips a flashcard to the alternate side for the user to view. */ @@ -216,9 +278,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() * Changes the view option to hover for a flashcard. */ hoverFlip = (alternate: boolean) => { - if (this.revealOp === 'hover') this.useAlternate = alternate; + if (this.revealOp === RevealOp.Hover) this.useAlternate = alternate; }; - /** * Creates the button used to flip the flashcards. */ @@ -229,10 +290,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() className="formattedTextBox-alternateButton" onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => { - if (!this.revealOp || this.revealOp === 'flip') { + if (!this.revealOp || this.revealOp === 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 ?? '')); } }) } @@ -240,17 +302,73 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() background: this.useAlternate ? 'white' : 'black', color: this.useAlternate ? 'black' : 'white', }}> - <FontAwesomeIcon icon="turn-up" size="sm" /> + <div key="alternate" className="formattedTextBox-flip"> + <FontAwesomeIcon icon="turn-up" size="1x" /> + </div> </div> </Tooltip> ); } + @computed get flashcardMenu() { + return ( + <div> + <Tooltip + title={this.useAlternate ? <div className="dash-tooltip">Flip to front side to use GPT</div> : <div className="dash-tooltip">Ask GPT to create an answer on the back side of the flashcard based on your question on the front</div>}> + <div style={{ position: 'absolute', bottom: '3px', right: '50px', cursor: 'pointer' }} onPointerDown={e => (!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? this.askGPT(GPTCallType.CHATCARD) : null)}> + <FontAwesomeIcon icon="lightbulb" size="xl" /> + </div> + </Tooltip> + {DocCast(this.Document.embedContainer).type_collection === 'carousel' ? null : ( + <div> + <Tooltip title={<div>Create a flashcard pile</div>}> + <div style={{ position: 'absolute', bottom: '3px', right: '74px', cursor: 'pointer' }} onPointerDown={e => this.createFlashcardPile([this.Document], false)}> + <FontAwesomeIcon icon="folder-plus" size="xl" /> + </div> + </Tooltip> + <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}> + <div style={{ position: 'absolute', bottom: '3px', right: '104px', cursor: 'pointer' }} onClick={e => this.gptFlashcardPile()}> + <FontAwesomeIcon icon="layer-group" size="xl" /> + </div> + </Tooltip> + </div> + )} + <Tooltip title={<div className="dash-tooltip">Hover to reveal</div>}> + <div style={{ position: 'absolute', bottom: '3px', right: '25px', cursor: 'pointer' }} onClick={e => this.handleHover()}> + <FontAwesomeIcon color={this.revealOp === RevealOp.Hover ? 'blue' : 'black'} icon="hand-point-up" size="xl" /> + </div> + </Tooltip> + {/* <Tooltip title={<div className="dash-tooltip">Remove this side of the flashcard</div>}> + <div + style={{ position: 'absolute', bottom: '3px', right: '80px', cursor: 'pointer' }} + onPointerDown={e => this.closeDown(e, this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this._props.fieldKey + '_1' : this._props.fieldKey + '_0')}> + <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="trash-can" size="xl" /> + </div> + </Tooltip> */} + {/* {this.overlayAlternateIcon} */} + </div> + ); + } + // this.closeDown(e, this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this.fieldKey + '_0' : this.fieldKey + '_1')} + @action activateContent = () => { + this.childActive = true; + }; + @action handleRenderGPTClick = () => { // Call the GPT model and get the output this.useAlternate = true; this._outputValue = ''; - if (this._inputValue) this.askGPT(); + if (this._inputValue) this.askGPT(GPTCallType.QUIZ); + }; + + @action handleHover = () => { + if (this.revealOp === RevealOp.Hover) { + this.revealOp = RevealOp.Flip; + this.Document.forceActive = false; + } else { + this.revealOp = RevealOp.Hover; + this.Document.forceActive = true; + } }; @action handleRenderClick = () => { @@ -258,38 +376,86 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() this.useAlternate = false; }; + animateRes = (resIndex: number, newText: string, callType: GPTCallType) => { + if (resIndex < newText.length) { + // const marks = this._editorView?.state.storedMarks ?? []; + 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, callType), 20); + } + }; + /** * 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<string | undefined> => { + askGPT = async (callType: GPTCallType): Promise<string | undefined> => { const questionText = 'Question: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text); const rubricText = ' Rubric: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text); const queryText = questionText + ' UserAnswer: ' + this._inputValue + '. ' + rubricText; - + this._loading = true; + const doc = DocCast(this.dataDoc[this.props.fieldKey + '_0']); + if (callType == GPTCallType.CHATCARD) { + if (StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '') { + this._loading = false; + return; + } + 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._outputValue = res; + // 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; + else if (callType === GPTCallType.FLASHCARD) { + // console.log(res); + this._loading = false; + return res; + } + // console.log(res); } catch (err) { console.error('GPT call failed'); } + this._loading = false; }; 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' }); + // }; + @observable childActive = false; + render() { const clearButton = (which: string) => ( <Tooltip title={<div className="dash-tooltip">remove</div>}> <div + // style={{ position: 'relative', top: '0px', left: '10px' }} ref={this._closeRef} className={`clear-button ${which}`} onPointerDown={e => this.closeDown(e, which)} // prevent triggering slider movement in registerSliding > - <FontAwesomeIcon className={`clear-button ${which}`} icon="times" size="sm" /> + <FontAwesomeIcon className={`clear-button ${which}`} icon="times" size="xs" /> </div> </Tooltip> ); @@ -297,6 +463,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() const whichDoc = DocCast(this.dataDoc[whichSlot]); const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc); const layoutString = targetDoc ? '' : this.testForTextFields(whichSlot); + // whichDoc['backgroundColor'] = this.layoutDoc['backgroundColor']; return targetDoc || layoutString ? ( <> @@ -312,15 +479,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() 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 missing + {/* <div style={{ position: 'absolute', top: '-5px', left: '2px' }}>{layoutString ? null : clearButton(whichSlot)}</div> */} + </> // placeholder image if doc is missingleft: `${NumCast(this.layoutDoc.width, 200) - 33}px` ) : ( <div className="placeholder"> <FontAwesomeIcon className="upload-icon" icon="cloud-upload-alt" size="lg" /> @@ -328,8 +496,17 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() ); }; const displayBox = (which: string, index: number, cover: number) => ( - <div className={`${index === 0 ? 'before' : 'after'}Box-cont`} key={which} style={{ width: this._props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which, index)}> - {displayDoc(which)} + <div + className={`${index === 0 ? 'before' : 'after'}Box-cont`} + key={which} + style={{ width: this._props.PanelWidth() }} + onPointerDown={e => { + this.registerSliding(e, cover); + this.activateContent(); + }} + ref={ele => this.createDropTarget(ele, which, index)}> + {!this._isEmpty ? displayDoc(which) : null} + {/* {this.dataDoc[this.fieldKey + '_0'] !== 'empty' ? displayDoc(which) : null} */} </div> ); @@ -337,60 +514,90 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() const side = this.useAlternate ? 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')) { - const dataSplit = StrCast(this.dataDoc.data).split('Answer'); + // (!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. // eslint-disable-next-line prefer-destructuring - newDoc[DocData].text = dataSplit[1]; + // newDoc.text = dataSplit[1]; + newDoc['backgroundColor'] = 'lightgray'; this.addDoc(newDoc, this.fieldKey + '_0'); + // DocCast(this.dataDoc[this.fieldKey + '_0'])[DocData].text = dataSplit[1]; + // DocCast(this.dataDoc[this.fieldKey + '_0']).text = dataSplit[1]; + // console.log('HI' + DocCast(this.dataDoc[this.fieldKey + '_0']).text); + //console.log('HEREEE' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text)); } - if (!(this.dataDoc[this.fieldKey + '_1'] || this.dataDoc[this.fieldKey + '_1'] === 'empty')) { - const dataSplit = StrCast(this.dataDoc.data).split('Answer'); + + if (!this.dataDoc[this.fieldKey + '_1'] && !this._isEmpty) { + const dataSplit = StrCast(this.dataDoc.data).split('Answer: '); const newDoc = Docs.Create.TextDocument(dataSplit[0]); + this.addDoc(newDoc, this.fieldKey + '_1'); // if there is text from the pdf ai cards, put the answer on the alternate side. // eslint-disable-next-line prefer-destructuring - newDoc[DocData].text = dataSplit[0]; - this.addDoc(newDoc, this.fieldKey + '_1'); + + // newDoc[DocData].text = dataSplit[0]; + // console.log('HEREEE' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text)); + // console.log('HI' + DocCast(this.dataDoc[this.fieldKey + '_1']).text); + // DocCast(this.dataDoc[this.props.fieldKey + '_1'])[DocData].text = dataSplit[0]; + // console.log('HEREEE' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text)); + // DocCast(this.dataDoc[this.fieldKey + '_1'])[DocData].text = dataSplit[0]; } // render the QuizCards - if (DocCast(this.Document.embedContainer) && DocCast(this.Document.embedContainer).filterOp === 'quiz') { + // console.log('GERE' + DocCast(this.Document.embedContainer).filterOp); + if (DocCast(this.Document.embedContainer) && DocCast(this.Document.embedContainer).practiceMode === 'quiz') { + const text = StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text); return ( <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} style={{ display: 'flex', flexDirection: 'column' }}> - <p style={{ color: 'white', padding: 10 }}>{StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text)}</p> - {/* {StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text)} */} + <p style={{ color: 'white', padding: 10 }}>{text}</p> + <p style={{ display: text === '' ? 'flex' : 'none', color: 'white', marginLeft: '10px' }}>Return to all flashcards and add text to both sides. </p> <div className="input-box"> <textarea value={this.useAlternate ? this._outputValue : this._inputValue} - onChange={action(e => { - this._inputValue = e.target.value; - })} + onChange={this.handleInputChange} + onScroll={e => e.stopPropagation()} + placeholder={!this.useAlternate ? 'Enter a response for GPT to evaluate.' : ''} readOnly={this.useAlternate} /> + + {this._loading ? ( + <div className="loading-spinner" style={{ position: 'absolute' }}> + <ReactLoading type="spin" height={30} width={30} color={'blue'} /> + </div> + ) : null} </div> - <div className="submit-button" style={{ display: this.useAlternate ? 'none' : 'flex' }}> - <button type="button" onClick={this.handleRenderGPTClick}> + <div className="submit-button" style={{ overflow: 'hidden', display: this.useAlternate ? 'none' : 'flex' }}> + <button type="button" onClick={this.handleRenderGPTClick} style={{ borderRadius: '2px', marginBottom: '3px' }}> Submit </button> </div> - <div className="submit-button" style={{ display: this.useAlternate ? 'flex' : 'none' }}> - <button type="button" onClick={this.handleRenderClick}> - Edit Your Response + <div className="submit-button" style={{ overflow: 'hidden', marginBottom: '2px', display: this.useAlternate ? 'flex' : 'none' }}> + <button type="button" onClick={this.handleRenderClick} style={{ borderRadius: '2px' }}> + Redo the Question </button> </div> </div> ); } + //console.log('HEREEE2' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text)); // render a normal flashcard when not a QuizCard return ( <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */ - style={{ display: 'flex', flexDirection: 'column' }} + // onContextMenu={this.specificMenu} + style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden' }} onMouseEnter={() => this.hoverFlip(true)} onMouseLeave={() => this.hoverFlip(false)}> + {!this.useAlternate && StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '' && !this.childActive ? <p className="comparisonBox-explain">Enter text in the flashcard. </p> : null} {displayBox(`${this.fieldKey}_${side === 0 ? 1 : 0}`, side, this._props.PanelWidth() - 3)} + {this._loading ? ( + <div className="loading-spinner" style={{ position: 'absolute' }}> + <ReactLoading type="spin" height={30} width={30} color={'blue'} /> + </div> + ) : null} + {this._props.isContentActive() ? this.flashcardMenu : null} {this.overlayAlternateIcon} </div> ); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 7a1f94948..d4c31a5b3 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -111,6 +111,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document private _downTime: number = 0; private _lastTap: number = 0; private _doubleTap = false; + private _loading = false; private _mainCont = React.createRef<HTMLDivElement>(); private _titleRef = React.createRef<EditableView>(); private _dropDisposer?: DragManager.DragDropDisposer; @@ -501,22 +502,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document input.click(); }; - askGPT = async (): Promise<string | undefined> => { - 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 (this._props.dontSelect?.()) return; if (e && this.layoutDoc.layout_hideContextMenu && Doc.noviceMode) { e.preventDefault(); e.stopPropagation(); @@ -574,21 +561,18 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => 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 ChatCard', 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' }); // 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.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...'); @@ -1395,7 +1379,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { 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<StyleProviderFuncType>; diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 3ffda5a35..be68ac8cd 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -139,3 +139,24 @@ .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; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 68c313480..8068407bb 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -35,6 +35,15 @@ import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; import './ImageBox.scss'; import { OpenWhere } from './OpenWhere'; +import { URLField } from '../../../fields/URLField'; +import { gptImageLabel } from '../../apis/gpt/GPT'; +import ReactLoading from 'react-loading'; +import { FollowLinkScript } from '../../documents/DocUtils'; +import { basename } from 'path'; +import { ImageUtility } from './generativeFill/generativeFillUtils/ImageHandler'; +import { dropActionType } from '../../util/DropActionTypes'; +import { canvasSize } from './generativeFill/generativeFillUtils/generativeFillConstants'; +import axios from 'axios'; export class ImageEditorData { // eslint-disable-next-line no-use-before-define @@ -59,6 +68,8 @@ export class ImageEditorData { public static get AddDoc() { return ImageEditorData.imageData.addDoc; } // prettier-ignore public static set AddDoc(addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) { ImageEditorData.set(this.imageData.open, this.imageData.rootDoc, this.imageData.source, addDoc); } // prettier-ignore } + +const API_URL = 'https://api.unsplash.com/search/photos'; @observer export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { @@ -73,9 +84,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { private _marqueeref = React.createRef<MarqueeAnnotator>(); private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); + private _imageRef: HTMLImageElement | null = null; // <video> ref + @observable private _width: number = 0; + @observable private _height: number = 0; + @observable private searchInput = ''; @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @observable _curSuffix = ''; @observable _error = ''; + @observable private _loading = false; @observable _isHovering = false; // flag to switch between primary and alternate images on hover _ffref = React.createRef<CollectionFreeFormView>(); @@ -148,6 +164,32 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { Object.values(this._disposers).forEach(disposer => disposer?.()); } + fetchImages = async () => { + try { + const { data } = await axios.get(`${API_URL}?query=${this.searchInput}&page=1&per_page=${1}&client_id=${process.env.VITE_API_KEY}`); + console.log('data', data); + console.log(data.results); + const imageSnapshot = Docs.Create.ImageDocument(data.results[0].urls.small, { + _nativeWidth: Doc.NativeWidth(this.layoutDoc), + _nativeHeight: Doc.NativeHeight(this.layoutDoc), + x: NumCast(this.layoutDoc.x), + y: NumCast(this.layoutDoc.y), + onClick: FollowLinkScript(), + _width: 150, + _height: 150, + title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-', + }); + this._props.addDocument?.(imageSnapshot); + } catch (error) { + console.log(error); + } + }; + + handleSelection = async (selection: string) => { + this.searchInput = selection; + const images = await this.fetchImages(); + }; + @undoBatch drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData) { @@ -259,10 +301,189 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { return cropping; }; + createCanvas = async (downX?: number, downY?: number, cb?: (filename: string, x: number | undefined, y: number | undefined) => void) => { + const width = NumCast(this.layoutDoc._width); + const canvas = document.createElement('canvas'); + // canvas.width = 640; + // canvas.height = (640 * Doc.NativeHeight(this.layoutDoc)) / (Doc.NativeWidth(this.layoutDoc) || 1); + canvas.width = NumCast(this.layoutDoc._width); + canvas.height = NumCast(this.layoutDoc._height); + const ctx = canvas.getContext('2d'); // draw image to canvas. scale to target dimensions + if (ctx) { + // this._imageRef && ctx.drawImage(this._imageRef, 0, 0, canvas.width, canvas.height); + this._imageRef && ctx.drawImage(this._imageRef, NumCast(this._marqueeref.current?.left), NumCast(this._marqueeref.current?.top), this._width, this._height, 0, 0, 1000, 1000); + //this._imageRef && ctx.drawImage(this._imageRef, 0, 0, 2000, 1000, 0, 0, canvas.width, canvas.height); + // console.log(NumCast(this._marqueeref.current?.left) + 100); + } + const blob = await ImageUtility.canvasToBlob(canvas); + return ImageBox.selectUrlToBase64(blob); + + // if (this._imageRef) { + // const canv = ImageUtility.getCroppedImg(this._imageRef, this._imageRef.width, this._imageRef.height); + // console.log(this._imageRef.width); + // if (canv) { + // const blob = await ImageUtility.canvasToBlob(canv); + // return ImageBox.selectUrlToBase64(blob); + // } + // } + if (!this._imageRef) { + const b = Docs.Create.LabelDocument({ + x: NumCast(this.layoutDoc.x) + width, + y: NumCast(this.layoutDoc.y, 1), + _width: 150, + _height: 50, + // title: (this.layoutDoc._layout_currentTimecode || 0).toString(), + onClick: FollowLinkScript(), + }); + this._props.addDocument?.(b); + DocUtils.MakeLink(b, this.Document, { link_relationship: 'image snapshot' }); + } else { + // convert to desired file format + // const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' + // // if you want to preview the captured image, + // const retitled = StrCast(this.Document.title).replace(/[ -.:]/g, ''); + // const encodedFilename = encodeURIComponent(('snapshot' + retitled + '_' + (this.layoutDoc._layout_currentTimecode || 0).toString()).replace(/[./?=]/g, '_')); + // const filename = basename(encodedFilename); + // ClientUtils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY)); + } + // convert to desired file format + + // const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' + // // if you want to preview the captured image, + // const retitled = StrCast(this.Document.title).replace(/[ -.:]/g, ''); + // const encodedFilename = encodeURIComponent(('snapshot' + retitled + '_' + (this.layoutDoc._layout_currentTimecode || 0).toString()).replace(/[./?=]/g, '_')); + // const filename = basename(encodedFilename); + //ClientUtils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY)); + // } + // const docViewContent = this.DocumentView?.().ContentDiv!; + // if (docViewContent instanceof HTMLCanvasElement) { + // const canvas = docViewContent; + // const img = document.createElement('img'); // create a Image Element + // img.src = canvas.toDataURL(); // image sourcez + // img.style.width = canvas.style.width; + // img.style.height = canvas.style.height; + // const parEle = newCan.parentElement as HTMLElement; + // parEle.removeChild(newCan); + // parEle.appendChild(img); + // } + }; + + createSnapshotLink = (imagePath: string, downX?: number, downY?: number) => { + const url = !imagePath.startsWith('/') ? ClientUtils.CorsProxy(imagePath) : imagePath; + const width = NumCast(this.layoutDoc._width) || 1; + const height = NumCast(this.layoutDoc._height); + const imageSnapshot = Docs.Create.ImageDocument(url, { + _nativeWidth: Doc.NativeWidth(this.layoutDoc), + _nativeHeight: Doc.NativeHeight(this.layoutDoc), + x: NumCast(this.layoutDoc.x) + width, + y: NumCast(this.layoutDoc.y), + onClick: FollowLinkScript(), + _width: 150, + _height: (height / width) * 150, + title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-', + }); + Doc.SetNativeWidth(imageSnapshot[DocData], Doc.NativeWidth(this.layoutDoc)); + Doc.SetNativeHeight(imageSnapshot[DocData], Doc.NativeHeight(this.layoutDoc)); + this._props.addDocument?.(imageSnapshot); + DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' }); + // link && (DocCast(link.link_anchor_2)[DocData].timecodeToHide = NumCast(DocCast(link.link_anchor_2).timecodeToShow) + 3); // do we need to set an end time? should default to +0.1 + setTimeout(() => downX !== undefined && downY !== undefined && DocumentView.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, dropActionType.move, true)); + }; + + /** + * + if (oldDiv instanceof HTMLCanvasElement) { + const canvas = oldDiv; + const img = document.createElement('img'); // create a Image Element + img.src = canvas.toDataURL(); // image sourcez + img.style.width = canvas.style.width; + img.style.height = canvas.style.height; + const newCan = newDiv as HTMLCanvasElement; + const parEle = newCan.parentElement as HTMLElement; + parEle.removeChild(newCan); + parEle.appendChild(img); + } + */ + + static selectUrlToBase64 = async (blob: Blob): Promise<string> => { + 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; + } + }; + + static imageUrlToBase64 = async (imageUrl: string): Promise<string> => { + try { + const response = await fetch(imageUrl); + const blob = await response.blob(); + + 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; + } + }; + + getImageDesc = async () => { + // if (StrCast(this.dataDoc.description)) return StrCast(this.dataDoc.description); // Return existing description + const { href } = (this.dataDoc.data as URLField).url; + const hrefParts = href.split('.'); + const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`; + this._loading = true; + try { + // const hrefBase64 = await ImageBox.imageUrlToBase64(hrefComplete); + const hrefBase64 = await this.createCanvas(); + const response = await gptImageLabel(hrefBase64, 'Tell me what words you see on this image.'); + //const response = await gptImageLabel(hrefBase64, 'Make flashcards out of this text with each question and answer labeled as question and answer. Do not label each flashcard and do not include asterisks: '); + console.log(response); + // AnchorMenu.Instance.transferToFlashcard(response); + // this.Document[DocData].description = response.trim(); + // return response; // Return the response + } catch (error) { + console.log('Error'); + } + this._loading = false; + // return ''; + }; + + @action + setRef = (iref: HTMLImageElement | null) => { + this._imageRef = iref; + // if (iref) { + // this._videoRef!.ontimeupdate = this.updateTimecode; + // // @ts-ignore + // // vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen); + // this._disposers.reactionDisposer?.(); + // this._disposers.reactionDisposer = reaction( + // () => NumCast(this.layoutDoc._layout_currentTimecode), + // time => { + // !this._playing && (vref.currentTime = time); + // }, + // { fireImmediately: true } + // ); + + // (!this.dataDoc[this.fieldKey + '_thumbnails'] || StrListCast(this.dataDoc[this.fieldKey + '_thumbnails']).length !== VideoThumbnails.DENSE) && this.getVideoThumbnails(); + // } + }; + specificContextMenu = (): void => { const field = Cast(this.dataDoc[this.fieldKey], ImageField); if (field) { const funcs: ContextMenuProps[] = []; + // funcs.push({ description: 'Create ai flashcards', event: () => this.getImageDesc(), icon: 'id-card' }); + funcs.push({ description: 'Get Images', event: () => this.handleSelection('Cats'), icon: 'redo-alt' }); 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' }); @@ -391,6 +612,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { <div className="imageBox-fader" style={{ opacity: backAlpha }}> <img alt="" + ref={this.setRef} key="paths" src={srcpath} style={{ transform, transformOrigin }} @@ -432,6 +654,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { action(moveEv => { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); this._marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]); + this._width = moveEv.clientX; + this._height = moveEv.clientY; return true; }), returnFalse, @@ -443,6 +667,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @action finishMarquee = () => { this._getAnchor = AnchorMenu.Instance?.GetAnchor; + AnchorMenu.Instance.gptFlashcards = this.getImageDesc; + AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument; this._marqueeref.current?.onTerminateSelection(); this._props.select(false); }; @@ -505,6 +731,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { addDocument={this.addDocument}> {this.content} </CollectionFreeFormView> + {this._loading ? ( + <div className="loading-spinner" style={{ position: 'absolute' }}> + <ReactLoading type="spin" height={50} width={50} color={'blue'} /> + </div> + ) : null} {this.annotationLayer} {!this._mainCont.current || !this.DocumentView || !this._annotationLayer.current ? null : ( <MarqueeAnnotator @@ -523,6 +754,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { marqueeContainer={this._mainCont.current} highlightDragSrcColor="" anchorMenuCrop={this.crop} + // anchorMenuFlashcard={() => this.getImageDesc()} /> )} </div> diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 99b4a84fc..54643b4a5 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -88,10 +88,16 @@ audiotag:hover { background: black; right: 0; bottom: 0; - width: 11; - height: 11; + width: 15; + height: 22; cursor: default; } +.formattedTextBox-flip { + align-items: center; + position: absolute; + right: 2px; + bottom: 4px; +} .formattedTextBox-outer { position: relative; @@ -171,6 +177,8 @@ audiotag:hover { border-style: inset; border-width: 1px; } + // margin-left: 5px; + // margin-right: 5px; } .gpt-typing-wrapper { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 9f2a9b8e1..3e2befb5f 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -181,7 +181,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB @observable private gptRes: string = ''; - public makeAIFlashcards: () => void = unimplementedFunction; + // public makeAIFlashcards: () => void = unimplementedFunction; public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined; public static PasteOnLoad: ClipboardEvent | undefined; @@ -816,6 +816,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB isTargetToggler = (anchor: Doc) => 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 <a> node that wraps the hyerlink <span> @@ -969,7 +970,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB icon: 'star', }); optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' }); - optionItems.push({ description: `Make AI Flashcards`, event: () => this.makeAIFlashcards(), icon: 'lightbulb' }); + // optionItems.push({ description: `Make AI Flashcards`, event: () => this.makeAIFlashcards(), icon: 'lightbulb' }); optionItems.push({ description: `Ask GPT-3`, event: this.askGPT, icon: 'lightbulb' }); this._props.renderDepth && optionItems.push({ diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss index 55b8446e9..bc0810f22 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss @@ -13,7 +13,7 @@ box-shadow: 3px 3px 1.5px grey; max-width: 400; max-height: 235; - height:max-content; + height: max-content; .formattedTextBox-tooltipText { height: max-content; text-overflow: ellipsis; @@ -21,7 +21,7 @@ } .formattedTextBox-tooltip:before { - content: ""; + content: ''; height: 0; width: 0; position: absolute; @@ -34,7 +34,7 @@ } .formattedTextBox-tooltip:after { - content: ""; + content: ''; height: 0; width: 0; position: absolute; @@ -44,4 +44,4 @@ border: 5px solid transparent; border-bottom-width: 0; border-top-color: white; -}
\ No newline at end of file +} |
