diff options
Diffstat (limited to 'src/client/views/collections')
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. |
