diff options
Diffstat (limited to 'src/client/views')
17 files changed, 1917 insertions, 186 deletions
diff --git a/src/client/views/ExtractColors.ts b/src/client/views/ExtractColors.ts new file mode 100644 index 000000000..f6928c52a --- /dev/null +++ b/src/client/views/ExtractColors.ts @@ -0,0 +1,168 @@ +import { extractColors } from 'extract-colors'; +import { FinalColor } from 'extract-colors/lib/types/Color'; + +// Manages image color extraction +export class ExtractColors { + // loads all images into img elements + static loadImages = async (imageFiles: File[]): Promise<HTMLImageElement[]> => { + try { + const imageElements = await Promise.all(imageFiles.map(file => this.loadImage(file))); + return imageElements; + } catch (error) { + console.error(error); + return []; + } + }; + + // loads a single img into an img element + static loadImage = (file: File): Promise<HTMLImageElement> => { + return new Promise((resolve, reject) => { + const img = new Image(); + + img.onload = () => resolve(img); + img.onerror = error => reject(error); + + const url = URL.createObjectURL(file); + img.src = url; + }); + }; + + // loads all images into img elements + static loadImagesUrl = async (imageUrls: string[]): Promise<HTMLImageElement[]> => { + try { + const imageElements = await Promise.all(imageUrls.map(url => this.loadImageUrl(url))); + return imageElements; + } catch (error) { + console.error(error); + return []; + } + }; + + // loads a single img into an img element + static loadImageUrl = (url: string): Promise<HTMLImageElement> => { + return new Promise((resolve, reject) => { + const img = new Image(); + + img.onload = () => resolve(img); + img.onerror = error => reject(error); + + img.src = url; + }); + }; + + // extracts a list of collors from an img element + static getImgColors = async (img: HTMLImageElement) => { + const colors = await extractColors(img, { distance: 0.35 }); + return colors; + }; + + static simpleSort = (colors: FinalColor[]): FinalColor[] => { + colors.sort((a, b) => { + if (a.hue !== b.hue) { + return b.hue - a.hue; + } else { + return b.saturation - a.saturation; + } + }); + return colors; + }; + + static sortColors(colors: FinalColor[]): FinalColor[] { + // Convert color from RGB to CIELAB format + const convertToLab = (color: FinalColor): number[] => { + const r = color.red / 255; + const g = color.green / 255; + const b = color.blue / 255; + + const x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375; + const y = r * 0.2126729 + g * 0.7151522 + b * 0.072175; + const z = r * 0.0193339 + g * 0.119192 + b * 0.9503041; + + const pivot = 0.008856; + const factor = 903.3; + + const fx = x > pivot ? Math.cbrt(x) : (factor * x + 16) / 116; + const fy = y > pivot ? Math.cbrt(y) : (factor * y + 16) / 116; + const fz = z > pivot ? Math.cbrt(z) : (factor * z + 16) / 116; + + const L = 116 * fy - 16; + const a = (fx - fy) * 500; + const b1 = (fy - fz) * 200; + + return [L, a, b1]; + }; + + // Sort colors using CIELAB distance for smooth transitions + colors.sort((colorA, colorB) => { + const labA = convertToLab(colorA); + const labB = convertToLab(colorB); + + // Calculate Euclidean distance in CIELAB space + const distanceA = Math.sqrt(Math.pow(labA[0] - labB[0], 2) + Math.pow(labA[1] - labB[1], 2) + Math.pow(labA[2] - labB[2], 2)); + + const distanceB = Math.sqrt(Math.pow(labB[0] - labA[0], 2) + Math.pow(labB[1] - labA[1], 2) + Math.pow(labB[2] - labA[2], 2)); + + return distanceA - distanceB; // Sort by CIELAB distance + }); + + return colors; + } + + static hexToFinalColor = (hex: string): FinalColor => { + const rgb = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + + if (!rgb) { + throw new Error('Invalid hex color format.'); + } + + const red = parseInt(rgb[1], 16); + const green = parseInt(rgb[2], 16); + const blue = parseInt(rgb[3], 16); + + const max = Math.max(red, green, blue); + const min = Math.min(red, green, blue); + const area = max - min; + const intensity = (max + min) / 2; + + let hue = 0; + let saturation = 0; + let lightness = intensity; + + if (area !== 0) { + saturation = area / (1 - Math.abs(2 * intensity - 1)); + if (max === red) { + hue = (60 * ((green - blue) / area) + 360) % 360; + } else if (max === green) { + hue = (60 * ((blue - red) / area) + 120) % 360; + } else { + hue = (60 * ((red - green) / area) + 240) % 360; + } + } + + return { + hex, + red, + green, + blue, + area, + hue, + saturation, + lightness, + intensity, + }; + }; +} + +// for reference + +// type FinalColor = { +// hex: string; +// red: number; +// green: number; +// blue: number; +// area: number; +// hue: number; +// saturation: number; +// lightness: number; +// intensity: number; +// } diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss index 8581bdf73..b21828aa7 100644 --- a/src/client/views/PropertiesView.scss +++ b/src/client/views/PropertiesView.scss @@ -7,6 +7,21 @@ position: absolute; right: 4; } +.propertiesView-palette { + cursor: pointer; + padding: 8px; + border-radius: 4px; + transition: all 0.2s ease; + &:hover { + background-color: #3b3c3e; + } +} +.styling-chatbox { + color: #000000; + width: 100%; + outline: none; + border: none; +} .propertiesView { height: 100%; width: 250; diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index ae29382c1..6da46c229 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -2,7 +2,7 @@ import { IconLookup } from '@fortawesome/fontawesome-svg-core'; import { faAnchor, faArrowRight, faWindowMaximize } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Checkbox, Tooltip } from '@mui/material'; -import { Colors, EditableText, IconButton, NumberInput, Size, Slider, Type } from 'browndash-components'; +import { Button, ColorPicker, Colors, EditableText, IconButton, NumberInput, Size, Slider, Type } from 'browndash-components'; import { concat } from 'lodash'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; @@ -41,6 +41,12 @@ import { DocumentView, OpenWhere } from './nodes/DocumentView'; import { StyleProviderFuncType } from './nodes/FieldView'; import { KeyValueBox } from './nodes/KeyValueBox'; import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails'; +import { ExtractColors } from './ExtractColors'; +import TextareaAutosize from 'react-textarea-autosize'; +import { GeneratedResponse, StyleInput, generatePalette } from '../apis/gpt/customization'; +import { FaFillDrip } from 'react-icons/fa'; + +import { LinkBox } from './nodes/LinkBox'; const _global = (window /* browser */ || global) /* node */ as any; interface PropertiesViewProps { @@ -100,6 +106,58 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps @observable openAppearance: boolean = true; @observable openTransform: boolean = true; @observable openFilters: boolean = false; + @observable openStyling: boolean = true; + + // GPT styling + public styleInput: StyleInput | undefined; + @observable loadingStyles: boolean = false; + @observable generatedStyles: GeneratedResponse[] = []; + @observable inputDocs: Doc[] = []; + @observable selectedStyle: number = -1; + @observable useImageData = false; + + @observable chatInput: string = ''; + + @action + setChatInput = (input: string) => { + this.chatInput = input; + }; + + @action + setLoading = (loading: boolean) => { + this.loadingStyles = loading; + }; + + @action + gptStyling = async () => { + // this.generatedStyles = []; + this.selectedStyle = -1; + this.setLoading(true); + console.log('Style input: ', this.styleInput); + + if (!this.styleInput) return; + + try { + let res: any; + if (this.generatedStyles.length === 0) { + res = await generatePalette(this.styleInput, this.useImageData, this.chatInput); + } else { + res = await generatePalette(this.styleInput, this.useImageData, this.chatInput, this.generatedStyles); + } + if (typeof res === 'string') { + console.log('Generated palettes: ', res); + const resObj = JSON.parse(res) as GeneratedResponse[]; + this.setGeneratedStyles(resObj); + } + } catch (err) { + console.error(err); + } + this.setLoading(false); + }; + + @action + setGeneratedStyles = (responses: GeneratedResponse[]) => (this.generatedStyles = responses); + setInputDocs = (docs: Doc[]) => (this.inputDocs = docs); //Pres Trails booleans: @observable openPresTransitions: boolean = true; @@ -1139,6 +1197,60 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps } }; + @action + styleCollection = (i: number) => { + this.selectedStyle = i; + const resObj = this.generatedStyles[i]; + if (this.selectedDoc && this.selectedDoc.type === 'collection') { + this.selectedDoc.backgroundColor = resObj.collectionBackgroundColor; + resObj.documentsWithColors.forEach((elem, i) => (this.inputDocs[i].backgroundColor = elem.color)); + } + }; + + // GPT styling + @computed get stylingSubMenu() { + return ( + <PropertiesSection title="Styling" isOpen={this.openStyling} setIsOpen={bool => (this.openStyling = bool)} onDoubleClick={() => this.CloseAll()}> + <div className="propertiesView-content" style={{ position: 'relative', height: 'auto', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '4px' }}> + {this.generatedStyles.length > 0 && + this.generatedStyles.map((style, i) => ( + <div + key={i} + className="propertiesView-palette" + style={{ display: 'flex', gap: '4px', backgroundColor: this.selectedStyle === i ? StrCast(Doc.UserDoc().userVariantColor) : '#00000000' }} + onClick={() => this.styleCollection(i)}> + <div style={{ width: '24px', height: '24px', backgroundColor: style.collectionBackgroundColor, borderRadius: '2px' }}></div> + {ExtractColors.sortColors(style.documentsWithColors.map(doc => ExtractColors.hexToFinalColor(doc.color))).map((c, i) => ( + <div key={i} style={{ width: '24px', height: '24px', backgroundColor: c.hex, borderRadius: '2px' }}></div> + ))} + </div> + ))} + {this.loadingStyles && 'Generating styles...'} + <TextareaAutosize + minRows={3} + placeholder="Customize..." + className="styling-chatbox" + autoFocus={true} + value={this.chatInput} + onChange={e => { + this.setChatInput(e.target.value); + }} + onKeyDown={e => { + e.stopPropagation(); + }} + /> + <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '16px' }}> + <div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}> + <label style={{ margin: '0px' }}>Use Images </label> + <input style={{ margin: '0px' }} type="checkbox" checked={this.useImageData} onChange={action(e => (this.useImageData = e.target.checked))} /> + </div> + <Button text={'Regenerate'} type={Type.TERT} color={StrCast(Doc.UserDoc().userVariantColor)} onClick={this.gptStyling} /> + </div> + </div> + </PropertiesSection> + ); + } + @computed get filtersSubMenu() { return ( <PropertiesSection title="Filters" isOpen={this.openFilters} setIsOpen={bool => (this.openFilters = bool)} onDoubleClick={() => this.CloseAll()}> @@ -1644,6 +1756,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps <div className="propertiesView-name">{this.editableTitle}</div> <div className="propertiesView-type"> {this.currentType} </div> + {this.stylingSubMenu} {this.fieldsSubMenu} {this.optionsSubMenu} {this.linksSubMenu} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx index 69cbae86f..a140dd4b1 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx @@ -40,6 +40,8 @@ export class CollectionFreeFormPannableContents extends React.Component<Collecti ); render() { + console.log('transofmr', this.props.transform()); + console.log('transition', this.props.transition); return ( <div className={'collectionfreeformview' + (this.props.viewDefDivClick ? '-viewDef' : '-none')} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 791124f50..c5102070a 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -10,7 +10,6 @@ import { DocData, Height, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkData, InkField, InkTool, PointData, Segment } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; -import { RichTextField } from '../../../../fields/RichTextField'; import { listSpec } from '../../../../fields/Schema'; import { ScriptField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; @@ -27,7 +26,7 @@ import { ReplayMovements } from '../../../util/ReplayMovements'; import { CompileScript } from '../../../util/Scripting'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { SelectionManager } from '../../../util/SelectionManager'; -import { freeformScrollMode } from '../../../util/SettingsManager'; +import { freeformScrollMode, SettingsManager } from '../../../util/SettingsManager'; import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager'; @@ -54,6 +53,11 @@ import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannable import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors'; import './CollectionFreeFormView.scss'; import { MarqueeView } from './MarqueeView'; +import { PropertiesView } from '../../PropertiesView'; +import { ExtractColors } from '../../ExtractColors'; +import { smartLayout, smartLayout2, StyleInputDocument } from '../../../apis/gpt/customization'; +import { extname } from 'path'; +import { RichTextField } from '../../../../fields/RichTextField'; export interface collectionFreeformViewProps { NativeWidth?: () => number; @@ -106,6 +110,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection private _brushtimer: any; private _brushtimer1: any; + private _presEaseFunc: string = 'ease'; + + @action + setPresEaseFunc = (easeFunc: string) => { + this._presEaseFunc = easeFunc; + }; + public get isAnnotationOverlay() { return this._props.isAnnotationOverlay; } @@ -307,6 +318,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; focus = (anchor: Doc, options: FocusViewOptions) => { + // incorporate the easefunc here + if (options.easeFunc) { + this.setPresEaseFunc(options.easeFunc); + console.log('Ease func', options.easeFunc); + } + if (this._lightboxDoc) return; if (anchor === this.Document) { // if (options.willZoomCentered && options.zoomScale) { @@ -1038,6 +1055,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection setTimeout(() => (this.layoutDoc.layout_scrollTop = relTop * maxScrollTop), 10); newPanY = minPanY; } + // updating pan state !this.Document._verticalScroll && (this.Document[this.panXFieldKey] = this.isAnnotationOverlay ? newPanX : panX); !this.Document._horizontalScroll && (this.Document[this.panYFieldKey] = this.isAnnotationOverlay ? newPanY : panY); } @@ -1638,6 +1656,107 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } }; + printDoc = (doc: Doc) => { + console.log('Printing keys'); + Object.keys(doc).forEach(key => { + console.log(key, ':', doc[key]); + }); + }; + + @action + openProperties = () => { + SettingsManager.Instance.propertiesWidth = 300; + }; + + choosePath(url: URL) { + if (!url?.href) return ''; + const lower = url.href.toLowerCase(); + if (url.protocol === 'data') return url.href; + if (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf('dashblobstore') === -1) return Utils.CorsProxy(url.href); + if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return `/assets/unknown-file-icon-hi.png`; + + const ext = extname(url.href); + return url.href.replace(ext, '_m' + ext); + } + + @action + smartLayout = async () => {}; + + roundToRoundNumber = (num: number) => { + return Math.round(num / 10) * 10; + }; + + // gpt layout + @action + gptLayout = async () => { + const docLayouts = this.childDocs.map(doc => ({ + width: this.roundToRoundNumber(NumCast(doc.width)), + height: this.roundToRoundNumber(NumCast(doc.height)), + // content: StrCast(doc.title), + })); + console.log('Doc layouts', docLayouts); + + const res = await smartLayout2(docLayouts); + console.log('Smart layout result', res); + // make type-safe + const resObj = JSON.parse(res) as any[]; + resObj.forEach((elem, i) => { + this.childDocs[i].x = elem.x; + this.childDocs[i].y = elem.y; + }); + + // refit collection + setTimeout(() => { + this.fitContentOnce(); + }, 500); + }; + + // gpt styling + @action + gptStyling = async () => { + // clear it in properties instead + if (!PropertiesView.Instance) return; + this.openProperties(); + PropertiesView.Instance.setGeneratedStyles([]); + PropertiesView.Instance.selectedStyle = -1; + PropertiesView.Instance.useImageData = false; + + console.log('Title', this.Document.title); + console.log('bgcolor', this.layoutDoc._backgroundColor); + // doc.backgroundColor + const inputDocs = this.childDocs.filter(doc => doc.type == 'rich text'); + const imgDocs = this.childDocs.filter(doc => doc.type == 'image'); + const imgUrls = imgDocs.map(doc => this.choosePath((doc.data as ImageField).url)); + + const imageElements = await ExtractColors.loadImagesUrl(imgUrls); + const colors = await Promise.all(imageElements.map(elem => ExtractColors.getImgColors(elem))); + let colorHexes = colors + .reduce((acc, curr) => acc.concat(curr), []) + .map(color => color.hex) + .slice(0, 10); + console.log('Hexes', colorHexes); + + PropertiesView.Instance?.setInputDocs(inputDocs); + + // also pass it colors + const gptInput: StyleInputDocument[] = inputDocs.map((doc, i) => ({ + id: i, + textContent: (doc.text as RichTextField)?.Text, + textSize: 16, + })); + + const collectionDescription = StrCast(this.Document.title); + + const styleInput = { + collectionDescription, + documents: gptInput, + imageColors: colorHexes, + }; + + PropertiesView.Instance.styleInput = styleInput; + PropertiesView.Instance.gptStyling(); + }; + onContextMenu = (e: React.MouseEvent) => { if (this._props.isAnnotationOverlay || !ContextMenu.Instance) return; @@ -1667,6 +1786,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection optionItems.push({ description: (this._showAnimTimeline ? 'Close' : 'Open') + ' Animation Timeline', event: action(() => (this._showAnimTimeline = !this._showAnimTimeline)), icon: 'eye' }); this._props.renderDepth && optionItems.push({ description: 'Use Background Color as Default', event: () => (Cast(Doc.UserDoc().emptyCollection, Doc, null).backgroundColor = StrCast(this.layoutDoc.backgroundColor)), icon: 'palette' }); this._props.renderDepth && optionItems.push({ description: 'Fit Content Once', event: this.fitContentOnce, icon: 'object-group' }); + // Want to condense into a Smart Organize button + this._props.renderDepth && optionItems.push({ description: 'Style with AI', event: this.gptStyling, icon: 'paint-brush' }); + this._props.renderDepth && optionItems.push({ description: 'Smart Layout', event: this.gptLayout, icon: 'object-group' }); + // this._props.renderDepth && optionItems.push({ description: 'Smart Organize', event: this.smartOrganize, icon: 'object-group' }); if (!Doc.noviceMode) { optionItems.push({ description: (!Doc.NativeWidth(this.layoutDoc) || !Doc.NativeHeight(this.layoutDoc) ? 'Freeze' : 'Unfreeze') + ' Aspect', event: this.toggleNativeDimensions, icon: 'snowflake' }); } @@ -1777,7 +1900,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection brushedView={this.brushedView} isAnnotationOverlay={this.isAnnotationOverlay} transform={this.PanZoomCenterXf} - transition={this._panZoomTransition ? `transform ${this._panZoomTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', Cast(this.Document._viewTransition, 'string', null))} + // Inject timing function here + transition={this._panZoomTransition ? `transform ${this._panZoomTransition}ms ${this._presEaseFunc}` : Cast(this.layoutDoc._viewTransition, 'string', Cast(this.Document._viewTransition, 'string', null))} viewDefDivClick={this._props.viewDefDivClick}> {this.props.children ?? null} {/* most likely case of children is document content that's being annoated: eg., an image */} {this.contentViews} diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 2a5732708..8000cce29 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -33,6 +33,10 @@ ScriptingGlobals.add(function setView(view: string) { selected ? (selected._type_collection = view) : console.log('[FontIconBox.tsx] changeView failed'); }); +ScriptingGlobals.add(function setSettingBgColor(isSetting: boolean) { + Doc.UserDoc().settingBgColor = isSetting; +}); + // toggle: Set overlay status of selected document ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: boolean) { const selectedViews = SelectionManager.Views; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index fc2da18d9..01c1de137 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -50,6 +50,8 @@ import { KeyValueBox } from './KeyValueBox'; import { LinkAnchorBox } from './LinkAnchorBox'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { PresEffect, PresEffectDirection } from './trails'; +import SlideEffect, { EffectType } from './trails/SlideEffect'; +import { SpringSettings, SpringType } from './trails/SpringUtils'; interface Window { MediaRecorder: MediaRecorder; } @@ -1005,18 +1007,48 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document delay: 0, duration: Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null)), }; + + let timing = StrCast(presEffectDoc?.presEffectTiming); + let timingConfig: SpringSettings | undefined; + if (timing) { + timingConfig = JSON.parse(timing); + } + + if (!timingConfig) { + timingConfig = { + type: SpringType.DEFAULT, + stiffness: 600, + damping: 15, + mass: 1, + }; + } //prettier-ignore + switch (StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect))) { - default: + default: + // package used: react-awesome-reveal case PresEffect.None: return renderDoc; - case PresEffect.Zoom: return <Zoom {...effectProps}>{renderDoc}</Zoom>; - case PresEffect.Fade: return <Fade {...effectProps}>{renderDoc}</Fade>; + case PresEffect.Zoom: return <SlideEffect dir={dir as PresEffectDirection} effectType={EffectType.ZOOM} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect> + case PresEffect.Fade: return <SlideEffect dir={dir as PresEffectDirection} effectType={EffectType.FADE} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect> case PresEffect.Flip: return <Flip {...effectProps}>{renderDoc}</Flip>; - case PresEffect.Rotate: return <Rotate {...effectProps}>{renderDoc}</Rotate>; - case PresEffect.Bounce: return <Bounce {...effectProps}>{renderDoc}</Bounce>; + case PresEffect.Rotate: return <SlideEffect dir={dir as PresEffectDirection} effectType={EffectType.ROTATE} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect> + // Changed to move in since anything can be "bouncy" + case PresEffect.Bounce: return <SlideEffect dir={dir as PresEffectDirection} effectType={EffectType.BOUNCE} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect> case PresEffect.Roll: return <Roll {...effectProps}>{renderDoc}</Roll>; case PresEffect.Lightspeed: return <JackInTheBox {...effectProps}>{renderDoc}</JackInTheBox>; } + // switch (StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect))) { + // default: + // // package used: react-awesome-reveal + // case PresEffect.None: return renderDoc; + // case PresEffect.Zoom: return <Zoom {...effectProps}>{renderDoc}</Zoom>; + // case PresEffect.Fade: return <Fade {...effectProps}>{renderDoc}</Fade>; + // case PresEffect.Flip: return <Flip {...effectProps}>{renderDoc}</Flip>; + // case PresEffect.Rotate: return <Rotate {...effectProps}>{renderDoc}</Rotate>; + // case PresEffect.Bounce: return <Bounce {...effectProps}>{renderDoc}</Bounce>; + // case PresEffect.Roll: return <Roll {...effectProps}>{renderDoc}</Roll>; + // case PresEffect.Lightspeed: return <JackInTheBox {...effectProps}>{renderDoc}</JackInTheBox>; + // } } public static recordAudioAnnotation(dataDoc: Doc, field: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) { let gumStream: any; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index c2f3a6e4b..f07c2d191 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -67,6 +67,8 @@ import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu'; import { RichTextRules } from './RichTextRules'; import { schema } from './schema_rts'; import { SummaryView } from './SummaryView'; +import { isDarkMode } from '../../../util/reportManager/reportManagerUtils'; +import Select from 'react-select'; // import * as applyDevTools from 'prosemirror-dev-tools'; interface FormattedTextBoxProps extends FieldViewProps { @@ -994,13 +996,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB try { let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION); if (!res) { - console.error('GPT call failed'); this.animateRes(0, 'Something went wrong.'); } else { this.animateRes(0, res); } } catch (err) { - console.error('GPT call failed'); + console.error(err); this.animateRes(0, 'Something went wrong.'); } }); @@ -1014,6 +1015,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; breakupDictation = () => { + console.log('breakup'); if (this._editorView && this._recordingDictation) { this.stopDictation(true); this._break = true; @@ -1196,6 +1198,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB @computed get contentScaling() { return Doc.NativeAspect(this.Document, this.dataDoc, false) ? this._props.NativeDimScaling?.() || 1 : 1; } + + @action + checkBackgroundColor() { + console.log('checking bg color 1'); + if (BoolCast(Doc.UserDoc().settingBgColor)) return; + console.log('checking bg color 2'); + let fontColor = '#000000'; + if (isDarkMode(StrCast(this.Document._backgroundColor))) { + fontColor = '#ffffff'; + } + // set text to white + if (!this._editorView) return; + const tr = this._editorView?.state.tr; + + tr.setSelection(TextSelection.create(tr.doc, 0, tr.doc.content.size)); + tr.addMark(0, tr.doc.content.size, schema.marks.pFontColor.create({ color: fontColor })); + this._editorView.dispatch(tr); + } + componentDidMount() { !this._props.dontSelectOnLoad && this._props.setContentViewBox?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. this._cachedLinks = LinkManager.Links(this.Document); @@ -1218,6 +1239,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ({ width, scrollHeight, layout_autoHeight }) => width && layout_autoHeight && this.resetNativeHeight(scrollHeight), { fireImmediately: true } ); + this._disposers.bgColor = reaction( + () => this.Document._backgroundColor, + color => this.checkBackgroundColor() + ); this._disposers.componentHeights = reaction( // set the document height when one of the component heights changes and layout_autoHeight is on () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layout_autoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }), diff --git a/src/client/views/nodes/trails/CubicBezierEditor.tsx b/src/client/views/nodes/trails/CubicBezierEditor.tsx new file mode 100644 index 000000000..d6ce9db1f --- /dev/null +++ b/src/client/views/nodes/trails/CubicBezierEditor.tsx @@ -0,0 +1,228 @@ +import React, { useEffect, useState } from 'react'; +import { StrCast } from '../../../../fields/Types'; + +type Props = { + setFunc: (newPoints: { p1: number[]; p2: number[] }) => void; + currPoints: { p1: number[]; p2: number[] }; + easeFunc: string; +}; + +const ANIMATION_DURATION = 750; +// const ANIMATION_TIMING_FUNC = "cubic-bezier(.42,.97,.52,1.49)"; +const ANIMATION_TIMING_FUNC = 'cubic-bezier(0.3, .2, .2, 1.4)'; +const CONTAINER_WIDTH = 200; +const EDITOR_WIDTH = 100; +const OFFSET = (CONTAINER_WIDTH - EDITOR_WIDTH) / 2; + +export const TIMING_DEFAULT_MAPPINGS = { + ease: 'cubic-bezier(0.25, 0.1, 0.25, 1.0)', + linear: 'cubic-bezier(0.0, 0.0, 1.0, 1.0)', + 'ease-in': 'cubic-bezier(0.42, 0, 1.0, 1.0)', + 'ease-out': 'cubic-bezier(0, 0, 0.58, 1.0)', + 'ease-in-out': 'cubic-bezier(0.42, 0, 0.58, 1.0)', +}; + +const CubicBezierEditor = ({ setFunc, currPoints, easeFunc }: Props) => { + const [animating, setAnimating] = useState(false); + // Should let parent control state + const [cPoints, setCPoints] = useState( + currPoints + ? currPoints + : { + p1: [0.25, 0.1], + p2: [0.25, 1.0], + } + ); + const [c1Down, setC1Down] = useState(false); + const [c2Down, setC2Down] = useState(false); + + const roundToHundredth = (num: number) => { + return Math.round(num * 100) / 100; + }; + + const convertToPoints = (func: string) => { + console.log('getting curr c points'); + let strPoints = func ? func : 'ease'; + if (!strPoints.startsWith('cubic')) { + switch (func) { + case 'linear': + strPoints = 'cubic-bezier(0.0, 0.0, 1.0, 1.0)'; + break; + case 'ease': + strPoints = 'cubic-bezier(0.25, 0.1, 0.25, 1.0)'; + break; + case 'ease-in': + strPoints = 'cubic-bezier(0.42, 0, 1.0, 1.0)'; + break; + case 'ease-out': + strPoints = 'cubic-bezier(0, 0, 0.58, 1.0)'; + break; + case 'ease-in-out': + strPoints = 'cubic-bezier(0.42, 0, 0.58, 1.0)'; + break; + default: + strPoints = 'cubic-bezier(0.25, 0.1, 0.25, 1.0)'; + } + } + console.log('str points', strPoints); + const components = strPoints + .split('(')[1] + .split(')')[0] + .split(',') + .map(elem => parseFloat(elem)); + console.log('bezier components', components); + return { + p1: [components[0], components[1]], + p2: [components[2], components[3]], + }; + }; + + useEffect(() => { + if (!easeFunc.startsWith('cubic')) { + setCPoints(convertToPoints(easeFunc)); + } + }, [easeFunc]); + + useEffect(() => { + if (animating) { + setTimeout(() => { + setAnimating(false); + }, ANIMATION_DURATION * 2); + } + }, [animating]); + + useEffect(() => { + if (!c1Down) return; + window.addEventListener('pointerup', () => { + setC1Down(false); + }); + const handlePointerMove = (e: PointerEvent) => { + const newX = cPoints.p1[0] + e.movementX / EDITOR_WIDTH; + if (newX < 0 || newX > 1) { + return; + } + + setCPoints(prev => ({ + ...prev, + p1: [roundToHundredth(prev.p1[0] + e.movementX / EDITOR_WIDTH), roundToHundredth(prev.p1[1] - e.movementY / EDITOR_WIDTH)], + })); + setFunc({ + ...cPoints, + p1: [roundToHundredth(cPoints.p1[0] + e.movementX / EDITOR_WIDTH), roundToHundredth(cPoints.p1[1] - e.movementY / EDITOR_WIDTH)], + }); + }; + + window.addEventListener('pointermove', handlePointerMove); + + return () => window.removeEventListener('pointermove', handlePointerMove); + }, [c1Down, cPoints]); + + useEffect(() => { + if (!c2Down) return; + window.addEventListener('pointerup', () => { + setC2Down(false); + }); + const handlePointerMove = (e: PointerEvent) => { + const newX = cPoints.p2[0] + e.movementX / EDITOR_WIDTH; + if (newX < 0 || newX > 1) { + return; + } + + setCPoints(prev => ({ + ...prev, + p2: [roundToHundredth(prev.p2[0] + e.movementX / EDITOR_WIDTH), roundToHundredth(prev.p2[1] - e.movementY / EDITOR_WIDTH)], + })); + setFunc({ + ...cPoints, + p2: [roundToHundredth(cPoints.p2[0] + e.movementX / EDITOR_WIDTH), roundToHundredth(cPoints.p2[1] - e.movementY / EDITOR_WIDTH)], + }); + }; + + window.addEventListener('pointermove', handlePointerMove); + + return () => window.removeEventListener('pointermove', handlePointerMove); + }, [c2Down, cPoints]); + + return ( + <div + // onPointerDown={e => { + // e.stopPropagation; + // }} + // onPointerMove={e => { + // e.stopPropagation; + // }} + // onPointerUp={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 ${cPoints.p1[0] * EDITOR_WIDTH + OFFSET} ${EDITOR_WIDTH - cPoints.p1[1] * EDITOR_WIDTH + OFFSET}, ${ + cPoints.p2[0] * EDITOR_WIDTH + OFFSET + } ${EDITOR_WIDTH - cPoints.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={`${cPoints.p1[0] * EDITOR_WIDTH + OFFSET}`} + y2={`${EDITOR_WIDTH - cPoints.p1[1] * EDITOR_WIDTH + OFFSET}`} + stroke="#00000000" + strokeWidth="5" + /> + <line x1={`${0 + OFFSET}`} y1={`${EDITOR_WIDTH + OFFSET}`} x2={`${cPoints.p1[0] * EDITOR_WIDTH + OFFSET}`} y2={`${EDITOR_WIDTH - cPoints.p1[1] * EDITOR_WIDTH + OFFSET}`} stroke="#ffffff" strokeWidth="1" /> + <circle + cx={`${cPoints.p1[0] * EDITOR_WIDTH + OFFSET}`} + cy={`${EDITOR_WIDTH - cPoints.p1[1] * EDITOR_WIDTH + OFFSET}`} + r="5" + fill={`${c1Down ? '#3fa9ff' : '#ffffff'}`} + onPointerDown={e => { + e.stopPropagation(); + setC1Down(true); + }} + onPointerUp={e => { + setC1Down(false); + }} + /> + {/* Top right */} + <line onPointerDown={e => { + e.stopPropagation(); + setC2Down(true); + }} + onPointerUp={e => { + setC2Down(false); + }} x1={`${EDITOR_WIDTH + OFFSET}`} y1={`${0 + OFFSET}`} x2={`${cPoints.p2[0] * EDITOR_WIDTH + OFFSET}`} y2={`${EDITOR_WIDTH - cPoints.p2[1] * EDITOR_WIDTH + OFFSET}`} stroke="#00000000" + strokeWidth="5" /> + <line x1={`${EDITOR_WIDTH + OFFSET}`} y1={`${0 + OFFSET}`} x2={`${cPoints.p2[0] * EDITOR_WIDTH + OFFSET}`} y2={`${EDITOR_WIDTH - cPoints.p2[1] * EDITOR_WIDTH + OFFSET}`} stroke="#ffffff" strokeWidth="1" /> + <circle + cx={`${cPoints.p2[0] * EDITOR_WIDTH + OFFSET}`} + cy={`${EDITOR_WIDTH - cPoints.p2[1] * EDITOR_WIDTH + OFFSET}`} + r="5" + fill={`${c2Down ? '#3fa9ff' : '#ffffff'}`} + onPointerDown={e => { + e.stopPropagation(); + setC2Down(true); + }} + onPointerUp={e => { + setC2Down(false); + }} + /> + </svg> + </div> + ); +}; + +export default CubicBezierEditor; diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss index 3b34a1f90..43f9ec78e 100644 --- a/src/client/views/nodes/trails/PresBox.scss +++ b/src/client/views/nodes/trails/PresBox.scss @@ -1,5 +1,62 @@ @import '../../global/globalCssVariables.module.scss'; +.presBox-gpt-chat { + padding: 8px; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.pres-chat { + // padding: 8px; + display: flex; + flex-direction: column; + gap: 8px; + // display: flex; + // align-items: center; + // gap: 16px; +} + +.presBox-icon-list { + display: flex; + gap: 8px; +} +.pres-chatbox-container { + padding: 16px; + outline: 1px solid #999999; + border-radius: 16px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.pres-chatbox { + outline: none; + border: none; + resize: none; + font-family: Verdana, Geneva, sans-serif; + background-color: transparent; + overflow-y: hidden; +} + +// Bezier editor + +.presBox-bezier-editor { + border: 1px solid rgb(221, 221, 221); + border-radius: 4px; +} + +.presBox-option-block { + display: flex; + flex-direction: column; + gap: 1rem; + // padding: 8px; +} + +.presBox-option-center { + align-items: center; +} + .presBox-cont { cursor: auto; position: absolute; @@ -15,6 +72,29 @@ //overflow: hidden; transition: 0.7s opacity ease; + .presBox-chatbox { + position: fixed; + bottom: 8px; + left: 8px; + width: calc(100% - 16px); + min-height: 100px; + border-radius: 16px; + padding: 16px; + gap: 8px; + z-index: 999; + display: flex; + flex-direction: column; + justify-content: space-between; + background-color: #ffffff; + box-shadow: 0 2px 5px #7474748d; + + .pres-chatbox { + outline: none; + border: none; + resize: none; + } + } + .presBox-listCont { position: relative; height: calc(100% - 67px); @@ -150,6 +230,11 @@ } } +.presBox-toggles { + display: flex; + overflow-x: auto; +} + .presBox-ribbon { position: relative; display: inline; @@ -158,7 +243,9 @@ transition: 0.7s; .ribbon-doubleButton { - display: inline-flex; + display: flex; + justify-content: space-between; + align-items: center; } .presBox-reactiveGrid { @@ -186,16 +273,18 @@ .ribbon-property { font-size: 11; font-weight: 200; - height: 20; - display: flex; - margin-left: 5px; - margin-top: 5px; - margin-bottom: 5px; - width: max-content; - justify-content: center; - align-items: center; - padding-right: 10px; - padding-left: 10px; + padding: 8px; + border-radius: 4px; + // height: 20; + // display: flex; + // margin-left: 5px; + // margin-top: 5px; + // margin-bottom: 5px; + // width: max-content; + // justify-content: center; + // align-items: center; + // padding-right: 10px; + // padding-left: 10px; } .ribbon-propertyUpDown { @@ -392,11 +481,16 @@ } .presBox-input { - width: 30; - height: 100%; - background: none; border: none; - text-align: right; + background-color: transparent; + width: 40; + // padding: 8px; + // border-radius: 4px; + // width: 30; + // height: 100%; + // background: none; + // border: none; + // text-align: right; } .presBox-input:focus { @@ -606,15 +700,14 @@ background-color: $white; display: flex; color: $black; - margin-top: 5px; - margin-bottom: 5px; border-radius: 5px; - margin-right: 5px; width: max-content; justify-content: center; align-items: center; padding-right: 10px; padding-left: 10px; + margin: 4px; + text-wrap: nowrap; } .ribbon-toggle.active { @@ -638,7 +731,7 @@ grid-template-rows: max-content auto; justify-self: center; margin-top: 10px; - padding-right: 10px; + // padding-right: 10px; letter-spacing: normal; width: 100%; height: max-content; diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index cd9fec839..d3abaddd9 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -29,14 +29,27 @@ import { CollectionFreeFormView, MarqueeViewBounds } from '../../collections/col import { CollectionStackedTimeline } from '../../collections/CollectionStackedTimeline'; import { CollectionView } from '../../collections/CollectionView'; import { TreeView } from '../../collections/TreeView'; -import { ViewBoxBaseComponent } from '../../DocComponent'; +import { ViewBoxBaseComponent, ViewBoxInterface } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; import { LightboxView } from '../../LightboxView'; import { DocumentView, OpenWhere, OpenWhereMod } from '../DocumentView'; import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView'; import { ScriptingBox } from '../ScriptingBox'; import './PresBox.scss'; +import ReactLoading from 'react-loading'; import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums'; +import ReactTextareaAutosize from 'react-textarea-autosize'; +import { Button, Dropdown, DropdownType, IconButton, Toggle, ToggleType, Type } from 'browndash-components'; +import { BiMicrophone, BiX } from 'react-icons/bi'; +import { AiOutlineSend } from 'react-icons/ai'; +import { gptSlideProperties, gptTrailSlideCustomization } from '../../../apis/gpt/customization'; +import { DictationManager } from '../../../util/DictationManager'; +import CubicBezierEditor, { TIMING_DEFAULT_MAPPINGS } from './CubicBezierEditor'; +import Slider from '@mui/material/Slider'; +import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp, FaCompressArrowsAlt } from 'react-icons/fa'; +import SpringAnimationPreview from './SlideEffectPreview'; +import { effectTimings, SpringType, springMappings, effectItems, easeItems, movementItems, SpringSettings } from './SpringUtils'; + export interface pinDataTypes { scrollable?: boolean; dataviz?: number[]; @@ -104,7 +117,82 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @observable _treeViewMap: Map<Doc, number> = new Map(); @observable _presKeyEvents: boolean = false; @observable _forceKeyEvents: boolean = false; - @computed get isTreeOrStack() { + + // GPT + private _inputref: HTMLTextAreaElement | null = null; + @observable chatActive: boolean = false; + @observable chatInput: string = ''; + public slideToModify: Doc | null = null; + @observable isRecording: boolean = false; + @observable isLoading: boolean = false; + + @action + setChatInput = (input: string) => { + this.chatInput = input; + }; + + @action + setIsLoading = (isLoading: boolean) => { + this.isLoading = isLoading; + }; + + @action + public setChatActive = (active: boolean) => { + this.toggleProperties(); + }; + + @action + public setIsRecording = (isRecording: boolean) => { + this.isRecording = isRecording; + }; + + // Easing function variables + + @observable easeDropdownVal = 'ease'; + + // @observable bezierControlPoints: { p1: number[]; p2: number[] } = { p1: [0.67, 0.2], p2: [0.37, 0.88] }; + @action setBezierControlPoints = (newPoints: { p1: number[]; p2: number[] }) => { + // this.bezierControlPoints = newPoints; + this.setEaseFunc(this.activeItem, `cubic-bezier(${newPoints.p1[0]}, ${newPoints.p1[1]}, ${newPoints.p2[0]}, ${newPoints.p2[1]})`); + }; + + @computed + get currCPoints() { + let strPoints = this.activeItem.presEaseFunc ? StrCast(this.activeItem.presEaseFunc) : 'ease'; + if (!strPoints.startsWith('cubic')) { + switch (StrCast(this.activeItem.presEaseFunc)) { + case 'linear': + strPoints = 'cubic-bezier(0.0, 0.0, 1.0, 1.0)'; + break; + case 'ease': + strPoints = 'cubic-bezier(0.25, 0.1, 0.25, 1.0)'; + break; + case 'ease-in': + strPoints = 'cubic-bezier(0.42, 0, 1.0, 1.0)'; + break; + case 'ease-out': + strPoints = 'cubic-bezier(0, 0, 0.58, 1.0)'; + break; + case 'ease-in-out': + strPoints = 'cubic-bezier(0.42, 0, 0.58, 1.0)'; + break; + default: + strPoints = 'cubic-bezier(0.25, 0.1, 0.25, 1.0)'; + } + } + const components = strPoints + .split('(')[1] + .split(')')[0] + .split(',') + .map(elem => parseFloat(elem)); + return { + p1: [components[0], components[1]], + p2: [components[2], components[3]], + }; + } + + @computed + get isTreeOrStack() { return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(StrCast(this.layoutDoc._type_collection) as any); } @computed get isTree() { @@ -185,7 +273,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }, { fireImmediately: true } ); - this._props.setContentViewBox?.(this); + // Casted to viewboxinterface + this._props.setContentViewBox?.(this as ViewBoxInterface); this._unmounting = false; this.turnOffEdit(true); this._disposers.selection = reaction( @@ -227,6 +316,89 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } }; + // Recording for GPT customization + + recordDictation = () => { + this.setIsRecording(true); + this.setChatInput(''); + DictationManager.Controls.listen({ + interimHandler: this.setDictationContent, + continuous: { indefinite: false }, + }).then(results => { + if (results && [DictationManager.Controls.Infringed].includes(results)) { + DictationManager.Controls.stop(); + } + }); + }; + stopDictation = (abort: boolean) => { + this.setIsRecording(false); + DictationManager.Controls.stop(!abort); + }; + + setDictationContent = (value: string) => { + console.log('Dictation value', value); + this.setChatInput(value); + // // Get the current cursor position + // if (!this._inputref) return; + // const cursorPosition = this._inputref.selectionStart; + // const currentValue = this.chatInput; + + // // split before and after + // const textBeforeCursor = currentValue.slice(0, cursorPosition); + // const textAfterCursor = currentValue.slice(cursorPosition); + + // // insertion + // const updatedText = textBeforeCursor + value + textAfterCursor; + + // // Update the textarea value + // this.setChatInput(updatedText); + + // // set new cursor pos + // const newCursorPosition = cursorPosition + value.length; + // this._inputref.setSelectionRange(newCursorPosition, newCursorPosition); + }; + + @action + customizeWithGPT = async (input: string) => { + // const testInput = 'change title to Customized Slide, transition for 2.3s with fade in effect'; + // if (!this.slideToModify) return; + this.setIsRecording(false); + this.setIsLoading(true); + + let currSlideProperties: { [key: string]: any } = {}; + for (const key of gptSlideProperties) { + if (this.activeItem[key]) { + currSlideProperties[key] = this.activeItem[key]; + } else { + // default values + 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); + console.log('GPT Result:', res); + if (typeof res === 'string') { + const resObj = JSON.parse(res); + console.log('Parsed GPT Result ', resObj); + // this.activeItem + for (let key in resObj) { + if (resObj[key]) { + this.activeItem[key] = resObj[key]; + } + } + } + } catch (err) { + console.error(err); + } + this.setIsLoading(false); + }; + //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 // No more frames in current doc and next slide is defined, therefore move to next slide @@ -278,6 +450,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // Called when the user activates 'next' - to move to the next part of the pres. trail @action next = () => { + console.log('next slide'); const progressiveReveal = (first: boolean) => { const presIndexed = Cast(this.activeItem?.presentation_indexed, 'number', null); if (presIndexed !== undefined) { @@ -735,6 +908,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { */ navigateToActiveItem = (afterNav?: () => void) => { const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; const finished = () => { afterNav?.(); @@ -764,7 +938,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { (DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.(); return; } + console.log('pres_effect_dir', StrCast(activeItem.presentation_effectDirection)); const effect = activeItem.presentation_effect && activeItem.presentation_effect !== PresEffect.None ? activeItem.presentation_effect : undefined; + // default with effect: 750ms else 500ms const presTime = NumCast(activeItem.presentation_transition, effect ? 750 : 500); const options: FocusViewOptions = { willPan: activeItem.presentation_movement !== PresMovement.None, @@ -1191,6 +1367,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @action keyEvents = (e: KeyboardEvent) => { if (e.target instanceof HTMLInputElement) return; + if (e.target instanceof HTMLTextAreaElement) return; let handled = false; const anchorNode = document.activeElement as HTMLDivElement; if (anchorNode && anchorNode.className?.includes('lm_title')) return; @@ -1459,11 +1636,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; @undoBatch - updateEffectDirection = (effect: PresEffectDirection, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presentation_effectDirection = effect)); + setEaseFunc = (activeItem: Doc, easeFunc: string) => { + activeItem.presEaseFunc = easeFunc; + this.selectedArray.forEach(doc => (doc.presEaseFunc = activeItem.presEaseFunc)); + }; + + @undoBatch + updateEffectDirection = (effectDir: PresEffectDirection, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presentation_effectDirection = effectDir)); @undoBatch updateEffect = (effect: PresEffect, bullet: boolean, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (bullet ? (doc.presBulletEffect = effect) : (doc.presentation_effect = effect))); + @undoBatch + updateEffectTiming = (activeItem: Doc, timing: SpringSettings) => { + activeItem.presEffectTiming = JSON.stringify(timing); + this.selectedArray.forEach(doc => (doc.presEffectTiming = activeItem.presEffectTiming)); + }; + static _sliderBatch: any; static endBatch = () => { PresBox._sliderBatch.end(); @@ -1471,25 +1660,46 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?: number) => { return ( - <input - type="range" - step={step} - min={min} - max={max} - value={value} - readOnly={true} - style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)`, background: SettingsManager.userColor, color: SettingsManager.userVariantColor }} - className={`toolbar-slider ${active ? '' : 'none'}`} + <div onPointerDown={e => { PresBox._sliderBatch = UndoManager.StartBatch('pres slider'); document.addEventListener('pointerup', PresBox.endBatch, true); e.stopPropagation(); - }} - onChange={e => { - e.stopPropagation(); - change(e.target.value); - }} - /> + }}> + <Slider + onChange={(e, val) => { + e.stopPropagation(); + change(val.toString()); + }} + step={parseFloat(step)} + min={parseFloat(min)} + max={parseFloat(max)} + value={value} + size="small" + defaultValue={value} + aria-label="Small" + valueLabelDisplay="auto" + /> + </div> + // <input + // type="range" + // step={step} + // min={min} + // max={max} + // value={value} + // readOnly={true} + // style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)`, background: SettingsManager.userColor, color: SettingsManager.userVariantColor }} + // className={`toolbar-slider ${active ? '' : 'none'}`} + // onPointerDown={e => { + // PresBox._sliderBatch = UndoManager.StartBatch('pres slider'); + // document.addEventListener('pointerup', PresBox.endBatch, true); + // e.stopPropagation(); + // }} + // onChange={e => { + // e.stopPropagation(); + // change(e.target.value); + // }} + // /> ); }; @@ -1516,7 +1726,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration); return ( <div className="presBox-ribbon"> - <div className="ribbon-doubleButton"> + <div className="presBox-toggles"> <Tooltip title={<div className="dash-tooltip">Hide before presented</div>}> <div className={`ribbon-toggle ${activeItem.presentation_hideBefore ? 'active' : ''}`} @@ -1533,7 +1743,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { Hide </div> </Tooltip> - <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}> <div className={`ribbon-toggle ${activeItem.presentation_hideAfter ? 'active' : ''}`} @@ -1555,14 +1764,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { Lightbox </div> </Tooltip> - <Tooltip title={<div className="dash-tooltip">Transition movement style</div>}> + {/* <Tooltip title={<div className="dash-tooltip">Transition movement style</div>}> <div className="ribbon-toggle" style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presEaseFunc === 'ease' ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }} onClick={() => this.updateEaseFunc(activeItem)}> {`${StrCast(activeItem.presEaseFunc, 'ease')}`} </div> - </Tooltip> + </Tooltip> */} </div> {[DocumentType.AUDIO, DocumentType.VID].includes(targetType as any as DocumentType) ? null : ( <> @@ -1681,8 +1890,33 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } return null; } + + @computed get gptDropdown() { + const activeItem = this.activeItem; + return <div></div>; + } + @computed get transitionDropdown() { const activeItem = this.activeItem; + // Retrieving spring timing properties + let timing = StrCast(activeItem.presEffectTiming); + let timingConfig: SpringSettings | undefined; + if (timing) { + timingConfig = JSON.parse(timing); + } + + if (!timingConfig) { + timingConfig = { + type: SpringType.DEFAULT, + // stiffness: 300, + // damping: 12, + // mass: 2, + stiffness: 600, + damping: 15, + mass: 1, + }; + } + const preseEffect = (effect: PresEffect) => ( <div className={`presBox-dropdownOption ${activeItem.presentation_effect === effect || (effect === PresEffect.None && !activeItem.presentation_effect) ? 'active' : ''}`} @@ -1708,159 +1942,385 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </Tooltip> ); }; + if (activeItem && this.targetDoc) { const transitionSpeed = activeItem.presentation_transition ? NumCast(activeItem.presentation_transition) / 1000 : 0.5; const zoom = NumCast(activeItem.config_zoom, 1) * 100; const effect = activeItem.presentation_effect ? activeItem.presentation_effect : PresMovement.None; + return ( - <div - className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presentation_status === PresStatus.Edit ? 'active' : ''}`} - onPointerDown={StopEvent} - onPointerUp={StopEvent} - onClick={action(e => { - e.stopPropagation(); - this._openMovementDropdown = false; - this._openEffectDropdown = false; - this._openBulletEffectDropdown = false; - })}> - <div className="ribbon-box"> - Movement - <div - className="presBox-dropdown" - onClick={action(e => { - e.stopPropagation(); - this._openMovementDropdown = !this._openMovementDropdown; - })} - style={{ - color: SettingsManager.userColor, - background: SettingsManager.userVariantColor, - borderBottomLeftRadius: this._openMovementDropdown ? 0 : 5, - border: this._openMovementDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`, - }}> - {this.movementName(activeItem)} - <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> - <div - className="presBox-dropdownOptions" - id={'presBoxMovementDropdown'} - onPointerDown={StopEvent} + <> + {/* GPT Component */} + <div className="presBox-gpt-chat"> + <p>Customize with GPT</p> + <div className="pres-chat"> + <div className="pres-chatbox-container"> + <ReactTextareaAutosize + ref={r => (this._inputref = r)} + minRows={1} + placeholder="Customize..." + className="pres-chatbox" + autoFocus={true} + value={this.chatInput} + onChange={e => { + this.setChatInput(e.target.value); + }} + onKeyDown={e => { + this.stopDictation(true); + e.stopPropagation(); + }} + /> + <IconButton + type={Type.TERT} + color={this.isRecording ? '#2bcaff' : StrCast(Doc.UserDoc().userVariantColor)} + tooltip="Record" + icon={<BiMicrophone size={'16px'} />} + onClick={() => { + if (!this.isRecording) { + this.recordDictation(); + } else { + this.stopDictation(true); + } + }} + /> + </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={StrCast(Doc.UserDoc().userVariantColor)} + onClick={() => { + this.customizeWithGPT(this.chatInput); + }} + /> + </div> + </div> + <div + className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presentation_status === PresStatus.Edit ? 'active' : ''}`} + onPointerDown={StopEvent} + onPointerUp={StopEvent} + onClick={action(e => { + e.stopPropagation(); + this._openMovementDropdown = false; + this._openEffectDropdown = false; + this._openBulletEffectDropdown = false; + })}> + <div className="presBox-option-block" style={{ padding: '16px' }}> + Movement + {/* <div + className="presBox-dropdown" + onClick={action(e => { + e.stopPropagation(); + this._openMovementDropdown = !this._openMovementDropdown; + })} style={{ color: SettingsManager.userColor, - background: SettingsManager.userBackgroundColor, - display: this._openMovementDropdown ? 'grid' : 'none', + background: SettingsManager.userVariantColor, + borderBottomLeftRadius: this._openMovementDropdown ? 0 : 5, + border: this._openMovementDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`, }}> - {presMovement(PresMovement.None)} - {presMovement(PresMovement.Center)} - {presMovement(PresMovement.Zoom)} - {presMovement(PresMovement.Pan)} - {presMovement(PresMovement.Jump)} - </div> - </div> - <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 ${SettingsManager.userColor}` }}> - <input className="presBox-input" type="number" readOnly={true} value={zoom} onChange={e => this.updateZoom(e.target.value)} />% - </div> - <div className="ribbon-propertyUpDown" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userColor }}> - <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), 0.1)}> - <FontAwesomeIcon icon={'caret-up'} /> + {this.movementName(activeItem)} + <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> + <div + className="presBox-dropdownOptions" + id={'presBoxMovementDropdown'} + onPointerDown={StopEvent} + style={{ + color: SettingsManager.userColor, + background: SettingsManager.userBackgroundColor, + display: this._openMovementDropdown ? 'grid' : 'none', + }}> + {presMovement(PresMovement.None)} + {presMovement(PresMovement.Center)} + {presMovement(PresMovement.Zoom)} + {presMovement(PresMovement.Pan)} + {presMovement(PresMovement.Jump)} </div> - <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), -0.1)}> - <FontAwesomeIcon icon={'caret-down'} /> + </div> */} + <Dropdown + color={StrCast(Doc.UserDoc().userColor)} + formLabel={'Movement'} + closeOnSelect={true} + items={movementItems} + selectedVal={this.movementName(activeItem)} + 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 ${SettingsManager.userColor}` }}> + <input className="presBox-input" type="number" readOnly={true} value={zoom} onChange={e => this.updateZoom(e.target.value)} />% </div> + {/* <div className="ribbon-propertyUpDown" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userColor }}> + <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), 0.1)}> + <FontAwesomeIcon icon={'caret-up'} /> + </div> + <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), -0.1)}> + <FontAwesomeIcon icon={'caret-down'} /> + </div> + </div> */} </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 ${SettingsManager.userColor}` }}> - <input className="presBox-input" type="number" readOnly={true} value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s - </div> - <div className="ribbon-propertyUpDown" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userColor }}> - <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), 1000)}> - <FontAwesomeIcon icon={'caret-up'} /> - </div> - <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), -1000)}> - <FontAwesomeIcon icon={'caret-down'} /> + {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 ${SettingsManager.userColor}` }}> + <input className="presBox-input" type="number" readOnly={true} value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s </div> + {/* <div className="ribbon-propertyUpDown" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userColor }}> + <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), 1000)}> + <FontAwesomeIcon icon={'caret-up'} /> + </div> + <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), -1000)}> + <FontAwesomeIcon icon={'caret-down'} /> + </div> + </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 */} + <div className="presBox-option-block presBox-option-center"> + <Dropdown + color={StrCast(Doc.UserDoc().userColor)} + formLabel={'Easing Function'} + closeOnSelect={true} + items={easeItems} + selectedVal={this.activeItem.presEaseFunc ? (StrCast(this.activeItem.presEaseFunc).startsWith('cubic') ? 'custom' : StrCast(this.activeItem.presEaseFunc)) : 'ease'} + setSelectedVal={val => { + if (typeof val === 'string') { + if (val !== 'custom') { + this.setEaseFunc(this.activeItem, val); + } else { + this.setEaseFunc(this.activeItem, TIMING_DEFAULT_MAPPINGS.ease); + } + } + }} + dropdownType={DropdownType.SELECT} + type={Type.TERT} + /> + {/* Custom */} + <p className="presBox-submenu-label">Custom Timing Function</p> + <CubicBezierEditor setFunc={this.setBezierControlPoints} currPoints={this.currCPoints} easeFunc={StrCast(this.activeItem.presEaseFunc)} /> </div> </div> - {PresBox.inputter('0.1', '0.1', '100', 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> - </div> - <div className="ribbon-box"> - Effects - <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> - <div className="presBox-subheading">Play Audio Annotation</div> - <input - className="presBox-checkbox" - style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} - type="checkbox" - onChange={() => (activeItem.presPlayAudio = !BoolCast(activeItem.presPlayAudio))} - checked={BoolCast(activeItem.presPlayAudio)} + <div className="presBox-option-block" style={{ padding: '16px' }}> + Effects + <Toggle + formLabel={'Play Audio Annotation'} + toggleType={ToggleType.SWITCH} + toggleStatus={BoolCast(activeItem.presPlayAudio)} + onClick={() => (activeItem.presPlayAudio = !BoolCast(activeItem.presPlayAudio))} + color={SettingsManager.userColor} /> - </div> - <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> - <div className="presBox-subheading">Zoom Text Selections</div> - <input - className="presBox-checkbox" - style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} - type="checkbox" - onChange={() => (activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText))} - checked={BoolCast(activeItem.presentation_zoomText)} + {/* <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> + <div className="presBox-subheading">Play Audio Annotation</div> + <input + className="presBox-checkbox" + style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} + type="checkbox" + onChange={() => (activeItem.presPlayAudio = !BoolCast(activeItem.presPlayAudio))} + checked={BoolCast(activeItem.presPlayAudio)} + /> + </div> */} + <Toggle + formLabel={'Zoom Text Selections'} + toggleType={ToggleType.SWITCH} + toggleStatus={BoolCast(activeItem.presentation_zoomText)} + onClick={() => (activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText))} + color={SettingsManager.userColor} /> - </div> - <div - className="presBox-dropdown" - onClick={action(e => { - e.stopPropagation(); - this._openEffectDropdown = !this._openEffectDropdown; - })} - style={{ - color: SettingsManager.userColor, - background: SettingsManager.userVariantColor, - borderBottomLeftRadius: this._openEffectDropdown ? 0 : 5, - border: this._openEffectDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`, - }}> - {effect?.toString()} - <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> - <div - className="presBox-dropdownOptions" - id={'presBoxMovementDropdown'} + {/* <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> + <div className="presBox-subheading">Zoom Text Selections</div> + <input + className="presBox-checkbox" + style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }} + type="checkbox" + onChange={() => (activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText))} + checked={BoolCast(activeItem.presentation_zoomText)} + /> + </div> */} + {/* Effect dropdown */} + <Dropdown + color={StrCast(Doc.UserDoc().userColor)} + formLabel={'Slide Effect'} + closeOnSelect={true} + items={effectItems} + selectedVal={effect?.toString()} + setSelectedVal={val => { + this.updateEffect(val as PresEffect, false); + }} + dropdownType={DropdownType.SELECT} + type={Type.TERT} + /> + {/* <div + className="presBox-dropdown" + onClick={action(e => { + e.stopPropagation(); + this._openEffectDropdown = !this._openEffectDropdown; + })} style={{ color: SettingsManager.userColor, - background: SettingsManager.userBackgroundColor, - display: this._openEffectDropdown ? 'grid' : 'none', + background: SettingsManager.userVariantColor, + borderBottomLeftRadius: this._openEffectDropdown ? 0 : 5, + border: this._openEffectDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`, + }}> + {effect?.toString()} + <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> + <div + className="presBox-dropdownOptions" + id={'presBoxMovementDropdown'} + style={{ + color: SettingsManager.userColor, + background: SettingsManager.userBackgroundColor, + display: this._openEffectDropdown ? 'grid' : 'none', + }} + onPointerDown={e => e.stopPropagation()}> + {Object.values(PresEffect) + .filter(v => isNaN(Number(v))) + .map(effect => preseEffect(effect))} + </div> + </div> */} + {/* Effect direction */} + <div className="ribbon-doubleButton" style={{ display: effect === PresEffectDirection.None ? 'none' : 'inline-flex' }}> + <div className="presBox-subheading">Effect direction</div> + <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}> + {StrCast(this.activeItem.presentation_effectDirection)} + </div> + </div> + <div className="presBox-icon-list"> + <IconButton + type={Type.TERT} + color={activeItem.presentation_effectDirection === PresEffectDirection.Center ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor} + tooltip="Center" + icon={<FaCompressArrowsAlt size={'16px'} />} + onClick={() => this.updateEffectDirection(PresEffectDirection.Center)} + /> + <IconButton + type={Type.TERT} + color={activeItem.presentation_effectDirection === PresEffectDirection.Left ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor} + tooltip="Left" + icon={<FaArrowRight size={'16px'} />} + onClick={() => this.updateEffectDirection(PresEffectDirection.Left)} + /> + <IconButton + type={Type.TERT} + color={activeItem.presentation_effectDirection === PresEffectDirection.Right ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor} + tooltip="Right" + icon={<FaArrowLeft size={'16px'} />} + onClick={() => this.updateEffectDirection(PresEffectDirection.Right)} + /> + <IconButton + type={Type.TERT} + color={activeItem.presentation_effectDirection === PresEffectDirection.Top ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor} + tooltip="Top" + icon={<FaArrowDown size={'16px'} />} + onClick={() => this.updateEffectDirection(PresEffectDirection.Top)} + /> + <IconButton + type={Type.TERT} + color={activeItem.presentation_effectDirection === PresEffectDirection.Bottom ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor} + tooltip="Bottom" + icon={<FaArrowUp size={'16px'} />} + onClick={() => this.updateEffectDirection(PresEffectDirection.Bottom)} + /> + </div> + {/* <div className="effectDirection" style={{ display: effect === PresEffectDirection.None ? 'none' : 'grid', width: 40 }}> + {presDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})} + {presDirection(PresEffectDirection.Right, 'angle-left', 3, 2, {})} + {presDirection(PresEffectDirection.Top, 'angle-down', 2, 1, {})} + {presDirection(PresEffectDirection.Bottom, 'angle-up', 2, 3, {})} + {presDirection(PresEffectDirection.Center, '', 2, 2, { width: 10, height: 10, alignSelf: 'center' })} + </div> */} + {/* Effect spring settings */} + <Dropdown + color={StrCast(Doc.UserDoc().userColor)} + formLabel={'Effect Timing'} + closeOnSelect={true} + items={effectTimings} + selectedVal={timingConfig.type} + setSelectedVal={val => { + console.log('effect timing', val); + this.updateEffectTiming(activeItem, { + type: val as SpringType, + ...springMappings[val], + }); }} - onPointerDown={e => e.stopPropagation()}> - {Object.values(PresEffect) - .filter(v => isNaN(Number(v))) - .map(effect => preseEffect(effect))} + dropdownType={DropdownType.SELECT} + type={Type.TERT} + /> + <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, stiffness: val as number }); + }} + valueLabelDisplay="auto" + /> </div> - </div> - <div className="ribbon-doubleButton" style={{ display: effect === PresEffectDirection.None ? 'none' : 'inline-flex' }}> - <div className="presBox-subheading">Effect direction</div> - <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}> - {StrCast(this.activeItem.presentation_effectDirection)} + <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, 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, mass: val as number }); + }} + valueLabelDisplay="auto" + /> + </div> + <SpringAnimationPreview tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}> + <div style={{ width: '40px', height: '40px', backgroundColor: '#2e96ff', borderRadius: '4px' }}></div> + </SpringAnimationPreview> </div> - <div className="effectDirection" style={{ display: effect === PresEffectDirection.None ? 'none' : 'grid', width: 40 }}> - {presDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})} - {presDirection(PresEffectDirection.Right, 'angle-left', 3, 2, {})} - {presDirection(PresEffectDirection.Top, 'angle-down', 2, 1, {})} - {presDirection(PresEffectDirection.Bottom, 'angle-up', 2, 3, {})} - {presDirection(PresEffectDirection.Center, '', 2, 2, { width: 10, height: 10, alignSelf: 'center' })} - </div> - </div> - <div className="ribbon-final-box"> - <div className="ribbon-final-button-hidden" onClick={() => this.applyTo(this.childDocs)}> - Apply to all + <div className="ribbon-final-box"> + <div className="ribbon-final-button-hidden" onClick={() => this.applyTo(this.childDocs)}> + Apply to all + </div> </div> </div> - </div> + </> ); } } @@ -2625,7 +3085,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { /> ) : null} </div> - {/* { // if the document type is a presentation, then the collection stacking view has a "+ new slide" button at the bottom of the stack <Tooltip title={<div className="dash-tooltip">{'Click on document to pin to presentaiton or make a marquee selection to pin your desired view'}</div>}> @@ -2635,6 +3094,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </Tooltip> } */} </div> + {/* presbox chatbox */} + {this.chatActive && <div className="presBox-chatbox"></div>} </div> ); } diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 5b2aa1cde..4abfc5a94 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -512,6 +512,19 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </Tooltip> ); + items.push( + <Tooltip key="customize" title={<div className="dash-tooltip">Customize</div>}> + <div + className={'slideButton'} + onClick={() => { + PresBox.Instance.setChatActive(true); + PresBox.Instance.slideToModify = this.Document; + PresBox.Instance.recordDictation(); + }}> + <FontAwesomeIcon icon={'message'} onPointerDown={e => e.stopPropagation()} /> + </div> + </Tooltip> + ); return items; } diff --git a/src/client/views/nodes/trails/SlideEffect.tsx b/src/client/views/nodes/trails/SlideEffect.tsx new file mode 100644 index 000000000..7e2985a6a --- /dev/null +++ b/src/client/views/nodes/trails/SlideEffect.tsx @@ -0,0 +1,206 @@ +import { Button } from '@mui/material'; +import { useSpring, animated, easings, to } from '@react-spring/web'; +import React, { useEffect, useState } from 'react'; +import { PresEffectDirection } from './PresEnums'; + +export enum EffectType { + ZOOM = 'zoom', + FADE = 'fade', + BOUNCE = 'bounce', + ROTATE = 'rotate', +} + +interface Props { + dir: PresEffectDirection; + effectType: EffectType; + friction: number; + tension: number; + mass: number; + children: React.ReactNode; +} + +// TODO: add visibility detector when the slide comes into view +export default function SpringAnimation({ dir, friction, tension, mass, effectType, children }: Props) { + const [springs, api] = useSpring( + () => ({ + from: { + x: 0, + y: 0, + opacity: 0, + scale: 1, + }, + config: { + tension: tension, + friction: friction, + mass: mass, + }, + onStart: () => { + console.log('started'); + }, + onRest: () => { + console.log('resting'); + }, + }), + [tension, friction, mass] + ); + + // Whether the animation is currently playing + const [animating, setAnimating] = useState(false); + + const zoomConfig = { + from: { + scale: 0, + x: 0, + y: 0, + opacity: 1, + // opacity: 0, + }, + to: { + scale: 1, + x: 0, + y: 0, + opacity: 1, + // opacity: 1, + config: { + tension: tension, + friction: friction, + mass: mass, + }, + }, + }; + + const fadeConfig = { + from: { + opacity: 0, + scale: 1, + x: 0, + y: 0, + }, + to: { + opacity: 1, + scale: 1, + x: 0, + y: 0, + config: { + tension: tension, + friction: friction, + mass: mass, + }, + }, + }; + + const rotateConfig = { + from: { + x: 0, + }, + to: { + x: 360, + config: { + tension: tension, + friction: friction, + mass: mass, + }, + }, + }; + + const getBounceConfigFrom = () => { + switch (dir) { + case PresEffectDirection.Left: + return { + from: { + opacity: 0, + x: -200, + y: 0, + }, + }; + case PresEffectDirection.Right: + return { + from: { + opacity: 0, + x: 200, + y: 0, + }, + }; + case PresEffectDirection.Top: + return { + from: { + opacity: 0, + x: 0, + y: -200, + }, + }; + case PresEffectDirection.Bottom: + return { + from: { + opacity: 0, + x: 0, + y: 200, + }, + }; + default: + return { + from: { + opacity: 0, + x: 0, + y: 0, + }, + }; + } + }; + + const bounceConfig = { + ...getBounceConfigFrom(), + to: [ + { + opacity: 1, + x: 0, + y: 0, + config: { + tension: tension, + friction: friction, + mass: mass, + }, + }, + ], + }; + + const handleClick = () => { + if (effectType === EffectType.BOUNCE) { + api.start(bounceConfig); + } else if (effectType === EffectType.ZOOM) { + api.start(zoomConfig); + } else if (effectType === EffectType.ROTATE) { + api.start(rotateConfig); + } else if (effectType === EffectType.FADE) { + api.start(fadeConfig); + } + }; + + useEffect(() => { + setTimeout(() => { + handleClick(); + }, 200); + }, []); + + return ( + <div + // className="demo" + // onPointerEnter={() => { + // handleClick(); + // }} + > + {effectType !== EffectType.ROTATE ? ( + <animated.div + style={{ + // display: 'inline-block', + // transform: 'translate(50px, 50px)', + ...springs, + }}> + {children} + </animated.div> + ) : ( + <animated.div style={{ transform: to(springs.x, val => `rotate(${val}deg)`) }}>{children}</animated.div> + )} + </div> + ); +} diff --git a/src/client/views/nodes/trails/SlideEffectPreview.tsx b/src/client/views/nodes/trails/SlideEffectPreview.tsx new file mode 100644 index 000000000..aacb37b48 --- /dev/null +++ b/src/client/views/nodes/trails/SlideEffectPreview.tsx @@ -0,0 +1,134 @@ +import { Button } from '@mui/material'; +import { useSpring, animated, easings } from '@react-spring/web'; +import React, { useEffect, useState } from 'react'; + +interface Props { + friction: number; + tension: number; + mass: number; + children: React.ReactNode; +} + +export default function SpringAnimationPreview({ friction, tension, mass, children }: Props) { + const [springs, api] = useSpring( + () => ({ + from: { + x: 0, + y: 0, + opacity: 1, + scale: 1, + }, + config: { + tension: tension, + friction: friction, + mass: mass, + }, + onStart: () => { + console.log('started'); + }, + onRest: () => { + console.log('resting'); + }, + }), + [tension, friction, mass] + ); + + // Whether the animation is currently playing + const [animating, setAnimating] = useState(false); + + const zoomConfig = { + from: { + x: 0, + y: 0, + opacity: 0, + scale: 0, + }, + to: [ + { + x: 0, + y: 0, + opacity: 1, + scale: 1, + config: { + tension: tension, + friction: friction, + mass: mass, + }, + }, + { + opacity: 0, + scale: 0, + x: 0, + y: 0, + config: { + duration: 500, + easing: easings.easeInOutCubic, + }, + }, + ], + }; + + const bounceConfig = { + from: { + x: -50, + y: 0, + }, + to: [ + { + x: 50, + y: 0, + config: { + tension: tension, + friction: friction, + mass: mass, + }, + }, + { + x: -50, + y: 0, + config: { + duration: 500, + easing: easings.easeInOutCubic, + }, + }, + ], + }; + + const animate = () => { + api.start(bounceConfig); + }; + + useEffect(() => { + animate(); + }, []); + + return ( + <div + style={{ + width: '200px', + height: '150px', + borderRadius: '4px', + border: '1px solid #696969', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }} + onPointerEnter={() => { + animate(); + }}> + {/* style={{ + width: "50px", + height: "50px", + backgroundColor: "#ff6d6d", + borderRadius: "4px", + ...springs, + }} */} + <animated.div + style={{ + ...springs, + }}> + {children} + </animated.div> + </div> + ); +} diff --git a/src/client/views/nodes/trails/SpringUtils.ts b/src/client/views/nodes/trails/SpringUtils.ts new file mode 100644 index 000000000..5feb1628a --- /dev/null +++ b/src/client/views/nodes/trails/SpringUtils.ts @@ -0,0 +1,111 @@ +import { PresEffect, PresMovement } from './PresEnums'; + +// the type of slide effect timing (spring-driven) +export enum SpringType { + DEFAULT = 'default', + GENTLE = 'gentle', + BOUNCY = 'bouncy', + CUSTOM = 'custom', + QUICK = 'quick', +} + +// settings that control slide effect spring settings +export interface SpringSettings { + type: SpringType; + stiffness: number; + damping: number; + mass: number; +} + +export const easeItems = [ + { + text: 'Ease', + val: 'ease', + }, + { + text: 'Ease In', + val: 'ease-in', + }, + { + text: 'Ease Out', + val: 'ease-out', + }, + { + text: 'Ease In Out', + val: 'ease-in-out', + }, + { + text: 'Linear', + val: 'linear', + }, + { + text: 'Custom', + val: 'custom', + }, +]; + +export const movementItems = [ + { text: 'None', val: PresMovement.None }, + { text: 'Center', val: PresMovement.Center }, + { text: 'Zoom', val: PresMovement.Zoom }, + { text: 'Pan', val: PresMovement.Pan }, + { text: 'Jump', val: PresMovement.Jump }, +]; + +export const effectItems = Object.values(PresEffect) + .filter(v => isNaN(Number(v))) + .map(effect => ({ + text: effect, + val: effect, + })); + +export const effectTimings = [ + { text: 'Default', val: SpringType.DEFAULT }, + { + text: 'Gentle', + val: SpringType.GENTLE, + }, + { + text: 'Quick', + val: SpringType.QUICK, + }, + { + text: 'Bouncy', + val: SpringType.BOUNCY, + }, + { + text: 'Custom', + val: SpringType.CUSTOM, + }, +]; + +// Maps spring names to spring parameters +export const springMappings: { + [key: string]: { stiffness: number; damping: number; mass: number }; +} = { + default: { + stiffness: 600, + damping: 15, + mass: 1, + }, + gentle: { + stiffness: 100, + damping: 15, + mass: 1, + }, + quick: { + stiffness: 300, + damping: 20, + mass: 1, + }, + bouncy: { + stiffness: 600, + damping: 15, + mass: 1, + }, + custom: { + stiffness: 100, + damping: 10, + mass: 1, + }, +}; diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss index 5d966395c..48659d0e7 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.scss +++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss @@ -11,8 +11,8 @@ $highlightedText: #82e0ff; right: 10px; width: 250px; min-height: 200px; - border-radius: 15px; - padding: 15px; + border-radius: 16px; + padding: 16px; padding-bottom: 0; z-index: 999; display: flex; diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index da8a88803..42562986f 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -119,13 +119,15 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { try { let image_urls = await gptImageCall(this.imgDesc); if (image_urls && image_urls[0]) { + // need to fix this const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [image_urls[0]] }); + console.log('Result', result); + console.log('Client', result.accessPaths.agnostic.client); const source = Utils.prepend(result.accessPaths.agnostic.client); this.setImgUrls([[image_urls[0], source]]); } } catch (err) { console.log(err); - return ''; } this.setLoading(false); }; |
