diff options
-rw-r--r-- | package-lock.json | 3 | ||||
-rw-r--r-- | src/client/apis/gpt/GPT.ts | 19 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 5 | ||||
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 5 | ||||
-rw-r--r-- | src/client/views/collections/CollectionCarouselView.scss | 18 | ||||
-rw-r--r-- | src/client/views/collections/CollectionCarouselView.tsx | 137 | ||||
-rw-r--r-- | src/client/views/collections/CollectionView.tsx | 14 | ||||
-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 | ||||
-rw-r--r-- | src/client/views/pdf/AnchorMenu.tsx | 76 | ||||
-rw-r--r-- | src/client/views/pdf/GPTPopup/GPTPopup.tsx | 3 | ||||
-rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 5 |
14 files changed, 706 insertions, 74 deletions
diff --git a/package-lock.json b/package-lock.json index 3a4ca6668..9500b1488 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39228,7 +39228,8 @@ "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true }, "node_modules/textarea-caret": { "version": "3.1.0", diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 7af71a6a2..9efe4ec39 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -5,6 +5,9 @@ enum GPTCallType { SUMMARY = 'summary', COMPLETION = 'completion', EDIT = 'edit', + CHATCARD = 'chatcard', + FLASHCARD = 'flashcard', + QUIZ = 'quiz', SORT = 'sort', DESCRIBE = 'describe', MERMAID = 'mermaid', @@ -20,9 +23,10 @@ type GPTCallOpts = { const callTypeMap: { [type: string]: GPTCallOpts } = { // newest model: gpt-4 - summary: { model: 'gpt-3.5-turbo', maxTokens: 256, temp: 0.5, prompt: 'Summarize the text given in simpler terms.' }, - edit: { model: 'gpt-3.5-turbo', maxTokens: 256, temp: 0.5, prompt: 'Reword the text.' }, - completion: { model: 'gpt-3.5-turbo', maxTokens: 256, temp: 0.5, prompt: "You are a helpful assistant. Answer the user's prompt." }, + summary: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Summarize the text given in simpler terms.' }, + edit: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Reword the text.' }, + flashcard: { model: 'gpt-4-turbo', maxTokens: 512, temp: 0.5, prompt: 'Make flashcards out of this text with each question and answer labeled. Do not label each flashcard and do not include asterisks: ' }, + completion: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: "You are a helpful assistant. Answer the user's prompt." }, mermaid: { model: 'gpt-4-turbo', maxTokens: 2048, @@ -42,6 +46,13 @@ const callTypeMap: { [type: string]: GPTCallOpts } = { prompt: "I'm going to give you a list of descriptions. Each one is seperated by ====== on either side. They will vary in length, so make sure to only seperate when you see ======. Sort them into lists by shared content. MAKE SURE EACH DESCRIPTOR IS IN ONLY ONE LIST. Generate only the list with each list seperated by ====== with the elements seperated by ~~~~~~. Try to do around 4 groups, but a little more or less is ok.", }, describe: { model: 'gpt-4-vision-preview', maxTokens: 2048, temp: 0, prompt: 'Describe these images in 3-5 words' }, + chatcard: { model: 'gpt-4-turbo', maxTokens: 512, temp: 0.5, prompt: 'Answer the following question as a short flashcard response. Do not include a label.' }, + quiz: { + model: 'gpt-4-turbo', + maxTokens: 1024, + temp: 0, + prompt: 'List unique differences between the content of the UserAnswer and Rubric. Before each difference, label it and provide any additional information the UserAnswer missed and explain it in second person without separating it into UserAnswer and Rubric content and additional information. If there are no differences, say correct', + }, }; let lastCall = ''; @@ -53,7 +64,7 @@ let lastResp = ''; * @returns AI Output */ const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: any) => { - const inputText = callType === GPTCallType.SUMMARY ? inputTextIn + '.' : inputTextIn; + const inputText = callType === GPTCallType.SUMMARY || callType == GPTCallType.FLASHCARD || GPTCallType.QUIZ ? inputTextIn + '.' : inputTextIn; const opts: GPTCallOpts = callTypeMap[callType]; if (lastCall === inputText) return lastResp; try { diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index e16e0e834..640fb251d 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -281,6 +281,7 @@ export class DocumentOptions { _layout_fitWidth?: BOOLt = new BoolInfo('whether document should scale its contents to fit its rendered width or not (e.g., for PDFviews)'); _layout_fieldKey?: STRt = new StrInfo('the field key containing the current layout definition', false); _layout_enableAltContentUI?: BOOLt = new BoolInfo('whether to show alternate content button'); + _layout_isFlashcard?: BOOLt = new BoolInfo('whether comparison node should be displayed as a flashcard'); _layout_showTitle?: string; // field name to display in header (:hover is an optional suffix) _layout_showSidebar?: BOOLt = new BoolInfo('whether an annotationsidebar should be displayed for text docuemnts'); _layout_showCaption?: string; // which field to display in the caption area. leave empty to have no caption @@ -778,8 +779,8 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.SCREENSHOT), '', options); } - export function ComparisonDocument(options: DocumentOptions = { title: 'Comparison Box' }) { - return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), undefined, options); + export function ComparisonDocument(text: string, options: DocumentOptions = { title: 'Comparison Box' }) { + return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), text, options); } export function DiagramDocument(options: DocumentOptions = { title: 'bruh box' }) { return InstanceFromProto(Prototypes.get(DocumentType.DIAGRAM), undefined, options); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index c2e915b33..b9c4d8b5f 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -354,14 +354,15 @@ pie title Minerals in my tap water creator:(opts:DocumentOptions)=> any // how to create the empty thing if it doesn't exist }[] = [ {key: "Note", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true }}, + {key: "Flashcard", creator: opts => Docs.Create.ComparisonDocument("", opts), opts: { _layout_isFlashcard: true, _width: 300, _height: 300}}, + // {key: "Flashcard", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true, _layout_enableAltContentUI: true}}, {key: "Image", creator: opts => Docs.Create.ImageDocument("", opts), opts: { _width: 400, _height:400 }}, - {key: "Flashcard", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true, _layout_enableAltContentUI: true}}, {key: "Equation", creator: opts => Docs.Create.EquationDocument("",opts), opts: { _width: 300, _height: 35, }}, {key: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200, _layout_fitWidth: true}}, {key: "Simulation", creator: opts => Docs.Create.SimulationDocument(opts), opts: { _width: 300, _height: 300, }}, {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100, _layout_fitWidth: true }}, {key: "Webpage", creator: opts => Docs.Create.WebDocument("",opts), opts: { _width: 400, _height: 512, _nativeWidth: 850, data_useCors: true, }}, - {key: "Comparison", creator: Docs.Create.ComparisonDocument, opts: { _width: 300, _height: 300 }}, + {key: "Comparison", creator: opts => Docs.Create.ComparisonDocument("",opts), opts: { _width: 300, _height: 300 }}, {key: "Diagram", creator: Docs.Create.DiagramDocument, opts: { _width: 300, _height: 300, _type_collection: CollectionViewType.Freeform, layout_diagramEditor: CollectionView.LayoutString("data") }, scripts: { onPaint: `toggleDetail(documentView, "diagramEditor","")`}}, {key: "Audio", creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, }}, {key: "Audio", creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, _layout_fitWidth: true, }}, diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss index 130b31325..f115bb40a 100644 --- a/src/client/views/collections/CollectionCarouselView.scss +++ b/src/client/views/collections/CollectionCarouselView.scss @@ -13,7 +13,10 @@ } } .carouselView-back, -.carouselView-fwd { +.carouselView-fwd, +.carouselView-star, +.carouselView-remove, +.carouselView-check { position: absolute; display: flex; top: 42.5%; @@ -34,6 +37,19 @@ .carouselView-back { left: 20; } +.carouselView-star { + top: 0; + right: 20; +} +.carouselView-remove { + top: 80%; + left: 52%; +} +.carouselView-check { + top: 80%; + right: 52%; +} + .carouselView-back:hover, .carouselView-fwd:hover { background: lightgray; diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 45b64d3e6..b736c7ced 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -42,14 +42,114 @@ export class CollectionCarouselView extends CollectionSubView() { return this.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK); } + /** + * Goes to the next flashcard in the stack and filters + * based on the the currently selected option. + */ advance = (e: React.MouseEvent) => { e.stopPropagation(); - this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + 1) % this.carouselItems.length; + this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + 1) % this.childLayoutPairs.length; + var startInd = this.layoutDoc._carousel_index; + + // if the star filter is selected + if (this.layoutDoc[`filterOp`] == 'star') { + // go to a flashcard that is starred, skip the ones that aren't + while (!this.childLayoutPairs?.[NumCast(startInd)].layout[`${this.fieldKey}_star`] && (startInd + 1) % this.childLayoutPairs.length != this.layoutDoc._carousel_index) { + startInd = (startInd + 1) % this.childLayoutPairs.length; + } + this.layoutDoc._carousel_index = startInd; + // if there aren't any starred, show all cards + if (!this.childLayoutPairs?.[NumCast(startInd)].layout[`${this.fieldKey}_star`]) { + this.layoutDoc[`filterOp`] = 'all'; + } + } + + // if the practice filter is selected + if (this.layoutDoc[`filterOp`] == 'practice') { + // go to a new index that is missed, skip the ones that are correct + while (this.childLayoutPairs?.[NumCast(startInd)].layout[`${this.fieldKey}_missed`] == 'correct' && (startInd + 1) % this.childLayoutPairs.length != this.layoutDoc._carousel_index) { + startInd = (startInd + 1) % this.childLayoutPairs.length; + } + this.layoutDoc._carousel_index = startInd; + + // if the user has gone through all of the cards and gotten them all correct, show all cards and exit practice mode + if (this.childLayoutPairs?.[NumCast(startInd)].layout[`${this.fieldKey}_missed`] == 'correct') { + this.layoutDoc[`filterOp`] = 'all'; + + // set all the cards to missed + for (var i = 0; i < this.childLayoutPairs.length; i++) { + const curDoc = this.childLayoutPairs?.[NumCast(i)]; + curDoc.layout[`${this.fieldKey}_missed`] = undefined; + } + } + } }; + + /** + * Goes to the previous flashcard in the stack and filters + * based on the the currently selected option. + */ goback = (e: React.MouseEvent) => { e.stopPropagation(); - this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) - 1 + this.carouselItems.length) % this.carouselItems.length; + this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length; + + var startInd = this.layoutDoc._carousel_index; + + // if the star filter is selected + if (this.layoutDoc[`filterOp`] == 'star') { + // go to a new index that is starred, skip the ones that aren't + while (!this.childLayoutPairs?.[NumCast(startInd)].layout[`${this.fieldKey}_star`] && (startInd - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length != this.layoutDoc._carousel_index) { + startInd = (startInd - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length; + } + this.layoutDoc._carousel_index = startInd; + // if there aren't any starred, show all cards + if (!this.childLayoutPairs?.[NumCast(startInd)].layout[`${this.fieldKey}_star`]) { + this.layoutDoc[`filterOp`] = 'all'; + } + } + + // if the practice filter is selected + if (this.layoutDoc[`filterOp`] == 'practice') { + // go to a new index that is missed, skip the ones that are correct + while (this.childLayoutPairs?.[NumCast(startInd)].layout[`${this.fieldKey}_missed`] == 'correct' && (startInd - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length != this.layoutDoc._carousel_index) { + startInd = (startInd - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length; + } + + this.layoutDoc._carousel_index = startInd; + + // See all flashcards when finish going through practice mode and set all of the flashcards back to + if (this.childLayoutPairs?.[NumCast(startInd)].layout[`${this.fieldKey}_missed`] == 'correct') { + this.layoutDoc[`filterOp`] = 'all'; + + for (var i = 0; i < this.childLayoutPairs.length; i++) { + const curDoc = this.childLayoutPairs?.[NumCast(i)]; + curDoc.layout[`${this.fieldKey}_missed`] = undefined; + } + } + } + }; + + /* + * Stars the document when the star button is pressed. + */ + star = (e: React.MouseEvent) => { + e.stopPropagation(); + const curDoc = this.childLayoutPairs?.[NumCast(this.layoutDoc._carousel_index)]; + if (!curDoc) return; + if (curDoc.layout[`${this.fieldKey}_star`] == undefined) curDoc.layout[`${this.fieldKey}_star`] = true; + else curDoc.layout[`${this.fieldKey}_star`] = !curDoc.layout[`${this.fieldKey}_star`]; + }; + + /* + * Sets a flashcard to either missed or correct depending on if they got the question right in practice mode. + */ + missed = (e: React.MouseEvent, val: string) => { + e.stopPropagation(); + const curDoc = this.childLayoutPairs?.[NumCast(this.layoutDoc._carousel_index)]; + curDoc.layout[`${this.fieldKey}_missed`] = val; + this.advance(e); }; + captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string): any => { // first look for properties on the document in the carousel, then fallback to properties on the container const childValue = doc?.['caption_' + property] ? this._props.styleProvider?.(doc, captionProps, property) : undefined; @@ -107,6 +207,7 @@ export class CollectionCarouselView extends CollectionSubView() { ); } @computed get buttons() { + if (!this.childLayoutPairs?.[NumCast(this.layoutDoc._carousel_index)]) return; return ( <> <div key="back" className="carouselView-back" onClick={this.goback}> @@ -115,6 +216,15 @@ export class CollectionCarouselView extends CollectionSubView() { <div key="fwd" className="carouselView-fwd" onClick={this.advance}> <FontAwesomeIcon icon="chevron-right" size="2x" /> </div> + <div key="star" className="carouselView-star" onClick={this.star}> + <FontAwesomeIcon icon={'star'} color={this.childLayoutPairs?.[NumCast(this.layoutDoc._carousel_index)].layout[`${this.fieldKey}_star`] ? 'yellow' : 'gray'} size={'1x'} /> + </div> + <div key="remove" className="carouselView-remove" onClick={e => this.missed(e, 'missed')} style={{ visibility: this.layoutDoc[`filterOp`] == 'practice' ? 'visible' : 'hidden' }}> + <FontAwesomeIcon icon={'xmark'} color={'red'} size={'1x'} /> + </div> + <div key="check" className="carouselView-check" onClick={e => this.missed(e, 'correct')} style={{ visibility: this.layoutDoc[`filterOp`] == 'practice' ? 'visible' : 'hidden' }}> + <FontAwesomeIcon icon={'check'} color={'green'} size={'1x'} /> + </div> </> ); } @@ -129,6 +239,29 @@ export class CollectionCarouselView extends CollectionSubView() { color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color), }}> {this.content} + {/* Displays a message to the user to add more flashcards if they are in practice mode and no flashcards are there. */} + <p + style={{ + display: !this.childLayoutPairs?.[NumCast(this.layoutDoc._carousel_index)] && this.layoutDoc[`filterOp`] == 'practice' ? 'flex' : 'none', + justifyContent: 'center', + alignItems: 'center', + height: '100%', + zIndex: '-1', + }}> + Add flashcards! + </p> + {/* Displays a message to the user that a flashcard was recently missed if they had previously gotten it wrong. */} + <p + style={{ + color: 'red', + zIndex: '999', + position: 'relative', + left: '10px', + top: '10px', + display: this.childLayoutPairs?.[NumCast(this.layoutDoc._carousel_index)] ? (this.childLayoutPairs?.[NumCast(this.layoutDoc._carousel_index)].layout[`${this.fieldKey}_missed`] == 'missed' ? 'block' : 'none') : 'none', + }}> + Recently missed! + </p> {this.Document._chromeHidden ? null : this.buttons} </div> ); diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 5c304b4a9..d084a2aec 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -52,6 +52,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr super(props); makeObservable(this); this._annotationKeySuffix = returnEmptyString; + this.layoutDoc[`filterOp`] = 'all'; } componentDidMount() { @@ -150,6 +151,19 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr return newRendition; }); + // creates menu option for flashcard filters + const revealOptions = cm.findByDescription('Filter Flashcards'); + const revealItems: ContextMenuProps[] = revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : []; + revealItems.push({ description: 'All', event: () => (this.layoutDoc[`filterOp`] = 'all'), icon: 'layer-group' }); + revealItems.push({ description: 'Star', event: () => (this.layoutDoc[`filterOp`] = 'star'), icon: 'star' }); + revealItems.push({ description: 'Practice Mode', event: () => (this.layoutDoc[`filterOp`] = 'practice'), icon: 'check' }); + revealItems.push({ description: 'Quiz Cards', event: () => (this.layoutDoc[`filterOp`] = 'quiz'), icon: 'pencil' }); + + // only show the filter options if it is a collection of type Carousel view + if (this.Document?._type_collection === CollectionViewType.Carousel) { + !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' }); + } + const options = cm.findByDescription('Options...'); const optionItems = options && 'subitems' in options ? options.subitems : []; !Doc.noviceMode ? optionItems.splice(0, 0, { description: `${this.Document.forceActive ? 'Select' : 'Force'} Contents Active`, event: () => {this.Document.forceActive = !this.Document.forceActive}, icon: 'project-diagram' }) : null; // prettier-ignore 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 diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index 495ea59f0..b7247a034 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -7,13 +7,15 @@ import { ColorResult } from 'react-color'; import { ClientUtils, returnFalse, setupMoveUpEvents } from '../../../ClientUtils'; import { emptyFunction, unimplementedFunction } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; -import { DocumentType } from '../../documents/DocumentTypes'; +import { gptAPICall } from '../../apis/gpt/GPT'; +import { GPTCallType } from '../../apis/gpt/setup'; import { SettingsManager } from '../../util/SettingsManager'; import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu'; import { LinkPopup } from '../linking/LinkPopup'; -import './AnchorMenu.scss'; -import { GPTPopup } from './GPTPopup/GPTPopup'; import { DocumentView } from '../nodes/DocumentView'; +import './AnchorMenu.scss'; +import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup'; +import { Docs } from '../../documents/Documents'; @observer export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @@ -59,6 +61,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { public get Active() { return this._left > 0; } + public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined; componentWillUnmount() { this._disposer?.(); @@ -75,9 +78,63 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { * Invokes the API with the selected text and stores it in the summarized text. * @param e pointer down event */ - gptSummarize = async () => { - GPTPopup.Instance?.setSelectedText(this._selectedText); - GPTPopup.Instance.generateSummary(); + gptSummarize = async (e: React.PointerEvent) => { + GPTPopup.Instance.setVisible(true); + GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY); + GPTPopup.Instance.setLoading(true); + + try { + const res = await gptAPICall(this.selectedText, GPTCallType.SUMMARY); + GPTPopup.Instance.setText(res || 'Something went wrong.'); + } catch (err) { + console.error(err); + } + GPTPopup.Instance.setLoading(false); + }; + // gptSummarize = async () => { + // GPTPopup.Instance?.setSelectedText(this._selectedText); + // GPTPopup.Instance.generateSummary(); + // }; + + /** + * Invokes the API with the selected text and stores it in the selected text. + * @param e pointer down event + */ + gptFlashcards = async (e: React.PointerEvent) => { + const queryText = this.selectedText; + try { + const res = await gptAPICall(queryText, GPTCallType.FLASHCARD); + console.log(res); + GPTPopup.Instance.setText(res || 'Something went wrong.'); + this.transferToFlashcard(res || 'Something went wrong'); + } catch (err) { + console.error(err); + } + GPTPopup.Instance.setLoading(false); + }; + + /* + * Transfers the flashcard text generated by GPT on flashcards and creates a collection out them. + */ + transferToFlashcard = (text: string) => { + // put each question generated by GPT on the front of the flashcard + const senArr = text.split('Question'); + const collectionArr: Doc[] = []; + for (var i = 1; i < senArr.length; i++) { + console.log('Arr ' + i + ': ' + senArr[i]); + const newDoc = Docs.Create.ComparisonDocument(senArr[i], { _layout_isFlashcard: true, _width: 300, _height: 300 }); + newDoc.text = senArr[i]; + collectionArr.push(newDoc); + } + // create a new carousel collection of these flashcards + const newCol = Docs.Create.CarouselDocument(collectionArr, { + _width: 250, + _height: 200, + _layout_fitWidth: false, + _layout_autoHeight: true, + }); + + this.addToCollection?.(newCol); }; pointerDown = (e: React.PointerEvent) => { @@ -162,6 +219,13 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { color={SettingsManager.userColor} /> )} + {/* Adds a create flashcards option to the anchor menu, which calls the gptFlashcard method. */} + <IconButton + tooltip="Create flashcards" // + onPointerDown={this.gptFlashcards} + icon={<FontAwesomeIcon icon="id-card" size="lg" />} + color={SettingsManager.userColor} + /> {AnchorMenu.Instance.OnAudio === unimplementedFunction ? null : ( <IconButton tooltip="Click to Record Annotation" // diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index 8bb2e2844..cb5aad32d 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -24,6 +24,7 @@ export enum GPTPopupMode { SUMMARY, EDIT, IMAGE, + FLASHCARD, DATA, SORT, } @@ -412,7 +413,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { <div className="btns-wrapper"> {this.done ? ( <> - <IconButton tooltip="Generate Again" onClick={this.generateSummary} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(SettingsManager.userVariantColor)} /> + <IconButton tooltip="Generate Again" onClick={this.generateSummary /* this.callSummaryApi */} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(SettingsManager.userVariantColor)} /> <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(SettingsManager.userVariantColor)} type={Type.TERT} /> </> ) : ( diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 2327ee0d8..6c1617c38 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -24,11 +24,13 @@ import { FieldViewProps } from '../nodes/FieldView'; import { FocusViewOptions } from '../nodes/FocusViewOptions'; import { LinkInfo } from '../nodes/LinkDocPreview'; import { PDFBox } from '../nodes/PDFBox'; +import { ComparisonBox } from '../nodes/ComparisonBox'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { StyleProp } from '../StyleProp'; import { AnchorMenu } from './AnchorMenu'; import { Annotation } from './Annotation'; import { GPTPopup } from './GPTPopup/GPTPopup'; +import { Docs } from '../../documents/Documents'; import './PDFViewer.scss'; // pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`; @@ -430,9 +432,10 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { AnchorMenu.Instance.jumpTo(e.clientX, e.clientY); } - // Changing which document to add the annotation to (the currently selected PDF) GPTPopup.Instance.setSidebarId('data_sidebar'); GPTPopup.Instance.addDoc = this._props.sidebarAddDoc; + // allows for creating collection + AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument; }; @action |