aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/ComparisonBox.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/ComparisonBox.tsx')
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx225
1 files changed, 196 insertions, 29 deletions
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index e1d16549c..adb380f12 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) {
@@ -84,6 +105,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
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 = '';
@@ -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;
}
@@ -143,10 +170,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
e,
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.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => addDocument(doc);
de.canEmbed = true;
DragManager.StartDocumentDrag([this._closeRef.current!], de, moveEv.clientX, moveEv.clientY);
return true;
@@ -165,7 +189,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
remDoc2 = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true);
/**
- * Tests for whether a comparison box slot (ie, before or after) has renderable text content
+ * Tests for whether a comparison box slot (ie, before or after) has renderable text content.
+ * If it does, render a FormattedTextBox for that slot that references the comparisonBox's slot field
* @param whichSlot field key for start or end slot
* @returns a JSX layout string if a text field is found, othwerise undefined
*/
@@ -196,27 +221,104 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
};
_closeRef = React.createRef<HTMLDivElement>();
+
+ /**
+ * 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, () => {
+ 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();
+ };
+
+ @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 {
+ const 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');
+ }
+ };
+ layoutWidth = () => NumCast(this.layoutDoc.width, 200);
+ layoutHeight = () => NumCast(this.layoutDoc.height, 200);
+
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>
+ <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>
);
-
- /**
- * 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
- */
const displayDoc = (whichSlot: string) => {
const whichDoc = DocCast(this.dataDoc[whichSlot]);
const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc);
const layoutString = targetDoc ? '' : this.testForTextFields(whichSlot);
+
return targetDoc || layoutString ? (
<>
<DocumentView
@@ -229,8 +331,8 @@ 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={this.layoutWidth}
+ NativeHeight={this.layoutHeight}
isContentActive={emptyFunction}
isDocumentActive={returnFalse}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
@@ -252,6 +354,71 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
</div>
);
+ 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.
+ // eslint-disable-next-line prefer-destructuring
+ 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.
+ // eslint-disable-next-line prefer-destructuring
+ 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 type="button" onClick={this.handleRenderGPTClick}>
+ Submit
+ </button>
+ </div>
+ <div className="submit-button" style={{ display: this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? 'flex' : 'none' }}>
+ <button type="button" onClick={this.handleRenderClick}>
+ Edit Your Response
+ </button>
+ </div>
+ </div>
+ );
+ }
+
+ // 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' }}
+ onMouseEnter={() => {
+ this.hoverFlip('alternate');
+ }}
+ onMouseLeave={() => {
+ this.hoverFlip(undefined);
+ }}>
+ {displayBox(`${this.fieldKey}_${side === 0 ? 1 : 0}`, side, this._props.PanelWidth() - 3)}
+ {this.overlayAlternateIcon}
+ </div>
+ );
+ }
+ // 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)}