diff options
Diffstat (limited to 'src')
21 files changed, 2263 insertions, 357 deletions
diff --git a/src/Utils.ts b/src/Utils.ts index 291d7c799..3b46f64f1 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -632,6 +632,7 @@ const easeFunc = (transition: 'ease' | 'linear' | undefined, currentTime: number }; export function smoothScroll(duration: number, element: HTMLElement | HTMLElement[], to: number, transition: 'ease' | 'linear' | undefined, stopper?: () => void) { + console.log('smooth scroll'); stopper?.(); const elements = element instanceof HTMLElement ? [element] : element; const starts = elements.map(element => element.scrollTop); diff --git a/src/client/apis/gpt/customization.ts b/src/client/apis/gpt/customization.ts new file mode 100644 index 000000000..7da04918d --- /dev/null +++ b/src/client/apis/gpt/customization.ts @@ -0,0 +1,133 @@ +import { openai } from './setup'; + +export enum CustomizationType { + PRES_TRAIL_SLIDE = 'trails', +} + +interface PromptInfo { + description: string; + features: { name: string; description: string; values?: string[] }[]; +} +const prompts: { [key: string]: PromptInfo } = { + trails: { + description: + 'We are customizing the properties and transition of a slide in a presentation. You are given the current properties of the slide in a json with the fields [title, presentation_transition, presentation_effect, config_zoom, presentation_effectDirection], as well as the prompt for how the user wants to change it. Return a json with the required fields: [title, presentation_transition, presentation_effect, config_zoom, presentation_effectDirection] by applying the changes in the prompt to the current state of the slide.', + features: [], + }, +}; + +// Allows you to register properties that are customizable +export const addCustomizationProperty = (type: CustomizationType, name: string, description: string, values?: string[]) => { + values ? prompts[type].features.push({ name, description, values }) : prompts[type].features.push({ name, description }); +}; + +// All the registered fields, make sure to update during registration, this +// includes most fields but is not yet fully comprehensive +export const gptSlideProperties = [ + 'title', + 'presentation_transition', + 'presEaseFunc', + 'presentation_effect', + 'presentation_effectDirection', + 'presEffectTiming', + 'config_zoom', + 'presPlayAudio', + 'presentation_zoomText', + 'presentation_hideBefore', + 'presentation_hide', + 'presentation_hideAfter', + 'presentation_openInLightbox', +]; + +// Registers slide properties +const setupPresSlideCustomization = () => { + addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'title', 'is the title/name of the slide.'); + addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_transition', 'is a number in milliseconds for how long it should take to transition/move to a slide.'); + addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presEaseFunc', 'is the easing function for the movement to the slide.', ['Ease', 'Ease In', 'Ease Out', 'Ease Out', 'Ease In Out', 'Linear']); + + addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_effect', 'is an effect applied to the slide when we transition to it.', ['None', 'Zoom', 'Fade in', 'Bounce', 'Flip', 'Rotate', 'Roll']); + addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_effectDirection', 'is what direction the effect is applied.', ['Enter from left', 'Enter from right', 'Enter from bottom', 'Enter from Top', 'Enter from center']); + addCustomizationProperty( + CustomizationType.PRES_TRAIL_SLIDE, + 'presEffectTiming', + "is a json object of the format: {type: string, stiffness: number, damping: number, mass: number}. Type is always “custom”. Controls the spring-based timing of the presentation effect animation. Stiffness, damping, and mass control the physics-based properties of spring animations. This is used to create a more natural looking timing, bouncy effects, etc. Use spring physics to adjust these parameters to match the user's description of how they want to animate the effect." + ); + + addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'config_zoom', 'is a number from 0 to 1.0 indicating the percentage we should zoom into the slide.'); + + // boolean values + addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presPlayAudio', 'is a boolean value indicating if we should play audio when we go to the slide.'); + addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_zoomText', 'is a boolean value indicating if we should zoom into text selections when we go to the slide.'); + addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_hideBefore', 'is a boolean value indicating if we should hide the slide before going to it.'); + addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_hide', 'is a boolean value indicating if we should hide the slide during the presentation.'); + addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_hideAfter', 'is a boolean value indicating if we should hide the slide after going to it.'); + addCustomizationProperty(CustomizationType.PRES_TRAIL_SLIDE, 'presentation_openInLightbox', 'is a boolean value indicating if we should open the slide in an overlay or lightbox view during the presentation.'); +}; + +setupPresSlideCustomization(); + +export const getSlideTransitionSuggestions = async (inputText: string) => { + /** + * Prompt: Generate an entrance animations from slower and gentler + * to bouncier and more high energy + * + * Format: + * { + * name: Slow Fade, Quick Flip, Springy + * effect: BOUNCE + * effectDirection: LEFT + * timingConfig: { + * } + * } + */ + + const prompt = + "I want to generate four distinct types of slide effect animations. Return a json of the form {effect: string, direction: string, stiffness: number, damping: number, mass: number}[] with four elements. Effect is the type of animation; its only possible values are ['Zoom', 'Fade in', 'Bounce', 'Flip', 'Rotate', 'Roll']. Direction is the direction that the animation starts from; its only possible values are ['Enter from left', 'Enter from right', 'Enter from bottom', 'Enter from Top', 'Enter from center']. Stiffness, damping, and mass control the physics-based properties of spring animations. This is used to create a more natural-looking timing, bouncy effects, etc. Use spring physics to adjust these parameters to animate the effect."; + + const customInput = inputText ?? 'Make them as contrasting as possible with different effects and timings ranging from gentle to energetic.'; + + try { + const response = await openai.chat.completions.create({ + model: 'gpt-4', + messages: [ + { role: 'system', content: prompt }, + { role: 'user', content: `${customInput}` }, + ], + temperature: 0, + max_tokens: 1000, + }); + return response.choices[0].message?.content; + } catch (err) { + console.log(err); + return 'Error connecting with API.'; + } +}; + +export const gptTrailSlideCustomization = async (inputText: string, properties: any | any[], applyToWhole?: boolean) => { + let prompt = prompts.trails.description; + + prompts.trails.features.forEach(feature => { + prompt += feature.name + ' ' + feature.description; + if (feature.values) { + prompt += `Its only possible values are [${feature.values.join(', ')}].`; + } + }); + + prompt += 'Set unchanged values to null and make sure you include new properties if they are specified in the prompt even if they do not exist in current properties. Please only return the json with the keys described and their values.'; + + try { + const response = await openai.chat.completions.create({ + model: 'gpt-4', + messages: [ + { role: 'system', content: prompt }, + { role: 'user', content: `Prompt: ${inputText}, Current properties: ${JSON.stringify(properties)}` }, + ], + temperature: 0, + max_tokens: 1000, + }); + return response.choices[0].message?.content; + } catch (err) { + console.log(err); + return 'Error connecting with API.'; + } +}; diff --git a/src/client/apis/gpt/setup.ts b/src/client/apis/gpt/setup.ts new file mode 100644 index 000000000..831c97eaa --- /dev/null +++ b/src/client/apis/gpt/setup.ts @@ -0,0 +1,30 @@ +// import { Configuration, OpenAIApi } from 'openai'; +import { ClientOptions, OpenAI } from 'openai'; + +export enum GPTCallType { + SUMMARY = 'summary', + COMPLETION = 'completion', + EDIT = 'edit', +} + +export type GPTCallOpts = { + model: string; + maxTokens: number; + temp: number; + prompt: string; +}; + +export const callTypeMap: { [type: string]: GPTCallOpts } = { + summary: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: 'Summarize this text in simpler terms: ' }, + edit: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: 'Reword this: ' }, + completion: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: '' }, +}; + +const configuration: ClientOptions = { + apiKey: process.env.OPENAI_KEY, + dangerouslyAllowBrowser: true, +}; + +export const openai = new OpenAI(configuration); + +// export const openai = new OpenAIApi(configuration); diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 40d28c690..8b4fec20f 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -276,10 +276,13 @@ export class DocumentManager { docContextPath.shift(); const childViewIterator = async (docView: DocumentView) => { const innerDoc = docContextPath.shift(); + + // point of interest? return { focused: false, viewSpec: innerDoc, childDocView: innerDoc && !innerDoc.layout_unrendered ? (await docView.ComponentView?.getView?.(innerDoc, options)) ?? this.getDocumentView(innerDoc) : undefined }; }; if (rootContextView) { + // try commenting out const target = await this.focusViewsInPath(rootContextView, options, childViewIterator); this.restoreDocView(target.viewSpec, target.docView, options, target.contextView ?? target.docView, targetDoc); finished?.(target.focused); @@ -293,7 +296,9 @@ export class DocumentManager { ) => { let contextView: DocumentView | undefined; // view containing context that contains target let focused = false; + let count = 0; while (true) { + console.log('Count: ', count); if (docView.Document.layout_fieldKey === 'layout_icon') { await new Promise<void>(res => docView.iconify(res)); options.didMove = true; @@ -304,6 +309,7 @@ export class DocumentManager { if (!childDocView) return { viewSpec: options.anchorDoc ?? viewSpec ?? docView.Document, docView, contextView, focused }; contextView = options.anchorDoc?.layout_unrendered && !childDocView.Document.layout_unrendered ? childDocView : docView; docView = childDocView; + count += 1; } }; 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 476b46905..840df41e7 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..a95477749 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,11 @@ 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 { FaFillDrip } from 'react-icons/fa'; + +import { LinkBox } from './nodes/LinkBox'; const _global = (window /* browser */ || global) /* node */ as any; interface PropertiesViewProps { @@ -100,6 +105,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps @observable openAppearance: boolean = true; @observable openTransform: boolean = true; @observable openFilters: boolean = false; + @observable openStyling: boolean = true; + + // GPT styling + // removed for now //Pres Trails booleans: @observable openPresTransitions: boolean = true; @@ -1644,6 +1653,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} @@ -1665,6 +1675,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps <div className="propertiesView" style={{ width: this._props.width }}> <div className="propertiesView-sectionTitle" style={{ width: this._props.width }}> Presentation + <div className="propertiesView-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/trails/')}> + <IconButton icon={<FontAwesomeIcon icon="info-circle" />} color={SettingsManager.userColor} /> + </div> </div> <div className="propertiesView-name" style={{ borderBottom: 0 }}> {this.editableTitle} 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 079a5d977..88fe10bfd 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,10 @@ import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannable import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors'; import './CollectionFreeFormView.scss'; import { MarqueeView } from './MarqueeView'; +import { PropertiesView } from '../../PropertiesView'; +import { ExtractColors } from '../../ExtractColors'; +import { extname } from 'path'; +import { RichTextField } from '../../../../fields/RichTextField'; export interface collectionFreeformViewProps { NativeWidth?: () => number; @@ -106,6 +109,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; } @@ -308,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) { @@ -1039,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); } @@ -1641,6 +1658,44 @@ 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 () => {}; + + // gpt styling + @action + gptStyling = async () => {}; + onContextMenu = (e: React.MouseEvent) => { if (this._props.isAnnotationOverlay || !ContextMenu.Instance) return; @@ -1670,6 +1725,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' }); } @@ -1780,7 +1839,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 ee7bbbdba..5897dc105 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 from './trails/SlideEffect'; +import { SpringSettings, SpringType, springMappings } from './trails/SpringUtils'; interface Window { MediaRecorder: MediaRecorder; } @@ -995,7 +997,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document * @returns a function that will wrap a JSX animation element wrapping any JSX element */ public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Opt<Doc>, root: Doc) { - const dir = presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection; + let dir = presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection; const effectProps = { left: dir === PresEffectDirection.Left, right: dir === PresEffectDirection.Right, @@ -1005,18 +1007,51 @@ 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.GENTLE, + ...springMappings['gentle'], + }; + } + + if (!dir) { + dir = PresEffectDirection.Center; + } + + const transitionTime = presEffectDoc?.presentation_transition ? NumCast(presEffectDoc?.presentation_transition) : 500; //prettier-ignore + switch (StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect))) { - default: + default: 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.Zoom: return <SlideEffect doc={root} dir={dir as PresEffectDirection} presEffect={PresEffect.Zoom} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect> + case PresEffect.Fade: return <SlideEffect doc={root} dir={dir as PresEffectDirection} presEffect={PresEffect.Fade} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect> + case PresEffect.Flip: return <SlideEffect doc={root} dir={dir as PresEffectDirection} presEffect={PresEffect.Flip} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect> + case PresEffect.Rotate: return <SlideEffect doc={root} dir={dir as PresEffectDirection} presEffect={PresEffect.Rotate} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect> + case PresEffect.Bounce: return <SlideEffect doc={root} dir={dir as PresEffectDirection} presEffect={PresEffect.Bounce} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect> + case PresEffect.Roll: return <SlideEffect doc={root} dir={dir as PresEffectDirection} presEffect={PresEffect.Roll} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect> + // keep as preset, doesn't really make sense with spring config 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 43010b2ed..34f03399a 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 { @@ -1015,6 +1017,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; breakupDictation = () => { + console.log('breakup'); if (this._editorView && this._recordingDictation) { this.stopDictation(true); this._break = true; @@ -1197,6 +1200,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); @@ -1219,6 +1241,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..a5e21259a --- /dev/null +++ b/src/client/views/nodes/trails/CubicBezierEditor.tsx @@ -0,0 +1,205 @@ +import React, { useEffect, useState } from 'react'; + +type Props = { + setFunc: (newPoints: { p1: number[]; p2: number[] }) => void; + currPoints: { p1: number[]; p2: number[] }; + easeFunc: string; +}; + +const ANIMATION_DURATION = 750; + +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)', +}; + +/** + * Visual editor for a bezier curve with draggable control points. + * */ + +const CubicBezierEditor = ({ setFunc, currPoints, easeFunc }: Props) => { + const [animating, setAnimating] = useState(false); + const [c1Down, setC1Down] = useState(false); + const [c2Down, setC2Down] = useState(false); + + const roundToHundredth = (num: number) => { + return Math.round(num * 100) / 100; + }; + + const convertToPoints = (func: string) => { + 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)'; + } + } + const components = strPoints + .split('(')[1] + .split(')')[0] + .split(',') + .map(elem => parseFloat(elem)); + return { + p1: [components[0], components[1]], + p2: [components[2], components[3]], + }; + }; + + 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 = currPoints.p1[0] + e.movementX / EDITOR_WIDTH; + if (newX < 0 || newX > 1) { + return; + } + + setFunc({ + ...currPoints, + p1: [roundToHundredth(currPoints.p1[0] + e.movementX / EDITOR_WIDTH), roundToHundredth(currPoints.p1[1] - e.movementY / EDITOR_WIDTH)], + }); + }; + + window.addEventListener('pointermove', handlePointerMove); + + return () => window.removeEventListener('pointermove', handlePointerMove); + }, [c1Down, currPoints]); + + // Sets up pointer events for moving the control points + useEffect(() => { + if (!c2Down) return; + window.addEventListener('pointerup', () => { + setC2Down(false); + }); + const handlePointerMove = (e: PointerEvent) => { + const newX = currPoints.p2[0] + e.movementX / EDITOR_WIDTH; + if (newX < 0 || newX > 1) { + return; + } + + setFunc({ + ...currPoints, + p2: [roundToHundredth(currPoints.p2[0] + e.movementX / EDITOR_WIDTH), roundToHundredth(currPoints.p2[1] - e.movementY / EDITOR_WIDTH)], + }); + }; + + window.addEventListener('pointermove', handlePointerMove); + + return () => window.removeEventListener('pointermove', handlePointerMove); + }, [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={e => { + setC1Down(false); + }} + /> + {/* Top right */} + <line + onPointerDown={e => { + e.stopPropagation(); + setC2Down(true); + }} + onPointerUp={e => { + 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={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..60d4e580d 100644 --- a/src/client/views/nodes/trails/PresBox.scss +++ b/src/client/views/nodes/trails/PresBox.scss @@ -1,5 +1,101 @@ @import '../../global/globalCssVariables.module.scss'; +.presBox-gpt-chat { + padding: 16px; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.pres-chat { + display: flex; + flex-direction: column; + gap: 8px; +} + +.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; +} + +// Effect Animations + +.presBox-effects { + display: grid; + grid-template-columns: auto auto; + gap: 8px; +} + +.presBox-effect-row { + display: flex; + gap: 8px; + margin: 4px; +} + +.presBox-effect-container { + cursor: pointer; + overflow: hidden; + width: 80px; + height: 80px; + display: flex; + justify-content: center; + align-items: center; + border: 1px solid rgb(118, 118, 118); + border-radius: 8px; +} + +.presBox-effect-demo-box { + width: 40px; + height: 40px; + border-radius: 4px; + // default bg + background-color: rgb(37, 161, 255); +} + +// Bezier editor + +.presBox-show-hide-dropdown { + cursor: pointer; + padding: 8px 0; + display: flex; + align-items: center; + gap: 4px; +} + +.presBox-bezier-editor { + border: 1px solid rgb(221, 221, 221); + border-radius: 4px; +} + +.presBox-option-block { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 16px; +} + +.presBox-option-center { + align-items: center; +} + .presBox-cont { cursor: auto; position: absolute; @@ -15,6 +111,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 +269,11 @@ } } +.presBox-toggles { + display: flex; + overflow-x: auto; +} + .presBox-ribbon { position: relative; display: inline; @@ -158,7 +282,9 @@ transition: 0.7s; .ribbon-doubleButton { - display: inline-flex; + display: flex; + justify-content: space-between; + align-items: center; } .presBox-reactiveGrid { @@ -186,16 +312,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 +520,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 +739,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 +770,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 91fdb90fc..cbbba1690 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -29,14 +29,28 @@ 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 } from 'react-icons/bi'; +import { AiOutlineSend } from 'react-icons/ai'; +import { getSlideTransitionSuggestions, 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 { effectTimings, SpringType, springMappings, effectItems, easeItems, movementItems, SpringSettings, presEffectDefaultTimings, AnimationSettings, springPreviewColors } from './SpringUtils'; +import SlideEffect from './SlideEffect'; +import { IoMdInformationCircleOutline } from 'react-icons/io'; + export interface pinDataTypes { scrollable?: boolean; dataviz?: number[]; @@ -104,7 +118,132 @@ 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; + 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 generatedAnimations: AnimationSettings[] = [ + // default presets + { + effect: PresEffect.Bounce, + direction: PresEffectDirection.Left, + stiffness: 400, + damping: 15, + mass: 1, + }, + { + effect: PresEffect.Fade, + direction: PresEffectDirection.Left, + stiffness: 100, + damping: 15, + mass: 1, + }, + { + effect: PresEffect.Flip, + direction: PresEffectDirection.Left, + stiffness: 100, + damping: 15, + mass: 1, + }, + { + effect: PresEffect.Rotate, + direction: PresEffectDirection.Left, + stiffness: 100, + damping: 15, + mass: 1, + }, + ]; + + @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 setChatActive = (active: boolean) => {}; + + @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[] }) => { + 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 +324,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 +367,85 @@ 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); + }; + + @action + customizeAnimations = async (input: string) => { + 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); + }; + + @action + customizeWithGPT = async (input: string) => { + // const testInput = 'change title to Customized Slide, transition for 2.3s with fade in effect'; + 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); + if (typeof res === 'string') { + const resObj = JSON.parse(res); + console.log('Parsed GPT Result ', resObj); + for (let 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); + }; + //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 +497,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) { @@ -736,6 +956,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { */ navigateToActiveItem = (afterNav?: () => void) => { const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; const finished = () => { afterNav?.(); @@ -765,7 +986,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, @@ -1139,7 +1362,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.updateCurrentPresentation(DocCast(doc.embedContainer)); }; - //Command click + // Command click @action multiSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => { if (!this.selectedArray.has(doc)) { @@ -1192,6 +1415,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; @@ -1460,40 +1684,56 @@ 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(); document.removeEventListener('pointerup', PresBox.endBatch, true); }; + 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> ); }; + // Applies the slide transiiton settings to all docs in the array @undoBatch applyTo = (array: Doc[]) => { this.updateMovement(this.activeItem.presentation_movement as PresMovement, true); @@ -1516,79 +1756,68 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { let duration = activeItem.presentation_duration ? NumCast(activeItem.presentation_duration) / 1000 : 0; if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration); return ( - <div className="presBox-ribbon"> - <div className="ribbon-doubleButton"> - <Tooltip title={<div className="dash-tooltip">Hide before presented</div>}> - <div - className={`ribbon-toggle ${activeItem.presentation_hideBefore ? 'active' : ''}`} - style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presentation_hideBefore ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }} - onClick={() => this.updateHideBefore(activeItem)}> - Hide before - </div> - </Tooltip> - <Tooltip title={<div className="dash-tooltip">{'Hide while presented'}</div>}> - <div - className={`ribbon-toggle ${activeItem.presentation_hide ? 'active' : ''}`} - style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presentation_hide ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }} - onClick={() => this.updateHide(activeItem)}> - Hide - </div> - </Tooltip> - - <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}> - <div - className={`ribbon-toggle ${activeItem.presentation_hideAfter ? 'active' : ''}`} - style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presentation_hideAfter ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }} - onClick={() => this.updateHideAfter(activeItem)}> - Hide after - </div> - </Tooltip> - - <Tooltip title={<div className="dash-tooltip">{'Open in lightbox view'}</div>}> - <div - className="ribbon-toggle" - style={{ - border: `solid 1px ${SettingsManager.userColor}`, - color: SettingsManager.userColor, - background: activeItem.presentation_openInLightbox ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor, - }} - onClick={() => this.updateOpenDoc(activeItem)}> - Lightbox - </div> - </Tooltip> - <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> - </div> - {[DocumentType.AUDIO, DocumentType.VID].includes(targetType as any as DocumentType) ? null : ( - <> - <div className="ribbon-doubleButton"> - <div className="presBox-subheading">Slide Duration</div> - <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}> - <input className="presBox-input" type="number" readOnly={true} value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s + <div className="presBox-option-block"> + <div className="presBox-ribbon"> + <div className="presBox-toggles"> + <Tooltip title={<div className="dash-tooltip">Hide before presented</div>}> + <div + className={`ribbon-toggle ${activeItem.presentation_hideBefore ? 'active' : ''}`} + style={{ + border: `solid 1px ${SettingsManager.userColor}`, + color: SettingsManager.userColor, + background: activeItem.presentation_hideBefore ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor, + }} + onClick={() => this.updateHideBefore(activeItem)}> + Hide before </div> - <div className="ribbon-propertyUpDown" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userColor }}> - <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), 1000)}> - <FontAwesomeIcon icon={'caret-up'} /> - </div> - <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), -1000)}> - <FontAwesomeIcon icon={'caret-down'} /> + </Tooltip> + <Tooltip title={<div className="dash-tooltip">{'Hide while presented'}</div>}> + <div + className={`ribbon-toggle ${activeItem.presentation_hide ? 'active' : ''}`} + style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presentation_hide ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }} + onClick={() => this.updateHide(activeItem)}> + Hide + </div> + </Tooltip> + <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}> + <div + className={`ribbon-toggle ${activeItem.presentation_hideAfter ? 'active' : ''}`} + style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presentation_hideAfter ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }} + onClick={() => this.updateHideAfter(activeItem)}> + Hide after + </div> + </Tooltip> + + <Tooltip title={<div className="dash-tooltip">{'Open in lightbox view'}</div>}> + <div + className="ribbon-toggle" + style={{ + border: `solid 1px ${SettingsManager.userColor}`, + color: SettingsManager.userColor, + background: activeItem.presentation_openInLightbox ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor, + }} + onClick={() => this.updateOpenDoc(activeItem)}> + Lightbox + </div> + </Tooltip> + </div> + {[DocumentType.AUDIO, DocumentType.VID].includes(targetType as any as DocumentType) ? null : ( + <> + <div className="ribbon-doubleButton"> + <div className="presBox-subheading">Slide Duration</div> + <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}> + <input className="presBox-input" type="number" readOnly={true} value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s </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> - </> - )} + {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> + </> + )} + </div> </div> ); } @@ -1606,74 +1835,76 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> ); return ( - <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 ${SettingsManager.userColor}` }} - type="checkbox" - onChange={() => { - activeItem.presentation_indexed = activeItem.presentation_indexed === undefined ? 0 : undefined; - activeItem.presentation_hideBefore = activeItem.presentation_indexed !== undefined; - const tagDoc = PresBox.targetRenderedDoc(this.activeItem); - const type = DocCast(tagDoc?.annotationOn)?.type ?? tagDoc.type; - activeItem.presentation_indexedStart = type === DocumentType.COL ? 1 : 0; - // a progressivized slide doesn't have sub-slides, but rather iterates over the data list of the target being progressivized. - // to avoid creating a new slide to correspond to each of the target's data list, we create a computedField to refernce the target's data list. - let dataField = Doc.LayoutFieldKey(tagDoc); - if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField = dataField + '_annotations'; - - if (DocCast(activeItem.presentation_targetDoc).annotationOn) activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc.annotationOn?.["${dataField}"]`); - else activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc?.["${dataField}"]`); - }} - checked={Cast(activeItem.presentation_indexed, 'number', null) !== undefined ? true : false} - /> - </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 ${SettingsManager.userColor}` }} - type="checkbox" - onChange={() => (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 ${SettingsManager.userColor}` }} - type="checkbox" - onChange={() => (activeItem.presBulletExpand = !activeItem.presBulletExpand)} - checked={BoolCast(activeItem.presBulletExpand)} - /> - </div> + <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 ${SettingsManager.userColor}` }} + type="checkbox" + onChange={() => { + activeItem.presentation_indexed = activeItem.presentation_indexed === undefined ? 0 : undefined; + activeItem.presentation_hideBefore = activeItem.presentation_indexed !== undefined; + const tagDoc = PresBox.targetRenderedDoc(this.activeItem); + const type = DocCast(tagDoc?.annotationOn)?.type ?? tagDoc.type; + activeItem.presentation_indexedStart = type === DocumentType.COL ? 1 : 0; + // a progressivized slide doesn't have sub-slides, but rather iterates over the data list of the target being progressivized. + // to avoid creating a new slide to correspond to each of the target's data list, we create a computedField to refernce the target's data list. + let dataField = Doc.LayoutFieldKey(tagDoc); + if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField = dataField + '_annotations'; + + if (DocCast(activeItem.presentation_targetDoc).annotationOn) activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc.annotationOn?.["${dataField}"]`); + else activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc?.["${dataField}"]`); + }} + checked={Cast(activeItem.presentation_indexed, 'number', null) !== undefined ? true : false} + /> + </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 ${SettingsManager.userColor}` }} + type="checkbox" + onChange={() => (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 ${SettingsManager.userColor}` }} + type="checkbox" + onChange={() => (activeItem.presBulletExpand = !activeItem.presBulletExpand)} + checked={BoolCast(activeItem.presBulletExpand)} + /> + </div> - <div className="ribbon-box"> - Bullet Effect - <div - className="presBox-dropdown" - onClick={action(e => { - e.stopPropagation(); - this._openBulletEffectDropdown = !this._openBulletEffectDropdown; - })} - style={{ - color: SettingsManager.userColor, - background: SettingsManager.userVariantColor, - borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5, - border: this._openBulletEffectDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`, - }}> - {effect?.toString()} - <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openBulletEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> + <div className="ribbon-box"> + Bullet Effect <div - className={'presBox-dropdownOptions'} - style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none', color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }} - onPointerDown={e => e.stopPropagation()}> - {Object.values(PresEffect) - .filter(v => isNaN(Number(v))) - .map(effect => bulletEffect(effect))} + className="presBox-dropdown" + onClick={action(e => { + e.stopPropagation(); + this._openBulletEffectDropdown = !this._openBulletEffectDropdown; + })} + style={{ + color: SettingsManager.userColor, + background: SettingsManager.userVariantColor, + borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5, + border: this._openBulletEffectDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`, + }}> + {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: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }} + onPointerDown={e => e.stopPropagation()}> + {Object.values(PresEffect) + .filter(v => isNaN(Number(v))) + .map(effect => bulletEffect(effect))} + </div> </div> </div> </div> @@ -1682,9 +1913,31 @@ 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; - const preseEffect = (effect: PresEffect) => ( + // Retrieving spring timing properties + let timing = StrCast(activeItem.presEffectTiming); + let timingConfig: SpringSettings | undefined; + if (timing) { + timingConfig = JSON.parse(timing); + } + + if (!timingConfig) { + timingConfig = { + type: SpringType.GENTLE, + stiffness: 100, + damping: 15, + mass: 1, + }; + } + + const presEffect = (effect: PresEffect) => ( <div className={`presBox-dropdownOption ${activeItem.presentation_effect === effect || (effect === PresEffect.None && !activeItem.presentation_effect) ? 'active' : ''}`} onPointerDown={StopEvent} @@ -1709,159 +1962,402 @@ 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; + const effect = StrCast(activeItem.presentation_effect) ? StrCast(activeItem.presentation_effect) : PresEffect.None; + const direction = StrCast(activeItem.presentation_effectDirection); + 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} - 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)} + <> + {/* 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' }}> + Customize Slide Properties{' '} + <div className="propertiesView-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/trails/#transitions')}> + <IconButton icon={<FontAwesomeIcon icon="info-circle" />} color={SettingsManager.userColor} /> </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)} />% + </span> + <div className="pres-chat"> + <div className="pres-chatbox-container"> + <ReactTextareaAutosize + placeholder="Describe how you would like to modify the slide properties." + className="pres-chatbox" + 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> - <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'} /> + <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> + {/* Movement */} + <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 + <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" readOnly={true} type="number" value={zoom} onChange={e => this.updateZoom(e.target.value)} />% </div> - <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), -0.1)}> - <FontAwesomeIcon icon={'caret-down'} /> + </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> - </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 + {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> - <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> + {/* Easing function */} + <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.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> </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> - <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)} - /> + + {/* 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> + <CubicBezierEditor setFunc={this.setBezierControlPoints} currPoints={this.currCPoints} easeFunc={StrCast(this.activeItem.presEaseFunc)} /> </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'} - 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))} + )} + + {/* 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(true); + 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={StrCast(Doc.UserDoc().userVariantColor)} + onClick={() => { + this.customizeAnimations(this.animationChat); + }} + /> </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> + + <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"> + 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 as PresEffectDirection} presEffect={elem.effect as PresEffect} tension={elem.stiffness} friction={elem.damping} mass={elem.mass} infinite> + <div className="presBox-effect-demo-box" style={{ backgroundColor: springPreviewColors[i] }}></div> + </SlideEffect> + </div> + ))} + </div> </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); + // 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 ${SettingsManager.userColor}` }}> + {StrCast(this.activeItem.presentation_effectDirection)} + </div> + </div> + <div className="presBox-icon-list"> + <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)} + /> + {effect !== PresEffect.Roll && ( + <> + <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> + </> + )} + {/* Spring settings */} + {/* No spring settings for jackinthebox (lightspeed) */} + {effect !== PresEffect.Lightspeed && ( + <> + <Dropdown + color={StrCast(Doc.UserDoc().userColor)} + formLabel={'Effect Timing'} + closeOnSelect={true} + 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" + /> + </div> + Preview Effect + <div className="presBox-option-block presBox-option-center"> + <div className="presBox-effect-container"> + <SlideEffect dir={direction as PresEffectDirection} presEffect={effect as PresEffect} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass} infinite> + <div className="presBox-effect-demo-box" style={{ backgroundColor: springPreviewColors[0] }}></div> + </SlideEffect> + </div> + </div> + </> + )} + </> + )} </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 + + {/* Toggles */} + <div className="presBox-option-block"> + <Toggle + formLabel={'Play Audio Annotation'} + toggleType={ToggleType.SWITCH} + toggleStatus={BoolCast(activeItem.presPlayAudio)} + onClick={() => (activeItem.presPlayAudio = !BoolCast(activeItem.presPlayAudio))} + color={SettingsManager.userColor} + /> + <Toggle + formLabel={'Zoom Text Selections'} + toggleType={ToggleType.SWITCH} + toggleStatus={BoolCast(activeItem.presentation_zoomText)} + onClick={() => (activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText))} + color={SettingsManager.userColor} + /> + <Button text="Apply to all" type={Type.TERT} color={StrCast(Doc.UserDoc().userVariantColor)} onClick={() => this.applyTo(this.childDocs)} /> </div> </div> - </div> + </> ); } } @@ -2032,23 +2528,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { /> <div>On slide change</div> </div> - {/* <div className="checkbox-container"> - <input className="presBox-checkbox" - type="checkbox" - onChange={() => activeItem.mediaStop = "afterSlide"} - checked={activeItem.mediaStop === "afterSlide"} - /> - <div className="checkbox-dropdown"> - After chosen slide - <select className="presBox-viewPicker" - style={{ opacity: activeItem.mediaStop === "afterSlide" && this.itemIndex !== this.childDocs.length - 1 ? 1 : 0.3 }} - onPointerDown={e => e.stopPropagation()} - onChange={this.mediaStopChanged} - value={mediaStopDocStr}> - {this.mediaStopSlides} - </select> - </div> - </div> */} </div> </div> </div> @@ -2272,6 +2751,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @action toggleProperties = () => (SettingsManager.Instance.propertiesWidth = SettingsManager.Instance.propertiesWidth > 0 ? 0 : 250); + @action + openProperties = () => { + // need to also focus slide + SettingsManager.Instance.propertiesWidth = 250; + }; + @computed get toolbar() { const propIcon = SettingsManager.Instance.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left'; const propTitle = SettingsManager.Instance.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel'; @@ -2626,7 +3111,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>}> @@ -2636,6 +3120,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 28139eb14..f78e29821 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -512,6 +512,20 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </Tooltip> ); + items.push( + <Tooltip key="customize-slide" title={<div className="dash-tooltip">Customize Slide</div>}> + <div + className={'slideButton'} + onClick={() => { + this.presBoxView?.regularSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, true, false); + PresBox.Instance.navigateToActiveItem(); + PresBox.Instance.openProperties(); + PresBox.Instance.slideToModify = this.Document; + }}> + <FontAwesomeIcon icon={'edit'} onPointerDown={e => e.stopPropagation()} /> + </div> + </Tooltip> + ); return items; } diff --git a/src/client/views/nodes/trails/SlideEffect.scss b/src/client/views/nodes/trails/SlideEffect.scss new file mode 100644 index 000000000..cc851354e --- /dev/null +++ b/src/client/views/nodes/trails/SlideEffect.scss @@ -0,0 +1,19 @@ +.flip-container { + display: flex; + align-items: center; + height: 100%; + justify-content: center; +} + +.flip-side { + position: absolute; + will-change: transform, opacity; + backface-visibility: hidden; +} + +.flip-front { +} + +.flip-back { + // background-color: rgb(223, 223, 223); +} diff --git a/src/client/views/nodes/trails/SlideEffect.tsx b/src/client/views/nodes/trails/SlideEffect.tsx new file mode 100644 index 000000000..db2ac1ea0 --- /dev/null +++ b/src/client/views/nodes/trails/SlideEffect.tsx @@ -0,0 +1,371 @@ +import { useSpring, animated, easings, to, useInView } from '@react-spring/web'; +import React, { useEffect, useState } from 'react'; +import { PresEffect, PresEffectDirection } from './PresEnums'; +import './SlideEffect.scss'; +import { Doc } from '../../../../fields/Doc'; +import { NumCast } from '../../../../fields/Types'; + +interface Props { + // pass in doc to extract width, height, bg + doc?: Doc; + dir: PresEffectDirection; + presEffect: PresEffect; + // stiffness (figma) = tension (react-spring) + tension: number; + // damping (figma) = friction (react-spring) + friction: number; + mass: number; + children: React.ReactNode; + infinite?: boolean; +} + +const DEFAULT_WIDTH = 40; +const PREVIEW_OFFSET = 60; +const ACTUAL_OFFSET = 200; +const infiniteOptions = { + loop: true, + delay: 500, +}; + +/** + * This component wraps around the doc to create an effect animation, and also wraps the preview animations + * for the effects as well. + */ +export default function SpringAnimation({ doc, dir, friction, tension, mass, presEffect, children, infinite }: Props) { + const [springs, api] = useSpring( + () => ({ + from: { + x: 0, + y: 0, + opacity: 0, + scale: 1, + }, + config: { + tension: tension, + friction: friction, + mass: mass, + }, + onStart: () => {}, + onRest: () => {}, + }), + [tension, friction, mass] + ); + const [ref, inView] = useInView({ + once: true, + }); + + // Whether the animation is currently playing + const [animating, setAnimating] = useState(false); + + const zoomConfig = { + from: { + scale: 0, + x: 0, + y: 0, + opacity: 1, + }, + to: { + scale: 1, + x: 0, + y: 0, + 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: infinite ? -PREVIEW_OFFSET : -ACTUAL_OFFSET, + y: 0, + }, + }; + case PresEffectDirection.Right: + return { + from: { + opacity: 0, + x: infinite ? PREVIEW_OFFSET : ACTUAL_OFFSET, + y: 0, + }, + }; + case PresEffectDirection.Top: + return { + from: { + opacity: 0, + x: 0, + y: infinite ? -PREVIEW_OFFSET : -ACTUAL_OFFSET, + }, + }; + case PresEffectDirection.Bottom: + return { + from: { + opacity: 0, + x: 0, + y: infinite ? PREVIEW_OFFSET : ACTUAL_OFFSET, + }, + }; + default: + // no movement for center + 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 flipConfig = { + from: { + x: 0, + }, + to: { + x: 180, + config: { + tension: tension, + friction: friction, + mass: mass, + }, + }, + }; + + // only left and right for now + const getRollConfigFrom = () => { + switch (dir) { + case PresEffectDirection.Left: + return { + from: { + opacity: 0, + x: -100, + y: -120, + }, + }; + case PresEffectDirection.Right: + return { + from: { + opacity: 0, + x: 100, + y: 120, + }, + }; + case PresEffectDirection.Top: + return { + from: { + opacity: 0, + x: -100, + y: -120, + }, + }; + case PresEffectDirection.Bottom: + return { + from: { + opacity: 0, + x: -100, + y: -120, + }, + }; + default: + // no movement for center + return { + from: { + opacity: 0, + x: 0, + y: 0, + }, + }; + } + }; + + const rollConfig = { + ...getRollConfigFrom(), + to: { + opacity: 1, + x: 0, + y: 0, + config: { + tension: tension, + friction: friction, + mass: mass, + }, + }, + }; + + const lightspeedConfig = { + from: { + opacity: 0, + }, + to: [], + }; + + // Switch animation depending on slide effect + const startAnimation = () => { + api.stop(); + let config: any = zoomConfig; + switch (presEffect) { + case PresEffect.Bounce: + config = bounceConfig; + break; + case PresEffect.Zoom: + config = zoomConfig; + break; + case PresEffect.Rotate: + config = rotateConfig; + break; + case PresEffect.Fade: + config = fadeConfig; + break; + case PresEffect.Flip: + config = flipConfig; + break; + case PresEffect.Roll: + config = rollConfig; + break; + case PresEffect.Lightspeed: + break; + default: + break; + } + + if (infinite) { + config = { ...config, ...infiniteOptions }; + } + + api.start(config); + }; + + const getRenderDoc = () => { + switch (presEffect) { + case PresEffect.Rotate: + return ( + <animated.div ref={ref} style={{ transform: to(springs.x, val => `rotate(${val}deg)`) }}> + {children} + </animated.div> + ); + case PresEffect.Flip: + return ( + // Pass in doc dimensions + <div className="flip-container" ref={ref}> + {dir === PresEffectDirection.Bottom || dir === PresEffectDirection.Top ? ( + <> + <animated.div + className={'flip-side flip-back'} + style={{ + transform: to(springs.x, val => `perspective(600px) rotateX(${val}deg)`), + width: doc ? NumCast(doc.width) : DEFAULT_WIDTH, + height: doc ? NumCast(doc.height) : DEFAULT_WIDTH, + backgroundColor: infinite ? '#a825ff' : 'rgb(223, 223, 223);', + }} + /> + <animated.div + className={'flip-side flip-front'} + style={{ transform: to(springs.x, val => `perspective(600px) rotateX(${val}deg)`), rotateX: '180deg', width: doc ? NumCast(doc.width) : DEFAULT_WIDTH, height: doc ? NumCast(doc.height) : DEFAULT_WIDTH }}> + {children} + </animated.div> + </> + ) : ( + <> + <animated.div + className={'flip-side flip-back'} + style={{ transform: to(springs.x, val => `perspective(600px) rotateY(${val}deg)`), width: doc ? NumCast(doc.width) : DEFAULT_WIDTH, height: doc ? NumCast(doc.height) : DEFAULT_WIDTH }} + /> + <animated.div + className={'flip-side flip-front'} + style={{ transform: to(springs.x, val => `perspective(600px) rotateY(${val}deg)`), rotateY: '180deg', width: doc ? NumCast(doc.width) : DEFAULT_WIDTH, height: doc ? NumCast(doc.height) : DEFAULT_WIDTH }}> + {children} + </animated.div> + </> + )} + </div> + ); + case PresEffect.Roll: + return ( + <animated.div ref={ref} style={{ opacity: springs.opacity, transform: to([springs.x, springs.y], (val, val2) => `translate3d(${val}%, 0, 0) rotate3d(0, 0, 1, ${val2}deg)`) }}> + {children} + </animated.div> + ); + default: + return ( + <animated.div + ref={ref} + style={{ + ...springs, + }}> + {children} + </animated.div> + ); + } + }; + + useEffect(() => { + if (infinite || !inView) return; + setTimeout(() => { + startAnimation(); + }, 100); + }, [inView]); + + useEffect(() => { + if (infinite) { + startAnimation(); + } + }, [presEffect, tension, friction, mass]); + + return <div>{getRenderDoc()}</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..bfb22c46a --- /dev/null +++ b/src/client/views/nodes/trails/SpringUtils.ts @@ -0,0 +1,177 @@ +import { PresEffect, PresEffectDirection, PresMovement } from './PresEnums'; + +/** + * Utilities like enums and interfaces for spring-based transitions. + */ + +export const springPreviewColors = ['rgb(37, 161, 255)', 'rgb(99, 37, 255)', 'rgb(182, 37, 255)', 'rgb(255, 37, 168)']; +// the type of slide effect timing (spring-driven) +export enum SpringType { + GENTLE = 'gentle', + QUICK = 'quick', + BOUNCY = 'bouncy', + CUSTOM = 'custom', +} + +// settings that control slide effect spring settings +export interface SpringSettings { + type: SpringType; + stiffness: number; + damping: number; + mass: number; +} + +// Overall config + +export interface AnimationSettings { + effect: PresEffect; + direction: PresEffectDirection; + stiffness: number; + damping: number; + mass: number; +} + +// Options in the movement easing dropdown +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', + }, +]; + +// Options in the movement type dropdown +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 }, +]; + +// Items in the slide effect dropdown +export const effectItems = Object.values(PresEffect) + .filter(v => isNaN(Number(v))) + .map(effect => ({ + text: effect, + val: effect, + })); + +// Maps each PresEffect to the default timing configuration +export const presEffectDefaultTimings: { + [key: string]: SpringSettings; +} = { + Zoom: { type: SpringType.GENTLE, stiffness: 100, damping: 15, mass: 1 }, + Bounce: { + type: SpringType.BOUNCY, + stiffness: 600, + damping: 15, + mass: 1, + }, + Lightspeed: { + type: SpringType.GENTLE, + stiffness: 100, + damping: 15, + mass: 1, + }, + Fade: { + type: SpringType.GENTLE, + stiffness: 100, + damping: 15, + mass: 1, + }, + Flip: { + type: SpringType.GENTLE, + stiffness: 100, + damping: 15, + mass: 1, + }, + Rotate: { + type: SpringType.GENTLE, + stiffness: 100, + damping: 15, + mass: 1, + }, + Roll: { + type: SpringType.GENTLE, + stiffness: 100, + damping: 15, + mass: 1, + }, + None: { + type: SpringType.GENTLE, + stiffness: 100, + damping: 15, + mass: 1, + }, +}; + +// Dropdown items of timings for the effect +export const effectTimings = [ + { + 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 29b1ca365..e312441ed 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -59,7 +59,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { public dataChatPrompt: string | null = null; @action public setDataJson = (text: string) => { - if (text=="") this.dataChatPrompt = ""; + if (text == '') this.dataChatPrompt = ''; this.dataJson = text; }; @@ -137,6 +137,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { let image_urls = await gptImageCall(this.imgDesc); console.log('Image urls: ', image_urls); if (image_urls && image_urls[0]) { + // need to fix this const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [image_urls[0]] }); console.log('Upload result: ', result); const source = Utils.prepend(result.accessPaths.agnostic.client); @@ -161,7 +162,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { console.error(err); } GPTPopup.Instance.setLoading(false); - } + }; generateDataAnalysis = async () => { GPTPopup.Instance.setVisible(true); @@ -173,7 +174,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { console.error(err); } GPTPopup.Instance.setLoading(false); - } + }; /** * Transfers the summarization text to a sidebar annotation text document. @@ -186,6 +187,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { _layout_autoHeight: true, }); this.addDoc(newDoc, this.sidebarId); + // newDoc.data = 'Hello world'; const anchor = AnchorMenu.Instance?.GetAnchor(undefined, false); if (anchor) { DocUtils.MakeLink(newDoc, anchor, { @@ -224,7 +226,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { */ private chatWithAI = () => { this.chatMode = true; - } + }; dataPromptChanged = action((e: React.ChangeEvent<HTMLInputElement>) => { this.dataChatPrompt = e.target.value; }); @@ -320,7 +322,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { dataAnalysisBox = () => ( <> <div> - {this.heading("ANALYSIS")} + {this.heading('ANALYSIS')} <div className="content-wrapper"> {!this.loading && (!this.done ? ( @@ -342,8 +344,8 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { </div> {!this.loading && ( <div className="btns-wrapper"> - {this.done? - this.chatMode?( + {this.done ? ( + this.chatMode ? ( <input defaultValue="" autoComplete="off" @@ -356,19 +358,26 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { placeholder="Ask GPT a question about the data..." id="search-input" className="searchBox-input" - style={{width: "100%"}} + style={{ width: '100%' }} /> - ) - :( - <> - <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} /> - <Button tooltip="Chat with AI" text="Chat with AI" onClick={this.chatWithAI} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} /> - </> + ) : ( + <> + <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} /> + <Button tooltip="Chat with AI" text="Chat with AI" onClick={this.chatWithAI} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} /> + </> + ) ) : ( <div className="summarizing"> <span>Summarizing</span> <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} /> - <Button text="Stop Animation" onClick={() => {this.setDone(true);}} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT}/> + <Button + text="Stop Animation" + onClick={() => { + this.setDone(true); + }} + color={StrCast(Doc.UserDoc().userVariantColor)} + type={Type.TERT} + /> </div> )} </div> @@ -396,7 +405,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { render() { return ( <div className="summary-box" style={{ display: this.visible ? 'flex' : 'none' }}> - {this.mode === GPTPopupMode.SUMMARY? this.summaryBox() : this.mode === GPTPopupMode.DATA? this.dataAnalysisBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : <></>} + {this.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.mode === GPTPopupMode.DATA ? this.dataAnalysisBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : <></>} </div> ); } |