aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoralyssaf16 <alyssa_feinberg@brown.edu>2024-05-17 13:18:40 -0400
committeralyssaf16 <alyssa_feinberg@brown.edu>2024-05-17 13:18:40 -0400
commit5ff0bef5d3c4825aa7210a26c98aae3b24f4a835 (patch)
tree9c08c1631f8aa59d1ca1073b7064228061ff5a83
parent3fb9eada221670022aa575c72fb89103638c3cbd (diff)
chatcards, quizcards, and ai flashcards
-rw-r--r--src/client/apis/gpt/GPT.ts36
-rw-r--r--src/client/util/CurrentUserUtils.ts2
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx79
-rw-r--r--src/client/views/collections/CollectionView.tsx15
-rw-r--r--src/client/views/nodes/ComparisonBox.scss143
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx109
-rw-r--r--src/client/views/nodes/DocumentView.tsx24
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx5
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx27
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx45
-rw-r--r--src/client/views/pdf/PDFViewer.tsx4
11 files changed, 367 insertions, 122 deletions
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 2757fc830..6600ddab2 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -1,10 +1,13 @@
import { ClientOptions, OpenAI } from 'openai';
+import { ChatCompletionMessageParam } from 'openai/resources';
enum GPTCallType {
SUMMARY = 'summary',
COMPLETION = 'completion',
EDIT = 'edit',
+ CHATCARD = 'chatcard',
FLASHCARD = 'flashcard',
+ QUIZ = 'quiz',
}
type GPTCallOpts = {
@@ -15,10 +18,17 @@ type GPTCallOpts = {
};
const callTypeMap: { [type: string]: GPTCallOpts } = {
- summary: { model: 'gpt-3.5-turbo-instruct', maxTokens: 256, temp: 0.5, prompt: 'Summarize this text in simpler terms: ' },
- edit: { model: 'gpt-3.5-turbo-instruct', maxTokens: 256, temp: 0.5, prompt: 'Reword this: ' },
- flashcard: { model: 'gpt-3.5-turbo-instruct', maxTokens: 512, temp: 0.5, prompt: 'Make flashcards out of this text with questions and answers: ' },
- completion: { model: 'gpt-3.5-turbo-instruct', maxTokens: 256, temp: 0.5, prompt: '' },
+ summary: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Summarize this text in simpler terms: ' },
+ edit: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Reword this: ' },
+ 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: '' },
+ 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',
+ },
};
/**
@@ -28,7 +38,8 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
* @returns AI Output
*/
const gptAPICall = async (inputText: string, callType: GPTCallType) => {
- if (callType === GPTCallType.SUMMARY || callType == GPTCallType.FLASHCARD) inputText += '.';
+ if (!inputText) return 'Please provide a response.';
+ if (callType === GPTCallType.SUMMARY || callType == GPTCallType.FLASHCARD || GPTCallType.QUIZ) inputText += '.';
const opts: GPTCallOpts = callTypeMap[callType];
try {
const configuration: ClientOptions = {
@@ -36,13 +47,20 @@ const gptAPICall = async (inputText: string, callType: GPTCallType) => {
dangerouslyAllowBrowser: true,
};
const openai = new OpenAI(configuration);
- const response = await openai.completions.create({
+
+ let messages: ChatCompletionMessageParam[] = [
+ { role: 'system', content: opts.prompt },
+ { role: 'user', content: inputText },
+ ];
+
+ const response = await openai.chat.completions.create({
model: opts.model,
- max_tokens: opts.maxTokens,
+ messages: messages,
temperature: opts.temp,
- prompt: `${opts.prompt}${inputText}`,
+ max_tokens: opts.maxTokens,
});
- return response.choices[0].text;
+
+ return response.choices[0].message.content;
} catch (err) {
console.log(err);
return 'Error connecting with API.';
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index ab0315ba1..b6ba3f187 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -342,8 +342,6 @@ pie title Minerals in my tap water
}[] = [
{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: 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: "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, }},
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 7f5176123..d45b0822b 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -33,29 +33,53 @@ export class CollectionCarouselView extends CollectionSubView() {
}
};
+ /**
+ * 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.childLayoutPairs.length;
var startInd = this.layoutDoc._carousel_index;
// if the star filter is selected
- if (this.layoutDoc[`_${this._props.fieldKey}_filterOp`] == 'star') {
- // go to a new index that is starred, skip the ones that aren't
+ 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[`_${this._props.fieldKey}_filterOp`] == 'practice') {
+ 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.childLayoutPairs.length) % this.childLayoutPairs.length;
@@ -63,38 +87,58 @@ export class CollectionCarouselView extends CollectionSubView() {
var startInd = this.layoutDoc._carousel_index;
// if the star filter is selected
- if (this.layoutDoc[`_${this._props.fieldKey}_filterOp`] == 'star') {
+ 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[`_${this._props.fieldKey}_filterOp`] == 'practice') {
+ 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();
- // stars the document when the button is pressed
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.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + 1) % this.childLayoutPairs.length;
- this.advance;
+ this.advance(e);
};
captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string): any => {
@@ -154,6 +198,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}>
@@ -165,10 +210,10 @@ export class CollectionCarouselView extends CollectionSubView() {
<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[`_${this._props.fieldKey}_filterOp`] == 'practice' ? 'visible' : 'hidden' }}>
+ <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[`_${this._props.fieldKey}_filterOp`] == 'practice' ? 'visible' : 'hidden' }}>
+ <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>
</>
@@ -185,6 +230,18 @@ 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',
@@ -192,7 +249,7 @@ export class CollectionCarouselView extends CollectionSubView() {
position: 'relative',
left: '10px',
top: '10px',
- visibility: this.childLayoutPairs?.[NumCast(this.layoutDoc._carousel_index)].layout[`${this.fieldKey}_missed`] == 'missed' ? 'visible' : 'hidden',
+ 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>
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 168176edf..2f0f2a773 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -80,6 +80,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
super(props);
makeObservable(this);
this._annotationKeySuffix = returnEmptyString;
+ this.layoutDoc[`filterOp`] = 'all';
}
componentDidMount() {
@@ -176,14 +177,18 @@ 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[`_${this._props.fieldKey}_filterOp`] = 'all'), icon: 'eye-slash' });
- revealItems.push({ description: 'Star', event: () => (this.layoutDoc[`_${this._props.fieldKey}_filterOp`] = 'star'), icon: 'hand-point-up' });
- revealItems.push({ description: 'Practice Mode', event: () => (this.layoutDoc[`_${this._props.fieldKey}_filterOp`] = 'practice'), icon: 'rotate' });
+ 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' });
- //revealItems.push({ description: 'Bring to Front', event: () => SelectionManager.Views.forEach(dv => dv._props.bringToFront?.(dv.Document, false)), icon: 'arrow-up' });
- !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
+ // 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 : [];
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 19fccce8a..9fd4d696a 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -2,7 +2,7 @@ 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 } from '../../../Utils';
+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';
@@ -34,6 +34,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() {
@@ -160,7 +171,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
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 => {
- //this.clearDoc(which);
return addDocument(doc);
};
de.canEmbed = true;
@@ -181,15 +191,24 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
remDoc2 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true);
_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 (
@@ -203,21 +222,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
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);
-
- //const queryText = RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text;
- // DocCast(this.dataDoc[this.fieldKey + '_1'])[DocData].text = 'hello';
- // const mes = gptAPICall(queryText, GPTCallType.COMPLETION).trim();
- // const res = await gptAPICall(queryText, GPTCallType.COMPLETION)
- // console.log(res);
- //.then(value => (DocCast(this.dataDoc[this.fieldKey + '_1']).text = value.trim()));
- if (usepath !== 'alternate') {
- this.askGPT();
- }
}
})
}
style={{
- //display: this._props.isContentActive() && !SnappingManager.IsDragging ? 'flex' : 'none',
background: usepath === 'alternate' ? 'white' : 'black',
color: usepath === 'alternate' ? 'black' : 'white',
}}>
@@ -227,15 +235,34 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
);
}
+ @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 queryText = RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text;
+ 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(StrCast(queryText), GPTCallType.COMPLETION);
+ let res = await gptAPICall(queryText, GPTCallType.QUIZ);
if (!res) {
console.error('GPT call failed');
return;
}
- DocCast(this.dataDoc[this.fieldKey + '_0'])[DocData].text = res;
+ this.outputValue = res;
console.log(res);
} catch (err) {
console.error('GPT call failed');
@@ -292,8 +319,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
);
};
const displayBox = (which: string, index: number, cover: number) => {
- // if (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] == 'hide/reveal') this.layoutDoc[this.clipHeightKey] = 100;
- // else this.layoutDoc.height = 300;
return (
<div className={`${index === 0 ? 'before' : 'after'}Box-cont`} key={which} style={{ width: this._props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which, index)}>
{displayDoc(which)}
@@ -326,36 +351,54 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
if (this.Document._layout_isFlashcard) {
const side = this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? 1 : 0;
- // add text box when first created
+ // 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]);
- newDoc[DocData].text = 'placeholder...';
+ // 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');
}
- if (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] == 'hide/reveal') {
- {
- return (
- <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} style={{ display: 'flex', flexDirection: 'column' }}>
- {displayBox(`${this.fieldKey}_0`, side, this._props.PanelHeight() - 3)}
- {displayBox(`${this.fieldKey}_1`, 1, this._props.PanelHeight() - 3)}
+ // 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>
- );
- }
- } else {
+ <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' }}
- // style={{ position: 'absolute', top: '0px' }}
+ style={{ display: 'flex', flexDirection: 'column' }}
onMouseEnter={() => {
this.hoverFlip('alternate');
}}
@@ -363,12 +406,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
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)}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 26e5ec622..e1b501c5a 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -48,6 +48,9 @@ import { LinkAnchorBox } from './LinkAnchorBox';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
import { PresEffect, PresEffectDirection } from './trails';
import { FieldsDropdown } from '../FieldsDropdown';
+import { RTFCast } from '../../../fields/Types';
+import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT';
+
interface Window {
MediaRecorder: MediaRecorder;
}
@@ -517,6 +520,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();
@@ -569,17 +587,19 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => LightboxView.Instance.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' });
- revealItems.push({ description: 'Hide & Reveal', event: () => (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'hide/reveal'), icon: 'eye-slash' });
- //revealItems.push({ description: 'Bring to Front', event: () => SelectionManager.Views.forEach(dv => dv._props.bringToFront?.(dv.Document, false)), icon: 'arrow-up' });
!revealOptions && cm.addItem({ description: 'Reveal Options', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 4fce72cdc..3192ac537 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -178,6 +178,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
@observable
private gptRes: string = '';
+ public makeAIFlashcards: () => void = unimplementedFunction;
+ public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
+
public static PasteOnLoad: ClipboardEvent | undefined;
private static SelectOnLoad: Doc | undefined;
public static SetSelectOnLoad(doc: Doc) {
@@ -959,7 +962,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const options = cm.findByDescription('Options...');
const optionItems = options && 'subitems' in options ? options.subitems : [];
optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' });
- optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' });
+ optionItems.push({ description: `Ask GPT-3`, event: () => this.makeAIFlashcards(), icon: 'lightbulb' });
this._props.renderDepth &&
optionItems.push({
description: !this.Document._createDocOnCR ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns',
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 7cb6a20f4..a0c3cf487 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -76,7 +76,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
* @param e pointer down event
*/
gptSummarize = async (e: React.PointerEvent) => {
- // move this logic to gptpopup, need to implement generate again
GPTPopup.Instance.setVisible(true);
GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY);
GPTPopup.Instance.setLoading(true);
@@ -90,24 +89,28 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
GPTPopup.Instance.setLoading(false);
};
+ /**
+ * Invokes the API with the selected text and stores it in the selected text.
+ * @param e pointer down event
+ */
gptFlashcards = async (e: React.PointerEvent) => {
- // move this logic to gptpopup, need to implement generate again
- // GPTPopup.Instance.setVisible(true);
- // GPTPopup.Instance.setMode(GPTPopupMode.FLASHCARD);
- // GPTPopup.Instance.setLoading(true);
-
+ const queryText = this.selectedText;
try {
- const res = await gptAPICall(this.selectedText, GPTCallType.FLASHCARD);
-
+ const res = await gptAPICall(queryText, GPTCallType.FLASHCARD);
+ console.log(res);
GPTPopup.Instance.setText(res || 'Something went wrong.');
- this.transferToFlashcard(res);
+ 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++) {
@@ -116,10 +119,11 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
newDoc.text = senArr[i];
collectionArr.push(newDoc);
}
+ // create a new carousel collection of these flashcards
const newCol = Docs.Create.CarouselDocument(collectionArr, {
- _width: 200,
+ _width: 250,
_height: 200,
- _layout_fitWidth: true,
+ _layout_fitWidth: false,
_layout_autoHeight: true,
});
@@ -215,6 +219,7 @@ 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}
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 32c1721ec..0b741c85e 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -144,15 +144,6 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
_layout_autoHeight: true,
});
this.addDoc(newDoc, this.sidebarId);
- // const arr = [newDoc];
- // const newCol = Docs.Create.CarouselDocument(arr, {
- // _width: 200,
- // _height: 200,
- // _layout_fitWidth: true,
- // _layout_autoHeight: true,
- // });
- // this.addDoc(newDoc, this.sidebarId);
- // this.addDoc(newCol, this.sidebarId);
const anchor = AnchorMenu.Instance?.GetAnchor(undefined, false);
if (anchor) {
DocUtils.MakeLink(newDoc, anchor, {
@@ -161,25 +152,6 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
}
};
- // transferToFlashcard = () => {
- // const senArr = this.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);
- // }
- // const newCol = Docs.Create.CarouselDocument(collectionArr, {
- // _width: 200,
- // _height: 200,
- // _layout_fitWidth: true,
- // _layout_autoHeight: true,
- // });
- // this.addDoc(newCol, this.sidebarId);
- // this.addToCollection?.(newCol);
- // };
-
/**
* Transfers the image urls to actual image docs
*/
@@ -244,23 +216,6 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
);
};
- flashcardBox = () => {
- // const textArr = this.text.split(".");
- // textArr.forEach(function(sentence) {
- // console.log(sentence);
-
- // });
- // const newDoc = Docs.Create.ComparisonDocument();
- // this.addToCollection?.(newDoc);
- // // const newDoc = Docs.Create.ComparisonDocument();
- // DocUtils.copyDragFactory(Doc.UserDoc().emptyFlashcard as Doc);
- // // this.addToCollection?.(newDoc);
- // // return newDoc;
- // <ComparisonBox/>
- const newDoc = Docs.Create.TextDocument('Hello there');
- this.addDoc?.(newDoc);
- };
-
data = () => {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index cecaf17ff..fe1ed8159 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -414,12 +414,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;
- // const newDoc = Docs.Create.ComparisonDocument({ _layout_isFlashcard: true, _width: 300, _height: 300 });
- // this.props.addDocument?.(newDoc);
};
@action