aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/collections')
-rw-r--r--src/client/views/collections/CollectionCardDeckView.tsx2
-rw-r--r--src/client/views/collections/CollectionCarouselView.scss77
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx223
-rw-r--r--src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx4
5 files changed, 225 insertions, 83 deletions
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 70b751f4c..8a9cc46f6 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -484,7 +484,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 01b20d6d3..c40f471d6 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,7 +13,16 @@
width: 100%;
user-select: none;
}
+ .message {
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ height: 60%;
+ z-index: -1;
+ // margin: 15px;
+ }
}
+
.collectionCarouselView-addFlashcards {
justify-content: center;
align-items: center;
@@ -31,12 +42,12 @@
.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;
@@ -47,14 +58,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%;
@@ -64,6 +82,57 @@
top: 80%;
right: 52%;
}
+.carouselView-quiz {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ height: 20px;
+ align-items: center;
+ margin: auto;
+ &:hover {
+ color: white;
+ }
+}
+
+.carouselView-practice {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ height: 20px;
+ align-items: center;
+ margin: auto;
+ &:hover {
+ color: white;
+ }
+}
+.carouselView-starFilter {
+ position: relative;
+ display: flex;
+ height: 20px;
+ align-items: center;
+ &:hover {
+ color: white;
+ }
+}
+
+.carouselView-practiceModes {
+ width: 100%;
+ height: 40px;
+ display: flex;
+ flex-direction: column;
+}
+.carouselView-menu {
+ position: absolute;
+ flex-direction: column;
+ align-items: center;
+ display: flex;
+ top: 2px;
+ right: 2px;
+ width: 30;
+ 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 974cd3e36..e0cee2126 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -1,13 +1,14 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@mui/material';
import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
-import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { Doc, Opt } from '../../../fields/Doc';
import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
-import { ContextMenu } from '../ContextMenu';
import { StyleProp } from '../StyleProp';
import { DocumentView } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
@@ -17,8 +18,11 @@ import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
import { TagItem } from '../TagsView';
enum cardMode {
- PRACTICE = 'practice',
STAR = 'star',
+ ALL = 'all',
+}
+enum practiceMode {
+ PRACTICE = 'practice',
QUIZ = 'quiz',
}
enum practiceVal {
@@ -29,10 +33,10 @@ enum practiceVal {
export class CollectionCarouselView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore
+ get sideField() { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore
get starField() { return "#star"; } // prettier-ignore
_fadeTimer: NodeJS.Timeout | undefined;
- _resetter: IReactionDisposer | undefined;
constructor(props: SubCollectionViewProps) {
super(props);
@@ -42,23 +46,8 @@ export class CollectionCarouselView extends CollectionSubView() {
@observable _last_index = this.carouselIndex;
@observable _last_opacity = 1;
- componentDidMount() {
- this._resetter = reaction(
- // automatically reset practice fields when all cards have been marked as correct
- () => this.carouselItems.length,
- itemsCount => {
- if (this.layoutDoc.filterOp === cardMode.PRACTICE && !itemsCount) {
- 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[this.practiceField] = undefined;
- });
- }
- } // prettier-ignore
- );
- }
componentWillUnmount() {
this._dropDisposer?.();
- this._resetter?.();
}
protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
@@ -68,23 +57,45 @@ export class CollectionCarouselView extends CollectionSubView() {
}
};
+ @computed get practiceMode() {
+ return this.childDocs.some(doc => doc._layout_isFlashcard) ? StrCast(this.layoutDoc.practiceMode) : '';
+ }
+ @computed get practiceMessage() {
+ const cardCount = this.carouselItems.length;
+ if (this.practiceMode) {
+ if (!Doc.hasDocFilter(this.layoutDoc, 'tags', this.starField) && !cardCount) {
+ return 'Finished! Click here to view all flashcards.';
+ }
+ }
+ return '';
+ }
+
+ @computed get filterMessage() {
+ const cardCount = this.carouselItems.length;
+ if (!this.practiceMessage) {
+ if (Doc.hasDocFilter(this.layoutDoc, 'tags', this.starField) && !cardCount) {
+ return 'No starred items. Click here to view all flash cards.';
+ }
+ if (this.practiceMode) {
+ if (!cardCount) return 'No flashcards to show! Click here to leave practice mode';
+ }
+ }
+ return '';
+ }
@computed get marginX() { return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore
@computed get carouselIndex() { return NumCast(this.layoutDoc._carousel_index) % this.carouselItems.length; } // prettier-ignore
- @computed get carouselItems() {
- return DocListCast(this.childDocList)
- .filter(doc => doc.type !== DocumentType.LINK)
- .filter(doc => {
- switch (StrCast(this.layoutDoc.filterOp)) {
- case cardMode.STAR: return !!doc[this.starField]; // show only cards that are starred
- case cardMode.PRACTICE: return doc[this.practiceField] !== practiceVal.CORRECT;// show only cards that aren't marked as correct
- default: return true;
- } // prettier-ignore
- });
- }
+ @computed get carouselItems() { return this.childDocs
+ .filter(doc => doc.type !== DocumentType.LINK)
+ .filter(doc => !this.practiceMode || (BoolCast(doc?._layout_isFlashcard) && doc[this.practiceField] !== practiceVal.CORRECT))// show only cards that aren't marked as correct
+ } // prettier-ignore
+ /**
+ * Move forward or backward the specified number of Docs
+ * @param dir signed number indicating Docs to move forward or backward
+ */
move = action((dir: number) => {
this._last_index = this.carouselIndex;
- this.layoutDoc._carousel_index = (this.carouselIndex + dir + this.carouselItems.length) % this.carouselItems.length;
+ this.layoutDoc._carousel_index = this.carouselItems.length ? (this.carouselIndex + dir + this.carouselItems.length) % this.carouselItems.length : 0;
});
/**
@@ -104,9 +115,9 @@ export class CollectionCarouselView extends CollectionSubView() {
};
/*
- * Stars the document when the star button is pressed.
+ * Toggles whether the 'star' metadata field is set on the current Doc
*/
- star = (e: React.MouseEvent) => {
+ toggleStar = (e: React.MouseEvent) => {
e.stopPropagation();
const curDoc = this.carouselItems[this.carouselIndex];
if (curDoc) {
@@ -125,25 +136,28 @@ export class CollectionCarouselView extends CollectionSubView() {
this.advance(e);
};
+ /**
+ * Sets the practice mode answer style for flashcards
+ * @param mode practiceMode or undefined for no practice
+ */
+ setPracticeMode = (mode: practiceMode | undefined) => {
+ this.layoutDoc.practiceMode = mode;
+ this.carouselItems?.map(doc => (doc[this.practiceField] = undefined));
+ if (mode === practiceMode.QUIZ) this.carouselItems?.map(doc => (doc[this.sideField] = undefined));
+ };
+
captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string) => {
// 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;
return childValue ?? this._props.styleProvider?.(this.layoutDoc, captionProps, property);
};
- panelHeight = () => this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0);
+ contentPanelHeight = () => this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0) - 2 * NumCast(this.layoutDoc.yMargin);
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 = 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' });
- };
+ contentScreentToLocalXf = () => this._props.ScreenToLocalTransform().translate(-NumCast(this.layoutDoc.xMargin), -NumCast(this.layoutDoc.yMargin));
+
+ contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin);
isChildContentActive = () =>
this._props.isContentActive?.() === false
@@ -174,7 +188,12 @@ export class CollectionCarouselView extends CollectionSubView() {
LayoutTemplate={this._props.childLayoutTemplate}
LayoutTemplateString={this._props.childLayoutString}
TemplateDataDocument={DocCast(Doc.Layout(doc).resolvedDataDoc)}
- PanelHeight={this.panelHeight}
+ childFilters={this.childDocFilters}
+ hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)}
+ addDocument={this._props.addDocument}
+ ScreenToLocalTransform={this.contentScreentToLocalXf}
+ PanelWidth={this.contentPanelWidth}
+ PanelHeight={this.contentPanelHeight}
/>
);
};
@@ -234,61 +253,115 @@ export class CollectionCarouselView extends CollectionSubView() {
</>
);
}
+
+ 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?.[this.carouselIndex]) return null;
return (
<>
+ <div>
+ <Tooltip title="star">
+ <div key="star" className="carouselView-star" onClick={this.toggleStar}>
+ <FontAwesomeIcon icon="star" color={TagItem.docHasTag(this.carouselItems?.[this.carouselIndex], 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>
<div key="back" className="carouselView-back" onClick={this.goback}>
<FontAwesomeIcon icon="chevron-left" size="2x" />
</div>
<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={TagItem.docHasTag(this.carouselItems?.[this.carouselIndex], 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.practiceMode ? (
+ <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}
</>
);
}
- /**
- * Prompts user to add more flashcaards if they are in practice mode but there are no flashcards
- */
- renderAddFlashcards = () => <p
- className="collectionCarouselView-addFlashcards"
- style={{display: !this.carouselItems?.[this.carouselIndex] && this.layoutDoc.filterOp === cardMode.PRACTICE ? 'flex' : 'none'}}>
- Add flashcards!
- </p> // prettier-ignore
+ togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode);
+ toggleFilterMode = () => Doc.setDocFilter(this.Document, 'tags', this.starField, 'check', true);
+ setColor = (mode: practiceMode | cardMode, which: string) => { return which === mode ? 'white' : 'light gray'}; //prettier-ignore
- /**
- * Displays message that a flashcard was recently missed if it had previously been marked as wrong.
- * */
- renderRecentlyMissed = () => <p
- className="collectionCarouselView-recentlyMissed"
- style={{display: this.carouselItems?.[this.carouselIndex]?.[this.practiceField] === practiceVal.MISSED ? 'block' : 'none'}}>
- Recently missed!
- </p> // prettier-ignore
+ @computed get menu() {
+ const curDoc = this.carouselItems?.[this.carouselIndex];
+ return (
+ <div className="carouselView-menu">
+ <Tooltip title={Doc.hasDocFilter(this.Document, 'tags', this.starField) ? 'Show all cards' : 'Show only starred cards'}>
+ <div key="back" className="carouselView-starFilter" onClick={e => this.toggleFilterMode()}>
+ <FontAwesomeIcon icon="filter" color={Doc.hasDocFilter(this.Document, 'tags', this.starField) ? 'white' : 'lightgray'} size="1x" />
+ </div>
+ </Tooltip>
+ <div className="carouselView-practiceModes" style={{ display: BoolCast(curDoc?._layout_isFlashcard) ? undefined : 'none' }}>
+ <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.practiceMode))} size="1x" />
+ </div>
+ </Tooltip>
+ <Tooltip title={this.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.practiceMode))} size="1x" />
+ </div>
+ </Tooltip>
+ </div>
+ </div>
+ );
+ }
render() {
return (
<div
className="collectionCarouselView-outer"
ref={this.createDashEventsTarget}
- onContextMenu={this.specificMenu}
style={{
background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string,
color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
}}>
- {this.content}
- {this.renderAddFlashcards()}
- {this.renderRecentlyMissed()}
- {this.Document._chromeHidden ? null : this.buttons}
+ {!this.practiceMessage && !this.filterMessage ? (
+ this.content
+ ) : (
+ <p
+ className="message"
+ onClick={e => {
+ if (this.filterMessage || this.practiceMessage) {
+ this.setPracticeMode(undefined);
+ Doc.setDocFilter(this.layoutDoc, 'tags', this.starField, 'remove');
+ }
+ }}>
+ {this.filterMessage || this.practiceMessage}
+ </p>
+ )}
+ {!this.Document._chromeHidden ? this.menu : null}
+ {!this.Document._chromeHidden ? this.buttons : null}
</div>
);
}
diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
index 033d1590d..753685b97 100644
--- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
@@ -167,7 +167,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
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 labels to describe this image.').then(labels =>
({ doc, labels }))) ; // prettier-ignore
}
});
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index ccb6bc9be..917aaaea8 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -433,14 +433,14 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
* Classifies images and assigns the labels as document fields.
*/
@undoBatch
- classifyImages = action(async () => {
+ classifyImages = async () => {
const groupButton = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyImageGrouper);
if (groupButton) {
this._selectedDocs = this.marqueeSelect(false, DocumentType.IMG);
ImageLabelBoxData.Instance.setData(this._selectedDocs);
MainView.Instance.expandFlyout(groupButton);
}
- });
+ };
/**
* Groups images to most similar labels.