aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/apis/gpt/GPT.ts8
-rw-r--r--src/client/documents/Documents.ts1
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx526
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx20
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx34
-rw-r--r--src/fields/util.ts1
6 files changed, 287 insertions, 303 deletions
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 66c49abc7..8a2c91269 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -5,8 +5,8 @@ enum GPTCallType {
SUMMARY = 'summary',
COMPLETION = 'completion',
EDIT = 'edit',
- CHATCARD = 'chatcard',
- FLASHCARD = 'flashcard',
+ CHATCARD = 'chatcard', // a single flashcard style response to a question
+ FLASHCARD = 'flashcard', // a set of flashcard qustion/answer responses to a topic
QUIZ = 'quiz',
SORT = 'sort',
DESCRIBE = 'describe',
@@ -38,7 +38,6 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
// newest model: gpt-4
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 as question and answer. Do not label each flashcard and do not include asterisks: ' },
stack: {
model: 'gpt-4o',
maxTokens: 2048,
@@ -66,6 +65,7 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
prompt: "The user is going to give you a list of descriptions. Each one is separated by `======` on either side. Descriptions will vary in length, so make sure to only separate when you see `======`. Sort them by the user's specifications. Make sure each description is only in the list once. Each item should be separated by `======`. Immediately afterward, surrounded by `------` on BOTH SIDES, provide some insight into your reasoning for the way you sorted (and mention nothing about the formatting details given in this description). It is VERY important that you format it exactly as described, ensuring the proper number of `=` and `-` (6 of each) and NO commas",
},
describe: { model: 'gpt-4-vision-preview', maxTokens: 2048, temp: 0, prompt: 'Describe these images in 3-5 words' },
+ flashcard: { model: 'gpt-4-turbo', maxTokens: 512, temp: 0.5, prompt: 'Make flashcards out of this text with each question and answer labeled as question and answer. Do not label each flashcard and do not include asterisks: ' },
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',
@@ -127,7 +127,7 @@ let lastResp = '';
*/
const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: string, dontCache?: boolean) => {
const inputText = [GPTCallType.SUMMARY, GPTCallType.FLASHCARD, GPTCallType.QUIZ, GPTCallType.STACK].includes(callType) ? inputTextIn + '.' : inputTextIn;
- const opts: GPTCallOpts = callTypeMap[callType];
+ const opts = callTypeMap[callType];
if (lastCall === inputText && dontCache !== true) return lastResp;
try {
lastCall = inputText;
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 0d7e0b20e..f71b9f879 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -305,6 +305,7 @@ export class DocumentOptions {
_text_fontFamily?: string;
_text_fontWeight?: string;
text_align?: STRt = new StrInfo('horizontal text alignment default');
+ text_placeholder?: BOOLt = new BoolInfo('makes the text act like a placeholder and automatically select when the text box is selected');
fontSize?: string;
_pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 0582bc996..80ef126dc 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -9,7 +9,6 @@ import { imageUrlToBase64, returnFalse, returnNone, returnTrue, returnZero, setu
import { emptyFunction } from '../../../Utils';
import { Doc, Opt } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
-import { Id } from '../../../fields/FieldSymbols';
import { RichTextField } from '../../../fields/RichTextField';
import { BoolCast, DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types';
import { nullAudio } from '../../../fields/URLField';
@@ -26,21 +25,22 @@ import { ViewBoxAnnotatableComponent } from '../DocComponent';
import { PinDocView, PinProps } from '../PinFuncs';
import { StyleProp } from '../StyleProp';
import { flashcardRevealOp, practiceMode } from '../collections/FlashcardPracticeUI';
+import { CollectionFreeFormView } from '../collections/collectionFreeForm';
import '../pdf/GPTPopup/GPTPopup.scss';
import './ComparisonBox.scss';
import { DocumentView } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
-import { CollectionFreeFormView } from '../collections/collectionFreeForm';
const API_URL = 'https://api.unsplash.com/search/photos';
/**
- * This view serves two distinct functions depending on the metadata field layout_isFlashcard
+ * This view serves three distinct functions depending on the metadata field layout_isFlashcard
* 1) it provides a before/after animated sliding transition between two Docs
* 2) it provides a question/answer switch between two Docs (flashcard)
+ * 3) it provides a quiz view that displays a question and a user answer that can be "scored" by GPT
*
- * In either case, the two docs are stored in the <fieldKey>_front and <fieldKey>_back fields
+ * In each case, the two docs are stored in the <fieldKey>_front and <fieldKey>_back fields
*
* In the case of the flashcard, there is an icon that allows the user to choose between a
* hover and a flip action to switch between cards. The transition is stored in the 'revealOp' field.
@@ -49,6 +49,9 @@ const API_URL = 'https://api.unsplash.com/search/photos';
* One option is to allow the user to enter a topic and, by clicking on the flashcard stack button,
* convert the comparision box into a stack of comparison boxes filled in by GPT about the topic.
*
+ * Quiz mode is activated when the parent collection has its 'quiz' field set when it renders a flashcard.
+ * NOTE: this should probably be changed to passing down a prop to the flashcard telling it to render as a quiz.
+ *
*/
@observer
@@ -56,6 +59,54 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(ComparisonBox, fieldKey);
}
+ /**
+ * Creates a flashcard (or fills in flashcard data to a specified Doc) from a control string containing a question and answer
+ * @param tuple string containing Question:, Answer: and optionally a Keyword:
+ * @param useDoc doc to fill in instead of creating a Doc
+ * @returns the resulting flashcard Doc
+ */
+ public static createFlashcard(tuple: string, frontKey: string, backKey: string, useDoc?: Doc) {
+ const [ktoken, atoken] = [ComparisonBox.ktoken, ComparisonBox.atoken];
+ const newDoc = useDoc ?? Docs.Create.ComparisonDocument('', { _layout_isFlashcard: true, _width: 300, _height: 300 });
+ const question = (tuple.includes(ktoken) ? tuple.split(ktoken)[0] : tuple).split(atoken)[0];
+ const rest = tuple.replace(question, '');
+ // prettier-ignore
+ const answer = rest.startsWith(ktoken) ? // if keyword comes first,
+ tuple.includes(atoken) ? tuple.split(atoken)[1] : "" : //if tuple includes answer, split at answer and take what's left, otherwise there's no answer
+ rest.includes(ktoken) ? // otherwise if keyword is present it must come after answer,
+ rest.split(ktoken)[0].split(atoken)[1] : // split at keyword and take what comes first and split that at answer and take what's left
+ rest.replace(atoken,""); // finally if there's no keyword, just get rid of answer token and take what's left
+ const keyword = rest.replace(atoken, '').replace(answer, '').replace(ktoken, '').trim();
+ const fillInFlashcard = (img?: Doc) => {
+ newDoc[DocData][frontKey] = FormattedTextBox.centeredTextCreator('question', question, img);
+ newDoc[DocData][backKey] = FormattedTextBox.centeredTextCreator('answer', answer);
+ return newDoc;
+ };
+ return keyword && keyword.toLowerCase() !== 'none' ? ComparisonBox.fetchImages(keyword).then(img => fillInFlashcard(img)) : fillInFlashcard();
+ }
+
+ /**
+ * Create a carousel of flashcards from a GPT response string where questions and answers are given in a format loosely defined by:
+ * Question: ... Answer: ... Keyword: ...
+ * Note that Keyword or Answer may not be present, or their orders may be reversed.
+ */
+ public static createFlashcardDeck(text: string, width: number, height: number, front: string, back: string) {
+ return Promise.all(
+ text
+ .split(ComparisonBox.qtoken)
+ .filter(t => t)
+ .map(tuple => ComparisonBox.createFlashcard(tuple, front, back))
+ ).then(docs => {
+ return Docs.Create.CarouselDocument(docs, {
+ _width: width,
+ _height: height,
+ _layout_fitWidth: false,
+ _layout_autoHeight: true,
+ _xMargin: 5,
+ _yMargin: 5,
+ });
+ });
+ }
private SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
static qtoken = 'Question: ';
@@ -118,9 +169,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
return undefined;
}, 'internal drop');
+ @computed get isQuizMode() { return DocCast(this.Document.embedContainer)?.practiceMode === practiceMode.QUIZ; } // prettier-ignore
@computed get isFlashcard() { return BoolCast(this.Document.layout_isFlashcard); } // prettier-ignore
@computed get frontKey() { return this._props.fieldKey + '_front'; } // prettier-ignore
@computed get backKey() { return this._props.fieldKey + '_back'; } // prettier-ignore
+ @computed get frontText() { return RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text; } // prettier-ignore
+ @computed get backText() { return RTFCast(DocCast(this.dataDoc[this.backKey]).text)?.Text; } // prettier-ignore
@computed get revealOpKey() { return `_${this._props.fieldKey}_revealOp`; } // prettier-ignore
@computed get clipHeightKey() { return `_${this._props.fieldKey}_clipHeight`; } // prettier-ignore
@computed get clipWidthKey() { return `_${this._props.fieldKey}_clipWidth`; } // prettier-ignore
@@ -177,9 +231,19 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
</div>
</Tooltip>
)}
- {!this._props.isSelected() || this._renderSide === this.backKey || CollectionFreeFormView.from(this.DocumentView?.()) ? null : (
+ {!this._props.isSelected() || this._renderSide === this.backKey || !CollectionFreeFormView.from(this.DocumentView?.()) || (this.dataDoc[this.backKey] && !DocCast(this.dataDoc[this.backKey])?.text_placeholder) ? null : (
<Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}>
- <div className="comparisonBox-button" onClick={() => this.askGPT(GPTCallType.STACK).then(this.createFlashcardDeck)}>
+ <div
+ className="comparisonBox-button"
+ onClick={() =>
+ this.askGPT(GPTCallType.STACK).then(async text => {
+ const newCol = await ComparisonBox.createFlashcardDeck(text, NumCast(this.layoutDoc._width, 250) + 50, NumCast(this.layoutDoc._height, 200), this.frontKey, this.backKey);
+ newCol.x = NumCast(this.layoutDoc.x);
+ newCol.y = NumCast(this.layoutDoc.y);
+ this._props.DocumentView?.()._props.addDocument?.(newCol);
+ this._props.removeDocument?.(this.Document);
+ })
+ }>
<FontAwesomeIcon icon="layer-group" size="xl" />
</div>
</Tooltip>
@@ -397,124 +461,51 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
axios
.post(
'http://localhost:105/youtube/', //
- { file: this.getYouTubeVideoId(StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text)) },
+ { file: this.getYouTubeVideoId(this.frontText) },
{ headers: { 'Content-Type': 'application/json' } }
)
.then(response => response.data.transcription);
/**
- * Creates a flashcard (or fills in flashcard data to a specified Doc) from a control string containing a question and answer
- * @param tuple string containing Question:, Answer: and optionally a Keyword:
- * @param useDoc doc to fill in instead of creating a Doc
- * @returns the resulting flashcard Doc
- */
- createFlashcard = (tuple: string, useDoc?: Doc) => {
- const [ktoken, atoken] = [ComparisonBox.ktoken, ComparisonBox.atoken];
- const newDoc = useDoc ?? Docs.Create.ComparisonDocument('', { _layout_isFlashcard: true, _width: 300, _height: 300 });
- const question = (tuple.includes(ktoken) ? tuple.split(ktoken)[0] : tuple).split(atoken)[0];
- const rest = tuple.replace(question, '');
- // prettier-ignore
- const answer = rest.startsWith(ktoken) ? // if keyword comes first,
- tuple.includes(atoken) ? tuple.split(atoken)[1] : "" : //if tuple includes answer, split at answer and take what's left, otherwise there's no answer
- rest.includes(ktoken) ? // otherwise if keyword is present it must come after answer,
- rest.split(ktoken)[0].split(atoken)[1] : // split at keyword and take what comes first and split that at answer and take what's left
- rest.replace(atoken,""); // finally if there's no keyword, just get rid of answer token and take what's left
- const keyword = rest.replace(atoken, '').replace(answer, '').replace(ktoken, '').trim();
- const fillInFlashcard = (img?: Doc) => {
- newDoc[DocData][this.frontKey] = this.textCreator('question', question, img);
- newDoc[DocData][this.backKey] = this.textCreator('answer', answer);
- return newDoc;
- };
- return keyword && keyword.toLowerCase() !== 'none' ? this.fetchImages(keyword).then(img => fillInFlashcard(img)) : fillInFlashcard();
- };
-
- /**
- * Create a carousel of flashcards from a GPT response string where questions and answers are given in a format loosely defined by:
- * Question: ... Answer: ... Keyword: ...
- * Note that Keyword or Answer may not be present, or their orders may be reversed.
- */
- createFlashcardDeck = (text: string) => {
- Promise.all(
- text
- .split(ComparisonBox.qtoken)
- .filter(t => t)
- .map(tuple => this.createFlashcard(tuple))
- ).then(docs => {
- const newCol = Docs.Create.CarouselDocument(docs, {
- _width: NumCast(this.layoutDoc._width, 250) + 50,
- _height: NumCast(this.layoutDoc._height, 200) + 50,
- _layout_fitWidth: false,
- _layout_autoHeight: true,
- _xMargin: 5,
- _yMargin: 5,
- x: NumCast(this.layoutDoc.x),
- y: NumCast(this.layoutDoc.y),
- });
-
- this._props.DocumentView?.()._props.addDocument?.(newCol);
- this._props.removeDocument?.(this.Document);
- });
- };
-
- /**
* Calls GPT for each flashcard type.
*/
askGPT = async (callType: GPTCallType) => {
- const frontText = RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text;
- const backText = RTFCast(DocCast(this.dataDoc[this.backKey]).text)?.Text;
- const questionText = 'Question: ' + frontText;
- const queryText = questionText + (callType == GPTCallType.QUIZ ? ' UserAnswer: ' + this._inputValue + '. ' + ' Rubric: ' + backText : '');
+ const questionText = 'Question: ' + this.frontText;
+ const queryText = questionText + (callType == GPTCallType.QUIZ ? ' UserAnswer: ' + this._inputValue + '. ' + ' Rubric: ' + this.backText : '');
+
this.loading = true;
- let res = '';
-
- if (callType !== GPTCallType.CHATCARD || frontText) {
- try {
- res = await gptAPICall(queryText, callType);
- if (!res) {
- console.error('GPT call failed');
- } else
- switch (callType) {
- case GPTCallType.CHATCARD:
- DocCast(this.dataDoc[this.backKey])[DocData].text = res;
- break;
- case GPTCallType.QUIZ:
- runInAction(() => {
- this._renderSide = this.backKey;
- this._outputValue = res.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
- });
- break;
- case GPTCallType.FLASHCARD:
- default:
- }
- } catch (err) {
- console.error('GPT call failed', err);
- }
- }
+ const res = !this.frontText
+ ? ''
+ : await gptAPICall(queryText, callType).then(
+ action(resp => {
+ switch (resp && callType) {
+ case GPTCallType.CHATCARD:
+ DocCast(this.dataDoc[this.backKey])[DocData].text = resp;
+ break;
+ case GPTCallType.QUIZ:
+ this._renderSide = this.backKey;
+ this._outputValue = resp.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
+ break;
+ case GPTCallType.FLASHCARD:
+ default:
+ }
+ return resp;
+ })
+ );
this.loading = false;
+ if (!res) console.error('GPT call failed');
return res;
};
layoutWidth = () => NumCast(this.layoutDoc.width, 200);
layoutHeight = () => NumCast(this.layoutDoc.height, 200);
- findImageTags = async () => {
- const c = this.DocumentView?.().ContentDiv?.getElementsByTagName('img');
- if (c?.length === 0) this.askGPT(GPTCallType.CHATCARD);
- if (c) {
- this.loading = true;
- for (const i of c) {
- if (i.className !== 'ProseMirror-separator') this.getImageDesc(i.src);
- }
- this.loading = false;
- }
- };
-
/**
* Ask GPT for advice on how to improve speech by comparing the phonetic transcription of
* a users audio recording with the phonetic transcription of their intended sentence.
* @param phonemes
*/
askGPTPhonemes = async (phonemes: string) => {
- const sentence = StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text);
+ const sentence = this.frontText;
const phon6 = 'huː ɑɹ juː tədeɪ';
const phon4 = 'kamo estas hɔi';
const promptEng =
@@ -567,24 +558,20 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
* @param selection
* @returns Image Document
*/
- fetchImages = async (selection: string) => {
+ public static async fetchImages(selection: string) {
try {
const { data } = await axios.get(`${API_URL}?query=${selection}&page=1&per_page=${1}&client_id=Q4zruu6k6lum2kExiGhLNBJIgXDxD6NNj0SRHH_XXU0`);
const imageSnapshot = Docs.Create.ImageDocument(data.results[0].urls.small, {
- _nativeWidth: Doc.NativeWidth(this.layoutDoc),
- _nativeHeight: Doc.NativeHeight(this.layoutDoc),
- x: NumCast(this.layoutDoc.x),
- y: NumCast(this.layoutDoc.y),
onClick: FollowLinkScript(),
_width: 150,
_height: 150,
- title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-',
+ title: selection,
});
return imageSnapshot;
} catch (error) {
console.log(error);
}
- };
+ }
getImageDesc = async (u: string) => {
try {
@@ -610,9 +597,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
flashcardContextMenu = () => {
const appearance = ContextMenu.Instance.findByDescription('Appearance...');
const appearanceItems = appearance?.subitems ?? [];
- if (this.Document._layout_isFlashcard) {
- appearanceItems.push({ description: 'Create ChatCard', event: () => this.askGPT(GPTCallType.CHATCARD), icon: 'id-card' });
- }
+ appearanceItems.push({ description: 'Create ChatCard', event: () => this.askGPT(GPTCallType.CHATCARD), icon: 'id-card' });
!appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' });
};
@@ -641,181 +626,174 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
}
return layoutTemplateString;
};
- textCreator = (title: string, text: string, img?: Doc) => {
- const newDoc = Docs.Create.TextDocument(RichTextField.textToRtf(text, img?.[Id]), {
- title, //
- _layout_autoHeight: true,
- _layout_centered: true,
- text_align: 'center',
- _layout_fitWidth: true,
- });
- return newDoc;
- };
childActiveFunc = () => this._childActive;
contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
- render() {
- const clearButton = (which: string) => (
- <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="xs" />
- </div>
- </Tooltip>
- );
- 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
- {...this._props}
- showTags={undefined}
- fitWidth={undefined} // set to returnTrue to make images fill the comparisonBox-- should be a user option
- ignoreUsePath={layoutString ? true : undefined}
- renderDepth={this.props.renderDepth + 1}
- LayoutTemplateString={layoutString}
- Document={layoutString ? this.Document : targetDoc}
- containerViewPath={this._props.docViewPath}
- moveDocument={whichSlot === this.frontKey ? this.moveDocFront : this.moveDocBack}
- removeDocument={whichSlot === this.frontKey ? this.remDocFront : this.remDocBack}
- NativeWidth={returnZero}
- NativeHeight={returnZero}
- ScreenToLocalTransform={this.contentScreenToLocalXf}
- isContentActive={this.childActiveFunc}
- isDocumentActive={returnFalse}
- dontSelect={returnTrue}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- styleProvider={this._childActive ? this._props.styleProvider : this.docStyleProvider}
- hideLinkButton
- pointerEvents={this._childActive ? undefined : returnNone}
- />
- {!this.isFlashcard ? clearButton(whichSlot) : null}
- </>
- ) : (
- <div className="placeholder">
- <FontAwesomeIcon className="upload-icon" icon="cloud-upload-alt" size="lg" />
- </div>
- );
- };
- const displayBox = (which: string, cover: number) => (
+
+ clearButton = (which: string) => (
+ <Tooltip title={<div className="dash-tooltip">remove</div>}>
<div
- className={`${which === this.frontKey ? 'before' : 'after'}Box-cont`}
- key={which}
- style={{ width: this._props.PanelWidth() }}
- onPointerDown={e => {
- this.registerSliding(e, cover);
- this.isFlashcard && this.activateContent();
- }}
- ref={ele => this.createDropTarget(ele, which)}>
- {!this._isEmpty ? displayDoc(which) : null}
+ 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="xs" />
+ </div>
+ </Tooltip>
+ );
+ 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
+ {...this._props}
+ showTags={undefined}
+ fitWidth={undefined} // set to returnTrue to make images fill the comparisonBox-- should be a user option
+ ignoreUsePath={layoutString ? true : undefined}
+ renderDepth={this.props.renderDepth + 1}
+ LayoutTemplateString={layoutString}
+ Document={layoutString ? this.Document : targetDoc}
+ containerViewPath={this._props.docViewPath}
+ moveDocument={whichSlot === this.frontKey ? this.moveDocFront : this.moveDocBack}
+ removeDocument={whichSlot === this.frontKey ? this.remDocFront : this.remDocBack}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ ScreenToLocalTransform={this.contentScreenToLocalXf}
+ isContentActive={this.childActiveFunc}
+ isDocumentActive={returnFalse}
+ dontSelect={returnTrue}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ styleProvider={this._childActive ? this._props.styleProvider : this.docStyleProvider}
+ hideLinkButton
+ pointerEvents={this._childActive ? undefined : returnNone}
+ />
+ {!this.isFlashcard ? this.clearButton(whichSlot) : null}
+ </>
+ ) : (
+ <div className="placeholder">
+ <FontAwesomeIcon className="upload-icon" icon="cloud-upload-alt" size="lg" />
</div>
);
+ };
- if (this.isFlashcard) {
- if (this.dataDoc.data) {
- if (!this.dataDoc[this.backKey] || !this.dataDoc[this.frontKey]) this.createFlashcard(StrCast(this.dataDoc.data), this.Document);
- } else {
- // add text box to each side when comparison box is first created
- if (!this.dataDoc[this.backKey] && !this._isEmpty) {
- const answer = this.textCreator('answer', 'answer here');
- this.dataDoc[this.backKey] = answer;
- answer[DocData].text_placeholder = true;
- }
-
- if (!this.dataDoc[this.frontKey] && !this._isEmpty) {
- const question = this.textCreator('question', 'hint: Enter a topic, select this document and click the stack button to have GPT create a deck of cards');
- this.dataDoc[this.frontKey] = question;
- question[DocData].text_placeholder = true;
- }
- }
-
- if (DocCast(this.Document.embedContainer)?.practiceMode === practiceMode.QUIZ) {
- const text = StrCast(RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text);
- return (
- <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`}>
- <p style={{ color: 'white', padding: 10 }}>{text}</p>
- <p style={{ display: text === '' ? 'flex' : 'none', color: 'white', marginLeft: '10px' }}>Return to all flashcards and add text to both sides. </p>
- <div className="input-box">
- <textarea
- value={this._renderSide === this.backKey ? this._outputValue : this._inputValue}
- onChange={action(e => {
- this._inputValue = e.target.value;
- })}
- placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''}
- readOnly={this._renderSide === this.backKey}
- />
- {!this.loading ? null : (
- <div className="loading-spinner">
- <ReactLoading type="spin" height={30} width={30} color='blue' />
- </div>
- )}
- </div>
- <div>
- <div className="submit-button">
- <div className="submit-buttonschema-header-button" onPointerDown={e => this.openContextMenu(e.clientX, e.clientY, false)}>
- <FontAwesomeIcon color="white" icon="caret-down" />
- </div>
- <button className="submit-buttonrecord" onClick={this._listening ? this.stopListening : this.startListening} style={{ background: this._listening ? 'lightgray' : '' }}>
- {<FontAwesomeIcon icon="microphone" size="lg" />}
- </button>
- <div className="submit-buttonschema-header-button" onPointerDown={e => this.openContextMenu(e.clientX, e.clientY, true)} style={{ left: '50px', zIndex: '100' }}>
- <FontAwesomeIcon color="white" icon="caret-down" />
- </div>
- <button className="submit-buttonpronunciation" onClick={this.evaluatePronunciation}>
- Evaluate Pronunciation
- </button>
- <button className="submit-buttonsubmit" type="button" onClick={this._renderSide === this.backKey ? this.flipFlashcard : this.handleRenderGPTClick}>
- {this._renderSide === this.backKey ? 'Redo the Question' : 'Submit'}
- </button>
- </div>
- </div>
+ displayBox = (which: string, cover: number) => (
+ <div
+ className={`${which === this.frontKey ? 'before' : 'after'}Box-cont`}
+ key={which}
+ style={{ width: this._props.PanelWidth() }}
+ onPointerDown={e => {
+ this.registerSliding(e, cover);
+ this.isFlashcard && this.activateContent();
+ }}
+ ref={ele => this.createDropTarget(ele, which)}>
+ {!this._isEmpty ? this.displayDoc(which) : null}
+ </div>
+ );
+
+ /* renders front(qustion) and back(answer) at the same time, then on user input replaces the answer with a GPT analysis of the answer */
+ renderAsQuiz = (text: string) => (
+ <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`}>
+ <p style={{ color: 'white', padding: 10 }}>{text}</p>
+ <p style={{ display: text === '' ? 'flex' : 'none', color: 'white', marginLeft: '10px' }}>Return to all flashcards and add text to both sides. </p>
+ <div className="input-box">
+ <textarea
+ value={this._renderSide === this.backKey ? this._outputValue : this._inputValue}
+ onChange={action(e => {
+ this._inputValue = e.target.value;
+ })}
+ placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''}
+ readOnly={this._renderSide === this.backKey}
+ />
+ {!this.loading ? null : (
+ <div className="loading-spinner">
+ <ReactLoading type="spin" height={30} width={30} color="blue" />
+ </div>
+ )}
+ </div>
+ <div>
+ <div className="submit-button">
+ <div className="submit-buttonschema-header-button" onPointerDown={e => this.openContextMenu(e.clientX, e.clientY, false)}>
+ <FontAwesomeIcon color="white" icon="caret-down" />
</div>
- );
+ <button className="submit-buttonrecord" onClick={this._listening ? this.stopListening : this.startListening} style={{ background: this._listening ? 'lightgray' : '' }}>
+ {<FontAwesomeIcon icon="microphone" size="lg" />}
+ </button>
+ <div className="submit-buttonschema-header-button" onPointerDown={e => this.openContextMenu(e.clientX, e.clientY, true)} style={{ left: '50px', zIndex: '100' }}>
+ <FontAwesomeIcon color="white" icon="caret-down" />
+ </div>
+ <button className="submit-buttonpronunciation" onClick={this.evaluatePronunciation}>
+ Evaluate Pronunciation
+ </button>
+ <button className="submit-buttonsubmit" type="button" onClick={this._renderSide === this.backKey ? this.flipFlashcard : this.handleRenderGPTClick}>
+ {this._renderSide === this.backKey ? 'Redo the Question' : 'Submit'}
+ </button>
+ </div>
+ </div>
+ </div>
+ );
+
+ // if flashcard is rendered that has no data, then add some placeholders for question and answer
+ addPlaceholdersForEmptyFlashcard = () => {
+ if (this.dataDoc.data) {
+ if (!this.dataDoc[this.backKey] || !this.dataDoc[this.frontKey]) ComparisonBox.createFlashcard(StrCast(this.dataDoc.data), this.frontKey, this.backKey, this.Document);
+ } else {
+ // add text box to each side when comparison box is first created
+ if (!this.dataDoc[this.backKey] && !this._isEmpty) {
+ this.dataDoc[this.backKey] = FormattedTextBox.centeredTextCreator('answer', 'answer here', undefined, true);
}
- // 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 */
- onContextMenu={this.flashcardContextMenu}
- onMouseEnter={() => this.hoverFlip(this.backKey)}
- onMouseLeave={() => this.hoverFlip(this.frontKey)}>
- {displayBox(this._renderSide, this._props.PanelWidth() - 3)}
- {this.loading ? (
- <div className="loading-spinner">
- <ReactLoading type="spin" height={30} width={30} color="blue" />
- </div>
- ) : null}
- {this.flashcardMenu}
- </div>
- );
+ if (!this.dataDoc[this.frontKey] && !this._isEmpty) {
+ this.dataDoc[this.frontKey] = FormattedTextBox.centeredTextCreator('question', 'hint: Enter a topic, select this document and click the stack button to have GPT create a deck of cards', undefined, true);
+ }
}
- // 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.backKey, this._props.PanelWidth() - 3)}
- <div className="clip-div" style={{ width: this.clipWidth + '%', transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}>
- {displayBox(this.frontKey, 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" />
+ renderAsFlashcard = () => (
+ <div
+ className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */
+ onContextMenu={this.flashcardContextMenu}
+ onMouseEnter={() => this.hoverFlip(this.backKey)}
+ onMouseLeave={() => this.hoverFlip(this.frontKey)}>
+ {this.displayBox(this._renderSide, this._props.PanelWidth() - 3)}
+ {this.loading ? (
+ <div className="loading-spinner">
+ <ReactLoading type="spin" height={30} width={30} color="blue" />
</div>
+ ) : null}
+ {this.flashcardMenu}
+ </div>
+ );
+
+ // render a comparison box that compares items side by side
+ renderAsBeforeAfter = () => (
+ <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}>
+ {this.displayBox(this.backKey, this._props.PanelWidth() - 3)}
+ <div className="clip-div" style={{ width: this.clipWidth + '%', transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}>
+ {this.displayBox(this.frontKey, 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>
+ );
+
+ render() {
+ this.isFlashcard && this.addPlaceholdersForEmptyFlashcard();
+ return this.isFlashcard ?
+ this.isQuizMode ? this.renderAsQuiz(this.frontText) :
+ this.renderAsFlashcard() :
+ this.renderAsBeforeAfter(); // prettier-ignore
}
}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 29be8d285..9d3a899f5 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -76,6 +76,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
public static LayoutString(fieldStr: string) {
return FieldView.LayoutString(FormattedTextBox, fieldStr);
}
+ /**
+ * Creates a text box where the supplied text (and optional iimage) will be vertically
+ * and horizontally centered. If text_placeholder is set to true, then the text will be
+ * treated as placeholder text and automatically selected when the text box is selected.
+ * @param title name of text box
+ * @param text text to display in textbox
+ * @param img optional image to add to text box
+ * @param text_placeholder makes the text automatially select
+ * @returns
+ */
+ public static centeredTextCreator(title: string, text: string, img?: Doc, text_placeholder?: boolean) {
+ return Docs.Create.TextDocument(RichTextField.textToRtf(text, img?.[Id]), {
+ title, //
+ _layout_autoHeight: true,
+ _layout_centered: true,
+ text_align: 'center',
+ text_placeholder,
+ _layout_fitWidth: true,
+ });
+ }
public static MakeConfig(rules?: RichTextRules, props?: FormattedTextBoxProps) {
return {
schema,
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 7243473e0..5ab9b556c 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -10,7 +10,6 @@ import { emptyFunction, unimplementedFunction } from '../../../Utils';
import { Doc, Opt } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
-import { Docs } from '../../documents/Documents';
import { SettingsManager } from '../../util/SettingsManager';
import { undoBatch } from '../../util/UndoManager';
import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu';
@@ -19,6 +18,7 @@ import { DocumentView } from '../nodes/DocumentView';
import { DrawingOptions, SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
import './AnchorMenu.scss';
import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup';
+import { ComparisonBox } from '../nodes/ComparisonBox';
@observer
export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -117,29 +117,15 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
*/
transferToFlashcard = (text: string, x: number, y: number) => {
- // put each question generated by GPT on the front of the flashcard
- const senArr = text.trim().split('Question:');
- const collectionArr: Doc[] = [];
- for (let i = 1; i < senArr.length; 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,
- });
-
- newCol.x = x;
- newCol.y = y;
- newCol.zIndex = 1000;
-
- this.addToCollection?.(newCol);
- this._loading = false;
+ ComparisonBox.createFlashcardDeck(text, 250, 200, 'data_front', 'data_back').then(
+ action(newCol => {
+ newCol.x = x;
+ newCol.y = y;
+ newCol.zIndex = 1000;
+ this.addToCollection?.(newCol);
+ this._loading = false;
+ })
+ );
};
/**
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 60eadcdfd..33764aca5 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -227,7 +227,6 @@ function getEffectiveAcl(target: Doc | ListImpl<FieldType>, user?: string): symb
* @param allowUpgrade whether permissions can be made less restrictive
* @param layoutOnly just sets the layout doc's ACL (unless the data doc has no entry for the ACL, in which case it will be set as well)
*/
-// eslint-disable-next-line default-param-last
export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, visited: Doc[] = [], allowUpgrade?: boolean, layoutOnly = false) {
const selfKey = `acl_${normalizeEmail(ClientUtils.CurrentUserEmail())}`;
if (!target || visited.includes(target) || key === selfKey) return;