import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents, unimplementedFunction } from '../../../Utils'; import { Doc, Opt, DocListCast } from '../../../fields/Doc'; import { DocCast, NumCast, RTFCast, StrCast } from '../../../fields/Types'; import { DocUtils, Docs } from '../../documents/Documents'; import { DragManager, dropActionType } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; import { SnappingManager } from '../../util/SnappingManager'; import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent'; import { StyleProp } from '../StyleProvider'; import { Tooltip } from '@mui/material'; import { CSSTransition } from 'react-transition-group'; import './ComparisonBox.scss'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { PinProps, PresBox } from './trails'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { RichTextField } from '../../../fields/RichTextField'; import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT'; import { DocData } from '../../../fields/DocSymbols'; import { KeyValueBox } from './KeyValueBox'; @observer export class ComparisonBox extends ViewBoxAnnotatableComponent() implements ViewBoxInterface { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ComparisonBox, fieldKey); } private _disposers: (DragManager.DragDropDisposer | undefined)[] = [undefined, undefined]; constructor(props: FieldViewProps) { super(props); makeObservable(this); } @observable inputValue = ''; @observable outputValue = ''; @observable loading = false; @observable errorMessage = ''; @observable outputMessage = ''; @action handleInputChange = (e: React.ChangeEvent) => { this.inputValue = e.target.value; console.log(this.inputValue); }; @observable _animating = ''; @computed get clipWidth() { return NumCast(this.layoutDoc[this.clipWidthKey], 50); } 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); } protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => { this._disposers[disposerId]?.(); if (ele) { this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.internalDrop(e, dropEvent, fieldKey), this.layoutDoc); } }; @undoBatch private internalDrop = (e: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { if (dropEvent.complete.docDragData) { const droppedDocs = dropEvent.complete.docDragData?.droppedDocuments; const added = dropEvent.complete.docDragData.moveDocument?.(droppedDocs, this.Document, (doc: Doc | Doc[]) => this.addDoc(doc instanceof Doc ? doc : doc.lastElement(), fieldKey)); Doc.SetContainer(droppedDocs.lastElement(), this.dataDoc); !added && e.preventDefault(); e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place return added; } }; private registerSliding = (e: React.PointerEvent, targetWidth: number) => { if (e.button !== 2) { setupMoveUpEvents( this, e, this.onPointerMove, emptyFunction, action((e, doubleTap) => { if (doubleTap) { this._isAnyChildContentActive = true; if (!this.dataDoc[this.fieldKey + '_1']) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); if (!this.dataDoc[this.fieldKey + '_2']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); } }), false, undefined, action(() => { 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.clipHeightKey] = (targetWidth * 100) / this._props.PanelHeight(); setTimeout( action(() => (this._animating = '')), 200 ); }) ); } }; @action private onPointerMove = ({ movementX }: PointerEvent) => { const width = movementX * this.ScreenToLocalBoxXf().Scale + (this.clipWidth / 100) * this._props.PanelWidth(); if (width && width > 5 && width < this._props.PanelWidth()) { this.layoutDoc[this.clipWidthKey] = (width * 100) / this._props.PanelWidth(); } return false; }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { const anchor = Docs.Create.ConfigDocument({ title: 'CompareAnchor:' + this.Document.title, // set presentation timing properties for restoring view presentation_transition: 1000, annotationOn: this.Document, }); if (anchor) { if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; /* addAsAnnotation &&*/ this.addDocument(anchor); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), clippable: true } }, this.Document); return anchor; } return this.Document; }; @undoBatch clearDoc = (fieldKey: string) => { delete this.dataDoc[fieldKey]; this.dataDoc[fieldKey] = 'empty'; }; // 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; 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; } return false; }; whenChildContentsActiveChanged = action((isActive: boolean) => (this._isAnyChildContentActive = isActive)); closeDown = (e: React.PointerEvent, which: string) => { setupMoveUpEvents( this, e, e => { 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 => { return addDocument(doc); }; de.canEmbed = true; DragManager.StartDocumentDrag([this._closeRef.current!], de, e.clientX, e.clientY); return true; }, emptyFunction, e => this.clearDoc(which) ); }; docStyleProvider = (doc: Opt, props: Opt, property: string): any => { if (property === StyleProp.PointerEvents) return 'none'; return this._props.styleProvider?.(doc, props, property); }; moveDoc1 = (doc: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_1'), true); moveDoc2 = (doc: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true); remDoc1 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true); remDoc2 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true); _closeRef = React.createRef(); /** * 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 ( flip}>
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', }}>
); } @action handleRenderGPTClick = () => { // Call the GPT model and get the output this.layoutDoc[`_${this._props.fieldKey}_usePath`] = 'alternate'; this.outputValue = ''; if (this.inputValue) this.askGPT(); }; @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 => { 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 ( remove}>
this.closeDown(e, which)} // prevent triggering slider movement in registerSliding >
); }; const displayDoc = (which: string) => { const whichDoc = DocCast(this.dataDoc[which]); const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc); // 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 layoutTemplateString = !targetDoc && which.endsWith('1') && this.Document[this.fieldKey] instanceof RichTextField ? FormattedTextBox.LayoutString(this.fieldKey) : undefined; return targetDoc || layoutTemplateString ? ( <> NumCast(this.layoutDoc.width, 200)} NativeHeight={(): number => { return NumCast(this.layoutDoc.height, 200); }} isContentActive={emptyFunction} isDocumentActive={returnFalse} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} styleProvider={this._isAnyChildContentActive ? this._props.styleProvider : this.docStyleProvider} hideLinkButton={true} pointerEvents={this._isAnyChildContentActive ? undefined : returnNone} /> {layoutTemplateString ? null : clearButton(which)} // placeholder image if doc is missing ) : (
); }; const displayBox = (which: string, index: number, cover: number) => { return (
this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which, index)}> {displayDoc(which)}
); }; const displayBoxReveal = (which: string, which2: string, index: number, cover: number) => { return (
this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which, 0)}> {displayDoc(which)}
this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which2, 1)}> {displayDoc(which2)}
); }; 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 (

{StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text)}

{/* {StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text)} */}
{