diff options
Diffstat (limited to 'src/client/views/nodes')
| -rw-r--r-- | src/client/views/nodes/ComparisonBox.scss | 143 | ||||
| -rw-r--r-- | src/client/views/nodes/ComparisonBox.tsx | 281 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 33 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 38 |
4 files changed, 441 insertions, 54 deletions
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss index 39c864b2b..093b9c004 100644 --- a/src/client/views/nodes/ComparisonBox.scss +++ b/src/client/views/nodes/ComparisonBox.scss @@ -1,4 +1,5 @@ .comparisonBox-interactive, +.quiz-card, .comparisonBox { border-radius: inherit; width: 100%; @@ -7,6 +8,40 @@ z-index: 0; pointer-events: none; display: flex; + p { + color: rgb(0, 0, 0); + -webkit-text-stroke-color: black; + -webkit-text-stroke-width: 0.2px; + } + + .input-box { + position: relative; + padding: 10px; + width: 100%; + height: 100%; + display: flex; + } + + .submit-button { + position: relative; + padding-bottom: 10px; + padding-left: 5px; + padding-right: 5px; + width: 100%; + height: 15%; + display: flex; + + button { + flex: 1; + position: relative; + } + } + textarea { + flex: 1; + padding: 10px; + position: relative; + resize: none; + } .clip-div { position: absolute; @@ -95,4 +130,112 @@ display: flex; } } + // .input-box { + // position: relative; + // padding: 10px; + // } + // input[type='text'] { + // flex: 1; + // position: relative; + // margin-right: 10px; + // width: 100px; + // } +} + +// .quiz-card { +// position: relative; + +// input[type='text'] { +// flex: 1; +// position: relative; +// margin-right: 10px; +// width: 100px; +// } +// } +.QuizCard { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + .QuizCard-wrapper { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + .QuizCardBox { + /* existing code */ + + .DIYNodeBox-iframe { + height: 100%; + width: 100%; + border: none; + } + } + + .search-bar { + display: flex; + justify-content: left; + align-items: left; + width: 100%; + padding: 10px; + + input[type='text'] { + flex: 1; + margin-right: 10px; + } + + button { + padding: 5px 10px; + } + } + + .content { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + .diagramBox { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + svg { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + } + } + } + + .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 e1d16549c..84d14d4ef 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -1,18 +1,21 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip } from '@mui/material'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../ClientUtils'; +import { returnFalse, returnNone, 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 { DocUtils } from '../../documents/DocUtils'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { dropActionType } from '../../util/DropActionTypes'; -import { undoBatch } from '../../util/UndoManager'; +import { undoable } from '../../util/UndoManager'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { PinDocView, PinProps } from '../PinFuncs'; import { StyleProp } from '../StyleProp'; @@ -32,6 +35,17 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() makeObservable(this); } + @observable inputValue = ''; + @observable outputValue = ''; + @observable loading = false; + @observable errorMessage = ''; + @observable outputMessage = ''; + + @action handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { + this.inputValue = e.target.value; + console.log(this.inputValue); + }; + @observable _animating = ''; @computed get clipWidth() { @@ -40,6 +54,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() get clipWidthKey() { return '_' + this._props.fieldKey + '_clipWidth'; } + + @computed get clipHeight() { + return NumCast(this.layoutDoc[this.clipHeightKey], 200); + } + get clipHeightKey() { + return '_' + this._props.fieldKey + '_clipHeight'; + } + componentDidMount() { this._props.setContentViewBox?.(this); } @@ -50,8 +72,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() } }; - @undoBatch - private internalDrop = (e: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { + 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)); @@ -61,7 +82,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() return added; } return undefined; - }; + }, 'internal drop'); private registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => { if (e.button !== 2) { @@ -83,7 +104,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() if (this._isAnyChildContentActive) return; this._animating = 'all 200ms'; // on click, animate slider movement to the targetWidth - this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth(); + // this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth(); + this.layoutDoc[this.clipHeightKey] = (targetWidth * 100) / this._props.PanelHeight(); + setTimeout( action(() => { this._animating = ''; @@ -120,17 +143,21 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() return this.Document; }; - @undoBatch - clearDoc = (fieldKey: string) => delete this.dataDoc[fieldKey]; + clearDoc = undoable((fieldKey: string) => { + delete this.dataDoc[fieldKey]; + this.dataDoc[fieldKey] = 'empty'; + }, '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]) return false; + if (this.dataDoc[which] && this.dataDoc[which] !== 'empty') return false; this.dataDoc[which] = doc; return true; }; remDoc = (doc: Doc, which: string) => { if (this.dataDoc[which] === doc) { + // this.dataDoc[which] = 'empty'; this.dataDoc[which] = undefined; return true; } @@ -144,7 +171,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() moveEv => { const de = new DragManager.DocumentDragData([DocCast(this.dataDoc[which])], dropActionType.move); de.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => { - this.clearDoc(which); return addDocument(doc); }; de.canEmbed = true; @@ -196,27 +222,105 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() }; _closeRef = React.createRef<HTMLDivElement>(); - render() { - const clearButton = (which: string) => ( - <div - 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" /> - </div> + + /** + * Flips a flashcard to the alternate side for the user to view. + */ + flipFlashcard = () => { + const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; + this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : undefined; + }; + + /** + * Changes the view option to hover for a flashcard. + */ + 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. + */ + @computed get overlayAlternateIcon() { + const usepath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; + return ( + <Tooltip title={<div className="dash-tooltip">flip</div>}> + <div + className="formattedTextBox-alternateButton" + onPointerDown={e => + setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => { + console.log(this.layoutDoc[`_${this._props.fieldKey}_revealOp`]); + 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); + } + }) + } + style={{ + background: usepath === 'alternate' ? 'white' : 'black', + color: usepath === 'alternate' ? 'black' : 'white', + }}> + <FontAwesomeIcon icon="turn-up" size="sm" /> + </div> + </Tooltip> ); + } + + @action handleRenderGPTClick = () => { + // Call the GPT model and get the output + this.layoutDoc[`_${this._props.fieldKey}_usePath`] = 'alternate'; + this.outputValue = ''; + if (this.inputValue) this.askGPT(); + }; - /** - * Display the Docs in the before/after fields of the comparison. This also supports a GPT flash card use case - * where if there are no Docs in the slots, but the main fieldKey contains text, then - * @param whichSlot - * @returns - */ + @action handleRenderClick = () => { + // Call the GPT model and get the output + this.layoutDoc[`_${this._props.fieldKey}_usePath`] = undefined; + }; + + /** + * 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> => { + 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; + + try { + let res = await gptAPICall(queryText, GPTCallType.QUIZ); + if (!res) { + console.error('GPT call failed'); + return; + } + this.outputValue = res; + console.log(res); + } catch (err) { + console.error('GPT call failed'); + } + }; + + render() { + const clearButton = (which: string) => { + return ( + <Tooltip title={<div className="dash-tooltip">remove</div>}> + <div + 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" /> + </div> + </Tooltip> + ); + }; const displayDoc = (whichSlot: string) => { const whichDoc = DocCast(this.dataDoc[whichSlot]); const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc); - const layoutString = targetDoc ? '' : this.testForTextFields(whichSlot); + // if there is no Doc in the first comparison slot, but the comparison box's fieldKey slot has a RichTextField, then render a text box to show the contents of the document's field key slot + const layoutString = !targetDoc && whichSlot.endsWith('1') && this.Document[this.fieldKey] instanceof RichTextField ? FormattedTextBox.LayoutString(this.fieldKey) : undefined; + return targetDoc || layoutString ? ( <> <DocumentView @@ -229,8 +333,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() containerViewPath={this.DocumentView?.().docViewPath} moveDocument={whichSlot.endsWith('1') ? this.moveDoc1 : this.moveDoc2} removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2} - NativeWidth={returnZero} - NativeHeight={returnZero} + NativeWidth={() => NumCast(this.layoutDoc.width, 200)} + NativeHeight={(): number => { + return NumCast(this.layoutDoc.height, 200); + }} isContentActive={emptyFunction} isDocumentActive={returnFalse} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} @@ -252,25 +358,112 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() </div> ); - return ( - <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}> - {displayBox(`${this.fieldKey}_2`, 1, this._props.PanelWidth() - 3)} - <div className="clip-div" style={{ width: this.clipWidth + '%', transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}> - {displayBox(`${this.fieldKey}_1`, 0, 0)} + const displayBoxReveal = (which: string, which2: string, index: number, cover: number) => { + return ( + <div style={{ display: 'flex', flexDirection: 'column' }}> + <div + className={`beforeBox-cont`} + key={which} + style={{ width: this._props.PanelWidth(), height: NumCast(this.layoutDoc.height, 200) / 2 }} + onPointerDown={e => this.registerSliding(e, cover)} + ref={ele => this.createDropTarget(ele, which, 0)}> + {displayDoc(which)} + </div> + <div + className={`afterBox-cont`} + key={which2} + style={{ width: this._props.PanelWidth(), height: NumCast(this.layoutDoc.height, 200) / 2 }} + onPointerDown={e => this.registerSliding(e, cover)} + ref={ele => this.createDropTarget(ele, which2, 1)}> + {displayDoc(which2)} + </div> </div> + ); + }; - <div - className="slide-bar" - style={{ - 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 */ - > - <div className="slide-handle" /> + if (this.Document._layout_isFlashcard) { + 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')) { + 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. + newDoc[DocData].text = dataSplit[1]; + this.addDoc(newDoc, this.fieldKey + '_0'); + } + if (!(this.dataDoc[this.fieldKey + '_1'] || this.dataDoc[this.fieldKey + '_1'] == 'empty')) { + 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. + newDoc[DocData].text = dataSplit[0]; + this.addDoc(newDoc, this.fieldKey + '_1'); + } + + // render the QuizCards + if (DocCast(this.Document.embedContainer) && DocCast(this.Document.embedContainer)[`filterOp`] == 'quiz') { + 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)} */} + <div className={'input-box'}> + { + <textarea + value={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this.outputValue : this.inputValue} + onChange={this.handleInputChange} + readOnly={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate'} + /> + } + </div> + <div className="submit-button" style={{ display: this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? 'none' : 'flex' }}> + <button onClick={this.handleRenderGPTClick}>Submit</button> + </div> + <div className="submit-button" style={{ display: this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? 'flex' : 'none' }}> + <button onClick={this.handleRenderClick}>Edit Your Response</button> + </div> + </div> + ); + } + + // render a normal flashcard when not a QuizCard + else { + return ( + <div + className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */ + style={{ display: 'flex', flexDirection: 'column' }} + onMouseEnter={() => { + this.hoverFlip('alternate'); + }} + onMouseLeave={() => { + this.hoverFlip(undefined); + }}> + {displayBox(`${this.fieldKey}_${side === 0 ? 1 : 0}`, side, this._props.PanelWidth() - 3)} + {this.overlayAlternateIcon} + </div> + ); + } + } else { + // render a comparison box that compares items side by side + return ( + <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}> + {displayBox(`${this.fieldKey}_2`, 1, this._props.PanelWidth() - 3)} + <div className="clip-div" style={{ width: this.clipWidth + '%', transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}> + {displayBox(`${this.fieldKey}_1`, 0, 0)} + </div> + + <div + className="slide-bar" + style={{ + 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 */ + > + <div className="slide-handle" /> + </div> </div> - </div> - ); + ); + } } } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c59cd0ee4..fca6cda81 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -17,11 +17,12 @@ import { List } from '../../../fields/List'; import { PrefetchProxy } from '../../../fields/Proxy'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types'; import { AudioField } from '../../../fields/URLField'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { AudioAnnoState } from '../../../server/SharedMediaTypes'; import { DocServer } from '../../DocServer'; +import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT'; import { DocUtils, FollowLinkScript } from '../../documents/DocUtils'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; @@ -500,6 +501,21 @@ 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 { + let 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(); @@ -558,9 +574,22 @@ 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' }); + revealItems.push({ description: 'Flip', event: () => (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip'), icon: 'rotate' }); + !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: ContextMenuProps[] = zorders && 'subitems' in zorders ? zorders.subitems : []; @@ -614,7 +643,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document } } - !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' }); + !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'eye' }); } const constantItems: ContextMenuProps[] = []; if (!Doc.IsSystem(this.Document) && this.Document._type_collection !== CollectionViewType.Docking) { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index de9ba87d3..542a68c3b 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -181,6 +181,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB @observable private gptRes: string = ''; + public makeAIFlashcards: () => void = unimplementedFunction; + public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined; + public static PasteOnLoad: ClipboardEvent | undefined; public static DontSelectInitialText = false; // whether initial text should be selected or not public static SelectOnLoadChar = ''; @@ -918,6 +921,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }, icon: !this.Document._layout_enableAltContentUI ? 'eye-slash' : 'eye', }); + if (this.Document._layout_enableAltContentUI) { + const usepath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; + appearanceItems.push({ + description: (this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate:hover' ? 'no hover' : 'hover') + ' to show alt content', + event: () => (this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usepath === 'alternate' || usepath === undefined ? 'alternate:hover' : undefined), + icon: !this.Document._layout_enableAltContentUI ? 'eye-slash' : 'eye', + }); + } + !Doc.noviceMode && appearanceItems.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' }); !Doc.noviceMode && appearanceItems.push({ @@ -955,6 +967,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: `Ask GPT-3`, event: this.askGPT, icon: 'lightbulb' }); this._props.renderDepth && optionItems.push({ @@ -1948,28 +1961,37 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB </div> ); } - cycleAlternateText = (skipHover?: boolean) => { - this.layoutDoc._layout_enableAltContentUI = true; - const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; - this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' && !skipHover ? 'alternate:hover' : undefined; + cycleAlternateText = () => { + if (this.layoutDoc._layout_enableAltContentUI) { + const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; + this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : undefined; + } }; + // cycleAlternateText = (skipHover?: boolean) => { + // this.layoutDoc._layout_enableAltContentUI = true; + // const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; + // this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' && !skipHover ? 'alternate:hover' : undefined; + // }; @computed get overlayAlternateIcon() { const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; return ( <Tooltip title={ <div className="dash-tooltip"> + flip + {/* + <div className="dash-tooltip"> toggle (%/) between <span style={{ color: usePath === undefined ? 'black' : undefined }}> - <em> primary, </em> - </span> + <em> primary </em> + </span> and <span style={{ color: usePath === 'alternate' ? 'black' : undefined }}> - <em>alternate, </em> + <em>alternate </em> </span> and show <span style={{ color: usePath === 'alternate:hover' ? 'black' : undefined }}> <em> alternate on hover</em> - </span> + </span> */} </div> }> <div |
