aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-rw-r--r--src/client/views/MainView.tsx1
-rw-r--r--src/client/views/MarqueeAnnotator.tsx1
-rw-r--r--src/client/views/collections/CollectionCardDeckView.tsx2
-rw-r--r--src/client/views/collections/CollectionCarouselView.scss66
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx240
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx2
-rw-r--r--src/client/views/nodes/ComparisonBox.scss68
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx301
-rw-r--r--src/client/views/nodes/DocumentView.tsx36
-rw-r--r--src/client/views/nodes/FieldView.tsx1
-rw-r--r--src/client/views/nodes/ImageBox.scss21
-rw-r--r--src/client/views/nodes/ImageBox.tsx232
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss12
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx5
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBoxComment.scss8
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx67
-rw-r--r--src/client/views/pdf/Annotation.scss19
-rw-r--r--src/client/views/pdf/PDFViewer.scss21
-rw-r--r--src/client/views/pdf/PDFViewer.tsx54
19 files changed, 969 insertions, 188 deletions
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 31d88fb87..541b15006 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -548,6 +548,7 @@ export class MainView extends ObservableReactComponent<{}> {
fa.faRobot,
fa.faSatellite,
fa.faStar,
+ fa.faFilePen,
]
);
}
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index c18ac6738..c7ffce2b4 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -34,6 +34,7 @@ export interface MarqueeAnnotatorProps {
getPageFromScroll?: (top: number) => number;
finishMarquee: (x?: number, y?: number) => void;
anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
+ anchorMenuFlashcard?: () => Promise<String>;
anchorMenuCrop?: (anchor: Doc | undefined, addCrop: boolean) => Doc | undefined;
highlightDragSrcColor?: string;
}
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index de46180e6..50af9df9e 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -373,7 +373,7 @@ export class CollectionCardView extends CollectionSubView() {
const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
try {
const hrefBase64 = await CollectionCardView.imageUrlToBase64(hrefComplete);
- const response = await gptImageLabel(hrefBase64);
+ const response = await gptImageLabel(hrefBase64, 'Give three to five labels to describe this image.');
image[DocData].description = response.trim();
return response; // Return the response from gptImageLabel
} catch (error) {
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss
index f115bb40a..b402a7a32 100644
--- a/src/client/views/collections/CollectionCarouselView.scss
+++ b/src/client/views/collections/CollectionCarouselView.scss
@@ -1,5 +1,7 @@
.collectionCarouselView-outer {
height: 100%;
+ position: relative;
+ overflow: hidden;
.collectionCarouselView-caption {
height: 50;
display: inline-block;
@@ -11,17 +13,26 @@
width: 100%;
user-select: none;
}
+ .message {
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ height: 60%;
+ z-index: -1;
+ // margin: 15px;
+ }
}
+
.carouselView-back,
.carouselView-fwd,
.carouselView-star,
.carouselView-remove,
-.carouselView-check {
+.carouselView-check,
+.carouselView-add {
position: absolute;
display: flex;
- top: 42.5%;
width: 30;
- height: 15%;
+ height: 30;
align-items: center;
border-radius: 5px;
justify-content: center;
@@ -32,14 +43,21 @@
}
}
.carouselView-fwd {
+ top: 42.5%;
right: 20;
}
.carouselView-back {
+ top: 42.5%;
left: 20;
}
.carouselView-star {
top: 0;
- right: 20;
+ left: 0;
+}
+.carouselView-add {
+ position: absolute;
+ bottom: 0;
+ left: 0;
}
.carouselView-remove {
top: 80%;
@@ -49,6 +67,46 @@
top: 80%;
right: 52%;
}
+.carouselView-quiz {
+ position: absolute;
+ display: flex;
+ top: 5px;
+ right: 8px;
+ &:hover {
+ color: white;
+ }
+}
+
+.carouselView-practice {
+ position: absolute;
+ display: flex;
+ top: 22px;
+ right: 8px;
+ &:hover {
+ color: white;
+ }
+}
+.carouselView-starFilter {
+ position: absolute;
+ display: flex;
+ top: 40px;
+ right: 7px;
+ &:hover {
+ color: white;
+ }
+}
+
+.carouselView-menu {
+ position: absolute;
+ display: flex;
+ top: 2px;
+ right: 2px;
+ width: 30;
+ height: 60;
+ border-radius: 5px;
+ color: rgba(255, 255, 255, 0.5);
+ background: rgba(0, 0, 0, 0.1);
+}
.carouselView-back:hover,
.carouselView-fwd:hover {
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 2adad68e0..6976deea5 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -2,8 +2,9 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable react/jsx-props-no-spreading */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { computed, makeObservable } from 'mobx';
+import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
+import { Docs } from '../../documents/Documents';
import * as React from 'react';
import { StopEvent, returnFalse, returnOne, returnTrue, returnZero } from '../../../ClientUtils';
import { emptyFunction } from '../../../Utils';
@@ -19,11 +20,20 @@ import { FieldViewProps } from '../nodes/FieldView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import './CollectionCarouselView.scss';
import { CollectionSubView } from './CollectionSubView';
+import { Tooltip } from '@mui/material';
+import { DocUtils } from '../../documents/DocUtils';
+import { Any } from '@react-spring/web';
enum cardMode {
- PRACTICE = 'practice',
+ // PRACTICE = 'practice',
STAR = 'star',
+ // QUIZ = 'quiz',
+ ALL = 'all',
+}
+enum practiceMode {
+ PRACTICE = 'practice',
QUIZ = 'quiz',
+ NORMAL = 'normal',
}
enum practiceVal {
MISSED = 'missed',
@@ -32,12 +42,20 @@ enum practiceVal {
@observer
export class CollectionCarouselView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
+ @observable private _practiceMessage: string | undefined;
+ @observable private _filterMessage: string | undefined;
get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore
- get starField() { return this.fieldKey + "_star"; } // prettier-ignore
+ get sideField() { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore
+ get starField() { return "star"; } // prettier-ignore
constructor(props: any) {
super(props);
makeObservable(this);
+ // this.setModes();
+ this.layoutDoc.filterOp = cardMode.ALL;
+ Doc.setDocFilter(this.Document, 'star', undefined, 'match');
+ this.layoutDoc.practiceMode = practiceMode.NORMAL;
+ this.layoutDoc._carousel_index = 0;
}
componentWillUnmount() {
@@ -52,16 +70,33 @@ export class CollectionCarouselView extends CollectionSubView() {
};
@computed get carouselItems() {
+ this.childLayoutPairs.map(pair => {
+ pair.layout.embedContainer = this.Document;
+ });
return this.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK);
}
@computed get marginX() {
return NumCast(this.layoutDoc.caption_xMargin, 50);
}
+ @action setPracticeMessage = (mes: string | undefined) => {
+ this._practiceMessage = mes;
+ };
+ @action setFilterMessage = (mes: string | undefined) => {
+ this._filterMessage = mes;
+ };
+
+ setModes = () => {
+ this.layoutDoc.filterOp = cardMode.ALL;
+ Doc.setDocFilter(this.Document, 'data_star', undefined, 'match');
+ this.layoutDoc.practiceMode = practiceMode.NORMAL;
+ this.layoutDoc._carousel_index = 0;
+ };
+
move = (dir: number) => {
const moveToCardWithField = (match: (doc: Doc) => boolean): boolean => {
let startInd = (NumCast(this.layoutDoc._carousel_index) + dir) % this.carouselItems.length;
- while (!match(this.carouselItems?.[startInd].layout) && (startInd + dir + this.carouselItems.length) % this.carouselItems.length !== this.layoutDoc._carousel_index) {
+ while (!match(this.carouselItems?.[startInd].layout) && (startInd + this.carouselItems.length) % this.carouselItems.length !== this.layoutDoc._carousel_index) {
startInd = (startInd + dir + this.carouselItems.length) % this.carouselItems.length;
}
if (match(this.carouselItems?.[startInd].layout)) {
@@ -70,23 +105,28 @@ export class CollectionCarouselView extends CollectionSubView() {
}
return match(this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout);
};
- switch (StrCast(this.layoutDoc.filterOp)) {
- case cardMode.STAR: // go to a flashcard that is starred, skip the ones that aren't
+
+ switch (this.layoutDoc.practiceMode && this.layoutDoc.filterOp) {
+ case practiceMode.PRACTICE && cardMode.ALL:
+ if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT)) {
+ this._practiceMessage = 'Finished! Unselect practice mode to view all flashcards.';
+ this.carouselItems.forEach(item => { item.layout[this.practiceField] = undefined}); //prettier-ignore
+ }
+ break;
+ case !practiceMode.PRACTICE && cardMode.STAR:
if (!moveToCardWithField((doc: Doc) => !!doc[this.starField])) {
- this.layoutDoc.filterOp = undefined; // if there aren't any starred, show all cards
+ this._filterMessage = 'No starred items. Unselect this view to see all flashcards and star them.';
}
break;
- case cardMode.PRACTICE: // go to a new index that is missed, skip the ones that are correct
- if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT)) {
- this.layoutDoc.filterOp = undefined; // if all of the cards are correct, show all cards and exit practice mode
-
- this.carouselItems.forEach(item => { // reset all the practice values
- item.layout[this.practiceField] = undefined;
- });
+ case practiceMode.PRACTICE && cardMode.STAR:
+ if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT && doc[this.starField] === true)) {
+ this._filterMessage = 'No flashcards to show! Unselect mode to view all flashcards.';
+ this._practiceMessage = undefined;
}
break;
- default: moveToCardWithField(returnTrue);
- } // prettier-ignore
+ default:
+ moveToCardWithField(returnTrue);
+ }
};
/**
@@ -112,6 +152,8 @@ export class CollectionCarouselView extends CollectionSubView() {
e.stopPropagation();
const curDoc = this.carouselItems[NumCast(this.layoutDoc._carousel_index)];
curDoc.layout[this.starField] = curDoc.layout[this.starField] ? undefined : true;
+ // if (!curDoc.layout[this.starField]) this.move(1);
+ // this.layoutDoc._carousel_index = undefined;
};
/*
@@ -123,7 +165,6 @@ export class CollectionCarouselView extends CollectionSubView() {
curDoc.layout[this.practiceField] = 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;
@@ -133,22 +174,42 @@ export class CollectionCarouselView extends CollectionSubView() {
onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
onContentClick = () => ScriptCast(this.layoutDoc.onChildClick);
captionWidth = () => this._props.PanelWidth() - 2 * this.marginX;
- specificMenu = (): void => {
- const cm = ContextMenu.Instance;
-
- const revealOptions = cm.findByDescription('Filter Flashcards');
- const revealItems: ContextMenuProps[] = revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : [];
- revealItems.push({description: 'All', event: () => {this.layoutDoc.filterOp = undefined;}, icon: 'layer-group',}); // prettier-ignore
- revealItems.push({description: 'Star', event: () => {this.layoutDoc.filterOp = cardMode.STAR;}, icon: 'star',}); // prettier-ignore
- revealItems.push({description: 'Practice Mode', event: () => {this.layoutDoc.filterOp = cardMode.PRACTICE;}, icon: 'check',}); // prettier-ignore
- revealItems.push({description: 'Quiz Cards', event: () => {this.layoutDoc.filterOp = cardMode.QUIZ;}, icon: 'pencil',}); // prettier-ignore
- !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
+
+ setPracticeMode = (mode: practiceMode) => {
+ this.layoutDoc.practiceMode = mode;
+ this.carouselItems?.map(doc => (doc.layout[this.practiceField] = undefined));
+ switch (mode) {
+ case practiceMode.QUIZ:
+ this.carouselItems?.map(doc => (doc.layout[this.sideField] = undefined));
+ break;
+ case practiceMode.NORMAL:
+ this.setPracticeMessage(undefined);
+ break;
+ }
};
+
+ setFilterMode = (mode: cardMode) => {
+ this.layoutDoc.filterOp = mode;
+ switch (mode) {
+ case cardMode.STAR:
+ // Doc.setDocFilter(this.Document, 'data_star', true, 'match');
+ this.move(1);
+ break;
+ default:
+ this.setFilterMessage(undefined); // prettier-ignore
+ // Doc.setDocFilter(this.Document, 'data_star', true, 'remove');
+ }
+ };
+
@computed get content() {
+ if (this.layoutDoc._carousel_index === this.carouselItems.length && this.layoutDoc._carousel_index !== 0) {
+ this.move(1);
+ }
const index = NumCast(this.layoutDoc._carousel_index);
const curDoc = this.carouselItems?.[index];
const captionProps = { ...this._props, NativeScaling: returnOne, PanelWidth: this.captionWidth, fieldKey: 'caption', setHeight: undefined, setContentView: undefined };
const carouselShowsCaptions = StrCast(this.layoutDoc._layout_showCaption);
+
return !(curDoc?.layout instanceof Doc) ? null : (
<>
<div className="collectionCarouselView-image" key="image">
@@ -158,10 +219,12 @@ export class CollectionCarouselView extends CollectionSubView() {
NativeHeight={returnZero}
fitWidth={undefined}
setContentViewBox={undefined}
+ childFilters={this.childDocFilters}
onDoubleClickScript={this.onContentDoubleClick}
onClickScript={this.onContentClick}
isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
isContentActive={this._props.childContentsActive ?? this._props.isContentActive() === false ? returnFalse : emptyFunction}
+ addDocument={this._props.addDocument}
hideCaptions={!!carouselShowsCaptions} // hide captions if the carousel is configured to show the captions
renderDepth={this._props.renderDepth + 1}
LayoutTemplate={this._props.childLayoutTemplate}
@@ -188,6 +251,25 @@ export class CollectionCarouselView extends CollectionSubView() {
</>
);
}
+
+ containsDifTypes = (): boolean => {
+ return this.carouselItems.filter(doc => !doc.layout._layout_isFlashcard).length !== 0;
+ };
+
+ addFlashcard() {
+ const newDoc = Docs.Create.ComparisonDocument('', { _layout_isFlashcard: true, _width: 300, _height: 300 });
+ this.addDocument?.(newDoc);
+ // DocUtils.copyDragFactory(newDoc);
+ // this._props.addDocument?.();
+ // newDoc.layout = this.layoutDoc;
+ // newDoc.data = this.dataDoc;
+ // Doc.AddDocToList()
+ // this._props.parent._props.addDocument();
+ // this.childLayoutPairs.push({ newDoc.layout, newDoc.data});
+ // this._props.addDocument?.(newDoc);
+ // console.log('HERE');
+ }
+
@computed get buttons() {
if (!this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)]) return null;
return (
@@ -198,54 +280,108 @@ 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.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" />
- </div>
- <div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)} style={{ visibility: this.layoutDoc.filterOp === cardMode.PRACTICE ? 'visible' : 'hidden' }}>
- <FontAwesomeIcon icon="xmark" color="red" size="1x" />
- </div>
- <div key="check" className="carouselView-check" onClick={e => this.setPracticeVal(e, practiceVal.CORRECT)} style={{ visibility: this.layoutDoc.filterOp === cardMode.PRACTICE ? 'visible' : 'hidden' }}>
- <FontAwesomeIcon icon="check" color="green" size="1x" />
- </div>
+ {!this.containsDifTypes() ? (
+ <div>
+ <Tooltip title="star">
+ <div key="star" className="carouselView-star" onClick={this.star}>
+ <FontAwesomeIcon icon="star" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" />
+ </div>
+ </Tooltip>
+ {/* <Tooltip title="add new flashcard to pile">
+ <div key="add" className="carouselView-add" onClick={this.addFlashcard}>
+ <FontAwesomeIcon icon="plus" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" />
+ </div>
+ </Tooltip> */}
+ </div>
+ ) : null}
+ {this.layoutDoc.practiceMode === practiceMode.PRACTICE ? (
+ <div>
+ <Tooltip title="Incorrect. View again later.">
+ <div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)}>
+ <FontAwesomeIcon icon="xmark" color="red" size="1x" />
+ </div>
+ </Tooltip>
+ <Tooltip title="Correct">
+ <div key="check" className="carouselView-check" onClick={e => this.setPracticeVal(e, practiceVal.CORRECT)}>
+ <FontAwesomeIcon icon="check" color="green" size="1x" />
+ </div>
+ </Tooltip>
+ </div>
+ ) : null}
</>
);
}
+ togglePracticeMode = (mode: practiceMode) => {
+ if (mode === this.layoutDoc.practiceMode) {
+ this.setPracticeMode(practiceMode.NORMAL);
+ // this.setPracticeMessage("undefined");
+ } else this.setPracticeMode(mode);
+ };
+ toggleFilterMode = () => { this.layoutDoc.filterOp === cardMode.STAR ? this.setFilterMode(cardMode.ALL) : this.setFilterMode(cardMode.STAR)}; //prettier-ignore
+ setColor = (mode: practiceMode | cardMode, which: string) => { return which === mode ? 'white' : 'light gray'}; //prettier-ignore
+
+ @computed get menu() {
+ return (
+ <div className="carouselView-menu">
+ <Tooltip title="Practice flashcards using GPT">
+ <div key="back" className="carouselView-quiz" onClick={e => this.togglePracticeMode(practiceMode.QUIZ)}>
+ <FontAwesomeIcon icon="file-pen" color={this.setColor(practiceMode.QUIZ, StrCast(this.layoutDoc.practiceMode))} size="1x" />
+ </div>
+ </Tooltip>
+ <Tooltip title={this.layoutDoc.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}>
+ <div key="back" className="carouselView-practice" onClick={e => this.togglePracticeMode(practiceMode.PRACTICE)}>
+ <FontAwesomeIcon icon="check" color={this.setColor(practiceMode.PRACTICE, StrCast(this.layoutDoc.practiceMode))} size="1x" />
+ </div>
+ </Tooltip>
+ <Tooltip title={this.layoutDoc.filterOp === cardMode.STAR ? 'Show all cards' : 'Show only starred cards'}>
+ <div key="back" className="carouselView-starFilter" onClick={e => this.toggleFilterMode()}>
+ <FontAwesomeIcon icon="filter" color={this.setColor(cardMode.STAR, StrCast(this.layoutDoc.filterOp))} size="1x" />
+ </div>
+ </Tooltip>
+ </div>
+ );
+ }
+
render() {
return (
<div
className="collectionCarouselView-outer"
ref={this.createDashEventsTarget}
- onContextMenu={this.specificMenu}
style={{
background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor),
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.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] && this.layoutDoc.filterOp === cardMode.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. */}
+ {!this._practiceMessage && !this._filterMessage ? (
+ this.content
+ ) : (
+ <p className="message">
+ {this._filterMessage}
+ {'\n'}
+ {this._practiceMessage}
+ </p>
+ )}
+ {!this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] && this.layoutDoc.practiceMode === practiceMode.PRACTICE ? <p className="message">Add flashcards </p> : null}
<p
+ className="missed-message"
style={{
color: 'red',
+ fontWeight: 'bold',
zIndex: '999',
position: 'relative',
left: '10px',
top: '10px',
- display: this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] ? (this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.practiceField] === practiceVal.MISSED ? 'block' : 'none') : 'none',
+ width: '10px',
+ display: this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)]
+ ? this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.practiceField] === practiceVal.MISSED && this.layoutDoc.practiceMode === practiceMode.PRACTICE && !this._practiceMessage
+ ? 'block'
+ : 'none'
+ : 'none',
}}>
Recently missed!
</p>
- {this.Document._chromeHidden ? null : this.buttons}
+ {!this.containsDifTypes() && this.carouselItems.length !== 0 ? this.menu : null}
+ {this.Document._chromeHidden || (!this._filterMessage && !this._practiceMessage) ? this.buttons : null}
</div>
);
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index dc15c83c5..0afda3e64 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -440,7 +440,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.');
return CollectionCardView.imageUrlToBase64(`${name}_o.${type}`).then(hrefBase64 =>
!hrefBase64 ? undefined :
- gptImageLabel(hrefBase64).then(labels =>
+ gptImageLabel(hrefBase64, 'Give three to five labels to describe this image.').then(labels =>
Promise.all(labels.split('\n').map(label => gptGetEmbedding(label))).then(embeddings =>
({ doc, embeddings, labels }))) ); // prettier-ignore
});
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index 08d9e6010..7848add24 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -5,31 +5,35 @@
width: 100%;
height: 100%;
position: relative;
+ background: gray;
z-index: 0;
pointer-events: none;
display: flex;
p {
+ // bcz: what is this styling for? if text in the comparison box is colored, then this causes it to render with a black outline
color: rgb(0, 0, 0);
-webkit-text-stroke-color: black;
-webkit-text-stroke-width: 0.2px;
}
-
.input-box {
- position: relative;
+ position: absolute;
+ top: 50;
padding: 10px;
width: 100%;
- height: 100%;
+ height: 70%;
display: flex;
}
.submit-button {
- position: relative;
+ position: absolute;
padding-bottom: 10px;
padding-left: 5px;
padding-right: 5px;
width: 100%;
+ border-radius: 2px;
height: 15%;
display: flex;
+ bottom: 0;
button {
flex: 1;
@@ -39,8 +43,13 @@
textarea {
flex: 1;
padding: 10px;
- position: relative;
+ // position: relative;
resize: none;
+ position: 'absolute';
+ width: '91%';
+ height: '80%';
+ z-index: '-1';
+ overscroll-behavior: contain;
}
.clip-div {
@@ -117,6 +126,33 @@
opacity: 0.5;
}
}
+
+ .loading-spinner {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 90%;
+ width: 93%;
+ font-size: 20px;
+ font-weight: bold;
+ color: #0b0a0a;
+ }
+
+ @keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+ }
+}
+
+.comparisonBox-explain {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ z-index: 200;
+ // padding: 5px;
+ background: #dfdfdf;
+ pointer-events: none;
}
.comparisonBox-interactive {
@@ -128,8 +164,9 @@
display: flex;
}
}
+
// .input-box {
- // position: relative;
+ // position: absolute;
// padding: 10px;
// }
// input[type='text'] {
@@ -216,24 +253,5 @@
}
}
}
-
- .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 efaf6807a..3e1c415d6 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -1,15 +1,16 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
-import { action, computed, makeObservable, observable } from 'mobx';
+import { action, computed, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { returnFalse, returnNone, setupMoveUpEvents } from '../../../ClientUtils';
+import { returnFalse, returnNone, returnTrue, 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 '../pdf/GPTPopup/GPTPopup.scss';
import { DocUtils } from '../../documents/DocUtils';
import { DocumentType } from '../../documents/DocumentTypes';
import { Docs } from '../../documents/Documents';
@@ -23,7 +24,15 @@ import './ComparisonBox.scss';
import { DocumentView } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
+import ReactLoading from 'react-loading';
+enum RevealOp {
+ Hover = 'hover',
+ Flip = 'flip',
+}
+enum UsePath {
+ Alternate = 'alternate',
+}
@observer
export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
public static LayoutString(fieldKey: string) {
@@ -37,14 +46,24 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
@observable _errorMessage = '';
@observable _outputMessage = '';
@observable _animating = '';
+ @observable private _isEmpty = false;
+ @observable _yRelativeToTop: boolean = true;
constructor(props: FieldViewProps) {
super(props);
makeObservable(this);
}
+ @action handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
+ this._inputValue = e.target.value;
+ console.log(this._inputValue);
+ };
componentDidMount() {
this._props.setContentViewBox?.(this);
+ reaction(
+ () => this._props.isSelected(), // when this reaction should update
+ selected => !selected && (this.childActive = false) // what it should update to
+ );
}
protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => {
@@ -54,11 +73,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
}
};
- @computed get revealOp() { return this.layoutDoc[`_${this.fieldKey}_revealOp`] as ('flip'|'hover'|undefined); } // prettier-ignore
+ @computed get useAlternate() { return this.layoutDoc[`_${this.fieldKey}_usePath`] === UsePath.Alternate; } // prettier-ignore
+ @computed get revealOp() { return this.layoutDoc[`_${this.fieldKey}_revealOp`] as Opt<RevealOp>; } // prettier-ignore
@computed get clipWidth() { return NumCast(this.layoutDoc[`_${this.fieldKey}_clipWidth`], 50); } // prettier-ignore
+ set useAlternate(alt: boolean) { this.layoutDoc[`_${this.fieldKey}_usePath`] = alt ? UsePath.Alternate : undefined; } // prettier-ignore
+ set revealOp(op: Opt<RevealOp>){ this.layoutDoc[`_${this.fieldKey}_revealOp`] = op; } // prettier-ignore
set clipWidth(width: number) { this.layoutDoc[`_${this.fieldKey}_clipWidth`] = width; } // prettier-ignore
- @computed get useAlternate() { return this.layoutDoc[`_${this.fieldKey}_usePath`] === 'alternate'; } // prettier-ignore
- set useAlternate(alt: boolean) { this.layoutDoc[`_${this.fieldKey}_usePath`] = alt ? 'alternate' : undefined; } // prettier-ignore
animateClipWidth = action((clipWidth: number, duration = 200 /* ms */) => {
this._animating = `all ${duration}ms`; // turn on clip animation transition, then turn it off at end of animation
@@ -73,6 +93,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
Doc.SetContainer(droppedDocuments.lastElement(), this.dataDoc);
!added && e.preventDefault();
e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place
+ // this.childActive = false;
return added;
}
return undefined;
@@ -87,7 +108,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
emptyFunction,
action((clickEv, doubleTap) => {
if (doubleTap) {
- this._isAnyChildContentActive = true;
+ this.childActive = true;
if (!this.dataDoc[this.fieldKey + '_1'] && !this.dataDoc[this.fieldKey]) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
if (!this.dataDoc[this.fieldKey + '_2'] && !this.dataDoc[this.fieldKey + '_alternate']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
// DocumentView.addViewRenderedCb(DocCast(this.dataDoc[this.fieldKey + '_1']), dv => {
@@ -127,23 +148,29 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
};
clearDoc = undoable((fieldKey: string) => {
- delete this.dataDoc[fieldKey];
- this.dataDoc[fieldKey] = 'empty';
+ // delete this.dataDoc[fieldKey];
+ this.dataDoc[fieldKey] = undefined;
+ this._isEmpty = true;
+ // this.dataDoc[fieldKey] = 'empty';
+ console.log('HERE' + fieldKey + ';');
}, '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] && this.dataDoc[which] !== 'empty') return false;
+ if (this.dataDoc[which] && !this._isEmpty) return false;
this.dataDoc[which] = doc;
return true;
};
remDoc = (doc: Doc, which: string) => {
if (this.dataDoc[which] === doc) {
+ this._isEmpty = true;
// this.dataDoc[which] = 'empty';
+ console.log('HEREEEE');
this.dataDoc[which] = undefined;
return true;
}
+ console.log('FALSE');
return false;
};
@@ -205,6 +232,41 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
return layoutTemplateString;
};
+ createFlashcardPile(collectionArr: Doc[], gpt: boolean) {
+ const newCol = Docs.Create.CarouselDocument(collectionArr, {
+ _width: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 250) + 50,
+ _height: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 200) + 50,
+ _layout_fitWidth: false,
+ _layout_autoHeight: true,
+ });
+ newCol['x'] = this.layoutDoc['x'];
+ newCol['y'] = NumCast(this.layoutDoc['y']) + 50;
+ newCol.type_collection = 'carousel';
+ // console.log(newCol.data);
+
+ if (gpt) {
+ this._props.DocumentView?.()._props.addDocument?.(newCol);
+ this._props.removeDocument?.(this.Document);
+ } else {
+ this._props.addDocument?.(newCol);
+ this._props.removeDocument?.(this.Document);
+ this.Document.embedContainer = newCol;
+ }
+ }
+
+ gptFlashcardPile = async () => {
+ var text = await this.askGPT(GPTCallType.FLASHCARD);
+
+ var senArr = text?.split('Question: ');
+ var 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);
+ }
+ this.createFlashcardPile(collectionArr, true);
+ };
+
/**
* Flips a flashcard to the alternate side for the user to view.
*/
@@ -216,9 +278,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
* Changes the view option to hover for a flashcard.
*/
hoverFlip = (alternate: boolean) => {
- if (this.revealOp === 'hover') this.useAlternate = alternate;
+ if (this.revealOp === RevealOp.Hover) this.useAlternate = alternate;
};
-
/**
* Creates the button used to flip the flashcards.
*/
@@ -229,10 +290,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
className="formattedTextBox-alternateButton"
onPointerDown={e =>
setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => {
- if (!this.revealOp || this.revealOp === 'flip') {
+ if (!this.revealOp || this.revealOp === 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 ?? ''));
+
+ // 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 ?? ''));
}
})
}
@@ -240,17 +302,73 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
background: this.useAlternate ? 'white' : 'black',
color: this.useAlternate ? 'black' : 'white',
}}>
- <FontAwesomeIcon icon="turn-up" size="sm" />
+ <div key="alternate" className="formattedTextBox-flip">
+ <FontAwesomeIcon icon="turn-up" size="1x" />
+ </div>
</div>
</Tooltip>
);
}
+ @computed get flashcardMenu() {
+ return (
+ <div>
+ <Tooltip
+ title={this.useAlternate ? <div className="dash-tooltip">Flip to front side to use GPT</div> : <div className="dash-tooltip">Ask GPT to create an answer on the back side of the flashcard based on your question on the front</div>}>
+ <div style={{ position: 'absolute', bottom: '3px', right: '50px', cursor: 'pointer' }} onPointerDown={e => (!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? this.askGPT(GPTCallType.CHATCARD) : null)}>
+ <FontAwesomeIcon icon="lightbulb" size="xl" />
+ </div>
+ </Tooltip>
+ {DocCast(this.Document.embedContainer).type_collection === 'carousel' ? null : (
+ <div>
+ <Tooltip title={<div>Create a flashcard pile</div>}>
+ <div style={{ position: 'absolute', bottom: '3px', right: '74px', cursor: 'pointer' }} onPointerDown={e => this.createFlashcardPile([this.Document], false)}>
+ <FontAwesomeIcon icon="folder-plus" size="xl" />
+ </div>
+ </Tooltip>
+ <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}>
+ <div style={{ position: 'absolute', bottom: '3px', right: '104px', cursor: 'pointer' }} onClick={e => this.gptFlashcardPile()}>
+ <FontAwesomeIcon icon="layer-group" size="xl" />
+ </div>
+ </Tooltip>
+ </div>
+ )}
+ <Tooltip title={<div className="dash-tooltip">Hover to reveal</div>}>
+ <div style={{ position: 'absolute', bottom: '3px', right: '25px', cursor: 'pointer' }} onClick={e => this.handleHover()}>
+ <FontAwesomeIcon color={this.revealOp === RevealOp.Hover ? 'blue' : 'black'} icon="hand-point-up" size="xl" />
+ </div>
+ </Tooltip>
+ {/* <Tooltip title={<div className="dash-tooltip">Remove this side of the flashcard</div>}>
+ <div
+ style={{ position: 'absolute', bottom: '3px', right: '80px', cursor: 'pointer' }}
+ onPointerDown={e => this.closeDown(e, this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this._props.fieldKey + '_1' : this._props.fieldKey + '_0')}>
+ <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="trash-can" size="xl" />
+ </div>
+ </Tooltip> */}
+ {/* {this.overlayAlternateIcon} */}
+ </div>
+ );
+ }
+ // this.closeDown(e, this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this.fieldKey + '_0' : this.fieldKey + '_1')}
+ @action activateContent = () => {
+ this.childActive = true;
+ };
+
@action handleRenderGPTClick = () => {
// Call the GPT model and get the output
this.useAlternate = true;
this._outputValue = '';
- if (this._inputValue) this.askGPT();
+ if (this._inputValue) this.askGPT(GPTCallType.QUIZ);
+ };
+
+ @action handleHover = () => {
+ if (this.revealOp === RevealOp.Hover) {
+ this.revealOp = RevealOp.Flip;
+ this.Document.forceActive = false;
+ } else {
+ this.revealOp = RevealOp.Hover;
+ this.Document.forceActive = true;
+ }
};
@action handleRenderClick = () => {
@@ -258,38 +376,86 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
this.useAlternate = false;
};
+ animateRes = (resIndex: number, newText: string, callType: GPTCallType) => {
+ if (resIndex < newText.length) {
+ // const marks = this._editorView?.state.storedMarks ?? [];
+ switch (callType) {
+ case GPTCallType.CHATCARD:
+ DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text += newText[resIndex];
+ break;
+ case GPTCallType.QUIZ:
+ this._outputValue += newText[resIndex];
+ break;
+ default:
+ return;
+ }
+
+ // this._editorView?.dispatch(this._editorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(this._outputValue));
+ setTimeout(() => this.animateRes(resIndex + 1, newText, callType), 20);
+ }
+ };
+
/**
* 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> => {
+ askGPT = async (callType: GPTCallType): 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;
-
+ this._loading = true;
+ const doc = DocCast(this.dataDoc[this.props.fieldKey + '_0']);
+ if (callType == GPTCallType.CHATCARD) {
+ if (StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '') {
+ this._loading = false;
+ return;
+ }
+ this.flipFlashcard();
+ }
try {
- const res = await gptAPICall(queryText, GPTCallType.QUIZ);
+ const res = await gptAPICall(callType == GPTCallType.QUIZ ? queryText : questionText, callType);
if (!res) {
console.error('GPT call failed');
return;
}
- this._outputValue = res;
+ // this.animateRes(0, res, callType);
+ if (callType == GPTCallType.CHATCARD) {
+ DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res;
+ // this.flipFlashcard();
+ }
+ if (callType == GPTCallType.QUIZ) this._outputValue = res;
+ // DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res;
+ // this._outputValue = res;
+ else if (callType === GPTCallType.FLASHCARD) {
+ // console.log(res);
+ this._loading = false;
+ return res;
+ }
+ // console.log(res);
} catch (err) {
console.error('GPT call failed');
}
+ this._loading = false;
};
layoutWidth = () => NumCast(this.layoutDoc.width, 200);
layoutHeight = () => NumCast(this.layoutDoc.height, 200);
+ // specificMenu = (): void => {
+ // const cm = ContextMenu.Instance;
+ // cm.addItem({ description: 'Create an Answer on the Back', event: () => this.askGPT(GPTCallType.CHATCARD), icon: 'pencil' });
+ // };
+ @observable childActive = false;
+
render() {
const clearButton = (which: string) => (
<Tooltip title={<div className="dash-tooltip">remove</div>}>
<div
+ // style={{ position: 'relative', top: '0px', left: '10px' }}
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" />
+ <FontAwesomeIcon className={`clear-button ${which}`} icon="times" size="xs" />
</div>
</Tooltip>
);
@@ -297,6 +463,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
const whichDoc = DocCast(this.dataDoc[whichSlot]);
const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc);
const layoutString = targetDoc ? '' : this.testForTextFields(whichSlot);
+ // whichDoc['backgroundColor'] = this.layoutDoc['backgroundColor'];
return targetDoc || layoutString ? (
<>
@@ -312,15 +479,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2}
NativeWidth={this.layoutWidth}
NativeHeight={this.layoutHeight}
- isContentActive={emptyFunction}
+ isContentActive={() => this.childActive}
isDocumentActive={returnFalse}
+ dontSelect={returnTrue}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- styleProvider={this._isAnyChildContentActive ? this._props.styleProvider : this.docStyleProvider}
+ styleProvider={this.childActive ? this._props.styleProvider : this.docStyleProvider}
hideLinkButton
- pointerEvents={this._isAnyChildContentActive ? undefined : returnNone}
+ pointerEvents={this.childActive ? undefined : returnNone}
/>
- {layoutString ? null : clearButton(whichSlot)}
- </> // placeholder image if doc is missing
+ {/* <div style={{ position: 'absolute', top: '-5px', left: '2px' }}>{layoutString ? null : clearButton(whichSlot)}</div> */}
+ </> // placeholder image if doc is missingleft: `${NumCast(this.layoutDoc.width, 200) - 33}px`
) : (
<div className="placeholder">
<FontAwesomeIcon className="upload-icon" icon="cloud-upload-alt" size="lg" />
@@ -328,8 +496,17 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
);
};
const displayBox = (which: string, index: number, cover: number) => (
- <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)}
+ <div
+ className={`${index === 0 ? 'before' : 'after'}Box-cont`}
+ key={which}
+ style={{ width: this._props.PanelWidth() }}
+ onPointerDown={e => {
+ this.registerSliding(e, cover);
+ this.activateContent();
+ }}
+ ref={ele => this.createDropTarget(ele, which, index)}>
+ {!this._isEmpty ? displayDoc(which) : null}
+ {/* {this.dataDoc[this.fieldKey + '_0'] !== 'empty' ? displayDoc(which) : null} */}
</div>
);
@@ -337,60 +514,90 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
const side = this.useAlternate ? 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');
+ // (!this.dataDoc[this.fieldKey + '_0'] && this.dataDoc[this._props.fieldKey + '_0'] !== 'empty')
+ if (!this.dataDoc[this.fieldKey + '_0'] && !this._isEmpty) {
+ const dataSplit = StrCast(this.dataDoc.data).split('Answer: ');
const newDoc = Docs.Create.TextDocument(dataSplit[1]);
// if there is text from the pdf ai cards, put the question on the front side.
// eslint-disable-next-line prefer-destructuring
- newDoc[DocData].text = dataSplit[1];
+ // newDoc.text = dataSplit[1];
+ newDoc['backgroundColor'] = 'lightgray';
this.addDoc(newDoc, this.fieldKey + '_0');
+ // DocCast(this.dataDoc[this.fieldKey + '_0'])[DocData].text = dataSplit[1];
+ // DocCast(this.dataDoc[this.fieldKey + '_0']).text = dataSplit[1];
+ // console.log('HI' + DocCast(this.dataDoc[this.fieldKey + '_0']).text);
+ //console.log('HEREEE' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text));
}
- if (!(this.dataDoc[this.fieldKey + '_1'] || this.dataDoc[this.fieldKey + '_1'] === 'empty')) {
- const dataSplit = StrCast(this.dataDoc.data).split('Answer');
+
+ if (!this.dataDoc[this.fieldKey + '_1'] && !this._isEmpty) {
+ const dataSplit = StrCast(this.dataDoc.data).split('Answer: ');
const newDoc = Docs.Create.TextDocument(dataSplit[0]);
+ this.addDoc(newDoc, this.fieldKey + '_1');
// if there is text from the pdf ai cards, put the answer on the alternate side.
// eslint-disable-next-line prefer-destructuring
- newDoc[DocData].text = dataSplit[0];
- this.addDoc(newDoc, this.fieldKey + '_1');
+
+ // newDoc[DocData].text = dataSplit[0];
+ // console.log('HEREEE' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text));
+ // console.log('HI' + DocCast(this.dataDoc[this.fieldKey + '_1']).text);
+ // DocCast(this.dataDoc[this.props.fieldKey + '_1'])[DocData].text = dataSplit[0];
+ // console.log('HEREEE' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text));
+ // DocCast(this.dataDoc[this.fieldKey + '_1'])[DocData].text = dataSplit[0];
}
// render the QuizCards
- if (DocCast(this.Document.embedContainer) && DocCast(this.Document.embedContainer).filterOp === 'quiz') {
+ // console.log('GERE' + DocCast(this.Document.embedContainer).filterOp);
+ if (DocCast(this.Document.embedContainer) && DocCast(this.Document.embedContainer).practiceMode === 'quiz') {
+ const text = StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text);
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)} */}
+ <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.useAlternate ? this._outputValue : this._inputValue}
- onChange={action(e => {
- this._inputValue = e.target.value;
- })}
+ onChange={this.handleInputChange}
+ onScroll={e => e.stopPropagation()}
+ placeholder={!this.useAlternate ? 'Enter a response for GPT to evaluate.' : ''}
readOnly={this.useAlternate}
/>
+
+ {this._loading ? (
+ <div className="loading-spinner" style={{ position: 'absolute' }}>
+ <ReactLoading type="spin" height={30} width={30} color={'blue'} />
+ </div>
+ ) : null}
</div>
- <div className="submit-button" style={{ display: this.useAlternate ? 'none' : 'flex' }}>
- <button type="button" onClick={this.handleRenderGPTClick}>
+ <div className="submit-button" style={{ overflow: 'hidden', display: this.useAlternate ? 'none' : 'flex' }}>
+ <button type="button" onClick={this.handleRenderGPTClick} style={{ borderRadius: '2px', marginBottom: '3px' }}>
Submit
</button>
</div>
- <div className="submit-button" style={{ display: this.useAlternate ? 'flex' : 'none' }}>
- <button type="button" onClick={this.handleRenderClick}>
- Edit Your Response
+ <div className="submit-button" style={{ overflow: 'hidden', marginBottom: '2px', display: this.useAlternate ? 'flex' : 'none' }}>
+ <button type="button" onClick={this.handleRenderClick} style={{ borderRadius: '2px' }}>
+ Redo the Question
</button>
</div>
</div>
);
}
+ //console.log('HEREEE2' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text));
// render a normal flashcard when not a QuizCard
return (
<div
className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */
- style={{ display: 'flex', flexDirection: 'column' }}
+ // onContextMenu={this.specificMenu}
+ style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
onMouseEnter={() => this.hoverFlip(true)}
onMouseLeave={() => this.hoverFlip(false)}>
+ {!this.useAlternate && StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '' && !this.childActive ? <p className="comparisonBox-explain">Enter text in the flashcard. </p> : null}
{displayBox(`${this.fieldKey}_${side === 0 ? 1 : 0}`, side, this._props.PanelWidth() - 3)}
+ {this._loading ? (
+ <div className="loading-spinner" style={{ position: 'absolute' }}>
+ <ReactLoading type="spin" height={30} width={30} color={'blue'} />
+ </div>
+ ) : null}
+ {this._props.isContentActive() ? this.flashcardMenu : null}
{this.overlayAlternateIcon}
</div>
);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 7a1f94948..d4c31a5b3 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -111,6 +111,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
private _downTime: number = 0;
private _lastTap: number = 0;
private _doubleTap = false;
+ private _loading = false;
private _mainCont = React.createRef<HTMLDivElement>();
private _titleRef = React.createRef<EditableView>();
private _dropDisposer?: DragManager.DragDropDisposer;
@@ -501,22 +502,8 @@ 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 {
- const 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 (this._props.dontSelect?.()) return;
if (e && this.layoutDoc.layout_hideContextMenu && Doc.noviceMode) {
e.preventDefault();
e.stopPropagation();
@@ -574,21 +561,18 @@ 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' }); // prettier-ignore
- revealItems.push({ description: 'Flip', event: () => { this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip'; }, icon: 'rotate' }); // prettier-ignore
- !revealOptions && cm.addItem({ description: 'Reveal Options', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
- }
+ // 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' }); // prettier-ignore
+ // revealItems.push({ description: 'Flip', event: () => { this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip'; }, icon: 'rotate' }); // prettier-ignore
+ // !revealOptions && cm.addItem({ description: 'Reveal Options', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
+ // }
if (this._props.bringToFront) {
const zorders = cm.findByDescription('ZOrder...');
@@ -1395,7 +1379,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
screenToLocalScale = () => this._props.ScreenToLocalTransform().Scale;
isSelected = () => this.IsSelected;
select = (extendSelection: boolean, focusSelection?: boolean) => {
- DocumentView.SelectView(this, extendSelection);
+ if (!this._props.dontSelect?.()) DocumentView.SelectView(this, extendSelection);
if (focusSelection) {
DocumentView.showDocument(this.Document, {
willZoomCentered: true,
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 818d26956..138f00492 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -50,6 +50,7 @@ export interface FieldViewSharedProps {
PanelHeight: () => number;
isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events
isContentActive: () => boolean | undefined; // whether document contents should handle pointer events
+ dontSelect: () => boolean | undefined;
childFilters: () => string[];
childFiltersByRanges: () => string[];
styleProvider: Opt<StyleProviderFuncType>;
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 3ffda5a35..be68ac8cd 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -139,3 +139,24 @@
.imageBox-fadeBlocker-hover {
opacity: 0;
}
+
+.loading-spinner {
+ position: absolute;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ width: 100%;
+ // left: 50%;
+ // top: 50%;
+ z-index: 200;
+ font-size: 20px;
+ font-weight: bold;
+ color: #17175e;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 68c313480..8068407bb 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -35,6 +35,15 @@ import { FieldView, FieldViewProps } from './FieldView';
import { FocusViewOptions } from './FocusViewOptions';
import './ImageBox.scss';
import { OpenWhere } from './OpenWhere';
+import { URLField } from '../../../fields/URLField';
+import { gptImageLabel } from '../../apis/gpt/GPT';
+import ReactLoading from 'react-loading';
+import { FollowLinkScript } from '../../documents/DocUtils';
+import { basename } from 'path';
+import { ImageUtility } from './generativeFill/generativeFillUtils/ImageHandler';
+import { dropActionType } from '../../util/DropActionTypes';
+import { canvasSize } from './generativeFill/generativeFillUtils/generativeFillConstants';
+import axios from 'axios';
export class ImageEditorData {
// eslint-disable-next-line no-use-before-define
@@ -59,6 +68,8 @@ export class ImageEditorData {
public static get AddDoc() { return ImageEditorData.imageData.addDoc; } // prettier-ignore
public static set AddDoc(addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) { ImageEditorData.set(this.imageData.open, this.imageData.rootDoc, this.imageData.source, addDoc); } // prettier-ignore
}
+
+const API_URL = 'https://api.unsplash.com/search/photos';
@observer
export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
public static LayoutString(fieldKey: string) {
@@ -73,9 +84,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
private _marqueeref = React.createRef<MarqueeAnnotator>();
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
+ private _imageRef: HTMLImageElement | null = null; // <video> ref
+ @observable private _width: number = 0;
+ @observable private _height: number = 0;
+ @observable private searchInput = '';
@observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
@observable _curSuffix = '';
@observable _error = '';
+ @observable private _loading = false;
@observable _isHovering = false; // flag to switch between primary and alternate images on hover
_ffref = React.createRef<CollectionFreeFormView>();
@@ -148,6 +164,32 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
Object.values(this._disposers).forEach(disposer => disposer?.());
}
+ fetchImages = async () => {
+ try {
+ const { data } = await axios.get(`${API_URL}?query=${this.searchInput}&page=1&per_page=${1}&client_id=${process.env.VITE_API_KEY}`);
+ console.log('data', data);
+ console.log(data.results);
+ 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-',
+ });
+ this._props.addDocument?.(imageSnapshot);
+ } catch (error) {
+ console.log(error);
+ }
+ };
+
+ handleSelection = async (selection: string) => {
+ this.searchInput = selection;
+ const images = await this.fetchImages();
+ };
+
@undoBatch
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.complete.docDragData) {
@@ -259,10 +301,189 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return cropping;
};
+ createCanvas = async (downX?: number, downY?: number, cb?: (filename: string, x: number | undefined, y: number | undefined) => void) => {
+ const width = NumCast(this.layoutDoc._width);
+ const canvas = document.createElement('canvas');
+ // canvas.width = 640;
+ // canvas.height = (640 * Doc.NativeHeight(this.layoutDoc)) / (Doc.NativeWidth(this.layoutDoc) || 1);
+ canvas.width = NumCast(this.layoutDoc._width);
+ canvas.height = NumCast(this.layoutDoc._height);
+ const ctx = canvas.getContext('2d'); // draw image to canvas. scale to target dimensions
+ if (ctx) {
+ // this._imageRef && ctx.drawImage(this._imageRef, 0, 0, canvas.width, canvas.height);
+ this._imageRef && ctx.drawImage(this._imageRef, NumCast(this._marqueeref.current?.left), NumCast(this._marqueeref.current?.top), this._width, this._height, 0, 0, 1000, 1000);
+ //this._imageRef && ctx.drawImage(this._imageRef, 0, 0, 2000, 1000, 0, 0, canvas.width, canvas.height);
+ // console.log(NumCast(this._marqueeref.current?.left) + 100);
+ }
+ const blob = await ImageUtility.canvasToBlob(canvas);
+ return ImageBox.selectUrlToBase64(blob);
+
+ // if (this._imageRef) {
+ // const canv = ImageUtility.getCroppedImg(this._imageRef, this._imageRef.width, this._imageRef.height);
+ // console.log(this._imageRef.width);
+ // if (canv) {
+ // const blob = await ImageUtility.canvasToBlob(canv);
+ // return ImageBox.selectUrlToBase64(blob);
+ // }
+ // }
+ if (!this._imageRef) {
+ const b = Docs.Create.LabelDocument({
+ x: NumCast(this.layoutDoc.x) + width,
+ y: NumCast(this.layoutDoc.y, 1),
+ _width: 150,
+ _height: 50,
+ // title: (this.layoutDoc._layout_currentTimecode || 0).toString(),
+ onClick: FollowLinkScript(),
+ });
+ this._props.addDocument?.(b);
+ DocUtils.MakeLink(b, this.Document, { link_relationship: 'image snapshot' });
+ } else {
+ // convert to desired file format
+ // const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
+ // // if you want to preview the captured image,
+ // const retitled = StrCast(this.Document.title).replace(/[ -.:]/g, '');
+ // const encodedFilename = encodeURIComponent(('snapshot' + retitled + '_' + (this.layoutDoc._layout_currentTimecode || 0).toString()).replace(/[./?=]/g, '_'));
+ // const filename = basename(encodedFilename);
+ // ClientUtils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY));
+ }
+ // convert to desired file format
+
+ // const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
+ // // if you want to preview the captured image,
+ // const retitled = StrCast(this.Document.title).replace(/[ -.:]/g, '');
+ // const encodedFilename = encodeURIComponent(('snapshot' + retitled + '_' + (this.layoutDoc._layout_currentTimecode || 0).toString()).replace(/[./?=]/g, '_'));
+ // const filename = basename(encodedFilename);
+ //ClientUtils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY));
+ // }
+ // const docViewContent = this.DocumentView?.().ContentDiv!;
+ // if (docViewContent instanceof HTMLCanvasElement) {
+ // const canvas = docViewContent;
+ // const img = document.createElement('img'); // create a Image Element
+ // img.src = canvas.toDataURL(); // image sourcez
+ // img.style.width = canvas.style.width;
+ // img.style.height = canvas.style.height;
+ // const parEle = newCan.parentElement as HTMLElement;
+ // parEle.removeChild(newCan);
+ // parEle.appendChild(img);
+ // }
+ };
+
+ createSnapshotLink = (imagePath: string, downX?: number, downY?: number) => {
+ const url = !imagePath.startsWith('/') ? ClientUtils.CorsProxy(imagePath) : imagePath;
+ const width = NumCast(this.layoutDoc._width) || 1;
+ const height = NumCast(this.layoutDoc._height);
+ const imageSnapshot = Docs.Create.ImageDocument(url, {
+ _nativeWidth: Doc.NativeWidth(this.layoutDoc),
+ _nativeHeight: Doc.NativeHeight(this.layoutDoc),
+ x: NumCast(this.layoutDoc.x) + width,
+ y: NumCast(this.layoutDoc.y),
+ onClick: FollowLinkScript(),
+ _width: 150,
+ _height: (height / width) * 150,
+ title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-',
+ });
+ Doc.SetNativeWidth(imageSnapshot[DocData], Doc.NativeWidth(this.layoutDoc));
+ Doc.SetNativeHeight(imageSnapshot[DocData], Doc.NativeHeight(this.layoutDoc));
+ this._props.addDocument?.(imageSnapshot);
+ DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' });
+ // link && (DocCast(link.link_anchor_2)[DocData].timecodeToHide = NumCast(DocCast(link.link_anchor_2).timecodeToShow) + 3); // do we need to set an end time? should default to +0.1
+ setTimeout(() => downX !== undefined && downY !== undefined && DocumentView.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, dropActionType.move, true));
+ };
+
+ /**
+ *
+ if (oldDiv instanceof HTMLCanvasElement) {
+ const canvas = oldDiv;
+ const img = document.createElement('img'); // create a Image Element
+ img.src = canvas.toDataURL(); // image sourcez
+ img.style.width = canvas.style.width;
+ img.style.height = canvas.style.height;
+ const newCan = newDiv as HTMLCanvasElement;
+ const parEle = newCan.parentElement as HTMLElement;
+ parEle.removeChild(newCan);
+ parEle.appendChild(img);
+ }
+ */
+
+ static selectUrlToBase64 = async (blob: Blob): Promise<string> => {
+ try {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.readAsDataURL(blob);
+ reader.onloadend = () => resolve(reader.result as string);
+ reader.onerror = error => reject(error);
+ });
+ } catch (error) {
+ console.error('Error:', error);
+ throw error;
+ }
+ };
+
+ static imageUrlToBase64 = async (imageUrl: string): Promise<string> => {
+ try {
+ const response = await fetch(imageUrl);
+ const blob = await response.blob();
+
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.readAsDataURL(blob);
+ reader.onloadend = () => resolve(reader.result as string);
+ reader.onerror = error => reject(error);
+ });
+ } catch (error) {
+ console.error('Error:', error);
+ throw error;
+ }
+ };
+
+ getImageDesc = async () => {
+ // if (StrCast(this.dataDoc.description)) return StrCast(this.dataDoc.description); // Return existing description
+ const { href } = (this.dataDoc.data as URLField).url;
+ const hrefParts = href.split('.');
+ const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
+ this._loading = true;
+ try {
+ // const hrefBase64 = await ImageBox.imageUrlToBase64(hrefComplete);
+ const hrefBase64 = await this.createCanvas();
+ const response = await gptImageLabel(hrefBase64, 'Tell me what words you see on this image.');
+ //const response = await gptImageLabel(hrefBase64, '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: ');
+ console.log(response);
+ // AnchorMenu.Instance.transferToFlashcard(response);
+ // this.Document[DocData].description = response.trim();
+ // return response; // Return the response
+ } catch (error) {
+ console.log('Error');
+ }
+ this._loading = false;
+ // return '';
+ };
+
+ @action
+ setRef = (iref: HTMLImageElement | null) => {
+ this._imageRef = iref;
+ // if (iref) {
+ // this._videoRef!.ontimeupdate = this.updateTimecode;
+ // // @ts-ignore
+ // // vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
+ // this._disposers.reactionDisposer?.();
+ // this._disposers.reactionDisposer = reaction(
+ // () => NumCast(this.layoutDoc._layout_currentTimecode),
+ // time => {
+ // !this._playing && (vref.currentTime = time);
+ // },
+ // { fireImmediately: true }
+ // );
+
+ // (!this.dataDoc[this.fieldKey + '_thumbnails'] || StrListCast(this.dataDoc[this.fieldKey + '_thumbnails']).length !== VideoThumbnails.DENSE) && this.getVideoThumbnails();
+ // }
+ };
+
specificContextMenu = (): void => {
const field = Cast(this.dataDoc[this.fieldKey], ImageField);
if (field) {
const funcs: ContextMenuProps[] = [];
+ // funcs.push({ description: 'Create ai flashcards', event: () => this.getImageDesc(), icon: 'id-card' });
+ funcs.push({ description: 'Get Images', event: () => this.handleSelection('Cats'), icon: 'redo-alt' });
funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' });
funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' });
funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' });
@@ -391,6 +612,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
<div className="imageBox-fader" style={{ opacity: backAlpha }}>
<img
alt=""
+ ref={this.setRef}
key="paths"
src={srcpath}
style={{ transform, transformOrigin }}
@@ -432,6 +654,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
action(moveEv => {
MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
this._marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]);
+ this._width = moveEv.clientX;
+ this._height = moveEv.clientY;
return true;
}),
returnFalse,
@@ -443,6 +667,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@action
finishMarquee = () => {
this._getAnchor = AnchorMenu.Instance?.GetAnchor;
+ AnchorMenu.Instance.gptFlashcards = this.getImageDesc;
+ AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument;
this._marqueeref.current?.onTerminateSelection();
this._props.select(false);
};
@@ -505,6 +731,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
addDocument={this.addDocument}>
{this.content}
</CollectionFreeFormView>
+ {this._loading ? (
+ <div className="loading-spinner" style={{ position: 'absolute' }}>
+ <ReactLoading type="spin" height={50} width={50} color={'blue'} />
+ </div>
+ ) : null}
{this.annotationLayer}
{!this._mainCont.current || !this.DocumentView || !this._annotationLayer.current ? null : (
<MarqueeAnnotator
@@ -523,6 +754,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
marqueeContainer={this._mainCont.current}
highlightDragSrcColor=""
anchorMenuCrop={this.crop}
+ // anchorMenuFlashcard={() => this.getImageDesc()}
/>
)}
</div>
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 99b4a84fc..54643b4a5 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -88,10 +88,16 @@ audiotag:hover {
background: black;
right: 0;
bottom: 0;
- width: 11;
- height: 11;
+ width: 15;
+ height: 22;
cursor: default;
}
+.formattedTextBox-flip {
+ align-items: center;
+ position: absolute;
+ right: 2px;
+ bottom: 4px;
+}
.formattedTextBox-outer {
position: relative;
@@ -171,6 +177,8 @@ audiotag:hover {
border-style: inset;
border-width: 1px;
}
+ // margin-left: 5px;
+ // margin-right: 5px;
}
.gpt-typing-wrapper {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 9f2a9b8e1..3e2befb5f 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -181,7 +181,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
@observable
private gptRes: string = '';
- public makeAIFlashcards: () => void = unimplementedFunction;
+ // public makeAIFlashcards: () => void = unimplementedFunction;
public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
public static PasteOnLoad: ClipboardEvent | undefined;
@@ -816,6 +816,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
isTargetToggler = (anchor: Doc) => BoolCast(anchor.followLinkToggle);
specificContextMenu = (e: React.MouseEvent): void => {
+ if (this._props.dontSelect?.()) return;
const cm = ContextMenu.Instance;
let target = e.target as any; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span>
@@ -969,7 +970,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: `Make AI Flashcards`, event: () => this.makeAIFlashcards(), icon: 'lightbulb' });
optionItems.push({ description: `Ask GPT-3`, event: this.askGPT, icon: 'lightbulb' });
this._props.renderDepth &&
optionItems.push({
diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss
index 55b8446e9..bc0810f22 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss
@@ -13,7 +13,7 @@
box-shadow: 3px 3px 1.5px grey;
max-width: 400;
max-height: 235;
- height:max-content;
+ height: max-content;
.formattedTextBox-tooltipText {
height: max-content;
text-overflow: ellipsis;
@@ -21,7 +21,7 @@
}
.formattedTextBox-tooltip:before {
- content: "";
+ content: '';
height: 0;
width: 0;
position: absolute;
@@ -34,7 +34,7 @@
}
.formattedTextBox-tooltip:after {
- content: "";
+ content: '';
height: 0;
width: 0;
position: absolute;
@@ -44,4 +44,4 @@
border: 5px solid transparent;
border-bottom-width: 0;
border-top-color: white;
-} \ No newline at end of file
+}
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 2f6824466..777117f26 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -15,6 +15,7 @@ import { LinkPopup } from '../linking/LinkPopup';
import { DocumentView } from '../nodes/DocumentView';
import './AnchorMenu.scss';
import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup';
+import ReactLoading from 'react-loading';
@observer
export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -24,6 +25,9 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
private _disposer: IReactionDisposer | undefined;
private _commentRef = React.createRef<HTMLDivElement>();
private _cropRef = React.createRef<HTMLDivElement>();
+ @observable private _loading = false;
+ // @observable protected _top: number = -300;
+ // @observable protected _left: number = -300;
constructor(props: any) {
super(props);
@@ -38,11 +42,21 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
// GPT additions
@observable private _selectedText: string = '';
+ @observable private _x: number = 0;
+ @observable private _y: number = 0;
@action
public setSelectedText = (txt: string) => {
this._selectedText = txt.trim();
};
+ @action
+ public setLocation = (x: number, y: number) => {
+ this._x = x;
+ this._y = y;
+ };
+ @computed public get selectedText() {
+ return this._selectedText;
+ }
public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search
public OnCrop: (e: PointerEvent) => void = unimplementedFunction;
@@ -57,6 +71,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
public MakeTargetToggle: () => void = unimplementedFunction;
public ShowTargetTrail: () => void = unimplementedFunction;
public IsTargetToggler: () => boolean = returnFalse;
+ public gptFlashcards: () => void = unimplementedFunction;
public get Active() {
return this._left > 0;
}
@@ -99,26 +114,27 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
* Invokes the API with the selected text and stores it in the selected text.
* @param e pointer down event
*/
- gptFlashcards = async () => {
- 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);
- };
+ // gptPDFFlashcards = async () => {
+ // const queryText = this._selectedText;
+ // this._loading = true;
+ // 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.
- */
+ // /*
+ // * 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[] = [];
+ var senArr = text.trim().split('Question: ');
+ var collectionArr: Doc[] = [];
for (let 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 });
@@ -133,7 +149,12 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
_layout_autoHeight: true,
});
+ newCol.x = this._x;
+ newCol.y = this._y;
+ newCol.zIndex = 100;
+
this.addToCollection?.(newCol);
+ this._loading = false;
};
pointerDown = (e: React.PointerEvent) => {
@@ -219,12 +240,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
/>
)}
{/* 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}
- />
+ <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" //
@@ -250,6 +266,11 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
/>
</div>
)}
+ {/* {this._loading ? (
+ <div className="loading-spinner" style={{ position: 'absolute' }}>
+ <ReactLoading type="spin" height={30} width={30} color={'white'} />
+ </div>
+ ) : null} */}
</>
) : (
<>
diff --git a/src/client/views/pdf/Annotation.scss b/src/client/views/pdf/Annotation.scss
index 1de60ffed..26856b74e 100644
--- a/src/client/views/pdf/Annotation.scss
+++ b/src/client/views/pdf/Annotation.scss
@@ -7,4 +7,21 @@
&:hover {
cursor: pointer;
}
-} \ No newline at end of file
+}
+// .loading-spinner {
+// display: flex;
+// justify-content: center;
+// align-items: center;
+// height: 90%;
+// width: 93%;
+// left: 10;
+// font-size: 20px;
+// font-weight: bold;
+// color: #0b0a0a;
+// }
+
+// @keyframes spin {
+// to {
+// transform: rotate(360deg);
+// }
+// }
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index d3dd9f727..e70102ce9 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -107,3 +107,24 @@
.pdfViewerDash-interactive {
pointer-events: all;
}
+
+.loading-spinner {
+ position: absolute;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ width: 100%;
+ // left: 50%;
+ // top: 50%;
+ z-index: 200;
+ font-size: 20px;
+ font-weight: bold;
+ color: #17175e;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index db47a84e1..38d14014f 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -30,6 +30,8 @@ import { AnchorMenu } from './AnchorMenu';
import { Annotation } from './Annotation';
import { GPTPopup } from './GPTPopup/GPTPopup';
import './PDFViewer.scss';
+import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
+import ReactLoading from 'react-loading';
// pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`;
// The workerSrc property shall be specified.
@@ -68,6 +70,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
@observable _textSelecting = true;
@observable _showWaiting = true;
@observable Index: number = -1;
+ @observable private _loading = false;
private _pdfViewer: any;
private _styleRule: any; // stylesheet rule for making hyperlinks clickable
@@ -404,6 +407,49 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
}
};
+ gptPDFFlashcards = async () => {
+ const queryText = this._selectionText;
+ this._loading = true;
+ try {
+ const res = await gptAPICall(queryText, GPTCallType.FLASHCARD);
+ console.log(res);
+ // GPTPopup.Instance.setText(res || 'Something went wrong.');
+ AnchorMenu.Instance.transferToFlashcard(res || 'Something went wrong');
+ // this.transferToFlashcard(res || 'Something went wrong');
+ } catch (err) {
+ console.error(err);
+ }
+ this._loading = false;
+ // GPTPopup.Instance.setLoading(false);
+ };
+
+ // transferToFlashcard = (text: string) => {
+ // // put each question generated by GPT on the front of the flashcard
+ // var senArr = text.trim().split('Question: ');
+ // var collectionArr: Doc[] = [];
+ // for (let 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,
+ // });
+
+ // newCol.x = this._props.layoutDoc['x'];
+ // newCol.y = this._props.layoutDoc['y'];
+ // newCol.zIndex = 100;
+
+ // this._props.DocumentView?.()._props.addDocument?.(newCol);
+ // console.log('HERE');
+ // this._loading = false;
+ // };
+
@action
finishMarquee = (/* x?: number, y?: number */) => {
this._getAnchor = AnchorMenu.Instance?.GetAnchor;
@@ -423,6 +469,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
const sel = window.getSelection();
if (sel) {
AnchorMenu.Instance.setSelectedText(sel.toString());
+ AnchorMenu.Instance.setLocation(NumCast(this._props.layoutDoc['x']), NumCast(this._props.layoutDoc['y']));
}
if (sel?.type === 'Range') {
@@ -434,6 +481,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
GPTPopup.Instance.addDoc = this._props.sidebarAddDoc;
// allows for creating collection
AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument;
+ AnchorMenu.Instance.gptFlashcards = this.gptPDFFlashcards;
};
@action
@@ -461,6 +509,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
this._mainCont.current!.style.transform = '';
}
this._selectionContent = selRange.cloneContents();
+
this._selectionText = this._selectionContent?.textContent || '';
// clear selection
@@ -621,6 +670,11 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
/>
)}
</div>
+ {this._loading ? (
+ <div className="loading-spinner" style={{ position: 'absolute' }}>
+ <ReactLoading type="spin" height={80} width={80} color={'blue'} />
+ </div>
+ ) : null}
</div>
);
}