diff options
Diffstat (limited to 'src/client/views')
29 files changed, 1080 insertions, 1179 deletions
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 7abca5197..d748b70ae 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -20,7 +20,7 @@ import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { Docs } from '../documents/Documents'; import { CalendarManager } from '../util/CalendarManager'; import { CaptureManager } from '../util/CaptureManager'; -import { Button, CurrentUserUtils } from '../util/CurrentUserUtils'; +import { CurrentUserUtils, ToTagName } from '../util/CurrentUserUtils'; import { DocumentManager } from '../util/DocumentManager'; import { DragManager } from '../util/DragManager'; import { dropActionType } from '../util/DropActionTypes'; @@ -63,7 +63,6 @@ import { DocCreatorMenu } from './nodes/DataVizBox/DocCreatorMenu'; import { SchemaCSVPopUp } from './nodes/DataVizBox/SchemaCSVPopUp'; import { DocButtonState } from './nodes/DocumentLinksButton'; import { DocumentView, DocumentViewInternal } from './nodes/DocumentView'; -import { ButtonType } from './nodes/FontIconBox/FontIconBox'; import { ImageEditorData as ImageEditor } from './nodes/ImageBox'; import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup'; import { LinkDocPreview, LinkInfo } from './nodes/LinkDocPreview'; @@ -874,25 +873,9 @@ export class MainView extends ObservableReactComponent<object> { * @param hotKey tite of the new hotkey */ addHotKey = (hotKey: string) => { - const buttons = DocCast(Doc.UserDoc().myContextMenuBtns); - const filter = DocCast(buttons.Filter); - const title = hotKey.startsWith('#') ? hotKey.substring(1) : hotKey; - - const newKey: Button = { - title, - icon: 'question', - toolTip: `Click to toggle the ${title}'s group's visibility`, - btnType: ButtonType.ToggleButton, - expertMode: false, - toolType: '#' + title, - funcs: {}, - scripts: { onClick: '{ return handleTags(this.toolType, _readOnly_);}' }, - }; - - const newBtn = CurrentUserUtils.setupContextMenuBtn(newKey, filter); - newBtn.isSystem = newBtn[DocData].isSystem = undefined; - - Doc.AddToFilterHotKeys(newBtn); + const filterIcons = DocCast(DocCast(Doc.UserDoc().myContextMenuBtns)?.Filter); + const menuDoc = CurrentUserUtils.setupContextMenuBtn(CurrentUserUtils.filterBtnDesc(ToTagName(hotKey), 'question'), filterIcons); + Doc.AddToFilterHotKeys(menuDoc); }; @computed get mainInnerContent() { diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 42aa6782f..bed96f600 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -119,6 +119,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps // Pres Trails booleans: @observable openPresTransitions: boolean = true; @observable openPresProgressivize: boolean = false; + @observable openPresMedia: boolean = false; @observable openPresVisibilityAndDuration: boolean = false; @observable openAddSlide: boolean = false; @observable openSlideOptions: boolean = false; @@ -1936,74 +1937,74 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps </div> </div> {!selectedItem ? null : ( - <div className="propertiesView-presentationTrails"> + <div className="propertiesView-section"> <div - className="propertiesView-presentationTrails-title" + className="propertiesView-sectionTitle" onPointerDown={action(() => { - this.openPresTransitions = !this.openPresTransitions; + this.openPresVisibilityAndDuration = !this.openPresVisibilityAndDuration; })} style={{ color: SnappingManager.userColor, - backgroundColor: this.openPresTransitions ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor, + backgroundColor: SnappingManager.userVariantColor, }}> - <FontAwesomeIcon style={{ alignSelf: 'center' }} icon="rocket" /> Transitions + Visibility <div className="propertiesView-presentationTrails-title-icon"> - <FontAwesomeIcon icon={this.openPresTransitions ? 'caret-down' : 'caret-right'} size="lg" /> + <FontAwesomeIcon icon={this.openPresVisibilityAndDuration ? 'caret-down' : 'caret-right'} size="lg" /> </div> </div> - {this.openPresTransitions ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.transitionDropdown}</div> : null} + {this.openPresVisibilityAndDuration ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.visibilityDurationDropdown}</div> : null} </div> )} {!selectedItem ? null : ( - <div className="propertiesView-presentationTrails"> + <div className="propertiesView-section"> <div - className="propertiesView-presentationTrails-title" + className="propertiesView-sectionTitle" onPointerDown={action(() => { - this.openPresVisibilityAndDuration = !this.openPresVisibilityAndDuration; + this.openPresProgressivize = !this.openPresProgressivize; })} style={{ color: SnappingManager.userColor, - backgroundColor: this.openPresVisibilityAndDuration ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor, + backgroundColor: SnappingManager.userVariantColor, }}> - <FontAwesomeIcon style={{ alignSelf: 'center' }} icon="rocket" /> Visibility + Progressivize <div className="propertiesView-presentationTrails-title-icon"> - <FontAwesomeIcon icon={this.openPresVisibilityAndDuration ? 'caret-down' : 'caret-right'} size="lg" /> + <FontAwesomeIcon icon={this.openPresProgressivize ? 'caret-down' : 'caret-right'} size="lg" /> </div> </div> - {this.openPresVisibilityAndDuration ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.visibilityDurationDropdown}</div> : null} + {this.openPresProgressivize ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.progressivizeDropdown}</div> : null} </div> )} {!selectedItem ? null : ( - <div className="propertiesView-presentationTrails"> + <div className="propertiesView-section"> <div - className="propertiesView-presentationTrails-title" + className="propertiesView-sectionTitle" onPointerDown={action(() => { - this.openPresProgressivize = !this.openPresProgressivize; + this.openPresMedia = !this.openPresMedia; })} style={{ color: SnappingManager.userColor, - backgroundColor: this.openPresProgressivize ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor, + backgroundColor: SnappingManager.userVariantColor, }}> - <FontAwesomeIcon style={{ alignSelf: 'center' }} icon="rocket" /> Progressivize + Media <div className="propertiesView-presentationTrails-title-icon"> - <FontAwesomeIcon icon={this.openPresProgressivize ? 'caret-down' : 'caret-right'} size="lg" /> + <FontAwesomeIcon icon={this.openPresMedia ? 'caret-down' : 'caret-right'} size="lg" /> </div> </div> - {this.openPresProgressivize ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.progressivizeDropdown}</div> : null} + {this.openPresMedia ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.mediaDropdown}</div> : null} </div> )} {!selectedItem || (type !== DocumentType.VID && type !== DocumentType.AUDIO) ? null : ( - <div className="propertiesView-presentationTrails"> + <div className="propertiesView-section"> <div - className="propertiesView-presentationTrails-title" + className="propertiesView-sectionTitle" onPointerDown={action(() => { this.openSlideOptions = !this.openSlideOptions; })} style={{ color: SnappingManager.userColor, - backgroundColor: this.openSlideOptions ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor, + backgroundColor: SnappingManager.userVariantColor, }}> - <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={type === DocumentType.AUDIO ? 'file-audio' : 'file-video'} /> {type === DocumentType.AUDIO ? 'Audio Options' : 'Video Options'} + {type === DocumentType.AUDIO ? 'file-audio' : 'file-video'} <div className="propertiesView-presentationTrails-title-icon"> <FontAwesomeIcon icon={this.openSlideOptions ? 'caret-down' : 'caret-right'} size="lg" /> </div> @@ -2011,6 +2012,25 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps {this.openSlideOptions ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.mediaOptionsDropdown}</div> : null} </div> )} + {!selectedItem ? null : ( + <div className="propertiesView-section"> + <div + className="propertiesView-sectionTitle" + onPointerDown={action(() => { + this.openPresTransitions = !this.openPresTransitions; + })} + style={{ + color: SnappingManager.userColor, + backgroundColor: SnappingManager.userVariantColor, + }}> + Transitions + <div className="propertiesView-presentationTrails-title-icon"> + <FontAwesomeIcon icon={this.openPresTransitions ? 'caret-down' : 'caret-right'} size="lg" /> + </div> + </div> + {this.openPresTransitions ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.transitionDropdown}</div> : null} + </div> + )} </div> ); } diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index e825a27d3..bebc9a341 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -1,7 +1,7 @@ +import { Dropdown, DropdownType, IconButton, IListItemProps, Shadows, Size, Type } from '@dash/components'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { Dropdown, DropdownType, IconButton, IListItemProps, Shadows, Size, Type } from '@dash/components'; import { action, untracked } from 'mobx'; import { extname } from 'path'; import * as React from 'react'; @@ -10,6 +10,7 @@ import { FaFilter } from 'react-icons/fa'; import { ClientUtils, DashColor, lightOrDark } from '../../ClientUtils'; import { Doc, Opt, StrListCast } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; +import { InkInkTool } from '../../fields/InkField'; import { ScriptField } from '../../fields/ScriptField'; import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../fields/Types'; import { AudioAnnoState } from '../../server/SharedMediaTypes'; @@ -21,11 +22,9 @@ import { TreeSort } from './collections/TreeSort'; import { Colors } from './global/globalEnums'; import { DocumentView, DocumentViewProps } from './nodes/DocumentView'; import { FieldViewProps } from './nodes/FieldView'; -import { styleProviderQuiz } from './StyleProviderQuiz'; import { StyleProp } from './StyleProp'; import './StyleProvider.scss'; -import { TagsView } from './TagsView'; -import { InkInkTool } from '../../fields/InkField'; +import { styleProviderQuiz } from './StyleProviderQuiz'; function toggleLockedPosition(doc: Doc) { UndoManager.RunInBatch(() => Doc.toggleLockedPosition(doc), 'toggleBackground'); @@ -394,7 +393,6 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & </Tooltip> ); }; - const tags = () => docView?.() ? <TagsView Views={[docView?.()]}/> : null; return ( <> @@ -402,7 +400,6 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & {lock()} {filter()} {audio()} - {tags()} </> ); } diff --git a/src/client/views/TagsView.scss b/src/client/views/TagsView.scss index 303a08e1e..b21d303fb 100644 --- a/src/client/views/TagsView.scss +++ b/src/client/views/TagsView.scss @@ -12,7 +12,7 @@ .tagsView-list { display: flex; flex-wrap: wrap; - height: inherit; + height: 1; .iconButton-container { min-height: unset !important; } @@ -58,7 +58,7 @@ } .tagsView-editing-box { - margin-top: 8px; + margin-top: 20px; } .tagsView-input-box { diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx index 73f404596..b70e21918 100644 --- a/src/client/views/TagsView.tsx +++ b/src/client/views/TagsView.tsx @@ -9,7 +9,7 @@ import { emptyFunction } from '../../Utils'; import { Doc, DocListCast, Field, Opt, StrListCast } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { List } from '../../fields/List'; -import { DocCast, NumCast, StrCast } from '../../fields/Types'; +import { DocCast, StrCast } from '../../fields/Types'; import { DocumentType } from '../documents/DocumentTypes'; import { DragManager } from '../util/DragManager'; import { SnappingManager } from '../util/SnappingManager'; @@ -146,7 +146,7 @@ export class TagItem extends ObservableReactComponent<TagItemProps> { } } } - doc[DocData].tags = new List<string>((doc[DocData].tags as List<string>).filter(label => label !== tag)); + doc[DocData].tags = new List<string>(StrListCast(doc[DocData].tags).filter(label => label !== tag)); }; private _ref: React.RefObject<HTMLDivElement>; diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx index d9ff21035..15683ebf2 100644 --- a/src/client/views/animationtimeline/Timeline.tsx +++ b/src/client/views/animationtimeline/Timeline.tsx @@ -16,6 +16,7 @@ import { RegionHelpers } from './Region'; import './Timeline.scss'; import { TimelineOverview } from './TimelineOverview'; import { Track } from './Track'; +import { Id } from '../../../fields/FieldSymbols'; /** * Timeline class controls most of timeline functions besides individual region and track mechanism. Main functions are @@ -56,7 +57,7 @@ export class Timeline extends ObservableReactComponent<FieldViewProps> { private DEFAULT_CONTAINER_HEIGHT: number = 330; private MIN_CONTAINER_HEIGHT: number = 205; - constructor(props: any) { + constructor(props: FieldViewProps) { super(props); makeObservable(this); } @@ -89,7 +90,7 @@ export class Timeline extends ObservableReactComponent<FieldViewProps> { */ @computed private get children(): Doc[] { - const annotatedDoc = [DocumentType.IMG, DocumentType.VID, DocumentType.PDF, DocumentType.MAP].includes(StrCast(this._props.Document.type) as any); + const annotatedDoc = [DocumentType.IMG, DocumentType.VID, DocumentType.PDF, DocumentType.MAP].includes(StrCast(this._props.Document.type) as unknown as DocumentType); if (annotatedDoc) { return DocListCast(this._props.Document[Doc.LayoutFieldKey(this._props.Document) + '_annotations']); } @@ -272,9 +273,9 @@ export class Timeline extends ObservableReactComponent<FieldViewProps> { * for displaying time to standard min:sec */ @action - toReadTime = (time: number): string => { - time = time / 1000; - const inSeconds = Math.round(time * 100) / 100; + toReadTime = (timeIn: number): string => { + const timeSecs = timeIn / 1000; + const inSeconds = Math.round(timeSecs * 100) / 100; const min = Math.floor(inSeconds / 60); const sec = Math.round((inSeconds % 60) * 100) / 100; @@ -552,6 +553,7 @@ export class Timeline extends ObservableReactComponent<FieldViewProps> { <div key="timeline_trackbox" className="trackbox" ref={this._trackbox} style={{ width: `${this._totalLength}px` }}> {[...this.children, this._props.Document].map(doc => ( <Track + key={doc[Id]} ref={ref => this.mapOfTracks.push(ref)} timeline={this} animatedDoc={doc} @@ -570,7 +572,7 @@ export class Timeline extends ObservableReactComponent<FieldViewProps> { <div className="currentTime">Current: {this.getCurrentTime()}</div> <div key="timeline_title" className="title-container" ref={this._titleContainer}> {[...this.children, this._props.Document].map(doc => ( - <div style={{ height: `${this._titleHeight}px` }} className="datapane" onPointerOver={() => Doc.BrushDoc(doc)} onPointerOut={() => Doc.UnBrushDoc(doc)}> + <div key={doc[Id]} style={{ height: `${this._titleHeight}px` }} className="datapane" onPointerOver={() => Doc.BrushDoc(doc)} onPointerOut={() => Doc.UnBrushDoc(doc)}> <p>{StrCast(doc.title)}</p> </div> ))} diff --git a/src/client/views/collections/CollectionCardDeckView.scss b/src/client/views/collections/CollectionCardDeckView.scss index 5283601bf..79c53db08 100644 --- a/src/client/views/collections/CollectionCardDeckView.scss +++ b/src/client/views/collections/CollectionCardDeckView.scss @@ -28,11 +28,14 @@ .collectionCardView-cardwrapper { display: grid; - grid-template-columns: repeat(10, 1fr); transform-origin: left 50%; align-items: center; z-index: 0; // so that setting z-index of active card doesn't make it land on top of things outside of the card-wrapper } +.collectionCardView-cardSizeDragger { + position: absolute; + top: 0; +} .no-card-span { position: relative; diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx index 836a5a2c3..43464e50c 100644 --- a/src/client/views/collections/CollectionCardDeckView.tsx +++ b/src/client/views/collections/CollectionCardDeckView.tsx @@ -1,15 +1,16 @@ -import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, trace } from 'mobx'; +import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; import * as React from 'react'; -import { ClientUtils, DashColor, imageUrlToBase64, returnFalse, returnNever, returnZero } from '../../../ClientUtils'; +import * as CSS from 'csstype'; +import { ClientUtils, imageUrlToBase64, returnFalse, returnNever, returnZero, setupMoveUpEvents } from '../../../ClientUtils'; import { emptyFunction } from '../../../Utils'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { Animation, DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, DateCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types'; import { URLField } from '../../../fields/URLField'; import { gptImageLabel } from '../../apis/gpt/GPT'; import { DocumentType } from '../../documents/DocumentTypes'; @@ -18,24 +19,17 @@ import { DragManager } from '../../util/DragManager'; import { dropActionType } from '../../util/DropActionTypes'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; -import { undoable } from '../../util/UndoManager'; +import { undoable, UndoManager } from '../../util/UndoManager'; import { PinDocView, PinProps } from '../PinFuncs'; import { StyleProp } from '../StyleProp'; import { TagItem } from '../TagsView'; import { DocumentView, DocumentViewProps } from '../nodes/DocumentView'; import { FocusViewOptions } from '../nodes/FocusViewOptions'; -import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup'; +import { GPTPopup } from '../pdf/GPTPopup/GPTPopup'; import './CollectionCardDeckView.scss'; -import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; - -enum cardSortings { - Time = 'time', - Type = 'type', - Color = 'color', - Chat = 'chat', - Tag = 'tag', - None = '', -} +import { CollectionSubView, docSortings, SubCollectionViewProps } from './CollectionSubView'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { SettingsManager } from '../../util/SettingsManager'; /** * New view type specifically for studying more dynamically. Allows you to reorder docs however you see fit, easily @@ -52,17 +46,16 @@ export class CollectionCardView extends CollectionSubView() { private _oldWheel: HTMLElement | null = null; private _dropped = false; // set when a card doc has just moved and the drop method has been called - prevents the pointerUp method from hiding doc decorations (which needs to be done when clicking on a card to animate it to front/center) private _setCurDocScript = () => ScriptField.MakeScript('scriptContext.layoutDoc._card_curDoc=this', { scriptContext: 'any' })!; + private _draggerRef = React.createRef<HTMLDivElement>(); @observable _forceChildXf = 0; @observable _hoveredNodeIndex = -1; @observable _docRefs = new ObservableMap<Doc, DocumentView>(); - @observable _maxRowCount = 10; - @observable _docDraggedIndex: number = -1; + @observable _cursor: CSS.Property.Cursor = 'ew-resize'; constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); - this.setRegenerateCallback(); } protected createDashEventsTarget = (ele: HTMLDivElement | null) => { this._dropDisposer?.(); @@ -74,36 +67,32 @@ export class CollectionCardView extends CollectionSubView() { // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); }; - /** - * Callback to ensure gpt's text versions of the child docs are updated - */ - setRegenerateCallback = () => GPTPopup.Instance.setRegenerateCallback(this.childPairStringListAndUpdateSortDesc); + @computed get cardWidth() { + return NumCast(this.layoutDoc._cardWidth, 50); + } + @computed get _maxRowCount() { + return Math.ceil(this.cardDeckWidth / this.cardWidth); + } /** * update's gpt's doc-text list and initializes callbacks */ - @action - childPairStringListAndUpdateSortDesc = async () => { - const sortDesc = await this.childPairStringList(); // Await the promise to get the string result - GPTPopup.Instance.setSortDesc(sortDesc.join()); - GPTPopup.Instance.onSortComplete = (sortResult: string, questionType: string, tag?: string) => this.processGptOutput(sortResult, questionType, tag); - GPTPopup.Instance.onQuizRandom = () => this.quizMode(); - }; + childPairStringListAndUpdateSortDesc = () => + this.childPairStringList().then(sortDesc => { + GPTPopup.Instance.setSortDesc(sortDesc.join()); + GPTPopup.Instance.onSortComplete = this.processGptOutput; + GPTPopup.Instance.onQuizRandom = this.quizMode; + }); componentDidMount() { - this._props.setContentViewBox?.(this); - this._disposers.sort = reaction( - () => GPTPopup.Instance.visible, - isVis => { - if (isVis) { - this.openChatPopup(); - } else { - this.Document.card_sort = this.cardSort === cardSortings.Chat ? '' : this.Document.card_sort; - } - } + this._disposers.chatVis = reaction( + () => GPTPopup.Instance.Visible, + vis => !vis && this.onGptHide() ); + GPTPopup.Instance.setRegenerateCallback(this.Document, this.childPairStringListAndUpdateSortDesc); + this._props.setContentViewBox?.(this); // if card deck moves, then the child doc views are hidden so their screen to local transforms will return empty rectangles - // when inquired from the dom (below in childScreenToLocal). When the doc is actually renders, we need to act like the + // when inquired from the dom (below in childScreenToLocal). When the doc is actually rendered, we need to act like the // dash data just changed and trigger a React involidation with the correct data (read from the dom). this._disposers.child = reaction( () => [this.Document.x, this.Document.y], @@ -121,19 +110,21 @@ export class CollectionCardView extends CollectionSubView() { ); } + onGptHide = () => Doc.setDocFilter(this.Document, 'tags', '#chat', 'remove'); componentWillUnmount() { + GPTPopup.Instance.setSortDesc(''); + GPTPopup.Instance.onSortComplete = undefined; + GPTPopup.Instance.onQuizRandom = undefined; + GPTPopup.Instance.setRegenerateCallback(undefined, null); Object.keys(this._disposers).forEach(key => this._disposers[key]?.()); this._dropDisposer?.(); } - @computed get cardSort() { - return StrCast(this.Document.card_sort) as cardSortings; - } /** * Number of rows of cards to be rendered */ @computed get numRows() { - return Math.ceil(this.sortedDocs.length / this._maxRowCount); + return Math.ceil(this.childDocs.length / this._maxRowCount); } /** * Circle arc size, in radians, to layout cards @@ -167,12 +158,19 @@ export class CollectionCardView extends CollectionSubView() { return this._props.NativeDimScaling?.() || 1; } + @computed get xMargin() { + return NumCast(this.layoutDoc._xMargin, Math.max(3, 0.05 * this._props.PanelWidth())); + } + + @computed get cardDeckWidth() { + return this._props.PanelWidth() - 2 * this.xMargin; + } + /** * When in quiz mode, randomly selects a document */ quizMode = () => { - const randomIndex = Math.floor(Math.random() * this.childDocs.length); - this.layoutDoc._card_curDoc = this.childDocs[randomIndex]; + this.layoutDoc._card_curDoc = this.childDocs[Math.floor(Math.random() * this.childDocs.length)]; }; setHoveredNodeIndex = action((index: number) => { @@ -193,7 +191,7 @@ export class CollectionCardView extends CollectionSubView() { * @returns the card's new index */ findCardDropIndex = (mouseX: number, mouseY: number) => { - const cardCount = this.sortedDocs.length; + const cardCount = this.childDocs.length; let index = 0; const cardWidth = cardCount < this._maxRowCount ? this._props.PanelWidth() / cardCount : this._props.PanelWidth() / this._maxRowCount; @@ -227,8 +225,8 @@ export class CollectionCardView extends CollectionSubView() { */ @action onPointerMove = (x: number, y: number) => { - if (DragManager.docsBeingDragged.some(doc => this.sortedDocs.includes(doc)) || SnappingManager.CanEmbed) { - this._docDraggedIndex = this.findCardDropIndex(x, y); + if (DragManager.docsBeingDragged.some(doc => this.childDocs.includes(doc)) || SnappingManager.CanEmbed) { + this.docDraggedIndex = this.findCardDropIndex(x, y); } }; @@ -241,14 +239,14 @@ export class CollectionCardView extends CollectionSubView() { onInternalDrop = undoable( action((e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData) { - const dragIndex = this._docDraggedIndex; + const dragIndex = this.docDraggedIndex; const draggedDoc = DragManager.docsBeingDragged[0]; if (dragIndex > -1 && draggedDoc) { - this._docDraggedIndex = -1; - const sorted = this.sortedDocs; + this.docDraggedIndex = -1; + const sorted = this.childDocs; const originalIndex = sorted.findIndex(doc => doc === draggedDoc); - this.Document.card_sort = ''; + this.Document[this._props.fieldKey + '_sort'] = ''; originalIndex !== -1 && sorted.splice(originalIndex, 1); sorted.splice(dragIndex, 0, draggedDoc); if (de.complete.docDragData.removeDocument?.(draggedDoc)) { @@ -277,49 +275,6 @@ export class CollectionCardView extends CollectionSubView() { .map(({ i }) => i) .join('.'); - /** - * Called in the sortedDocsType method. Compares the cards' value in regards to the desired sort type-- earlier cards are move to the - * front, latter cards to the back - * @param docs - * @param sortType - * @param isDesc - * @returns - */ - sort = (docsIn: Doc[], sortType: cardSortings, isDesc: boolean, dragIndex: number) => { - const docs = docsIn.slice(); // need make new object list since sort() modifies the incoming list which confuses mobx caching - sortType && - docs.sort((docA, docB) => { - const [typeA, typeB] = (() => { - switch (sortType) { - default: - case cardSortings.Type: return [StrCast(docA.type), StrCast(docB.type)]; - case cardSortings.Chat: return [NumCast(docA.chatIndex, 9999), NumCast(docB.chatIndex,9999)]; - case cardSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(), DateCast(docB.author_date)?.date ?? Date.now()]; - case cardSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()]; - } - })(); //prettier-ignore - return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? 1 : -1); - }); - if (dragIndex !== -1) { - const draggedDoc = DragManager.docsBeingDragged[0]; - const originalIndex = docs.findIndex(doc => doc === draggedDoc); - - originalIndex !== -1 && docs.splice(originalIndex, 1); - draggedDoc && docs.splice(dragIndex, 0, draggedDoc); - } - - return docs; - }; - - @computed get sortedDocs() { - return this.sort( - this.childCards.map(card => card.layout), - this.cardSort, - BoolCast(this.Document.card_sort_isDesc), - this._docDraggedIndex - ); - } - isChildContentActive = computedFn( (doc: Doc) => () => this._props.isContentActive?.() === false @@ -485,74 +440,51 @@ export class CollectionCardView extends CollectionSubView() { * Processes gpt's output depending on the type of question the user asked. Converts gpt's string output to * usable code * @param gptOutput + * @param questionType + * @param tag */ - @action - processGptOutput = undoable((gptOutput: string, questionType: string, tag?: string) => { - // Split the string into individual list items - const listItems = gptOutput.split('======').filter(item => item.trim() !== ''); - - if (questionType === '2' || questionType === '4') { - this.childDocs.forEach(d => { - d.chatFilter = false; - }); - } - - if (questionType === '6') { - this.Document.card_sort = 'chat'; - } + processGptOutput = (gptOutput: string, questionType: string, tag?: string) => + undoable(() => { + // Split the string into individual list items + const listItems = gptOutput.split('======').filter(item => item.trim() !== ''); + + if (questionType === '2' || questionType === '4') { + this.childDocs.forEach(d => { + TagItem.removeTagFromDoc(d, '#chat'); + }); + } - listItems.forEach((item, index) => { - const normalizedItem = item.trim(); - // find the corresponding Doc in the textToDoc map - const doc = this._textToDoc.get(normalizedItem); - - if (doc) { - switch (questionType) { - case '6': - doc.chatIndex = index; - break; - case '1': { - const allHotKeys = Doc.MyFilterHotKeys; - - let myTag = ''; - - if (tag) { - for (let i = 0; i < allHotKeys.length; i++) { - // bcz: CHECK THIS CODE OUT -- SOMETHING CHANGED - const keyTag = StrCast(allHotKeys[i].toolType); - if (tag.includes(keyTag)) { - myTag = keyTag; - break; - } - } + if (questionType === '6') { + this.Document[this._props.fieldKey + '_sort'] = docSortings.Chat; + } - if (myTag != '') { - doc[myTag] = true; + listItems.forEach((item, index) => { + const normalizedItem = item.trim(); + // find the corresponding Doc in the textToDoc map + const doc = this._textToDoc.get(normalizedItem); + if (doc) { + switch (questionType) { + case '6': + doc.chatIndex = index; + break; + case '1': + if (tag) { + const hashTag = tag.startsWith('#') ? tag : '#' + tag[0].toLowerCase() + tag.slice(1); + const filterTag = Doc.MyFilterHotKeys.map(key => StrCast(key.toolType)).find(key => key.includes(tag)) ?? hashTag; + TagItem.addTagToDoc(doc, filterTag); } - } - break; + break; + case '2': + case '4': + TagItem.addTagToDoc(doc, '#chat'); + Doc.setDocFilter(this.Document, 'tags', '#chat', 'check'); + break; } - case '2': - case '4': - doc.chatFilter = true; - Doc.setDocFilter(DocCast(this.Document.embedContainer), 'chatFilter', true, 'match'); - break; + } else { + console.warn(`No matching document found for item: ${normalizedItem}`); } - } else { - console.warn(`No matching document found for item: ${normalizedItem}`); - } - }); - }, ''); - - /** - * Opens up the chat popup and starts the process for smart sorting. - */ - openChatPopup = async () => { - GPTPopup.Instance.setVisible(true); - GPTPopup.Instance.setMode(GPTPopupMode.CARD); - GPTPopup.Instance.setCardsDoneLoading(true); // Set dataDoneLoading to true after data is loaded - await this.childPairStringListAndUpdateSortDesc(); - }; + }); + }, '')(); childScreenToLocal = computedFn((doc: Doc, index: number, isSelected: boolean) => () => { // need to explicitly trigger an invalidation since we're reading everything from the Dom @@ -591,6 +523,26 @@ export class CollectionCardView extends CollectionSubView() { } }); + cardSizerDown = (e: React.PointerEvent) => { + runInAction(() => { + this._cursor = 'grabbing'; + }); + const batch = UndoManager.StartBatch('card view size'); + setupMoveUpEvents( + this, + e, + (emove: PointerEvent, down: number[], delta: number[]) => { + this.layoutDoc._cardWidth = Math.max(10, delta[0] < 0 ? Math.floor(this.cardWidth + delta[0]) : Math.ceil(this.cardWidth + delta[0])); + return false; + }, + action(() => { + this._cursor = 'ew-resize'; + batch.end(); + }), + emptyFunction + ); + }; + /** * turns off the _dropped flag at the end of a drag/drop, or releases the focused Doc if a different Doc is clicked */ @@ -626,9 +578,8 @@ export class CollectionCardView extends CollectionSubView() { * Actually renders all the cards */ @computed get renderCards() { - trace(); // Map sorted documents to their rendered components - return this.sortedDocs.map((doc, index) => { + return this.childDocs.map((doc, index) => { const cardsInRow = this.cardsInRowThatIncludesCardIndex(index); const childScreenToLocal = this.childScreenToLocal(doc, index, doc === this.curDoc()); @@ -638,7 +589,7 @@ export class CollectionCardView extends CollectionSubView() { const aspect = NumCast(doc.height) / NumCast(doc.width, 1); const vscale = Math.max(1,Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale * this.nativeScaling) / (aspect * this.childPanelWidth()), (this._props.PanelHeight() - 80) / (aspect * (this._props.PanelWidth() / 10)))); // prettier-ignore - const hscale = Math.min(this.sortedDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size + const hscale = Math.min(this.childDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size return ( <div key={doc[Id]} @@ -674,17 +625,13 @@ export class CollectionCardView extends CollectionSubView() { curDoc = () => DocCast(this.layoutDoc._card_curDoc); render() { - trace(); const fitContentScale = this.childCards.length === 0 ? 1 : this.fitContentScale; return ( <div className="collectionCardView-outer" ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)} - onPointerDown={action(e => { - if (e.button === 2 || e.ctrlKey) return; - this.releaseCurDoc(); - })} - onPointerLeave={action(() => (this._docDraggedIndex = -1))} + onPointerDown={e => e.button !== 2 && !e.ctrlKey && this.releaseCurDoc()} + onPointerLeave={action(() => (this.docDraggedIndex = -1))} onPointerMove={e => this.onPointerMove(...this._props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY))} onDrop={this.onExternalDrop.bind(this)} style={{ @@ -701,6 +648,7 @@ export class CollectionCardView extends CollectionSubView() { <div className="collectionCardView-cardwrapper" style={{ + gridTemplateColumns: `repeat(${this._maxRowCount}, 1fr)`, gridAutoRows: `${100 / this.numRows}%`, height: `${this.cardSpacing}%`, }}> @@ -717,6 +665,14 @@ export class CollectionCardView extends CollectionSubView() { {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)} </div> </div> + + <div + className="collectionCardView-cardSizeDragger" + onPointerDown={this.cardSizerDown} + ref={this._draggerRef} + style={{ display: this._props.isContentActive() ? undefined : 'none', cursor: this._cursor, color: SettingsManager.userColor, left: `${this.cardWidth + this.xMargin}px` }}> + <FontAwesomeIcon icon="arrows-alt-h" /> + </div> </div> ); } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 0c059f729..5e99bec39 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,7 +1,7 @@ import { action, computed, makeObservable, observable } from 'mobx'; import * as React from 'react'; import * as rp from 'request-promise'; -import { ClientUtils, returnFalse } from '../../../ClientUtils'; +import { ClientUtils, DashColor, returnFalse } from '../../../ClientUtils'; import CursorField from '../../../fields/CursorField'; import { Doc, DocListCast, GetDocFromUrl, GetHrefFromHTML, Opt, RTFIsFragment, StrListCast } from '../../../fields/Doc'; import { AclPrivate, DocData } from '../../../fields/DocSymbols'; @@ -9,7 +9,7 @@ import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types'; +import { BoolCast, Cast, DateCast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; @@ -29,6 +29,14 @@ import { DocumentView, DocumentViewProps } from '../nodes/DocumentView'; import { FlashcardPracticeUI } from './FlashcardPracticeUI'; import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere'; +export enum docSortings { + Time = 'time', + Type = 'type', + Color = 'color', + Chat = 'chat', + Tag = 'tag', + None = '', +} export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> { isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc) isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently) @@ -150,6 +158,8 @@ export function CollectionSubView<X>() { unrecursiveDocFilters = () => [...(this._props.childFilters?.().filter(f => !ClientUtils.IsRecursiveFilter(f)) || [])]; childDocRangeFilters = () => [...(this._props.childFiltersByRanges?.() || []), ...this.collectionRangeDocFilters()]; searchFilterDocs = () => this._props.searchFilterDocs?.() ?? DocListCast(this.Document._searchFilterDocs); + + @observable docDraggedIndex = -1; @computed.struct get childDocs() { TraceMobx(); let rawdocs: (Doc | Promise<Doc>)[] = []; @@ -166,8 +176,10 @@ export function CollectionSubView<X>() { const templateRoot = this._props.TemplateDataDocument; rawdocs = templateRoot && !this._props.isAnnotationOverlay ? [Doc.GetProto(templateRoot)] : []; } - - const childDocs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this._props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc); + const childDocs = this.childSortedDocs( + rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this._props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc), + this.docDraggedIndex + ); const childDocFilters = this.childDocFilters(); const childFiltersByRanges = this.childDocRangeFilters(); @@ -214,6 +226,35 @@ export function CollectionSubView<X>() { return docsforFilter; } + childSortedDocs = (docsIn: Doc[], dragIndex: number) => { + const sortType = StrCast(this.Document[this._props.fieldKey + '_sort']) as docSortings; + const isDesc = BoolCast(this.Document[this._props.fieldKey + '_sort_desc']); + const docs = docsIn.slice(); + if (sortType) { + docs.sort((docA, docB) => { + const [typeA, typeB] = (() => { + switch (sortType) { + default: + case docSortings.Type: return [StrCast(docA.type), StrCast(docB.type)]; + case docSortings.Chat: return [NumCast(docA.chatIndex, 9999), NumCast(docB.chatIndex,9999)]; + case docSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(), DateCast(docB.author_date)?.date ?? Date.now()]; + case docSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()]; + case docSortings.Tag: return [StrListCast(docA.tags).join(""), StrListCast(docB.tags).join("")]; + } + })(); //prettier-ignore + return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? 1 : -1); + }); + } + if (dragIndex !== -1) { + const draggedDoc = DragManager.docsBeingDragged[0]; + const originalIndex = docs.findIndex(doc => doc === draggedDoc); + + originalIndex !== -1 && docs.splice(originalIndex, 1); + draggedDoc && docs.splice(dragIndex, 0, draggedDoc); + } + return docs; + }; + @action protected async setCursorPosition(position: [number, number]) { let ind; diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx index 77f1db9ad..c071c5fb8 100644 --- a/src/client/views/collections/FlashcardPracticeUI.tsx +++ b/src/client/views/collections/FlashcardPracticeUI.tsx @@ -58,7 +58,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp get practiceField() { return this._props.fieldKey + "_practice"; } // prettier-ignore @computed get filterDoc() { return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter'); } // prettier-ignore - @computed get practiceMode() { return this._props.allChildDocs().some(doc => doc._flashcardType) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore + @computed get practiceMode() { return this._props.allChildDocs().some(doc => doc._layout_flashcardType) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore btnHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this._props.ScreenToLocalBoxXf().Scale); btnWidth = () => (!this.filterDoc ? 1 : (this.btnHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height)); @@ -179,7 +179,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp </div> ); } - tryFilterOut = (doc: Doc) => (this.practiceMode && BoolCast(doc?._flashcardType) && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct + tryFilterOut = (doc: Doc) => (this.practiceMode && doc?._layout_flashcardType && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct render() { return ( <div className="FlashcardPracticeUI"> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index 272c13546..bebdbd731 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -44,6 +44,7 @@ export interface PoolData { transition?: string; highlight?: boolean; pointerEvents?: string; + showTags?: boolean; } export interface ViewDefResult { @@ -425,6 +426,7 @@ function normalizeResults( opacity: newPosRaw.opacity, color: newPosRaw.color, pair: ele[1].pair, + showTags: newPosRaw.showTags, }; if (newPosRaw.transition) newPos.transition = newPosRaw.transition; poolData.set(newPos.pair.layout[Id] + (newPos.replica || ''), { transition: 'all 1s', ...newPos }); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 796949378..3c31b584e 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -232,7 +232,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } public static gotoKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], duration: number) { - return DocumentView.SetViewTransition(docs, 'all', duration, timer, undefined, true); + return DocumentView.SetViewTransition(docs, 'all', duration, timer, true); } changeKeyFrame = (back = false) => { const currentFrame = Cast(this.Document._currentFrame, 'number', null); @@ -1658,6 +1658,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection width: _width, height: _height, transition: StrCast(childDocLayout.dataTransition), + showTags: BoolCast(childDocLayout.showTags) || BoolCast(this.Document.showChildTags) || BoolCast(this.Document._layout_showTags), pointerEvents: Cast(childDoc.pointerEvents, 'string', null), pair, replica: '', diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx index 16f8b86f3..da203abfa 100644 --- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx +++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx @@ -100,9 +100,7 @@ export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() { return infos; } - isolatedSelection = (doc: Doc) => { - return this.schemaView?.selectionOverlap(doc); - }; + isolatedSelection = (doc: Doc) => this.schemaView?.selectionOverlap(doc); setCursorIndex = (mouseY: number) => this.schemaView?.setRelCursorIndex(mouseY); selectedCol = () => this.schemaView._selectedCol; getFinfo = computedFn((fieldKey: string) => this.schemaView?.fieldInfos.get(fieldKey)); @@ -113,9 +111,7 @@ export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() { columnWidth = computedFn((index: number) => () => this.schemaView?.displayColumnWidths[index] ?? CollectionSchemaView._minColWidth); computeRowIndex = () => this.schemaView?.rowIndex(this.Document); highlightCells = (text: string) => this.schemaView?.highlightCells(text); - selectReference = (doc: Doc, col: number) => { - this.schemaView.selectReference(doc, col); - }; + selectReference = (doc: Doc, col: number) => this.schemaView.selectReference(doc, col); eqHighlightFunc = (text: string) => { const info = this.schemaView.findCellRefs(text); const cells: HTMLDivElement[] = []; diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 1738802b7..b44292164 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -40,6 +40,7 @@ import { WebBox } from '../nodes/WebBox'; import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup'; import { OpenWhere } from '../nodes/OpenWhere'; +import { docSortings } from '../collections/CollectionSubView'; // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function IsNoneSelected() { @@ -48,10 +49,20 @@ ScriptingGlobals.add(function IsNoneSelected() { // toggle: Set overlay status of selected document // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function setView(view: string, getSelected: boolean) { - if (getSelected) return DocumentView.SelectedDocs(); - const selected = DocumentView.SelectedDocs().lastElement(); - selected ? (selected._type_collection = view) : console.log('[FontIconBox.tsx] changeView failed'); +ScriptingGlobals.add(function setView(view: string, shiftKey: boolean, checkResult?: boolean) { + if (checkResult) return DocumentView.SelectedDocs(); + const selected = DocumentView.Selected().lastElement(); + if (selected) { + if (shiftKey) { + const newCol = Doc.MakeEmbedding(selected.Document); + newCol._type_collection = view; + selected._props.addDocTab?.(newCol, OpenWhere.addRight); + } else { + selected.Document._type_collection = view; + } + } else { + console.log('[FontIconBox.tsx] changeView failed'); + } return undefined; }); @@ -141,7 +152,7 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) { // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function showFreeform( - attr: 'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'up' | 'down' | 'pile' | 'toggle-chat' | 'toggle-tags' | 'tag', + attr: 'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'up' | 'down' | 'toggle-chat' | 'toggle-tags' | 'tag', checkResult?: boolean, persist?: boolean ) { @@ -152,7 +163,7 @@ ScriptingGlobals.add(function showFreeform( } // prettier-ignore - const map: Map<'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'up' | 'down' | 'pile' | 'toggle-chat' | 'toggle-tags' | 'tag', + const map: Map<'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'up' | 'down'| 'toggle-chat' | 'toggle-tags' | 'tag', { waitForRender?: boolean; checkResult: (doc: Doc) => boolean; @@ -187,43 +198,35 @@ ScriptingGlobals.add(function showFreeform( checkResult: (doc: Doc) => BoolCast(doc?._freeform_useClusters, false), setDoc: (doc: Doc) => { doc._freeform_useClusters = !doc._freeform_useClusters; }, }], - ['flashcards', { - checkResult: (doc: Doc) => BoolCast(Doc.UserDoc().defaultToFlashcards, false), - setDoc: (doc: Doc, dv: DocumentView) => { Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards}, // prettier-ignore - }], ['time', { - checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "time", - setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "time" ? doc.card_sort = '' : doc.card_sort = 'time'}, // prettier-ignore + checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "time", + setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "time" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Time}, // prettier-ignore }], ['docType', { - checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "type", - setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "type" ? doc.card_sort = '' : doc.card_sort = 'type'}, // prettier-ignore + checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "type", + setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "type" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Type}, // prettier-ignore }], ['color', { - checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "color", - setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "color" ? doc.card_sort = '' : doc.card_sort = 'color'}, // prettier-ignore + checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "color", + setDoc: (doc: Doc, dv: DocumentView) => { doc?.[Doc.LayoutFieldKey(doc)+"_sort"] === "color" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Color}, // prettier-ignore }], ['tag', { - checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "tag", - setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "tag" ? doc.card_sort = '' : doc.card_sort = 'tag'}, // prettier-ignore + checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "tag", + setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "tag" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Tag}, // prettier-ignore }], ['up', { - checkResult: (doc: Doc) => BoolCast(!doc?.card_sort_isDesc), - setDoc: (doc: Doc, dv: DocumentView) => { - doc.card_sort_isDesc = false; - }, + checkResult: (doc: Doc) => BoolCast(!doc?.[Doc.LayoutFieldKey(doc)+"_sort_desc"]), + setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_desc"] = undefined; }, }], ['down', { - checkResult: (doc: Doc) => BoolCast(doc?.card_sort_isDesc), - setDoc: (doc: Doc, dv: DocumentView) => { - doc.card_sort_isDesc = true; - }, + checkResult: (doc: Doc) => BoolCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort_desc"]), + setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_desc"] = true; }, }], ['toggle-chat', { - checkResult: (doc: Doc) => GPTPopup.Instance.visible, + checkResult: (doc: Doc) => GPTPopup.Instance.Visible, setDoc: (doc: Doc, dv: DocumentView) => { - if (GPTPopup.Instance.visible){ - doc.card_sort = '' + if (GPTPopup.Instance.Visible){ + doc[Doc.LayoutFieldKey(doc)+"_sort"] = ''; GPTPopup.Instance.setVisible(false); } else { @@ -242,20 +245,6 @@ ScriptingGlobals.add(function showFreeform( doc.showChildTags = !doc.showChildTags; }, }], - ['pile', { - checkResult: (doc: Doc) => doc._type_collection == CollectionViewType.Freeform, - setDoc: (doc: Doc, dv: DocumentView) => { - const newCol = Docs.Create.CarouselDocument(DocListCast(doc[Doc.LayoutFieldKey(doc)]), { - title: doc.title + "_carousel", - _width: 250, - _height: 200, - _layout_fitWidth: false, - _layout_autoHeight: true, - childFilters: new List<string>(StrListCast(doc.childFilters)) - }); - dv._props.addDocTab?.(newCol, OpenWhere.addRight); - }, - }], ]); if (checkResult) { diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 3cbfb1796..beea6ab3c 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -28,7 +28,7 @@ export enum GroupActive { // flags for whether a view is activate because of its } /// Ugh, typescript has no run-time way of iterating through the keys of an interface. so we need /// manaully keep this list of keys in synch wih the fields of the freeFormProps interface -const freeFormPropsKeys = ['x', 'y', 'z', 'zIndex', 'rotation', 'opacity', 'backgroundColor', 'color', 'highlight', 'width', 'height', 'autoDim', 'transition']; +const freeFormPropsKeys = ['x', 'y', 'z', 'width', 'height', 'zIndex', 'autoDim', 'rotation', 'color', 'backgroundColor', 'opacity', 'highlight', 'transition']; interface freeFormProps { x: number; y: number; @@ -198,7 +198,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF } public static updateKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], time: number) { - const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, timer, undefined, true); + const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, timer, true); const timecode = Math.round(time); docs.forEach(doc => { this.animFields.forEach(val => { diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index e0c360132..53fbd11c5 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -1,7 +1,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import axios from 'axios'; -import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction, trace } from 'mobx'; +import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import ReactLoading from 'react-loading'; @@ -510,7 +510,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() * Calls GPT for each flashcard type. */ askGPT = async (callType: GPTCallType) => { - const questionText = 'Question: ' + this.frontText; + const questionText = this.frontText; const queryText = questionText + (callType == GPTCallType.QUIZ ? ' UserAnswer: ' + this._inputValue + '. ' + ' Rubric: ' + this.backText : ''); this.loading = true; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index f1b48be4a..0193fd328 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -52,6 +52,7 @@ import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { PresEffect, PresEffectDirection } from './trails/PresEnums'; import SpringAnimation from './trails/SlideEffect'; import { SpringType, springMappings } from './trails/SpringUtils'; +import { TagsView } from '../TagsView'; export interface DocumentViewProps extends FieldViewSharedProps { hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected @@ -778,7 +779,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document transform: `scale(${this.uiBtnScaling})`, bottom: this.maxWidgetSize, }}> - {this.widgetDecorations ?? null} + {this._props.DocumentView?.() ? <TagsView Views={[this._props.DocumentView?.()]} /> : null} </div> ) : ( <> @@ -801,10 +802,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document ref={r => this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r))}> <div className="documentView-editorView-resizer" /> {this._componentView?.componentAIView?.() ?? null} - {this.widgetDecorations ?? null} + {this._props.DocumentView?.() ? <TagsView Views={[this._props.DocumentView?.()]} /> : null} </div> </> )} + {this.widgetDecorations ?? null} </> ); } @@ -1041,13 +1043,13 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document >, root: Doc ) { - const dir = ((presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection) || PresEffectDirection.Center) as PresEffectDirection; + const effectDirection = (presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection) as PresEffectDirection; const duration = Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null)); const effectProps = { - left: dir === PresEffectDirection.Left, - right: dir === PresEffectDirection.Right, - top: dir === PresEffectDirection.Top, - bottom: dir === PresEffectDirection.Bottom, + left: effectDirection === PresEffectDirection.Left, + right: effectDirection === PresEffectDirection.Right, + top: effectDirection === PresEffectDirection.Top, + bottom: effectDirection === PresEffectDirection.Bottom, opposite: true, delay: 0, duration, @@ -1061,7 +1063,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document const presEffect = StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect)); switch (presEffect) { case PresEffect.Expand: case PresEffect.Flip: case PresEffect.Rotate: case PresEffect.Bounce: - case PresEffect.Roll: return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={presEffect} springSettings={timingConfig}>{renderDoc}</SpringAnimation> + case PresEffect.Roll: return <SpringAnimation doc={root} startOpacity={0} dir={effectDirection || PresEffectDirection.Left} presEffect={presEffect} springSettings={timingConfig}>{renderDoc}</SpringAnimation> // case PresEffect.Fade: return <SlideEffect doc={root} dir={dir} presEffect={PresEffect.Fade} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect> case PresEffect.Fade: return <Fade {...effectProps}>{renderDoc}</Fade> // keep as preset, doesn't really make sense with spring config @@ -1388,8 +1390,8 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { this.Document[Animation] = presEffect; this._animEffectTimer = setTimeout(() => { this.Document[Animation] = undefined; }, timeInMs); // prettier-ignore }; - public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => { - this._viewTimer = DocumentView.SetViewTransition([this.layoutDoc], transProp, timeInMs, this._viewTimer, afterTrans, dataTrans); + public setViewTransition = (transProp: string, timeInMs: number, dataTrans = false) => { + this._viewTimer = DocumentView.SetViewTransition([this.layoutDoc], transProp, timeInMs, this._viewTimer, dataTrans); }; public setCustomView = undoable((custom: boolean, layout: string): void => { @@ -1588,21 +1590,15 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { ); } - public static SetViewTransition(docs: Doc[], transProp: string, timeInMs: number, timer?: NodeJS.Timeout | undefined, afterTrans?: () => void, dataTrans = false) { - docs.forEach(doc => { - doc._viewTransition = `${transProp} ${timeInMs}ms`; - dataTrans && (doc.dataTransition = `${transProp} ${timeInMs}ms`); - }); + public static SetViewTransition(docs: Doc[], transProp: string, timeInMs: number, timer?: NodeJS.Timeout | undefined, dataTrans = false) { + const setTrans = (transition?: string) => + docs.forEach(doc => { + doc._viewTransition = transition; + dataTrans && (doc.dataTransition = transition); + }); + setTrans(`${transProp} ${timeInMs}ms`); timer && clearTimeout(timer); - return setTimeout( - () => - docs.forEach(doc => { - doc._viewTransition = undefined; - dataTrans && (doc.dataTransition = 'inherit'); - afterTrans?.(); - }), - timeInMs + 10 - ); + return setTimeout(setTrans, timeInMs + 10); } // shows a stacking view collection (by default, but the user can change) of all documents linked to the source diff --git a/src/client/views/nodes/FocusViewOptions.ts b/src/client/views/nodes/FocusViewOptions.ts index bb0d2b03c..1c462e98f 100644 --- a/src/client/views/nodes/FocusViewOptions.ts +++ b/src/client/views/nodes/FocusViewOptions.ts @@ -22,3 +22,14 @@ export interface FocusViewOptions { pointFocus?: { X: number; Y: number }; // clientX and clientY coordinates to focus on instead of a document target (used by explore mode) contextPath?: Doc[]; // path of inner documents that will also be focused } + +/** + * if there's an options.effect, it will be handled from linkFollowHighlight. We delay the start of + * the highlight so that the target document can be somewhat centered so that the effect/highlight will be seen + * bcz: should this delay be an options parameter? + * @param options + * @returns + */ +export function FocusEffectDelay(options: FocusViewOptions) { + return (options.zoomTime ?? 0) * 0.5; +} diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index 60b2a7519..f58862028 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -160,6 +160,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { buttonList, jsx: undefined, selectedVal: script(), + toolTip: 'Set text font', getStyle: (val: string) => ({ fontFamily: val }), }; }; @@ -174,6 +175,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { buttonList: buttonList.filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value as CollectionViewType)), getStyle: undefined, selectedVal: StrCast(selected[0]._type_collection), + toolTip: 'change view type (press Shift to add as a new view)', } : { jsx: selected.length ? ( @@ -205,11 +207,11 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { @computed get dropdownListButton() { const script = ScriptCast(this.Document.script); const selectedFunc = () => script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result as string; - const { buttonList, selectedVal, getStyle, jsx } = (() => { + const { buttonList, selectedVal, getStyle, jsx, toolTip } = (() => { switch (this.Document.title) { case 'Font': return this.handleFontDropdown(selectedFunc, this.buttonList); case 'Perspective': return this.handleViewDropdown(script, this.buttonList); - default: return { buttonList: this.buttonList, selectedVal: selectedFunc(), jsx: undefined, getStyle: undefined }; + default: return { buttonList: this.buttonList, selectedVal: selectedFunc(), toolTip: undefined, jsx: undefined, getStyle: undefined }; } // prettier-ignore })(); if (jsx) return jsx; @@ -225,9 +227,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { return ( <Dropdown selectedVal={selectedVal} - setSelectedVal={undoable(value => script.script.run({ this: this.Document, value }), `dropdown select ${this.label}`)} + setSelectedVal={undoable((value, e) => script.script.run({ this: this.Document, value, shiftKey: e.shiftKey }), `dropdown select ${this.label}`)} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} + toolTip={toolTip} type={Type.TERT} closeOnSelect={false} dropdownType={DropdownType.SELECT} diff --git a/src/client/views/nodes/IconTagBox.scss b/src/client/views/nodes/IconTagBox.scss index 90cc06092..c79d662f4 100644 --- a/src/client/views/nodes/IconTagBox.scss +++ b/src/client/views/nodes/IconTagBox.scss @@ -10,8 +10,6 @@ gap: 5px; padding-left: 5px; padding-right: 5px; - padding-top: 2px; - padding-bottom: 2px; button { pointer-events: auto; diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx index 87469b50e..cef202256 100644 --- a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx @@ -109,7 +109,7 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { return this._left > 0; } - constructor(props: any) { + constructor(props: AntimodeMenuProps) { super(props); makeObservable(this); MapAnchorMenu.Instance = this; @@ -117,10 +117,12 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { } componentWillUnmount() { - this.destinationFeatures = []; - this.destinationSelected = false; - this.selectedDestinationFeature = undefined; - this.currentRouteInfoMap = undefined; + runInAction(() => { + this.destinationFeatures = []; + this.destinationSelected = false; + this.selectedDestinationFeature = undefined; + this.currentRouteInfoMap = undefined; + }); this._disposer?.(); } diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index f0313fba4..0684daeb6 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -5,7 +5,7 @@ import { observer } from 'mobx-react'; import { NodeSelection } from 'prosemirror-state'; import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; -import { returnFalse, returnZero, setupMoveUpEvents } from '../../../../ClientUtils'; +import { returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../../../ClientUtils'; import { Doc, DocListCast, Field } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; @@ -169,12 +169,17 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi selectedCells={this.selectedCells} selectedCol={returnZero} fieldKey={this._fieldKey} + highlightCells={emptyFunction} // fix + refSelectModeInfo={{ enabled: false, currEditing: undefined }} // fix + selectReference={emptyFunction} // + eqHighlightFunc={() => []} // fix + isolatedSelection={() => [true, true]} // fix + rowSelected={returnTrue} //fix rowHeight={returnZero} isRowActive={this.isRowActive} padding={0} getFinfo={emptyFunction} setColumnValues={returnFalse} - setSelectedColumnValues={returnFalse} allowCRs oneLine={!this._expanded && !this._props.nodeSelected()} finishEdit={this.finishEdit} diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx index 2ae6ee1dd..6b1d05031 100644 --- a/src/client/views/nodes/imageEditor/ImageEditor.tsx +++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx @@ -551,8 +551,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc img.src = src; if (!currImg.current || !originalImg.current || !imageRootDoc) return undefined; try { - const res = await createNewImgDoc(img, false); - return res; + return await createNewImgDoc(img, false); } catch (err) { console.log(err); } @@ -589,9 +588,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc // disable once edited has been clicked (doesn't make sense to change after first edit) disabled={edited} checked={isNewCollection} - onChange={() => { - setIsNewCollection(prev => !prev); - }} + onChange={() => setIsNewCollection(prev => !prev)} /> } label="Create New Collection" @@ -610,49 +607,13 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc return ( <div className="sideControlsContainer" style={{ backgroundColor: bgColor }}> <div className="sideControls"> - <div className="imageToolsContainer"> - {imageEditTools.map(tool => { - return ImageToolButton(tool, tool.type === currTool.type, changeTool); - })} - </div> + <div className="imageToolsContainer">{imageEditTools.map(tool => ImageToolButton(tool, tool.type === currTool.type, changeTool))}</div> {currTool.type == ImageToolType.Cut && ( <div className="cutToolsContainer"> - <Button - style={{ width: '100%' }} - text="Keep in" - type={Type.TERT} - color={cutType == CutMode.IN ? SettingsManager.userColor : bgColor} - onClick={() => { - setCutType(CutMode.IN); - }} - /> - <Button - style={{ width: '100%' }} - text="Keep out" - type={Type.TERT} - color={cutType == CutMode.OUT ? SettingsManager.userColor : bgColor} - onClick={() => { - setCutType(CutMode.OUT); - }} - /> - <Button - style={{ width: '100%' }} - text="Draw in" - type={Type.TERT} - color={cutType == CutMode.DRAW_IN ? SettingsManager.userColor : bgColor} - onClick={() => { - setCutType(CutMode.DRAW_IN); - }} - /> - <Button - style={{ width: '100%' }} - text="Erase" - type={Type.TERT} - color={cutType == CutMode.ERASE ? SettingsManager.userColor : bgColor} - onClick={() => { - setCutType(CutMode.ERASE); - }} - /> + <Button style={{ width: '100%' }} text="Keep in" type={Type.TERT} color={cutType == CutMode.IN ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.IN)} /> + <Button style={{ width: '100%' }} text="Keep out" type={Type.TERT} color={cutType == CutMode.OUT ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.OUT)} /> + <Button style={{ width: '100%' }} text="Draw in" type={Type.TERT} color={cutType == CutMode.DRAW_IN ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.DRAW_IN)} /> + <Button style={{ width: '100%' }} text="Erase" type={Type.TERT} color={cutType == CutMode.ERASE ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.ERASE)} /> </div> )} <div className="sliderContainer" onPointerDown={e => e.stopPropagation()}> @@ -669,9 +630,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc defaultValue={genFillTool.sliderDefault} size="small" valueLabelDisplay="auto" - onChange={(e, val) => { - setCursorData(prev => ({ ...prev, width: val as number })); - }} + onChange={(e, val) => setCursorData(prev => ({ ...prev, width: val as number }))} /> )} {currTool.type === ImageToolType.Cut && ( @@ -687,9 +646,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc defaultValue={cutTool.sliderDefault} size="small" valueLabelDisplay="auto" - onChange={(e, val) => { - setCursorData(prev => ({ ...prev, width: val as number })); - }} + onChange={(e, val) => setCursorData(prev => ({ ...prev, width: val as number }))} /> )} </div> @@ -701,9 +658,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc e.stopPropagation(); handleUndo(); }} - onPointerUp={e => { - e.stopPropagation(); - }} + onPointerUp={e => e.stopPropagation()} color={activeColor} tooltip="Undo" icon={<IoMdUndo />} @@ -714,9 +669,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc e.stopPropagation(); handleRedo(); }} - onPointerUp={e => { - e.stopPropagation(); - }} + onPointerUp={e => e.stopPropagation()} color={activeColor} tooltip="Redo" icon={<IoMdRedo />} diff --git a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx index de2116253..985dc914f 100644 --- a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx +++ b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx @@ -53,7 +53,7 @@ export function ApplyFuncButtons({ loading, onClick: getEdit, onReset, btnText } export function ImageToolButton(tool: ImageEditTool, isActive: boolean, selectTool: (type: ImageToolType) => void) { return ( - <div className="imageEditorButtonContainer"> + <div key={tool.name} className="imageEditorButtonContainer"> <Button style={{ width: '100%' }} text={tool.name} diff --git a/src/client/views/nodes/trails/CubicBezierEditor.tsx b/src/client/views/nodes/trails/CubicBezierEditor.tsx index e1ad1e6e5..627b77184 100644 --- a/src/client/views/nodes/trails/CubicBezierEditor.tsx +++ b/src/client/views/nodes/trails/CubicBezierEditor.tsx @@ -118,84 +118,82 @@ function CubicBezierEditor({ setFunc, currPoints }: Props) { }, [c2Down, currPoints]); return ( - <div - onPointerMove={e => { - e.stopPropagation; - }}> - <svg className="presBox-bezier-editor" width={`${CONTAINER_WIDTH}`} height={`${CONTAINER_WIDTH}`} xmlns="http://www.w3.org/2000/svg"> - {/* Outlines */} - <line x1={`${0 + OFFSET}`} y1={`${EDITOR_WIDTH + OFFSET}`} x2={`${EDITOR_WIDTH + OFFSET}`} y2={`${0 + OFFSET}`} stroke="#c1c1c1" strokeWidth="1" /> - {/* Box Outline */} - <rect x={`${0 + OFFSET}`} y={`${0 + OFFSET}`} width={EDITOR_WIDTH} height={EDITOR_WIDTH} stroke="#c5c5c5" fill="transparent" strokeWidth="1" /> - {/* Editor */} - <path - d={`M ${0 + OFFSET} ${EDITOR_WIDTH + OFFSET} C ${currPoints.p1[0] * EDITOR_WIDTH + OFFSET} ${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}, ${ - currPoints.p2[0] * EDITOR_WIDTH + OFFSET - } ${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}, ${EDITOR_WIDTH + OFFSET} ${0 + OFFSET}`} - stroke="#ffffff" - fill="transparent" - /> - {/* Bottom left */} - <line - onPointerDown={() => { - setC1Down(true); - }} - onPointerUp={() => { - setC1Down(false); - }} - x1={`${0 + OFFSET}`} - y1={`${EDITOR_WIDTH + OFFSET}`} - x2={`${currPoints.p1[0] * EDITOR_WIDTH + OFFSET}`} - y2={`${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}`} - stroke="#00000000" - strokeWidth="5" - /> - <line x1={`${0 + OFFSET}`} y1={`${EDITOR_WIDTH + OFFSET}`} x2={`${currPoints.p1[0] * EDITOR_WIDTH + OFFSET}`} y2={`${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}`} stroke="#ffffff" strokeWidth="1" /> - <circle - cx={`${currPoints.p1[0] * EDITOR_WIDTH + OFFSET}`} - cy={`${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}`} - r="5" - fill={`${c1Down ? '#3fa9ff' : '#ffffff'}`} - onPointerDown={e => { - e.stopPropagation(); - setC1Down(true); - }} - onPointerUp={() => { - setC1Down(false); - }} - /> - {/* Top right */} - <line - onPointerDown={e => { - e.stopPropagation(); - setC2Down(true); - }} - onPointerUp={() => { - setC2Down(false); - }} - x1={`${EDITOR_WIDTH + OFFSET}`} - y1={`${0 + OFFSET}`} - x2={`${currPoints.p2[0] * EDITOR_WIDTH + OFFSET}`} - y2={`${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}`} - stroke="#00000000" - strokeWidth="5" - /> - <line x1={`${EDITOR_WIDTH + OFFSET}`} y1={`${0 + OFFSET}`} x2={`${currPoints.p2[0] * EDITOR_WIDTH + OFFSET}`} y2={`${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}`} stroke="#ffffff" strokeWidth="1" /> - <circle - cx={`${currPoints.p2[0] * EDITOR_WIDTH + OFFSET}`} - cy={`${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}`} - r="5" - fill={`${c2Down ? '#3fa9ff' : '#ffffff'}`} - onPointerDown={e => { - e.stopPropagation(); - setC2Down(true); - }} - onPointerUp={() => { - setC2Down(false); - }} - /> - </svg> - </div> + <svg className="presBox-bezier-editor" width={`${CONTAINER_WIDTH}`} height={`${CONTAINER_WIDTH}`} xmlns="http://www.w3.org/2000/svg"> + {/* Outlines */} + <line x1={`${0 + OFFSET}`} y1={`${EDITOR_WIDTH + OFFSET}`} x2={`${EDITOR_WIDTH + OFFSET}`} y2={`${0 + OFFSET}`} stroke="#c1c1c1" strokeWidth="1" /> + {/* Box Outline */} + <rect x={`${0 + OFFSET}`} y={`${0 + OFFSET}`} width={EDITOR_WIDTH} height={EDITOR_WIDTH} stroke="#c5c5c5" fill="transparent" strokeWidth="1" /> + {/* Editor */} + <path + d={`M ${0 + OFFSET} ${EDITOR_WIDTH + OFFSET} C ${currPoints.p1[0] * EDITOR_WIDTH + OFFSET} ${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}, ${ + currPoints.p2[0] * EDITOR_WIDTH + OFFSET + } ${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}, ${EDITOR_WIDTH + OFFSET} ${0 + OFFSET}`} + stroke="#ffffff" + fill="transparent" + /> + {/* Bottom left */} + <line + onPointerDown={() => { + setC1Down(true); + }} + onPointerMove={e => { + e.stopPropagation; + }} + onPointerUp={() => { + setC1Down(false); + }} + x1={`${0 + OFFSET}`} + y1={`${EDITOR_WIDTH + OFFSET}`} + x2={`${currPoints.p1[0] * EDITOR_WIDTH + OFFSET}`} + y2={`${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}`} + stroke="#00000000" + strokeWidth="5" + /> + <line x1={`${0 + OFFSET}`} y1={`${EDITOR_WIDTH + OFFSET}`} x2={`${currPoints.p1[0] * EDITOR_WIDTH + OFFSET}`} y2={`${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}`} stroke="#ffffff" strokeWidth="1" /> + <circle + cx={`${currPoints.p1[0] * EDITOR_WIDTH + OFFSET}`} + cy={`${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}`} + r="5" + fill={`${c1Down ? '#3fa9ff' : '#ffffff'}`} + onPointerDown={e => { + e.stopPropagation(); + setC1Down(true); + }} + onPointerUp={() => { + setC1Down(false); + }} + /> + {/* Top right */} + <line + onPointerDown={e => { + e.stopPropagation(); + setC2Down(true); + }} + onPointerUp={() => { + setC2Down(false); + }} + x1={`${EDITOR_WIDTH + OFFSET}`} + y1={`${0 + OFFSET}`} + x2={`${currPoints.p2[0] * EDITOR_WIDTH + OFFSET}`} + y2={`${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}`} + stroke="#00000000" + strokeWidth="5" + /> + <line x1={`${EDITOR_WIDTH + OFFSET}`} y1={`${0 + OFFSET}`} x2={`${currPoints.p2[0] * EDITOR_WIDTH + OFFSET}`} y2={`${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}`} stroke="#ffffff" strokeWidth="1" /> + <circle + cx={`${currPoints.p2[0] * EDITOR_WIDTH + OFFSET}`} + cy={`${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}`} + r="5" + fill={`${c2Down ? '#3fa9ff' : '#ffffff'}`} + onPointerDown={e => { + e.stopPropagation(); + setC2Down(true); + }} + onPointerUp={() => { + setC2Down(false); + }} + /> + </svg> ); } diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss index 60d4e580d..e34e1b380 100644 --- a/src/client/views/nodes/trails/PresBox.scss +++ b/src/client/views/nodes/trails/PresBox.scss @@ -5,11 +5,25 @@ display: flex; flex-direction: column; gap: 1rem; + .presBox-gpt-chat-span { + display: flex; + align-items: center; + gap: 8px; + } + textarea { + width: 100%; + } +} +.presBox-subheading-slider { + max-width: calc(100% - 25px); + width: 100%; + padding: 15px; + padding-left: 0px; } .pres-chat { display: flex; - flex-direction: column; + flex-direction: row; gap: 8px; } @@ -18,30 +32,38 @@ gap: 8px; } -.pres-chatbox-container { - padding: 16px; +.pres-chatbox-container, +.pres-chatbox-container-ai { + width: 100%; + padding-left: 16px; + padding-right: 16px; outline: 1px solid #999999; - border-radius: 16px; + border-radius: 5px; display: flex; align-items: center; justify-content: space-between; + overflow: auto; + max-height: 200px; + .pres-chatbox { + outline: none; + border: none; + resize: none; + font-family: Verdana, Geneva, sans-serif; + background-color: transparent; + overflow-y: hidden; + } } -.pres-chatbox { - outline: none; - border: none; - resize: none; - font-family: Verdana, Geneva, sans-serif; - background-color: transparent; - overflow-y: hidden; +.pres-chatbox-container-ai { + padding-left: 8px; + padding-right: 8px; + margin-left: 8px; } - // Effect Animations .presBox-effects { - display: grid; - grid-template-columns: auto auto; - gap: 8px; + display: flow; + margin: auto; } .presBox-effect-row { @@ -55,7 +77,7 @@ overflow: hidden; width: 80px; height: 80px; - display: flex; + display: inline-flex; justify-content: center; align-items: center; border: 1px solid rgb(118, 118, 118); @@ -74,12 +96,19 @@ .presBox-show-hide-dropdown { cursor: pointer; - padding: 8px 0; display: flex; align-items: center; gap: 4px; } +.presBox-switches { + display: flex; + width: 100%; + > div { + width: 100%; + } +} + .presBox-bezier-editor { border: 1px solid rgb(221, 221, 221); border-radius: 4px; @@ -96,6 +125,18 @@ align-items: center; } +.presBox-previewContainer { + display: flex; + align-items: center; + width: fit-content; + margin: auto; + grid-template-columns: auto auto; + grid-gap: 10px; + .presBox-option-block { + padding: 0px; + } +} + .presBox-cont { cursor: auto; position: absolute; @@ -270,7 +311,7 @@ } .presBox-toggles { - display: flex; + display: flow; overflow-x: auto; } @@ -280,6 +321,9 @@ font-family: Roboto; z-index: 100; transition: 0.7s; + .form-wrapper.left .formLabel { + width: 100px; + } .ribbon-doubleButton { display: flex; @@ -352,6 +396,18 @@ font-size: 11; font-weight: 400; margin-top: 10px; + max-width: min(85px, 25%); + width: 100%; + } + .presBox-springSlider { + display: grid; + column-count: 2; + grid-template-columns: min(60px, 25%) calc(100% - min(60px, 25%) - min(5px, 10%)); + grid-gap: min(5px, 10%); + > span { + overflow: hidden; + text-overflow: ellipsis; + } } @media screen and (-webkit-min-device-pixel-ratio: 0) { @@ -459,7 +515,7 @@ justify-content: space-between; width: 100%; height: max-content; - grid-template-columns: auto auto auto; + grid-template-columns: auto auto; grid-template-rows: max-content; font-weight: 100; margin-top: 3px; @@ -520,20 +576,17 @@ } .presBox-input { - border: none; background-color: transparent; - width: 40; - // padding: 8px; - // border-radius: 4px; - // width: 30; - // height: 100%; - // background: none; - // border: none; - // text-align: right; + text-align: center; + width: 100%; + height: 15px; + font-size: 10; } - - .presBox-input:focus { - outline: none; + .presBox-inputNumber { + border: none; + background-color: transparent; + width: 100%; + max-width: 25px; } .ribbon-frameSelector { @@ -737,7 +790,7 @@ font-weight: 200; height: 20; background-color: $white; - display: flex; + display: inline-flex; color: $black; border-radius: 5px; width: max-content; diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index f23b32a48..7c23ca8e1 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -1,7 +1,8 @@ +import { Button, Dropdown, DropdownType, IconButton, Size, Type } from '@dash/components'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import Slider from '@mui/material/Slider'; -import { Button, Dropdown, DropdownType, IconButton, Toggle, ToggleType, Type } from '@dash/components'; +import _ from 'lodash'; import { IReactionDisposer, ObservableSet, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -40,7 +41,7 @@ import { CollectionFreeFormPannableContents } from '../../collections/collection import { Colors } from '../../global/globalEnums'; import { DocumentView } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; -import { FocusViewOptions } from '../FocusViewOptions'; +import { FocusEffectDelay, FocusViewOptions } from '../FocusViewOptions'; import { OpenWhere, OpenWhereMod } from '../OpenWhere'; import { ScriptingBox } from '../ScriptingBox'; import CubicBezierEditor, { EaseFuncToPoints, TIMING_DEFAULT_MAPPINGS } from './CubicBezierEditor'; @@ -48,7 +49,6 @@ import './PresBox.scss'; import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums'; import SlideEffect from './SlideEffect'; import { AnimationSettings, SpringSettings, SpringType, easeItems, effectItems, effectTimings, movementItems, presEffectDefaultTimings, springMappings, springPreviewColors } from './SpringUtils'; -import _ from 'lodash'; @observer export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -61,6 +61,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } static navigateToDocScript: ScriptField; + public static PanelName = 'PRESBOX'; // name of dockingview tab where presentations get added + constructor(props: FieldViewProps) { super(props); makeObservable(this); @@ -72,6 +74,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { private _disposers: { [name: string]: IReactionDisposer } = {}; public selectedArray = new ObservableSet<Doc>(); + public slideToModify: Doc | null = null; _batch: UndoManager.Batch | undefined = undefined; // undo batch for dragging sliders which generate multiple scene edit events as the cursor moves _keyTimer: NodeJS.Timeout | undefined; // timer for turning off transition flag when key frame change has completed. Need to clear this if you do a second navigation before first finishes, or else first timer can go off during second naviation. _unmounting = false; // flag that view is unmounting used to block RemFromMap from deleting things @@ -97,14 +100,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @observable _presKeyEvents: boolean = false; @observable _forceKeyEvents: boolean = false; + @observable _showAIGallery = false; + @observable _showPreview = true; + @observable _easeDropdownVal = 'ease'; + // GPT - private _inputref: HTMLTextAreaElement | null = null; - private _inputref2: HTMLTextAreaElement | null = null; - @observable chatActive: boolean = false; - @observable chatInput: string = ''; - public slideToModify: Doc | null = null; - @observable isRecording: boolean = false; - @observable isLoading: boolean = false; + @observable _chatActive: boolean = false; + @observable _animationChat: string = ''; + @observable _chatInput: string = ''; + @observable _isRecording: boolean = false; + @observable _isLoading: boolean = false; @observable generatedAnimations: AnimationSettings[] = [ // default presets @@ -138,54 +143,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }, ]; - @action - setGeneratedAnimations = (settings: AnimationSettings[]) => { - this.generatedAnimations = settings; - }; - - @observable animationChat: string = ''; - - @action - setChatInput = (input: string) => { - this.chatInput = input; - }; - - @action - setAnimationChat = (input: string) => { - this.animationChat = input; - }; - - @action - setIsLoading = (isLoading: boolean) => { - this.isLoading = isLoading; - }; - - @action - public setIsRecording = (isRecording: boolean) => { - this.isRecording = isRecording; - }; - - @observable showBezierEditor = false; - @action setBezierEditorVisibility = (visible: boolean) => { - this.showBezierEditor = visible; - }; - @observable showSpringEditor = true; - @action setSpringEditorVisibility = (visible: boolean) => { - this.showSpringEditor = visible; - }; - - // Easing function variables - - @observable easeDropdownVal = 'ease'; - - @action setBezierControlPoints = (newPoints: { p1: number[]; p2: number[] }) => { + setGeneratedAnimations = action((input: AnimationSettings[]) => { this.generatedAnimations = input; }) // prettier-ignore + setChatInput = action((input: string) => { this._chatInput = input; }); // prettier-ignore + setAnimationChat = action((input: string) => { this._animationChat = input; }); // prettier-ignore + setIsLoading = action((input?: boolean) => { this._isLoading = !!input; }); // prettier-ignore + setIsRecording = action((input: boolean) => { this._isRecording = input; }); // prettier-ignore + setShowAIGalleryVisibilty = action((visible: boolean) => { this._showAIGallery = visible; }); // prettier-ignore + setBezierControlPoints = action((newPoints: { p1: number[]; p2: number[] }) => { this.setEaseFunc(this.activeItem, `cubic-bezier(${newPoints.p1[0]}, ${newPoints.p1[1]}, ${newPoints.p2[0]}, ${newPoints.p2[1]})`); - }; + }); + + @computed get showEaseFunctions() { + return ![PresMovement.None, PresMovement.Jump, ''].includes(StrCast(this.activeItem?.presentation_movement) as PresMovement); + } @computed get currCPoints() { - const strPoints = this.activeItem.presentation_easeFunc ? StrCast(this.activeItem.presentation_easeFunc) : 'ease'; - return EaseFuncToPoints(strPoints); + return EaseFuncToPoints(this.activeItem.presentation_easeFunc ? StrCast(this.activeItem.presentation_easeFunc) : 'ease'); } @computed @@ -221,25 +195,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc.type as DocumentType) || this.targetDoc._type_collection === CollectionViewType.Stacking) return true; return false; } - @computed get panable() { - if ((this.targetDoc.type === DocumentType.COL && this.targetDoc._type_collection === CollectionViewType.Freeform) || this.targetDoc.type === DocumentType.IMG) return true; - return false; - } @computed get selectedDocumentView() { if (DocumentView.Selected().length) return DocumentView.Selected()[0]; if (this.selectedArray.size) return DocumentView.getDocumentView(this.Document); return undefined; } - @computed get isPres() { - return this.selectedDoc === this.Document; - } - @computed get selectedDoc() { - return this.selectedDocumentView?.Document; - } - isActiveItemTarget = (layoutDoc: Doc) => this.activeItem?.presentation_targetDoc === layoutDoc; - clearSelectedArray = () => this.selectedArray.clear(); - addToSelectedArray = action((doc: Doc) => this.selectedArray.add(doc)); - removeFromSelectedArray = action((doc: Doc) => this.selectedArray.delete(doc)); componentWillUnmount() { this._unmounting = true; @@ -256,7 +216,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { () => this.pauseAutoPres() ); this._disposers.keyboard = reaction( - () => this.selectedDoc, + () => this.selectedDocumentView?.Document, selected => { document.removeEventListener('keydown', PresBox.keyEventsWrapper, true); (this._presKeyEvents = selected === this.Document) && document.addEventListener('keydown', PresBox.keyEventsWrapper, true); @@ -293,13 +253,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { ); } + clearSelectedArray = () => this.selectedArray.clear(); + addToSelectedArray = action((doc: Doc) => this.selectedArray.add(doc)); + removeFromSelectedArray = action((doc: Doc) => this.selectedArray.delete(doc)); + @action updateCurrentPresentation = (pres?: Doc) => { Doc.ActivePresentation = pres ?? this.Document; PresBox.Instance = this; }; - _mediaTimer!: [NodeJS.Timeout, Doc]; // 'Play on next' for audio or video therefore first navigate to the audio/video before it should be played startTempMedia = (targetDoc: Doc, activeItem: Doc) => { const duration: number = NumCast(activeItem.config_clipEnd) - NumCast(activeItem.config_clipStart); @@ -318,7 +281,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; // Recording for GPT customization - recordDictation = () => { this.setIsRecording(true); this.setChatInput(''); @@ -336,64 +298,34 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { DictationManager.Controls.stop(); }; - setDictationContent = (value: string) => { - console.log('Dictation value', value); - this.setChatInput(value); - }; + setDictationContent = (value: string) => this.setChatInput(value); - @action - customizeAnimations = async () => { + customizeAnimations = action(() => { this.setIsLoading(true); - try { - const res = await getSlideTransitionSuggestions(this.animationChat); - if (typeof res === 'string') { - const resObj = JSON.parse(res); - console.log('Parsed GPT Result ', resObj); - this.setGeneratedAnimations(resObj as AnimationSettings[]); - } - } catch (err) { - console.error(err); - } - this.setIsLoading(false); - }; + getSlideTransitionSuggestions(this._animationChat) + .then(res => this.setGeneratedAnimations(JSON.parse(res) as AnimationSettings[])) + .catch(err => console.error(err)) + .finally(this.setIsLoading); + }); - @action - customizeWithGPT = async (input: string) => { + customizeWithGPT = action((input: string) => { // const testInput = 'change title to Customized Slide, transition for 2.3s with fade in effect'; this.setIsRecording(false); this.setIsLoading(true); - - const currSlideProperties: { [key: string]: FieldResult } = {}; - gptSlideProperties.forEach(key => { - if (this.activeItem[key]) { - currSlideProperties[key] = this.activeItem[key]; - } - // default values - else if (key === 'presentation_transition') { - currSlideProperties[key] = 500; - } else if (key === 'config_zoom') { - currSlideProperties[key] = 1.0; - } - }); - console.log('current slide props ', currSlideProperties); - - try { - const res = await gptTrailSlideCustomization(input, currSlideProperties); - if (typeof res === 'string') { - const resObj = JSON.parse(res); - console.log('Parsed GPT Result ', resObj); - for (const key in resObj) { - if (resObj[key]) { - console.log('typeof property', typeof resObj[key]); - this.activeItem[key] = resObj[key]; - } - } - } - } catch (err) { - console.error(err); - } - this.setIsLoading(false); - }; + const slideDefaults: { [key: string]: FieldResult } = { presentation_transition: 500, config_zoom: 1 }; + const currSlideProperties = gptSlideProperties.reduce( + (prev, key) => { prev[key] = Field.toString(this.activeItem[key]) ?? prev[key]; return prev; }, + slideDefaults); // prettier-ignore + + gptTrailSlideCustomization(input, JSON.stringify(currSlideProperties)) + .then(res => + (Object.entries(JSON.parse(res)) as string[][]).forEach(([key, val]) => { + this.activeItem[key] = (+val).toString() === val ? +val : (val ?? this.activeItem[key]); + }) + ) + .catch(e => console.error(e)) + .finally(this.setIsLoading); + }); // TODO: al: it seems currently that tempMedia doesn't stop onslidechange after clicking the button; the time the tempmedia stop depends on the start & end time // TODO: to handle child slides (entering into subtrail and exiting), also the next() and back() functions @@ -452,27 +384,25 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const progressiveReveal = (first: boolean) => { const presIndexed = Cast(this.activeItem?.presentation_indexed, 'number', null); if (presIndexed !== undefined) { - const targetRenderedDoc = PresBox.targetRenderedDoc(this.activeItem); - targetRenderedDoc._dataTransition = 'all 1s'; - targetRenderedDoc.opacity = 1; - setTimeout(() => { - targetRenderedDoc._dataTransition = 'inherit'; - }, 1000); const listItems = this.progressivizedItems(this.activeItem); - if (listItems && presIndexed < listItems.length) { + const listItemDoc = listItems?.[presIndexed]; + if (listItems && listItemDoc) { if (!first) { - const listItemDoc = listItems[presIndexed]; - const targetView = listItems && DocumentView.getFirstDocumentView(listItemDoc); + const presBulletTiming = 500; // bcz: hardwired for now Doc.linkFollowUnhighlight(); - Doc.HighlightDoc(listItemDoc); + Doc.linkFollowHighlight(listItemDoc); listItemDoc.presentation_effect = this.activeItem.presBulletEffect; - listItemDoc.presentation_transition = 500; - targetView?.setAnimEffect(listItemDoc, 500); - if (targetView && this.activeItem.presBulletExpand) { - targetView.setAnimateScaling(1.2, 400); - Doc.AddUnHighlightWatcher(() => targetView?.setAnimateScaling(0, undefined)); - } + listItemDoc.presentation_transition = presBulletTiming; listItemDoc.opacity = undefined; + + const targetView = DocumentView.getFirstDocumentView(listItemDoc); + if (targetView) { + targetView.setAnimEffect(listItemDoc, presBulletTiming); + if (this.activeItem.presBulletExpand) { + targetView.setAnimateScaling(1.2, presBulletTiming * 0.8); + Doc.AddUnHighlightWatcher(() => targetView.setAnimateScaling(0, undefined)); + } + } this.activeItem.presentation_indexed = presIndexed + 1; } return true; @@ -547,8 +477,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (!group) this.clearSelectedArray(); this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); // Update selected array this.turnOffEdit(); - this.navigateToActiveItem(finished); // Handles movement to element only when presentationTrail is list - this.doHideBeforeAfter(); // Handles hide after/before + this.navigateToActiveItem((options: FocusViewOptions) => { + setTimeout(this.doHideBeforeAfter, FocusEffectDelay(options)); // Handles hide after/before + finished?.(); + }); // Handles movement to element only when presentationTrail is list } }); static pinDataTypes(target?: Doc): dataTypes { @@ -784,11 +716,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }); setTimeout( () => - Array.from(transitioned).forEach( - action(doc => { - doc._dataTransition = undefined; - }) - ), + Array.from(transitioned).forEach(doc => { + doc._dataTransition = undefined; + }), transTime + 10 ); } @@ -826,16 +756,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { * a new tab. If presCollection is undefined it will open the document * on the right. */ - navigateToActiveItem = (afterNav?: () => void) => { + navigateToActiveItem = (afterNav?: (options: FocusViewOptions) => void) => { const { activeItem, targetDoc } = this; - const finished = () => { - afterNav?.(); + const finished = (options: FocusViewOptions) => { + afterNav?.(options); targetDoc[Animation] = undefined; }; const selViewCache = Array.from(this.selectedArray); const dragViewCache = Array.from(this._dragArray); const eleViewCache = Array.from(this._eleArray); - const resetSelection = action(() => { + const resetSelection = action((options: FocusViewOptions) => { if (!this._props.isSelected()) { const presDocView = DocumentView.getDocumentView(this.Document); if (presDocView) DocumentView.SelectView(presDocView, false); @@ -844,14 +774,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._dragArray.splice(0, this._dragArray.length, ...dragViewCache); this._eleArray.splice(0, this._eleArray.length, ...eleViewCache); } - finished(); + finished(options); }); PresBox.NavigateToTarget(targetDoc, activeItem, resetSelection); }; - public static PanelName = 'PRESBOX'; - - static NavigateToTarget(targetDoc: Doc, activeItem: Doc, finished?: () => void) { + static NavigateToTarget(targetDoc: Doc, activeItem: Doc, finished?: (options: FocusViewOptions) => void) { if (activeItem.presentation_movement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) { (DocumentView.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.(); return; @@ -886,9 +814,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (!DocumentView.getLightboxDocumentView(DocCast(targetDoc.annotationOn) ?? targetDoc)) { DocumentView.SetLightboxDoc(undefined); } - DocumentView.showDocument(targetDoc, options, finished); + DocumentView.showDocument(targetDoc, options, () => finished?.(options)); }); - } else finished?.(); + } else finished?.(options); } /** @@ -900,8 +828,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.childDocs.forEach((doc, index) => { const curDoc = Cast(doc, Doc, null); const tagDoc = PresBox.targetRenderedDoc(curDoc); - const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, curDoc); - let opacity: Opt<number> = index === this.itemIndex ? 1 : undefined; + const itemIndexes = this.getAllIndexes(this.tagDocs, curDoc); + let opacity = index === this.itemIndex ? 1 : undefined; if (curDoc.presentation_hide) { if (index !== this.itemIndex) { opacity = 1; @@ -913,9 +841,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { opacity = 0; } else if (index === this.itemIndex || !curDoc.presentation_hideAfter) { opacity = 1; - setTimeout(() => { - tagDoc._dataTransition = undefined; - }, 1000); } } const hidingIndAft = @@ -1687,20 +1612,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </Tooltip> </div> {[DocumentType.AUDIO, DocumentType.VID].includes(targetType as DocumentType) ? null : ( - <> - <div className="ribbon-doubleButton"> - <div className="presBox-subheading">Slide Duration</div> - <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}> - <input className="presBox-input" type="number" readOnly value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s + <div className="ribbon-doubleButton"> + <Tooltip title={<div>How long to view the slide before transitioning to the next slide</div>}> + <div className="presBox-subheading">DURATION</div> + </Tooltip> + <div className="presBox-subheading-slider"> + {PresBox.inputter('0.1', '0.1', '10', duration, targetType !== DocumentType.AUDIO, this.updateDurationTime)} + <div className="slider-headers"> + <div className="slider-text">Short</div> + <div className="slider-text">Long</div> </div> </div> - {PresBox.inputter('0.1', '0.1', '20', duration, targetType !== DocumentType.AUDIO, this.updateDurationTime)} - <div className="slider-headers" style={{ display: targetType === DocumentType.AUDIO ? 'none' : 'grid' }}> - <div className="slider-text">Short</div> - <div className="slider-text">Medium</div> - <div className="slider-text">Long</div> + <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}`, display: 'flex', maxWidth: 60, width: '100%' }}> + <input className="presBox-inputNumber" type="number" readOnly value={duration} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateDurationTime(e.target.value))} /> + <span>s</span> </div> - </> + </div> )} </div> </div> @@ -1708,28 +1635,62 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } return undefined; } - @computed get progressivizeDropdown() { + + @computed get mediaDropdown() { const { activeItem } = this; if (activeItem && this.targetDoc) { - const effect = activeItem.presBulletEffect ? activeItem.presBulletEffect : PresMovement.None; - const bulletEffect = (presEffect: PresEffect) => ( - <div - className={`presBox-dropdownOption ${activeItem.presentation_effect === presEffect || (presEffect === PresEffect.None && !activeItem.presentation_effect) ? 'active' : ''}`} - onPointerDown={StopEvent} - onClick={() => this.updateEffect(presEffect, true)}> - {presEffect} + return ( + <div className="presBox-option-block"> + <div className="presBox-ribbon presbox-toggles"> + <Tooltip title={<div className="dash-tooltip">Should first bullet be progressively disclosed or does it appear with slide.</div>}> + <div + className={`ribbon-toggle ${BoolCast(activeItem.presentation_playAudio) ? 'active' : ''}`} + style={{ + border: `solid 1px ${SnappingManager.userColor}`, + color: SnappingManager.userColor, + background: BoolCast(activeItem.presentation_playAudio) ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor, + }} + onClick={() => { + activeItem.presentation_playAudio = !BoolCast(activeItem.presentation_playAudio); + }}> + Play Audio Annotation + </div> + </Tooltip> + <Tooltip title={<div className="dash-tooltip">Should first bullet be progressively disclosed or does it appear with slide.</div>}> + <div + className={`ribbon-toggle ${BoolCast(activeItem.presentation_zoomText) ? 'active' : ''}`} + style={{ + border: `solid 1px ${SnappingManager.userColor}`, + color: SnappingManager.userColor, + background: BoolCast(activeItem.presentation_zoomText) ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor, + }} + onClick={() => { + activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText); + }}> + Zoom Text Selections + </div> + </Tooltip> + </div> </div> ); + } + return null; + } + @computed get progressivizeDropdown() { + const { activeItem } = this; + if (activeItem && this.targetDoc) { return ( <div className="presBox-option-block"> - <div className="presBox-ribbon"> - <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> - <div className="presBox-subheading">Progressivize Collection</div> - <input - className="presBox-checkbox" - style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }} - type="checkbox" - onChange={() => { + <div className="presBox-toggles presBox-ribbon"> + <Tooltip title={<div className="dash-tooltip">whether progressivization is active for this slide</div>}> + <div + className={`ribbon-toggle ${Cast(activeItem.presentation_indexed, 'number', null) !== undefined ? 'active' : ''}`} + style={{ + border: `solid 1px ${SnappingManager.userColor}`, + color: SnappingManager.userColor, + background: Cast(activeItem.presentation_indexed, 'number', null) !== undefined ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor, + }} + onClick={() => { activeItem.presentation_indexed = activeItem.presentation_indexed === undefined ? 0 : undefined; activeItem.presentation_hideBefore = activeItem.presentation_indexed !== undefined; const tagDoc = PresBox.targetRenderedDoc(this.activeItem); @@ -1742,62 +1703,51 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (DocCast(activeItem.presentation_targetDoc).annotationOn) activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc.annotationOn?.["${dataField}"]`); else activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc?.["${dataField}"]`); + }}> + Enable + </div> + </Tooltip> + <Tooltip title={<div className="dash-tooltip">Should first bullet be progressively disclosed or does it appear with slide.</div>}> + <div + className={`ribbon-toggle ${!NumCast(activeItem.presentation_indexedStart) ? 'active' : ''}`} + style={{ + border: `solid 1px ${SnappingManager.userColor}`, + color: SnappingManager.userColor, + background: !NumCast(activeItem.presentation_indexedStart) ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor, }} - checked={Cast(activeItem.presentation_indexed, 'number', null) !== undefined} - /> - </div> - <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> - <div className="presBox-subheading">Progressivize First Bullet</div> - <input - className="presBox-checkbox" - style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }} - type="checkbox" - onChange={() => { + onClick={() => { activeItem.presentation_indexedStart = activeItem.presentation_indexedStart ? 0 : 1; - }} - checked={!NumCast(activeItem.presentation_indexedStart)} - /> - </div> - <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> - <div className="presBox-subheading">Expand Current Bullet</div> - <input - className="presBox-checkbox" - style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }} - type="checkbox" - onChange={() => { - activeItem.presBulletExpand = !activeItem.presBulletExpand; - }} - checked={BoolCast(activeItem.presBulletExpand)} - /> - </div> - - <div className="ribbon-box"> - Bullet Effect + }}> + All Bullets + </div> + </Tooltip> + <Tooltip title={<div className="dash-tooltip">Whether the active bullet expands when active.</div>}> <div - className="presBox-dropdown" - onClick={action(e => { - e.stopPropagation(); - this._openBulletEffectDropdown = !this._openBulletEffectDropdown; - })} + className={`ribbon-toggle ${BoolCast(activeItem.presBulletExpand) ? 'active' : ''}`} style={{ + border: `solid 1px ${SnappingManager.userColor}`, color: SnappingManager.userColor, - background: SnappingManager.userVariantColor, - borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5, - border: this._openBulletEffectDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.userColor}`, + background: BoolCast(activeItem.presBulletExpand) ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor, + }} + onClick={() => { + activeItem.presBulletExpand = !activeItem.presBulletExpand; }}> - {effect?.toString()} - <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openBulletEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon="angle-down" /> - <div - className="presBox-dropdownOptions" - style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }} - onPointerDown={e => e.stopPropagation()}> - {Object.values(PresEffect) - .filter(v => isNaN(Number(v))) - .map(pEffect => bulletEffect(pEffect))} - </div> + Expand Active </div> - </div> + </Tooltip> </div> + <Dropdown + color={SnappingManager.userColor} + formLabel="Effect" + toolTip="Animation effect to use when bullet activates" + formLabelPlacement="left" + closeOnSelect + items={Object.values(PresEffect).map(v => ({ text: v.toString(), val: v }))} + selectedVal={StrCast(activeItem.presBulletEffect, PresMovement.None)} + setSelectedVal={val => this.updateEffect(val as PresEffect, true)} + dropdownType={DropdownType.SELECT} + type={Type.TERT} + /> </div> ); } @@ -1808,23 +1758,89 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return <div />; } + /** + * This chatbox is for getting slide effect transition suggestions from gpt and visualizing them + */ + @computed get aiEffects() { + return ( + <div className="presBox-gpt-chat" style={{ display: SnappingManager.PropertiesWidth < 1 || !this._showAIGallery ? 'none' : undefined }}> + {/* Custom */} + <div className="pres-chat"> + <div className="pres-chatbox-container-ai"> + <ReactTextareaAutosize + placeholder="Use AI to suggest effects. Leave blank for random results." + className="pres-chatbox" + ref={r => { + setTimeout(() => { + if (r && !r.textContent) { + r.style.height = ''; + r.style.height = r.scrollHeight + 'px'; + } + }); + }} + value={this._animationChat} + onChange={e => { + e.currentTarget.style.height = ''; + e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px'; + this.setAnimationChat(e.target.value); + }} + onKeyDown={e => { + this.stopDictation(); + e.stopPropagation(); + }} + /> + </div> + <Button + style={{ alignSelf: 'flex-end' }} + text="Send" + type={Type.TERT} + icon={this._isLoading ? <ReactLoading type="spin" color="#ffffff" width={20} height={20} /> : <AiOutlineSend />} + iconPlacement="right" + color={SnappingManager.userVariantColor} + onClick={this.customizeAnimations} + /> + </div> + <div style={{ alignItems: 'center' }}> + Click a box to use the effect. + {/* Preview Animations */} + <div className="presBox-effects"> + {this.generatedAnimations.map((elem, i) => ( + <div + key={i} + className="presBox-effect-container" + onClick={() => { + this.updateEffect(elem.effect, false); + this.updateEffectDirection(elem.direction); + this.updateEffectTiming(this.activeItem, { + type: SpringType.CUSTOM, + stiffness: elem.stiffness, + damping: elem.damping, + mass: elem.mass, + }); + }}> + <SlideEffect dir={elem.direction} presEffect={elem.effect} springSettings={elem} infinite> + <div className="presBox-effect-demo-box" style={{ backgroundColor: springPreviewColors[i] }} /> + </SlideEffect> + </div> + ))} + </div> + </div> + </div> + ); + } + @computed get transitionDropdown() { const { activeItem } = this; // Retrieving spring timing properties - const timing = StrCast(activeItem.presentation_effectTiming); - let timingConfig: SpringSettings | undefined; - if (timing) { - timingConfig = JSON.parse(timing); - } - - if (!timingConfig) { - timingConfig = { - type: SpringType.GENTLE, - stiffness: 100, - damping: 15, - mass: 1, - }; - } + const timing = StrCast(activeItem?.presentation_effectTiming); + const timingConfig: SpringSettings = timing + ? JSON.parse(timing) + : { + type: SpringType.GENTLE, + stiffness: 100, + damping: 15, + mass: 1, + }; if (activeItem && this.targetDoc) { const transitionSpeed = activeItem.presentation_transition ? NumCast(activeItem.presentation_transition) / 1000 : 0.5; @@ -1835,20 +1851,30 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return ( <> {/* This chatbox is for customizing the properties of trails, like transition time, movement type (zoom, pan) using GPT */} - <div className="presBox-gpt-chat"> - <span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> + <div className="presBox-gpt-chat" style={{ display: SnappingManager.PropertiesWidth < 1 ? 'none' : undefined }}> + <span className="presBox-gpt-chat-span"> Customize Slide Properties{' '} <div className="propertiesView-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/trails/#slide-customization')}> <IconButton icon={<FontAwesomeIcon icon="info-circle" />} color={SnappingManager.userColor} /> </div> </span> <div className="pres-chat"> - <div className="pres-chatbox-container"> + <div className="pres-chatbox-container-ai"> <ReactTextareaAutosize - placeholder="Describe how you would like to modify the slide properties." + placeholder="Describe how to modify the slide properties." className="pres-chatbox" - value={this.chatInput} + ref={r => { + setTimeout(() => { + if (r && !r.textContent) { + r.style.height = ''; + r.style.height = r.scrollHeight + 'px'; + } + }); + }} + value={this._chatInput} onChange={e => { + e.currentTarget.style.height = ''; + e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px'; this.setChatInput(e.target.value); }} onKeyDown={e => { @@ -1858,11 +1884,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { /> <IconButton type={Type.TERT} - color={this.isRecording ? '#2bcaff' : SnappingManager.userVariantColor} + color={this._isRecording ? '#2bcaff' : SnappingManager.userVariantColor} tooltip="Record" icon={<BiMicrophone size="16px" />} onClick={() => { - if (!this.isRecording) { + if (!this._isRecording) { this.recordDictation(); } else { this.stopDictation(); @@ -1874,16 +1900,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { style={{ alignSelf: 'flex-end' }} text="Send" type={Type.TERT} - icon={this.isLoading ? <ReactLoading type="spin" color="#ffffff" width={20} height={20} /> : <AiOutlineSend />} + icon={this._isLoading ? <ReactLoading type="spin" color="#ffffff" width={20} height={20} /> : <AiOutlineSend />} iconPlacement="right" color={SnappingManager.userVariantColor} onClick={() => { this.stopDictation(); - this.customizeWithGPT(this.chatInput); + this.customizeWithGPT(this._chatInput); }} /> </div> </div> + {/* Movement */} <div className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presentation_status === PresStatus.Edit ? 'active' : ''}`} @@ -1895,116 +1922,68 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._openEffectDropdown = false; this._openBulletEffectDropdown = false; })}> - <div - className="presBox-option-block" - // style={{ padding: '16px' }} - > - Movement + <div className="presBox-option-block"> + <div className="ribbon-doubleButton"> + <Tooltip title={<div>How long the transition (view navigation and slide animation effect) lasts</div>}> + <div className="presBox-subheading">SPEED</div> + </Tooltip> + <div className="presBox-subheading-slider"> + {PresBox.inputter('0.1', '0.1', '10', transitionSpeed, true, this.updateTransitionTime)} + <div className="slider-headers"> + <div className="slider-text">Fast</div> + <div className="slider-text">Slow</div> + </div> + </div> + <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}`, display: 'flex', maxWidth: 60, width: '100%' }}> + <input className="presBox-inputNumber" type="number" readOnly value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> + <span>s</span> + </div> + </div> <Dropdown color={SnappingManager.userColor} - formLabel="Movement" + formLabel="View" + formLabelPlacement="left" closeOnSelect items={movementItems} selectedVal={this.movementName(activeItem)} - setSelectedVal={val => { - this.updateMovement(val as PresMovement); - }} + setSelectedVal={val => this.updateMovement(val as PresMovement)} dropdownType={DropdownType.SELECT} type={Type.TERT} /> - <div className="ribbon-doubleButton" style={{ display: activeItem.presentation_movement === PresMovement.Zoom ? 'inline-flex' : 'none' }}> - <div className="presBox-subheading">Zoom (% screen filled)</div> - <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}> - <input className="presBox-input" readOnly type="number" value={zoom} onChange={e => this.updateZoom(e.target.value)} />% - </div> - </div> - {PresBox.inputter('0', '1', '100', zoom, activeItem.presentation_movement === PresMovement.Zoom, this.updateZoom)} - <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> - <div className="presBox-subheading">Transition Time</div> - <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}> - <input className="presBox-input" type="number" readOnly value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s + <div className="ribbon-doubleButton" style={{ display: activeItem.presentation_movement === PresMovement.Zoom ? undefined : 'none' }}> + <Tooltip title={<div>How much (%) of screen target should occupy</div>}> + <div className="presBox-subheading">ZOOM %</div> + </Tooltip> + <div className="presBox-subheading-slider">{PresBox.inputter('0', '1', '100', zoom, activeItem.presentation_movement === PresMovement.Zoom, this.updateZoom)}</div> + <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}`, display: 'flex', maxWidth: 60, width: '100%' }}> + <input className="presBox-inputNumber" readOnly type="number" value={zoom} onChange={e => this.updateZoom(e.target.value)} /> + <span>%</span> </div> </div> - {PresBox.inputter('0.1', '0.1', '10', transitionSpeed, true, this.updateTransitionTime)} - <div className="slider-headers"> - <div className="slider-text">Fast</div> - <div className="slider-text">Medium</div> - <div className="slider-text">Slow</div> - </div> {/* Easing function */} - <Dropdown - color={SnappingManager.userColor} - formLabel="Easing Function" - closeOnSelect - items={easeItems} - selectedVal={this.activeItem.presentation_easeFunc ? (StrCast(this.activeItem.presentation_easeFunc).startsWith('cubic') ? 'custom' : StrCast(this.activeItem.presentation_easeFunc)) : 'ease'} - setSelectedVal={val => { - if (typeof val === 'string') { - if (val !== 'custom') { - this.setEaseFunc(this.activeItem, val); - } else { - this.setBezierEditorVisibility(true); - this.setEaseFunc(this.activeItem, TIMING_DEFAULT_MAPPINGS.ease); - } - } - }} - dropdownType={DropdownType.SELECT} - type={Type.TERT} - /> - {/* Custom */} - <div - className="presBox-show-hide-dropdown" - style={{ alignSelf: 'flex-start' }} - onClick={e => { - e.stopPropagation(); - this.setBezierEditorVisibility(!this.showBezierEditor); - }}> - {`${this.showBezierEditor ? 'Hide' : 'Show'} Timing Editor`} - <FontAwesomeIcon icon={this.showBezierEditor ? 'chevron-up' : 'chevron-down'} /> - </div> + {!this.showEaseFunctions ? null : ( + <Dropdown + color={SnappingManager.userColor} + formLabel="Timing" + formLabelPlacement="left" + closeOnSelect + items={easeItems} + selectedVal={this.activeItem.presentation_easeFunc ? (StrCast(this.activeItem.presentation_easeFunc).startsWith('cubic') ? 'custom' : StrCast(this.activeItem.presentation_easeFunc)) : 'ease'} + setSelectedVal={val => typeof val === 'string' && this.setEaseFunc(this.activeItem, val !== 'custom' ? val : TIMING_DEFAULT_MAPPINGS.ease)} + dropdownType={DropdownType.SELECT} + type={Type.TERT} + /> + )} </div> </div> {/* Cubic bezier editor */} - {this.showBezierEditor && ( - <div className="presBox-option-block" style={{ paddingTop: 0 }}> - <p className="presBox-submenu-label" style={{ alignSelf: 'flex-start' }}> - Custom Timing Function - </p> + {this.showEaseFunctions && StrCast(activeItem.presentation_easeFunc).includes('cubic-bezier') && ( + <div className="presBox-option-block" style={{ paddingTop: 0, alignItems: 'center' }}> <CubicBezierEditor setFunc={this.setBezierControlPoints} currPoints={this.currCPoints} /> </div> )} - {/* This chatbox is for getting slide effect transition suggestions from gpt and visualizing them */} - <div className="presBox-gpt-chat"> - Effects - <div className="pres-chat"> - <div className="pres-chatbox-container"> - <ReactTextareaAutosize - placeholder="Customize prompt for effect suggestions. Leave blank for random results." - className="pres-chatbox" - value={this.animationChat} - onChange={e => { - this.setAnimationChat(e.target.value); - }} - onKeyDown={e => { - this.stopDictation(); - e.stopPropagation(); - }} - /> - </div> - <Button - style={{ alignSelf: 'flex-end' }} - text="Send" - type={Type.TERT} - icon={this.isLoading ? <ReactLoading type="spin" color="#ffffff" width={20} height={20} /> : <AiOutlineSend />} - iconPlacement="right" - color={SnappingManager.userVariantColor} - onClick={this.customizeAnimations} - /> - </div> - </div> - <div className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presentation_status === PresStatus.Edit ? 'active' : ''}`} onPointerDown={StopEvent} @@ -2016,213 +1995,168 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._openBulletEffectDropdown = false; })}> <div className="presBox-option-block"> - Click on a box to apply the effect. - <div className="presBox-option-block presBox-option-center"> - {/* Preview Animations */} - <div className="presBox-effects"> - {this.generatedAnimations.map((elem, i) => ( - <div - key={i} - className="presBox-effect-container" - onClick={() => { - this.updateEffect(elem.effect, false); - this.updateEffectDirection(elem.direction); - this.updateEffectTiming(this.activeItem, { - type: SpringType.CUSTOM, - stiffness: elem.stiffness, - damping: elem.damping, - mass: elem.mass, - }); - }}> - <SlideEffect dir={elem.direction} presEffect={elem.effect} springSettings={elem} infinite> - <div className="presBox-effect-demo-box" style={{ backgroundColor: springPreviewColors[i] }} /> - </SlideEffect> - </div> - ))} + {/* Effect dropdown */} + <div style={{ display: 'flex' }}> + <Dropdown + color={SnappingManager.userColor} + formLabel="Effect" + toolTip="Animation effect to apply when transitiong to slide" + formLabelPlacement="left" + closeOnSelect + items={effectItems} + selectedVal={effect?.toString()} + setSelectedVal={val => { + this.updateEffect(val as PresEffect, false); + // set default spring options for that effect + this.updateEffectTiming(activeItem, presEffectDefaultTimings[val as keyof typeof presEffectDefaultTimings]); + }} + dropdownType={DropdownType.SELECT} + type={Type.TERT} + /> + + <div + className={`ribbon-toggle ${this._showAIGallery ? 'active' : ''}`} + style={{ + border: `solid 1px ${SnappingManager.userColor}`, + color: SnappingManager.userColor, + background: this._showAIGallery ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor, + }} + onClick={() => this.setShowAIGalleryVisibilty(!this._showAIGallery)}> + MORE </div> </div> - {/* Effect dropdown */} - <Dropdown - color={SnappingManager.userColor} - formLabel="Slide Effect" - closeOnSelect - items={effectItems} - selectedVal={effect?.toString()} - setSelectedVal={val => { - this.updateEffect(val as PresEffect, false); - // set default spring options for that effect - this.updateEffectTiming(activeItem, presEffectDefaultTimings[val as keyof typeof presEffectDefaultTimings]); - }} - dropdownType={DropdownType.SELECT} - type={Type.TERT} - /> - {/* Effect direction */} - {/* Only applies to certain effects */} - {(effect === PresEffect.Flip || effect === PresEffect.Bounce || effect === PresEffect.Roll) && ( - <> - <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> - <div className="presBox-subheading">Effect direction</div> - <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}> - {StrCast(this.activeItem.presentation_effectDirection)} - </div> - </div> - <div className="presBox-icon-list"> - <IconButton - type={Type.TERT} - color={activeItem.presentation_effectDirection === PresEffectDirection.Left ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor} - tooltip="Left" - icon={<FaArrowRight size="16px" />} - onClick={() => this.updateEffectDirection(PresEffectDirection.Left)} - /> - <IconButton - type={Type.TERT} - color={activeItem.presentation_effectDirection === PresEffectDirection.Right ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor} - tooltip="Right" - icon={<FaArrowLeft size="16px" />} - onClick={() => this.updateEffectDirection(PresEffectDirection.Right)} - /> - {effect !== PresEffect.Roll && ( - <> + {this.aiEffects} + <div className="presBox-gpt-chat"> + {/* Effect direction */} + {/* Only applies to certain effects */} + {(effect === PresEffect.Flip || effect === PresEffect.Bounce || effect === PresEffect.Roll) && ( + <div className="ribbon-doubleButton"> + <div className="presBox-subheading">DIRECTION</div> + <div style={{ width: '100%' }}> + <div className="presBox-icon-list" style={{ width: 'fit-content', margin: 'auto' }}> <IconButton type={Type.TERT} - color={activeItem.presentation_effectDirection === PresEffectDirection.Top ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor} - tooltip="Top" - icon={<FaArrowDown size="16px" />} - onClick={() => this.updateEffectDirection(PresEffectDirection.Top)} + color={activeItem.presentation_effectDirection === PresEffectDirection.Left ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor} + tooltip="Left" + icon={<FaArrowRight size="16px" />} + onClick={() => this.updateEffectDirection(PresEffectDirection.Left)} /> <IconButton type={Type.TERT} - color={activeItem.presentation_effectDirection === PresEffectDirection.Bottom ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor} - tooltip="Bottom" - icon={<FaArrowUp size="16px" />} - onClick={() => this.updateEffectDirection(PresEffectDirection.Bottom)} - /> - </> - )} - </div> - </> - )} - {/* Spring settings */} - {/* No spring settings for jackinthebox (lightspeed) */} - {effect !== PresEffect.Lightspeed && ( - <> - <Dropdown - color={SnappingManager.userColor} - formLabel="Effect Timing" - closeOnSelect - items={effectTimings} - selectedVal={timingConfig.type} - setSelectedVal={val => { - this.updateEffectTiming(activeItem, { - type: val as SpringType, - ...springMappings[val], - }); - }} - dropdownType={DropdownType.SELECT} - type={Type.TERT} - /> - <div - className="presBox-show-hide-dropdown" - onClick={e => { - e.stopPropagation(); - this.setSpringEditorVisibility(!this.showSpringEditor); - }}> - {`${this.showSpringEditor ? 'Hide' : 'Show'} Spring Settings`} - <FontAwesomeIcon icon={this.showSpringEditor ? 'chevron-up' : 'chevron-down'} /> - </div> - {this.showSpringEditor && ( - <> - <div>Tension</div> - <div - onPointerDown={e => { - e.stopPropagation(); - }}> - <Slider - min={1} - max={1000} - step={5} - size="small" - value={timingConfig.stiffness} - onChange={(e, val) => { - if (!timingConfig) return; - this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, stiffness: val as number }); - }} - valueLabelDisplay="auto" - /> - </div> - <div>Damping</div> - <div - onPointerDown={e => { - e.stopPropagation(); - }}> - <Slider - min={1} - max={100} - step={1} - size="small" - value={timingConfig.damping} - onChange={(e, val) => { - if (!timingConfig) return; - this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, damping: val as number }); - }} - valueLabelDisplay="auto" - /> - </div> - <div>Mass</div> - <div - onPointerDown={e => { - e.stopPropagation(); - }}> - <Slider - min={1} - max={10} - step={1} - size="small" - value={timingConfig.mass} - onChange={(e, val) => { - if (!timingConfig) return; - this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, mass: val as number }); - }} - valueLabelDisplay="auto" + color={activeItem.presentation_effectDirection === PresEffectDirection.Right ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor} + tooltip="Right" + icon={<FaArrowLeft size="16px" />} + onClick={() => this.updateEffectDirection(PresEffectDirection.Right)} /> + {effect !== PresEffect.Roll && ( + <> + <IconButton + type={Type.TERT} + color={activeItem.presentation_effectDirection === PresEffectDirection.Top ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor} + tooltip="Top" + icon={<FaArrowDown size="16px" />} + onClick={() => this.updateEffectDirection(PresEffectDirection.Top)} + /> + <IconButton + type={Type.TERT} + color={activeItem.presentation_effectDirection === PresEffectDirection.Bottom ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor} + tooltip="Bottom" + icon={<FaArrowUp size="16px" />} + onClick={() => this.updateEffectDirection(PresEffectDirection.Bottom)} + /> + </> + )} </div> - Preview Effect - <div className="presBox-option-block presBox-option-center"> - <div className="presBox-effect-container"> - <SlideEffect dir={direction} presEffect={effect} springSettings={timingConfig} infinite> - <div className="presBox-effect-demo-box" style={{ backgroundColor: springPreviewColors[0] }} /> - </SlideEffect> - </div> - </div> - </> - )} - </> - )} + </div> + </div> + )} + {![PresEffect.Lightspeed, PresEffect.Fade, PresEffect.None, ''].includes(effect) && ( + <> + <Dropdown + color={SnappingManager.userColor} + formLabel="Springiness" + formLabelPlacement="left" + closeOnSelect + items={effectTimings} + selectedVal={timingConfig.type} + setSelectedVal={val => this.updateEffectTiming(activeItem, { type: val as SpringType, ...springMappings[val] })} + dropdownType={DropdownType.SELECT} + type={Type.TERT} + /> + + <div style={{ display: SnappingManager.PropertiesWidth < 1 ? 'none' : undefined }}> + {/* No spring settings for jackinthebox (lightspeed) */} + {StrCast(activeItem.presentation_effectTiming).includes('custom') && effect !== PresEffect.None && ( + <> + <div className="presBox-springSlider"> + <span>Tension</span> + <div onPointerDown={e => e.stopPropagation()}> + {/* prettier-ignore */} + <Slider min={1} max={1000} step={5} size="small" + value={timingConfig.stiffness} + onChange={(e, val) => timingConfig && this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, stiffness: val as number })} + valueLabelDisplay="auto" + /> + </div> + </div> + <div className="presBox-springSlider"> + <span>Damping</span> + <div onPointerDown={e => e.stopPropagation()}> + {/* prettier-ignore */} + <Slider min={1} max={100} step={1} size="small" + value={timingConfig.damping} + onChange={(e, val) => timingConfig && this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, damping: val as number })} + valueLabelDisplay="auto" + /> + </div> + </div> + <div className="presBox-springSlider"> + <span>Mass</span> + <div onPointerDown={e => e.stopPropagation()}> + {/* prettier-ignore */} + <Slider min={1} max={10} step={1} size="small" + value={timingConfig.mass} + onChange={(e, val) => timingConfig && this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, mass: val as number })} + valueLabelDisplay="auto" + /> + </div> + </div> + </> + )} + </div> + </> + )} + </div> </div> + </div> - {/* Toggles */} - <div className="presBox-option-block"> - <Toggle - formLabel="Play Audio Annotation" - toggleType={ToggleType.SWITCH} - toggleStatus={BoolCast(activeItem.presentation_playAudio)} - onClick={() => { - activeItem.presentation_playAudio = !BoolCast(activeItem.presentation_playAudio); - }} - color={SnappingManager.userColor} - /> - <Toggle - formLabel="Zoom Text Selections" - toggleType={ToggleType.SWITCH} - toggleStatus={BoolCast(activeItem.presentation_zoomText)} - onClick={() => { - activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText); - }} + {[PresEffect.None, PresEffect.Fade, ''].includes(effect) ? null : ( + <div className="presBox-previewContainer"> + <Button + type={Type.TERT} + tooltip="show preview of slide animation effect" + size={Size.SMALL} color={SnappingManager.userColor} + background="transparent" + onClick={action(() => { + this._showPreview = false; + setTimeout(action(() => { this._showPreview = true; }) ); // prettier-ignore + })} + text="Preview Effect" /> - <Button text="Apply to all" type={Type.TERT} color={SnappingManager.userVariantColor} onClick={() => this.applyTo(this.childDocs)} /> + <div className="presBox-option-block presBox-option-center"> + <div className="presBox-effect-container"> + {!this._showPreview ? null : ( + <SlideEffect dir={direction} presEffect={effect} springSettings={timingConfig}> + <div className="presBox-effect-demo-box" style={{ backgroundColor: springPreviewColors[0] }} /> + </SlideEffect> + )} + </div> + </div> </div> - </div> + )} + + <Button text="Apply to all slides" type={Type.TERT} color={SnappingManager.userVariantColor} onClick={() => this.applyTo(this.childDocs)} /> </> ); } @@ -2248,7 +2182,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div id="startTime" className="slider-number" style={{ color: SnappingManager.userColor, backgroundColor: SnappingManager.userBackgroundColor }}> <input className="presBox-input" - style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }} type="number" readOnly value={NumCast(activeItem.config_clipStart).toFixed(2)} @@ -2275,7 +2208,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <input className="presBox-input" onKeyDown={e => e.stopPropagation()} - style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }} type="number" readOnly value={configClipEnd.toFixed(2)} @@ -3083,7 +3015,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } */} </div> {/* presbox chatbox */} - {this.chatActive && <div className="presBox-chatbox" />} + {this._chatActive && <div className="presBox-chatbox" />} </div> ); } diff --git a/src/client/views/nodes/trails/SpringUtils.ts b/src/client/views/nodes/trails/SpringUtils.ts index 73e1e14f1..044afbeb1 100644 --- a/src/client/views/nodes/trails/SpringUtils.ts +++ b/src/client/views/nodes/trails/SpringUtils.ts @@ -22,7 +22,14 @@ export interface SpringSettings { } // Overall config - +// Keeps these settings in sync with the AnimationSettings interface (used by gpt); +export enum AnimationSettingsProperties { + effect = 'effect', + direction = 'direction', + stiffness = 'stiffness', + damping = 'damping', + mass = 'mass', +} export interface AnimationSettings { effect: PresEffect; direction: PresEffectDirection; diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index 22ca9dcd7..f5a9f9e6a 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, IconButton, Type } from '@dash/components'; -import { action, makeObservable, observable } from 'mobx'; +import { Button, IconButton, Toggle, ToggleType, Type } from '@dash/components'; +import { action, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { CgClose, CgCornerUpLeft } from 'react-icons/cg'; @@ -37,10 +37,8 @@ export enum GPTQuizType { MULTIPLE = 2, } -interface GPTPopupProps {} - @observer -export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { +export class GPTPopup extends ObservableReactComponent<object> { // eslint-disable-next-line no-use-before-define static Instance: GPTPopup; private messagesEndRef: React.RefObject<HTMLDivElement>; @@ -48,115 +46,84 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { @observable private chatMode: boolean = false; private correlatedColumns: string[] = []; - @observable - public visible: boolean = false; - @action - public setVisible = (vis: boolean) => { - this.visible = vis; + @observable public Visible: boolean = false; + @action public setVisible = (vis: boolean) => { + this.Visible = vis; }; - @observable - public loading: boolean = false; - @action - public setLoading = (loading: boolean) => { + @observable public loading: boolean = false; + @action public setLoading = (loading: boolean) => { this.loading = loading; }; - @observable - public text: string = ''; - @action - public setText = (text: string) => { + @observable public text: string = ''; + @action public setText = (text: string) => { this.text = text; }; - @observable - public selectedText: string = ''; - @action - public setSelectedText = (text: string) => { + @observable public selectedText: string = ''; + @action public setSelectedText = (text: string) => { this.selectedText = text; }; - @observable - public dataJson: string = ''; + @observable public dataJson: string = ''; public dataChatPrompt: string | undefined = undefined; - @action - public setDataJson = (text: string) => { + @action public setDataJson = (text: string) => { if (text === '') this.dataChatPrompt = ''; this.dataJson = text; }; - @observable - public imgDesc: string = ''; - @action - public setImgDesc = (text: string) => { + @observable public imgDesc: string = ''; + @action public setImgDesc = (text: string) => { this.imgDesc = text; }; - @observable - public imgUrls: string[][] = []; - @action - public setImgUrls = (imgs: string[][]) => { + @observable public imgUrls: string[][] = []; + @action public setImgUrls = (imgs: string[][]) => { this.imgUrls = imgs; }; - @observable - public mode: GPTPopupMode = GPTPopupMode.SUMMARY; - @action - public setMode = (mode: GPTPopupMode) => { + @observable public mode: GPTPopupMode = GPTPopupMode.SUMMARY; + @action public setMode = (mode: GPTPopupMode) => { this.mode = mode; }; - @observable - public highlightRange: number[] = []; + @observable public highlightRange: number[] = []; @action callSummaryApi = () => {}; - @observable - private done: boolean = false; - @action - public setDone = (done: boolean) => { + @observable private done: boolean = false; + @action public setDone = (done: boolean) => { this.done = done; this.chatMode = false; }; - @observable - private sortDone: boolean = false; // this is so redundant but the og done variable was causing weird unknown problems and im just a girl - - @action - public setSortDone = (done: boolean) => { - this.sortDone = done; - }; - // change what can be a ref into a ref - @observable - private sidebarId: string = ''; - @action - public setSidebarId = (id: string) => { + @observable private sidebarId: string = ''; + @action public setSidebarId = (id: string) => { this.sidebarId = id; }; - @observable - private imgTargetDoc: Doc | undefined; - @action - public setImgTargetDoc = (anchor: Doc) => { + @observable private imgTargetDoc: Doc | undefined; + @action public setImgTargetDoc = (anchor: Doc) => { this.imgTargetDoc = anchor; }; - @observable - private textAnchor: Doc | undefined; - @action - public setTextAnchor = (anchor: Doc) => { + @observable private textAnchor: Doc | undefined; + @action public setTextAnchor = (anchor: Doc) => { this.textAnchor = anchor; }; - @observable - public sortDesc: string = ''; - + @observable public sortDesc: string = ''; @action public setSortDesc = (t: string) => { this.sortDesc = t; }; - @observable onSortComplete?: (sortResult: string, questionType: string, tag?: string) => void; - @observable onQuizRandom?: () => void; + onSortComplete?: (sortResult: string, questionType: string, tag?: string) => void; + onQuizRandom?: () => void; @observable cardsDoneLoading = false; + @observable collectionDoc: Doc | undefined = undefined; + @action setCollectionDoc(doc: Doc | undefined) { + this.collectionDoc = doc; + } + @action setCardsDoneLoading(done: boolean) { - console.log(done + 'HI HIHI'); this.cardsDoneLoading = done; } @@ -186,39 +153,27 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { */ generateQuiz = async () => { this.setLoading(true); - this.setSortDone(false); - const quizType = this.quizMode; + await this.regenerateCallback?.(); const selected = DocumentView.SelectedDocs().lastElement(); - - const questionText = 'Question: ' + StrCast(selected['gptInputText']); - - if (StrCast(selected['gptRubric']) === '') { - const rubricText = 'Rubric: ' + (await this.generateRubric(StrCast(selected['gptInputText']), selected)); + if (!StrCast(selected.gptRubric)) { + await this.generateRubric(StrCast(selected.gptInputText), selected); } - const rubricText = 'Rubric: ' + StrCast(selected['gptRubric']); - const queryText = questionText + ' UserAnswer: ' + this.quizAnswer + '. ' + 'Rubric' + rubricText; - try { - const res = await gptAPICall(queryText, GPTCallType.QUIZ); - if (!res) { - console.error('GPT call failed'); - return; - } - console.log(res); - this.setQuizResp(res); - this.conversationArray.push(res); + const res = await gptAPICall('Question: ' + StrCast(selected.gptInputText) + ' UserAnswer: ' + this.quizAnswer + '. Rubric: ' + StrCast(selected.gptRubric), GPTCallType.QUIZ); + if (res) { + this.setQuizResp(res); + this.conversationArray.push(res); - this.setLoading(false); - this.setSortDone(true); + this.setLoading(false); + this.onQuizRandom?.(); + } else { + console.error('GPT provided no response'); + } } catch (err) { - console.error('GPT call failed'); - } - - if (this.onQuizRandom) { - this.onQuizRandom(); + console.error('GPT call failed', err); } }; @@ -231,10 +186,10 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { generateRubric = async (inputText: string, doc: Doc) => { try { const res = await gptAPICall(inputText, GPTCallType.RUBRIC); - doc['gptRubric'] = res; + doc.gptRubric = res; return res; } catch (err) { - console.error('GPT call failed'); + console.error('GPT call failed', err); } }; @@ -244,7 +199,8 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { * Callback function that causes the card view to update the childpair string list * @param callback */ - @action public setRegenerateCallback(callback: () => Promise<void>) { + @action public setRegenerateCallback(collectionDoc: Doc | undefined, callback: null | (() => Promise<void>)) { + this.setCollectionDoc(collectionDoc); this.regenerateCallback = callback; } @@ -262,37 +218,21 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { * Generates a response to the user's question depending on the type of their question */ generateCard = async () => { - console.log(this.chatSortPrompt + 'USER PROMPT'); this.setLoading(true); - this.setSortDone(false); - if (this.regenerateCallback) { - await this.regenerateCallback(); - } + await this.regenerateCallback?.(); try { - // const res = await gptAPICall(this.sortDesc, GPTCallType.SORT, this.chatSortPrompt); const questionType = await gptAPICall(this.chatSortPrompt, GPTCallType.TYPE); - const questionNumber = questionType.split(' ')[0]; - console.log(questionType); - let res = ''; - - switch (questionNumber) { - case '1': - case '2': - case '4': - res = await gptAPICall(this.sortDesc, GPTCallType.SUBSET, this.chatSortPrompt); - break; - case '6': - res = await gptAPICall(this.sortDesc, GPTCallType.SORT, this.chatSortPrompt); - break; - default: - const selected = DocumentView.SelectedDocs().lastElement(); - const questionText = StrCast(selected!['gptInputText']); - - res = await gptAPICall(questionText, GPTCallType.INFO, this.chatSortPrompt); - break; - } + const questionNumber = questionType.split(' ')[0][0]; + const res = await (() => { + switch (questionNumber) { + case '1': + case '2': + case '4': return gptAPICall(this.sortDesc, GPTCallType.SUBSET, this.chatSortPrompt); + case '6': return gptAPICall(this.sortDesc, GPTCallType.SORT, this.chatSortPrompt); + default: return gptAPICall(StrCast(DocumentView.SelectedDocs().lastElement()?.gptInputText), GPTCallType.INFO, this.chatSortPrompt); + }})(); // prettier-ignore // Trigger the callback with the result if (this.onSortComplete) { @@ -308,7 +248,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { // Set the extracted explanation to sortRespText this.setSortRespText(explanation); - this.conversationArray.push(this.sortRespText); + runInAction(() => this.conversationArray.push(this.sortRespText)); this.scrollToBottom(); console.log(res); @@ -318,7 +258,6 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { } this.setLoading(false); - this.setSortDone(true); }; /** @@ -448,7 +387,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { private getPreviewUrl = (source: string) => source.split('.').join('_m.'); - constructor(props: GPTPopupProps) { + constructor(props: object) { super(props); makeObservable(this); GPTPopup.Instance = this; @@ -498,9 +437,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { onClick={() => { this.conversationArray = ['Define the selected card!']; this.setMode(GPTPopupMode.QUIZ); - if (this.onQuizRandom) { - this.onQuizRandom(); - } + this.onQuizRandom?.(); }} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} @@ -515,18 +452,25 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { </div> ); - handleKeyPress = async (e: React.KeyboardEvent, isSort: boolean) => { + @action + handleKeyPress = (e: React.KeyboardEvent, isSort: boolean) => { if (e.key === 'Enter') { e.stopPropagation(); if (isSort) { this.conversationArray.push(this.chatSortPrompt); - await this.generateCard(); - this.chatSortPrompt = ''; + this.generateCard().then( + action(() => { + this.chatSortPrompt = ''; + }) + ); } else { this.conversationArray.push(this.quizAnswer); - await this.generateQuiz(); - this.quizAnswer = ''; + this.generateQuiz().then( + action(() => { + this.quizAnswer = ''; + }) + ); } this.scrollToBottom(); @@ -569,7 +513,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { }; sortBox = () => ( - <div style={{ height: '80%' }}> + <div className="gptPopup-sortBox" style={{ height: '80%' }}> {this.heading(this.mode === GPTPopupMode.SORT ? 'SORTING' : 'QUIZ')} <> { @@ -742,6 +686,15 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { {(this.mode === GPTPopupMode.SORT || this.mode === GPTPopupMode.QUIZ) && ( <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="back" icon={<CgCornerUpLeft size="16px" />} onClick={() => (this.mode = GPTPopupMode.CARD)} style={{ right: '50px', position: 'absolute' }} /> )} + <Toggle + tooltip="Clear Chat filter" + toggleType={ToggleType.BUTTON} + type={Type.PRIM} + toggleStatus={Doc.hasDocFilter(this.collectionDoc, 'tags', '#chat')} + text={Doc.hasDocFilter(this.collectionDoc, 'tags', '#chat') ? 'filtered' : ''} + color="red" + onClick={() => this.collectionDoc && Doc.setDocFilter(this.collectionDoc, 'tags', '#chat', 'remove')} + /> <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="close" @@ -778,7 +731,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { } return ( - <div className="summary-box" style={{ display: this.visible ? 'flex' : 'none' }}> + <div className="summary-box" style={{ display: this.Visible ? 'flex' : 'none' }}> {content} <div className="resize-handle" /> </div> |
