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