aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json322
-rw-r--r--package.json5
-rw-r--r--src/client/apis/gpt/customization.ts133
-rw-r--r--src/client/apis/gpt/setup.ts30
-rw-r--r--src/client/views/ExtractColors.ts168
-rw-r--r--src/client/views/PropertiesView.scss15
-rw-r--r--src/client/views/PropertiesView.tsx133
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx15
-rw-r--r--src/client/views/nodes/DocumentView.tsx59
-rw-r--r--src/client/views/nodes/trails/CubicBezierEditor.tsx205
-rw-r--r--src/client/views/nodes/trails/PresBox.scss170
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx1061
-rw-r--r--src/client/views/nodes/trails/PresElementBox.tsx15
-rw-r--r--src/client/views/nodes/trails/SlideEffect.scss19
-rw-r--r--src/client/views/nodes/trails/SlideEffect.tsx371
-rw-r--r--src/client/views/nodes/trails/SpringUtils.ts177
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.scss4
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx21
-rw-r--r--src/fields/Doc.ts4
19 files changed, 2358 insertions, 569 deletions
diff --git a/package-lock.json b/package-lock.json
index f756563ae..60198132c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,6 +30,7 @@
"@mui/material": "^5.14.19",
"@octokit/core": "^6.0.1",
"@react-google-maps/api": "^2.19.2",
+ "@react-spring/web": "^9.7.3",
"@turf/turf": "^6.5.0",
"@types/bezier-js": "^4.1.3",
"@types/brotli": "^1.3.4",
@@ -96,6 +97,7 @@
"express-flash": "0.0.2",
"express-session": "^1.17.3",
"express-validator": "^7.0.1",
+ "extract-colors": "^4.0.2",
"ffmpeg": "0.0.4",
"file-loader": "^6.2.0",
"file-saver": "^2.0.5",
@@ -144,7 +146,7 @@
"nodemailer": "^6.9.7",
"nodemon": "^3.0.2",
"npm": "^10.2.5",
- "openai": "^4.20.1",
+ "openai": "^4.26.0",
"p-limit": "^5.0.0",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
@@ -186,6 +188,7 @@
"react-measure": "^2.5.2",
"react-resizable": "^3.0.5",
"react-select": "^5.8.0",
+ "react-textarea-autosize": "^8.5.3",
"react-type-animation": "^3.2.0",
"react-xarrows": "^2.0.2",
"readline": "^1.3.0",
@@ -6070,6 +6073,66 @@
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
+ "node_modules/@react-spring/animated": {
+ "version": "9.7.3",
+ "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.3.tgz",
+ "integrity": "sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==",
+ "dependencies": {
+ "@react-spring/shared": "~9.7.3",
+ "@react-spring/types": "~9.7.3"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@react-spring/core": {
+ "version": "9.7.3",
+ "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.3.tgz",
+ "integrity": "sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ==",
+ "dependencies": {
+ "@react-spring/animated": "~9.7.3",
+ "@react-spring/shared": "~9.7.3",
+ "@react-spring/types": "~9.7.3"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-spring/donate"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@react-spring/shared": {
+ "version": "9.7.3",
+ "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.3.tgz",
+ "integrity": "sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA==",
+ "dependencies": {
+ "@react-spring/types": "~9.7.3"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@react-spring/types": {
+ "version": "9.7.3",
+ "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.3.tgz",
+ "integrity": "sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw=="
+ },
+ "node_modules/@react-spring/web": {
+ "version": "9.7.3",
+ "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.3.tgz",
+ "integrity": "sha512-BXt6BpS9aJL/QdVqEIX9YoUy8CE6TJrU0mNCqSoxdXlIeNcEBWOfIyE6B14ENNsyQKS3wOWkiJfco0tCr/9tUg==",
+ "dependencies": {
+ "@react-spring/animated": "~9.7.3",
+ "@react-spring/core": "~9.7.3",
+ "@react-spring/shared": "~9.7.3",
+ "@react-spring/types": "~9.7.3"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/@react-stately/calendar": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/@react-stately/calendar/-/calendar-3.5.0.tgz",
@@ -9635,12 +9698,6 @@
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
"optional": true
},
- "node_modules/@types/semver": {
- "version": "7.5.8",
- "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
- "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
- "dev": true
- },
"node_modules/@types/send": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
@@ -9819,21 +9876,19 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz",
- "integrity": "sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==",
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz",
+ "integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==",
"dev": true,
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "7.8.0",
- "@typescript-eslint/type-utils": "7.8.0",
- "@typescript-eslint/utils": "7.8.0",
- "@typescript-eslint/visitor-keys": "7.8.0",
- "debug": "^4.3.4",
+ "@typescript-eslint/scope-manager": "7.9.0",
+ "@typescript-eslint/type-utils": "7.9.0",
+ "@typescript-eslint/utils": "7.9.0",
+ "@typescript-eslint/visitor-keys": "7.9.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
- "semver": "^7.6.0",
"ts-api-utils": "^1.3.0"
},
"engines": {
@@ -9853,48 +9908,15 @@
}
}
},
- "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dev": true,
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
- "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
- "dev": true,
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true
- },
"node_modules/@typescript-eslint/parser": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.8.0.tgz",
- "integrity": "sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==",
- "dependencies": {
- "@typescript-eslint/scope-manager": "7.8.0",
- "@typescript-eslint/types": "7.8.0",
- "@typescript-eslint/typescript-estree": "7.8.0",
- "@typescript-eslint/visitor-keys": "7.8.0",
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz",
+ "integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "7.9.0",
+ "@typescript-eslint/types": "7.9.0",
+ "@typescript-eslint/typescript-estree": "7.9.0",
+ "@typescript-eslint/visitor-keys": "7.9.0",
"debug": "^4.3.4"
},
"engines": {
@@ -9914,12 +9936,12 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz",
- "integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==",
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz",
+ "integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==",
"dependencies": {
- "@typescript-eslint/types": "7.8.0",
- "@typescript-eslint/visitor-keys": "7.8.0"
+ "@typescript-eslint/types": "7.9.0",
+ "@typescript-eslint/visitor-keys": "7.9.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -9930,13 +9952,13 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz",
- "integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==",
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz",
+ "integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==",
"dev": true,
"dependencies": {
- "@typescript-eslint/typescript-estree": "7.8.0",
- "@typescript-eslint/utils": "7.8.0",
+ "@typescript-eslint/typescript-estree": "7.9.0",
+ "@typescript-eslint/utils": "7.9.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -9957,9 +9979,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz",
- "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==",
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz",
+ "integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==",
"engines": {
"node": "^18.18.0 || >=20.0.0"
},
@@ -9969,12 +9991,12 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz",
- "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==",
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz",
+ "integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==",
"dependencies": {
- "@typescript-eslint/types": "7.8.0",
- "@typescript-eslint/visitor-keys": "7.8.0",
+ "@typescript-eslint/types": "7.9.0",
+ "@typescript-eslint/visitor-keys": "7.9.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -9995,24 +10017,10 @@
}
}
},
- "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
- "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
+ "version": "7.6.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
+ "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
"bin": {
"semver": "bin/semver.js"
},
@@ -10020,24 +10028,16 @@
"node": ">=10"
}
},
- "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
- },
"node_modules/@typescript-eslint/utils": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz",
- "integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==",
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz",
+ "integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
- "@types/json-schema": "^7.0.15",
- "@types/semver": "^7.5.8",
- "@typescript-eslint/scope-manager": "7.8.0",
- "@typescript-eslint/types": "7.8.0",
- "@typescript-eslint/typescript-estree": "7.8.0",
- "semver": "^7.6.0"
+ "@typescript-eslint/scope-manager": "7.9.0",
+ "@typescript-eslint/types": "7.9.0",
+ "@typescript-eslint/typescript-estree": "7.9.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -10050,45 +10050,12 @@
"eslint": "^8.56.0"
}
},
- "node_modules/@typescript-eslint/utils/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dev": true,
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@typescript-eslint/utils/node_modules/semver": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
- "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
- "dev": true,
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@typescript-eslint/utils/node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true
- },
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz",
- "integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==",
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz",
+ "integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==",
"dependencies": {
- "@typescript-eslint/types": "7.8.0",
+ "@typescript-eslint/types": "7.9.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -19297,6 +19264,11 @@
"node": ">=4"
}
},
+ "node_modules/extract-colors": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/extract-colors/-/extract-colors-4.0.6.tgz",
+ "integrity": "sha512-U+pYyQKXCSHOmtZPIEJBGLJjLDiqS+oOub2ILA3a7UGt9+IvZvwAN3hOPFjUgT+gX/apSBwP5vBgnKMlV0fy8Q=="
+ },
"node_modules/extsprintf": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
@@ -20437,9 +20409,9 @@
}
},
"node_modules/get-tsconfig": {
- "version": "4.7.3",
- "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.3.tgz",
- "integrity": "sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==",
+ "version": "4.7.5",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.5.tgz",
+ "integrity": "sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==",
"dev": true,
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
@@ -20564,9 +20536,9 @@
}
},
"node_modules/globals": {
- "version": "15.1.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-15.1.0.tgz",
- "integrity": "sha512-926gJqg+4mkxwYKiFvoomM4J0kWESfk3qfTvRL2/oc/tK/eTDBbrfcKnSa2KtfdxB5onoL7D3A3qIHQFpd4+UA==",
+ "version": "15.2.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-15.2.0.tgz",
+ "integrity": "sha512-FQ5YwCHZM3nCmtb5FzEWwdUc9K5d3V/w9mzcz8iGD1gC/aOTHc6PouYu0kkKipNJqHAT7m51sqzQjEjIP+cK0A==",
"dev": true,
"engines": {
"node": ">=18"
@@ -30299,6 +30271,22 @@
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
+ "node_modules/react-textarea-autosize": {
+ "version": "8.5.3",
+ "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz",
+ "integrity": "sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.20.13",
+ "use-composed-ref": "^1.3.0",
+ "use-latest": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/react-themeable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/react-themeable/-/react-themeable-1.1.0.tgz",
@@ -33994,14 +33982,14 @@
"integrity": "sha512-7sI4e/bZijOzyURng88oOFZCISQPTHozfE2sUu5AviFYk5QV7fYGb6YiDl+vKjF/pICA354JImBImL9XJWUvdQ=="
},
"node_modules/typescript-eslint": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.8.0.tgz",
- "integrity": "sha512-sheFG+/D8N/L7gC3WT0Q8sB97Nm573Yfr+vZFzl/4nBdYcmviBPtwGSX9TJ7wpVg28ocerKVOt+k2eGmHzcgVA==",
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.9.0.tgz",
+ "integrity": "sha512-7iTn9c10teHHCys5Ud/yaJntXZrjt3h2mrx3feJGBOLgQkF3TB1X89Xs3aVQ/GgdXRAXpk2bPTdpRwHP4YkUow==",
"dev": true,
"dependencies": {
- "@typescript-eslint/eslint-plugin": "7.8.0",
- "@typescript-eslint/parser": "7.8.0",
- "@typescript-eslint/utils": "7.8.0"
+ "@typescript-eslint/eslint-plugin": "7.9.0",
+ "@typescript-eslint/parser": "7.9.0",
+ "@typescript-eslint/utils": "7.9.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -34408,6 +34396,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/use-composed-ref": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz",
+ "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/use-isomorphic-layout-effect": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
@@ -34421,6 +34417,22 @@
}
}
},
+ "node_modules/use-latest": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.1.tgz",
+ "integrity": "sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==",
+ "dependencies": {
+ "use-isomorphic-layout-effect": "^1.1.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/use-sync-external-store": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
diff --git a/package.json b/package.json
index 0e7955d54..833bebf44 100644
--- a/package.json
+++ b/package.json
@@ -115,6 +115,7 @@
"@mui/material": "^5.14.19",
"@octokit/core": "^6.0.1",
"@react-google-maps/api": "^2.19.2",
+ "@react-spring/web": "^9.7.3",
"@turf/turf": "^6.5.0",
"@types/bezier-js": "^4.1.3",
"@types/brotli": "^1.3.4",
@@ -181,6 +182,7 @@
"express-flash": "0.0.2",
"express-session": "^1.17.3",
"express-validator": "^7.0.1",
+ "extract-colors": "^4.0.2",
"ffmpeg": "0.0.4",
"file-loader": "^6.2.0",
"file-saver": "^2.0.5",
@@ -229,7 +231,7 @@
"nodemailer": "^6.9.7",
"nodemon": "^3.0.2",
"npm": "^10.2.5",
- "openai": "^4.20.1",
+ "openai": "^4.26.0",
"p-limit": "^5.0.0",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
@@ -271,6 +273,7 @@
"react-measure": "^2.5.2",
"react-resizable": "^3.0.5",
"react-select": "^5.8.0",
+ "react-textarea-autosize": "^8.5.3",
"react-type-animation": "^3.2.0",
"react-xarrows": "^2.0.2",
"readline": "^1.3.0",
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/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 e4ca3daeb..398ed4060 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -25,7 +25,6 @@ import { GetEffectiveAcl, SharingPermissions, normalizeEmail } from '../../field
import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
import { GroupManager } from '../util/GroupManager';
import { LinkManager } from '../util/LinkManager';
-import { SettingsManager } from '../util/SettingsManager';
import { SharingManager } from '../util/SharingManager';
import { Transform } from '../util/Transform';
import { UndoManager, undoBatch, undoable } from '../util/UndoManager';
@@ -44,6 +43,7 @@ import { StyleProviderFuncType } from './nodes/FieldView';
import { OpenWhere } from './nodes/OpenWhere';
import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails';
+import { SnappingManager } from '../util/SnappingManager';
const _global = (window /* browser */ || global) /* node */ as any;
interface PropertiesViewProps {
@@ -108,6 +108,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
@observable openAppearance: boolean = true;
@observable openTransform: boolean = true;
@observable openFilters: boolean = false;
+ @observable openStyling: boolean = true;
// Pres Trails booleans:
@observable openPresTransitions: boolean = true;
@@ -222,7 +223,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
});
rows.push(
- <div className="propertiesView-field" key="newKeyValue" style={{ marginTop: '3px', backgroundColor: SettingsManager.userBackgroundColor, textAlign: 'center' }}>
+ <div className="propertiesView-field" key="newKeyValue" style={{ marginTop: '3px', backgroundColor: SnappingManager.userBackgroundColor, textAlign: 'center' }}>
<EditableView key="editableView" oneLine contents="add key:value or #tags" height={13} fontSize={10} GetValue={returnEmptyString} SetValue={this.setKeyValue} />
</div>
);
@@ -400,7 +401,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
<IconButton
icon={<FontAwesomeIcon icon="ellipsis-h" />}
size={Size.XSMALL}
- color={SettingsManager.userColor}
+ color={SnappingManager.userColor}
onClick={action(() => {
if (this.selectedDocumentView || this.selectedDoc) {
SharingManager.Instance.open(this.selectedDocumentView?.Document === this.selectedDoc ? this.selectedDocumentView : undefined, this.selectedDoc);
@@ -546,7 +547,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
<div>
<br /> Individuals with Access to this Document
</div>
- <div className="propertiesView-sharingTable" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
+ <div className="propertiesView-sharingTable" style={{ background: SnappingManager.userBackgroundColor, color: SnappingManager.userColor }}>
<div> {individualTableEntries}</div>
</div>
{groupTableEntries.length > 0 ? (
@@ -554,7 +555,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
<div>
<br /> Groups with Access to this Document
</div>
- <div className="propertiesView-sharingTable" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
+ <div className="propertiesView-sharingTable" style={{ background: SnappingManager.userBackgroundColor, color: SnappingManager.userColor }}>
<div> {groupTableEntries}</div>
</div>
</div>
@@ -576,15 +577,15 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
};
@computed get color() {
- return SettingsManager.userColor;
+ return SnappingManager.userColor;
}
@computed get backgroundColor() {
- return SettingsManager.userBackgroundColor;
+ return SnappingManager.userBackgroundColor;
}
@computed get variantColor() {
- return SettingsManager.userVariantColor;
+ return SnappingManager.userVariantColor;
}
@computed get editableTitle() {
@@ -718,7 +719,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
marginLeft: title === '∠:' ? '39px' : '',
}}>
<div className="inputBox-title"> {title} </div>
- <input className="inputBox-input" type="text" value={value} style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} onChange={e => setter(e.target.value)} onKeyDown={e => e.stopPropagation()} />
+ <input className="inputBox-input" type="text" value={value} style={{ color: SnappingManager.userColor, backgroundColor: SnappingManager.userBackgroundColor }} onChange={e => setter(e.target.value)} onKeyDown={e => e.stopPropagation()} />
<div className="inputBox-button">
<div className="inputBox-button-up" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons('up', key)))}>
<FontAwesomeIcon icon="caret-up" size="sm" />
@@ -940,7 +941,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
regInput = (key: string, value: any, setter: (val: string) => {}) => (
<div className="inputBox">
- <input className="inputBox-input" type="text" value={value} style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} onChange={e => setter(e.target.value)} />
+ <input className="inputBox-input" type="text" value={value} style={{ color: SnappingManager.userColor, backgroundColor: SnappingManager.userBackgroundColor }} onChange={e => setter(e.target.value)} />
<div className="inputBox-button">
<div className="inputBox-button-up" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons('up', key)))}>
<FontAwesomeIcon icon="caret-up" size="sm" />
@@ -1043,30 +1044,32 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
setFinalNumber = () => {
this._sliderBatch?.end();
};
- getNumber = (label: string, unit: string, min: number, max: number, number: number, setNumber: any, autorange?: number, autorangeMinVal?: number) => (
- <div key={label + (this.selectedDoc?.title ?? '')}>
- <NumberInput formLabel={label} formLabelPlacement="left" type={Type.SEC} unit={unit} fillWidth color={this.color} number={number} setNumber={setNumber} min={min} max={max} />
- <Slider
- key={label}
- onPointerDown={() => {
- this._sliderBatch = UndoManager.StartBatch('slider ' + label);
- }}
- multithumb={false}
- color={this.color}
- size={Size.XSMALL}
- min={min}
- max={max}
- autorangeMinVal={autorangeMinVal}
- autorange={autorange}
- number={number}
- unit={unit}
- decimals={1}
- setFinalNumber={this.setFinalNumber}
- setNumber={setNumber}
- fillWidth
- />
- </div>
- );
+ getNumber = (label: string, unit: string, min: number, max: number, number: number, setNumber: any, autorange?: number, autorangeMinVal?: number) => {
+ return (
+ <div key={label + (this.selectedDoc?.title ?? '')}>
+ <NumberInput formLabel={label} formLabelPlacement="left" type={Type.SEC} unit={unit} fillWidth color={this.color} number={number} setNumber={setNumber} min={min} max={max} />
+ <Slider
+ key={label}
+ onPointerDown={() => {
+ this._sliderBatch = UndoManager.StartBatch('slider ' + label);
+ }}
+ multithumb={false}
+ color={this.color}
+ size={Size.XSMALL}
+ min={min}
+ max={max}
+ autorangeMinVal={autorangeMinVal}
+ autorange={autorange}
+ number={number}
+ unit={unit}
+ decimals={1}
+ setFinalNumber={this.setFinalNumber}
+ setNumber={setNumber}
+ fillWidth
+ />
+ </div>
+ );
+ };
setVal = (func: (doc: Doc, val: number) => void) => (val: number) => this.selectedDoc && !isNaN(val) && func(this.selectedDoc, val);
@computed get transformEditor() {
@@ -1402,7 +1405,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
get editRelationship() {
return (
<input
- style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}
+ style={{ color: SnappingManager.userColor, backgroundColor: SnappingManager.userBackgroundColor }}
autoComplete="off"
id="link_relationship_input"
value={StrCast(this.selectedLink?.link_relationship)}
@@ -1420,7 +1423,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
return (
<textarea
autoComplete="off"
- style={{ textAlign: 'left', color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}
+ style={{ textAlign: 'left', color: SnappingManager.userColor, backgroundColor: SnappingManager.userBackgroundColor }}
id="link_description_input"
value={StrCast(this.selectedLink?.link_description)}
onKeyDown={this.onDescriptionKey}
@@ -1463,7 +1466,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
<div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 84px)' }}>
<p>Follow by</p>
<select
- style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}
+ style={{ color: SnappingManager.userColor, backgroundColor: SnappingManager.userBackgroundColor }}
onChange={e => this.changeFollowBehavior(e.currentTarget.value === 'Default' ? undefined : e.currentTarget.value)}
value={Cast(this.sourceAnchor?.followLinkLocation, 'string', null)}>
<option value={undefined}>Default</option>
@@ -1482,7 +1485,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
<div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 134px) 50px' }}>
<p>Animation</p>
<select
- style={{ width: '100%', gridColumn: 2, color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}
+ style={{ width: '100%', gridColumn: 2, color: SnappingManager.userColor, backgroundColor: SnappingManager.userBackgroundColor }}
onChange={e => this.changeAnimationBehavior(e.currentTarget.value)}
value={StrCast(this.sourceAnchor?.followLinkAnimEffect, 'default')}>
<option value="default">Default</option>
@@ -1644,7 +1647,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
<div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 108px) 50px' }}>
<p>Zoom %</p>
<div className="ribbon-property" style={{ display: !targZoom ? 'none' : 'inline-flex' }}>
- <input className="presBox-input" style={{ width: '100%', color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} readOnly type="number" value={zoom} />
+ <input className="presBox-input" style={{ width: '100%', color: SnappingManager.userColor, backgroundColor: SnappingManager.userBackgroundColor }} readOnly type="number" value={zoom} />
<div className="ribbon-propertyUpDown" style={{ display: 'flex', flexDirection: 'column' }}>
<div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}>
<FontAwesomeIcon icon="caret-up" />
@@ -1714,8 +1717,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
<div
className="propertiesView"
style={{
- background: SettingsManager.userBackgroundColor,
- color: SettingsManager.userColor,
+ background: SnappingManager.userBackgroundColor,
+ color: SnappingManager.userColor,
width: this._props.width,
minWidth: this._props.width,
}}>
@@ -1723,13 +1726,14 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
<div className="propertiesView-sectionTitle" style={{ width: this._props.width }}>
Properties
<div className="propertiesView-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/properties')}>
- <IconButton icon={<FontAwesomeIcon icon="info-circle" />} color={SettingsManager.userColor} />
+ <IconButton icon={<FontAwesomeIcon icon="info-circle" />} color={SnappingManager.userColor} />
</div>
</div>
</div>
<div className="propertiesView-name">{this.editableTitle}</div>
<div className="propertiesView-type"> {this.currentType} </div>
+ {/* {this.stylingSubMenu} */}
{this.fieldsSubMenu}
{this.optionsSubMenu}
{this.linksSubMenu}
@@ -1744,13 +1748,16 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
}
if (this.isPres && PresBox.Instance) {
const selectedItem: boolean = PresBox.Instance.selectedArray.size > 0;
- const type = [DocumentType.AUDIO, DocumentType.VID].find(dt => dt === DocCast(PresBox.Instance.activeItem?.annotationOn)?.type)
- ? DocCast(PresBox.Instance.activeItem?.annotationOn)?.type
+ const type = [DocumentType.AUDIO, DocumentType.VID].includes(DocCast(PresBox.Instance.activeItem?.annotationOn)?.type as any as DocumentType)
+ ? (DocCast(PresBox.Instance.activeItem?.annotationOn)?.type as any as DocumentType)
: PresBox.targetRenderedDoc(PresBox.Instance.activeItem)?.type;
return (
<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={SnappingManager.userColor} />
+ </div>
</div>
<div className="propertiesView-name" style={{ borderBottom: 0 }}>
{this.editableTitle}
@@ -1763,14 +1770,12 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
<div className="propertiesView-presentationTrails">
<div
className="propertiesView-presentationTrails-title"
- onPointerDown={action(() => {
- this.openPresTransitions = !this.openPresTransitions;
- })}
+ onPointerDown={action(() => (this.openPresTransitions = !this.openPresTransitions))}
style={{
- color: SettingsManager.userColor,
- backgroundColor: this.openPresTransitions ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
+ color: SnappingManager.userColor,
+ backgroundColor: this.openPresTransitions ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor,
}}>
- &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon="rocket" /> &nbsp; Transitions
+ &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> &nbsp; Transitions
<div className="propertiesView-presentationTrails-title-icon">
<FontAwesomeIcon icon={this.openPresTransitions ? 'caret-down' : 'caret-right'} size="lg" />
</div>
@@ -1782,14 +1787,12 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
<div className="propertiesView-presentationTrails">
<div
className="propertiesView-presentationTrails-title"
- onPointerDown={action(() => {
- this.openPresVisibilityAndDuration = !this.openPresVisibilityAndDuration;
- })}
+ onPointerDown={action(() => (this.openPresVisibilityAndDuration = !this.openPresVisibilityAndDuration))}
style={{
- color: SettingsManager.userColor,
- backgroundColor: this.openPresVisibilityAndDuration ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
+ color: SnappingManager.userColor,
+ backgroundColor: this.openPresVisibilityAndDuration ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor,
}}>
- &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon="rocket" /> &nbsp; Visibility
+ &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> &nbsp; Visibility
<div className="propertiesView-presentationTrails-title-icon">
<FontAwesomeIcon icon={this.openPresVisibilityAndDuration ? 'caret-down' : 'caret-right'} size="lg" />
</div>
@@ -1801,14 +1804,12 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
<div className="propertiesView-presentationTrails">
<div
className="propertiesView-presentationTrails-title"
- onPointerDown={action(() => {
- this.openPresProgressivize = !this.openPresProgressivize;
- })}
+ onPointerDown={action(() => (this.openPresProgressivize = !this.openPresProgressivize))}
style={{
- color: SettingsManager.userColor,
- backgroundColor: this.openPresProgressivize ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
+ color: SnappingManager.userColor,
+ backgroundColor: this.openPresProgressivize ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor,
}}>
- &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon="rocket" /> &nbsp; Progressivize
+ &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> &nbsp; Progressivize
<div className="propertiesView-presentationTrails-title-icon">
<FontAwesomeIcon icon={this.openPresProgressivize ? 'caret-down' : 'caret-right'} size="lg" />
</div>
@@ -1820,12 +1821,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
<div className="propertiesView-presentationTrails">
<div
className="propertiesView-presentationTrails-title"
- onPointerDown={action(() => {
- this.openSlideOptions = !this.openSlideOptions;
- })}
+ onPointerDown={action(() => (this.openSlideOptions = !this.openSlideOptions))}
style={{
- color: SettingsManager.userColor,
- backgroundColor: this.openSlideOptions ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
+ color: SnappingManager.userColor,
+ backgroundColor: this.openSlideOptions ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor,
}}>
&nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={type === DocumentType.AUDIO ? 'file-audio' : 'file-video'} /> &nbsp; {type === DocumentType.AUDIO ? 'Audio Options' : 'Video Options'}
<div className="propertiesView-presentationTrails-title-icon">
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 92e29e94a..069132ec3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -14,7 +14,6 @@ import { DocData, Height, Width } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { InkData, InkField, InkTool, 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, toList } from '../../../../fields/Types';
@@ -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';
class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> {
render() {
@@ -101,6 +104,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
private _eraserLock = 0;
private _keyTimer: NodeJS.Timeout | undefined; // timer for turning off transition flag when key frame change has completed. Need to clear this if you do a second navigation before first finishes, or else first timer can go off during second naviation.
+ private _presEaseFunc: string = 'ease';
+
+ @action
+ setPresEaseFunc = (easeFunc: string) => {
+ this._presEaseFunc = easeFunc;
+ };
private get isAnnotationOverlay() { return this._props.isAnnotationOverlay; } // prettier-ignore
private get scaleFieldKey() { return (this._props.viewField ?? '') + '_freeform_scale'; } // prettier-ignore
private get panXFieldKey() { return (this._props.viewField ?? '') + '_freeform_panX'; } // prettier-ignore
@@ -355,6 +364,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
// which will override any group focus. (If we allowed the group to focus, it would mark didMove even if there were no net movement)
return undefined;
}
+ if (options.easeFunc) this.setPresEaseFunc(options.easeFunc);
if (this._lightboxDoc) return undefined;
if (options.pointFocus) return this.focusOnPoint(options);
const anchorInCollection = DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]).includes(anchor);
@@ -1585,6 +1595,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
</div>
);
}
+ transitionFunc = () => (this._panZoomTransition ? `transform ${this._panZoomTransition}ms ${this._presEaseFunc}` : Cast(this.layoutDoc._viewTransition, 'string', Cast(this.Document._viewTransition, 'string', null)));
get pannableContents() {
this.incrementalRender(); // needs to happen synchronously or freshly typed text documents will flash and miss their first characters
return (
@@ -1594,7 +1605,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
isAnnotationOverlay={this.isAnnotationOverlay}
transform={this.PanZoomCenterXf}
showPresPaths={this.showPresPaths}
- transition={this.panZoomTransition}
+ transition={this.transitionFunc}
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/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 804d014a1..a2e1f399d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -6,7 +6,7 @@ import { Howl } from 'howler';
import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Bounce, Fade, Flip, JackInTheBox, Roll, Rotate, Zoom } from 'react-awesome-reveal';
+import { Fade, JackInTheBox } from 'react-awesome-reveal';
import { ClientUtils, DivWidth, isTargetChildOf as isParentOf, lightOrDark, returnFalse, returnVal, simulateMouseClick } from '../../../ClientUtils';
import { Utils, emptyFunction } from '../../../Utils';
import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../fields/Doc';
@@ -50,6 +50,14 @@ import { FocusViewOptions } from './FocusViewOptions';
import { OpenWhere, OpenWhereMod } from './OpenWhere';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
import { PresEffect, PresEffectDirection } from './trails/PresEnums';
+import SlideEffect from './trails/SlideEffect';
+import { SpringSettings, SpringType, springMappings } from './trails/SpringUtils';
+interface Window {
+ MediaRecorder: MediaRecorder;
+}
+declare class MediaRecorder {
+ constructor(e: any); // whatever MediaRecorder has
+}
export interface DocumentViewProps extends FieldViewSharedProps {
hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected
@@ -925,7 +933,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
borderRadius: this.borderRounding,
pointerEvents: this._pointerEvents === 'visiblePainted' ? 'none' : this._pointerEvents, // visible painted means that the underlying doc contents are irregular and will process their own pointer events (otherwise, the contents are expected to fill the entire doc view box so we can handle pointer events here)
}}>
- {this._componentView?.isUnstyledView?.() ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.Document[Animation])}
+ {this._componentView?.isUnstyledView?.() ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.Document[Animation], this.Document)}
{borderPath?.jsx}
</div>
);
@@ -936,8 +944,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
* @param presEffectDoc presentation effects document that specifies the animation effect parameters
* @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;
+ public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Opt<Doc>, root: Doc) {
+ let dir = (presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection) as PresEffectDirection;
+ const transitionTime = presEffectDoc?.presentation_transition ? NumCast(presEffectDoc?.presentation_transition) : 500;
const effectProps = {
left: dir === PresEffectDirection.Left,
right: dir === PresEffectDirection.Right,
@@ -947,18 +956,37 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
delay: 0,
duration: Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null)),
};
- // prettier-ignore
+
+ 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;
+ }
+
switch (StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect))) {
- 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>;
+ default:
+ case PresEffect.None: return renderDoc;
+ case PresEffect.Zoom: return <SlideEffect doc={root} dir={dir} presEffect={PresEffect.Zoom} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect>
+ // case PresEffect.Fade: return <SlideEffect doc={root} dir={dir} presEffect={PresEffect.Fade} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect>
+ case PresEffect.Fade: return <Fade {...effectProps}>{renderDoc}</Fade>
+ case PresEffect.Flip: return <SlideEffect doc={root} dir={dir} presEffect={PresEffect.Flip} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect>
+ case PresEffect.Rotate: return <SlideEffect doc={root} dir={dir} presEffect={PresEffect.Rotate} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect>
+ case PresEffect.Bounce: return <SlideEffect doc={root} dir={dir} presEffect={PresEffect.Bounce} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect>
+ case PresEffect.Roll: return <SlideEffect doc={root} dir={dir} 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>;
- case PresEffect.None:
- default: return renderDoc;
- }
+ } // prettier-ignore
}
}
@@ -1386,7 +1414,8 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
<div className="webBox-textHighlight">
<ObserverJsxParser autoCloseVoidElements key={42} onError={(e: any) => console.log('PARSE error', e)} renderInWrapper={false} jsx={StrCast(this._htmlOverlayText)} />
</div>,
- { ...(this._htmlOverlayEffect ?? {}), presentation_effect: effect ?? PresEffect.Zoom } as any as Doc
+ { ...(this._htmlOverlayEffect ?? {}), presentation_effect: effect ?? PresEffect.Zoom } as any as Doc,
+ this.Document
)}
</div>
</div>
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 75492d2f9..c718b5b3c 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -37,8 +37,20 @@ import { FocusViewOptions } from '../FocusViewOptions';
import { OpenWhere, OpenWhereMod } from '../OpenWhere';
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';
@observer
export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
public static LayoutString(fieldKey: string) {
@@ -85,7 +97,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() {
@@ -213,6 +350,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();
+ };
+
+ 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
@@ -664,6 +880,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
return;
}
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,
@@ -1109,6 +1326,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;
@@ -1394,6 +1612,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
doc.presEaseFunc = activeItem.presEaseFunc;
});
};
+
+ setEaseFunc = (activeItem: Doc, easeFunc: string) => {
+ activeItem.presEaseFunc = easeFunc;
+ this.selectedArray.forEach(doc => (doc.presEaseFunc = activeItem.presEaseFunc));
+ };
@undoBatch
updateEffectDirection = (effect: PresEffectDirection, all?: boolean) =>
@@ -1407,6 +1630,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
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();
@@ -1434,6 +1663,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
/>
);
+ // Applies the slide transiiton settings to all docs in the array
@undoBatch
applyTo = (array: Doc[]) => {
this.updateMovement(this.activeItem.presentation_movement as PresMovement, true);
@@ -1457,79 +1687,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 ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presentation_hideBefore ? SnappingManager.userVariantColor : SnappingManager.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 ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presentation_hide ? SnappingManager.userVariantColor : SnappingManager.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 ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presentation_hideAfter ? SnappingManager.userVariantColor : SnappingManager.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 ${SnappingManager.userColor}`,
- color: SnappingManager.userColor,
- background: activeItem.presentation_openInLightbox ? SnappingManager.userVariantColor : SnappingManager.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 ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presEaseFunc === 'ease' ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor }}
- onClick={() => this.updateEaseFunc(activeItem)}>
- {`${StrCast(activeItem.presEaseFunc, 'ease')}`}
- </div>
- </Tooltip>
- </div>
- {[DocumentType.AUDIO, DocumentType.VID].find(dt => dt === targetType) ? null : (
- <>
- <div className="ribbon-doubleButton">
- <div className="presBox-subheading">Slide Duration</div>
- <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}>
- <input className="presBox-input" type="number" readOnly value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s
+ <div className="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 ${SnappingManager.userColor}`,
+ color: SnappingManager.userColor,
+ background: activeItem.presentation_hideBefore ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor,
+ }}
+ onClick={() => this.updateHideBefore(activeItem)}>
+ Hide before
</div>
- <div className="ribbon-propertyUpDown" style={{ color: SnappingManager.userBackgroundColor, background: SnappingManager.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 ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presentation_hide ? SnappingManager.userVariantColor : SnappingManager.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 ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presentation_hideAfter ? SnappingManager.userVariantColor : SnappingManager.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 ${SnappingManager.userColor}`,
+ color: SnappingManager.userColor,
+ background: activeItem.presentation_openInLightbox ? SnappingManager.userVariantColor : SnappingManager.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 ${SnappingManager.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>
);
}
@@ -1548,78 +1767,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 ${SnappingManager.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 += '_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}
- />
- </div>
- <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
- <div className="presBox-subheading">Progressivize First Bullet</div>
- <input
- className="presBox-checkbox"
- style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
- type="checkbox"
- onChange={() => {
- activeItem.presentation_indexedStart = activeItem.presentation_indexedStart ? 0 : 1;
- }}
- checked={!NumCast(activeItem.presentation_indexedStart)}
- />
- </div>
- <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
- <div className="presBox-subheading">Expand Current Bullet</div>
- <input
- className="presBox-checkbox"
- style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
- type="checkbox"
- onChange={() => {
- activeItem.presBulletExpand = !activeItem.presBulletExpand;
- }}
- checked={BoolCast(activeItem.presBulletExpand)}
- />
- </div>
+ <div className="presBox-option-block">
+ <div className="presBox-ribbon">
+ <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
+ <div className="presBox-subheading">Progressivize Collection</div>
+ <input
+ className="presBox-checkbox"
+ style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
+ type="checkbox"
+ onChange={() => {
+ 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 ${SnappingManager.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 ${SnappingManager.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: SnappingManager.userColor,
- background: SnappingManager.userVariantColor,
- borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5,
- border: this._openBulletEffectDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.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: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}
- onPointerDown={e => e.stopPropagation()}>
- {Object.values(PresEffect)
- .filter(v => isNaN(Number(v)))
- .map(peffect => bulletEffect(peffect))}
+ className="presBox-dropdown"
+ onClick={action(e => {
+ e.stopPropagation();
+ this._openBulletEffectDropdown = !this._openBulletEffectDropdown;
+ })}
+ style={{
+ color: SnappingManager.userColor,
+ background: SnappingManager.userVariantColor,
+ borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5,
+ border: this._openBulletEffectDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.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: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}
+ onPointerDown={e => e.stopPropagation()}>
+ {Object.values(PresEffect)
+ .filter(v => isNaN(Number(v)))
+ .map(effect => bulletEffect(effect))}
+ </div>
</div>
</div>
</div>
@@ -1628,9 +1845,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;
- 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}
@@ -1655,163 +1894,403 @@ 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: SnappingManager.userColor,
- background: SnappingManager.userVariantColor,
- borderBottomLeftRadius: this._openMovementDropdown ? 0 : 5,
- border: this._openMovementDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.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: SnappingManager.userColor,
- background: SnappingManager.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/#slide-customization')}>
+ <IconButton icon={<FontAwesomeIcon icon="info-circle" />} color={SnappingManager.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 ${SnappingManager.userColor}` }}>
- <input className="presBox-input" type="number" readOnly 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: SnappingManager.userBackgroundColor, background: SnappingManager.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.stopDictation(true);
+ 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 ${SnappingManager.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 ${SnappingManager.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 ${SnappingManager.userColor}` }}>
- <input className="presBox-input" type="number" readOnly 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: SnappingManager.userBackgroundColor, background: SnappingManager.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">
+
+ {/* 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>
+ )}
+
+ {/* This chatbox is for getting slide effect transition suggestions from gpt and visualizing them */}
+ <div className="presBox-gpt-chat">
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 ${SnappingManager.userColor}` }}
- type="checkbox"
- onChange={() => {
- activeItem.presPlayAudio = !BoolCast(activeItem.presPlayAudio);
+ <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);
}}
- 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 ${SnappingManager.userColor}` }}
- type="checkbox"
- onChange={() => {
- activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText);
+ </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]);
}}
- checked={BoolCast(activeItem.presentation_zoomText)}
+ dropdownType={DropdownType.SELECT}
+ type={Type.TERT}
/>
+ {/* Effect direction */}
+ {/* Only applies to certain effects */}
+ {(effect === PresEffect.Flip || effect === PresEffect.Bounce || effect === PresEffect.Roll) && (
+ <>
+ <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
+ <div className="presBox-subheading">Effect direction</div>
+ <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}>
+ {StrCast(this.activeItem.presentation_effectDirection)}
+ </div>
+ </div>
+ <div className="presBox-icon-list">
+ <IconButton
+ type={Type.TERT}
+ color={activeItem.presentation_effectDirection === PresEffectDirection.Left ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor}
+ tooltip="Left"
+ icon={<FaArrowRight size={'16px'} />}
+ onClick={() => this.updateEffectDirection(PresEffectDirection.Left)}
+ />
+ <IconButton
+ type={Type.TERT}
+ color={activeItem.presentation_effectDirection === PresEffectDirection.Right ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor}
+ tooltip="Right"
+ icon={<FaArrowLeft size={'16px'} />}
+ onClick={() => this.updateEffectDirection(PresEffectDirection.Right)}
+ />
+ {effect !== PresEffect.Roll && (
+ <>
+ <IconButton
+ type={Type.TERT}
+ color={activeItem.presentation_effectDirection === PresEffectDirection.Top ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor}
+ tooltip="Top"
+ icon={<FaArrowDown size={'16px'} />}
+ onClick={() => this.updateEffectDirection(PresEffectDirection.Top)}
+ />
+ <IconButton
+ type={Type.TERT}
+ color={activeItem.presentation_effectDirection === PresEffectDirection.Bottom ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor}
+ tooltip="Bottom"
+ icon={<FaArrowUp size={'16px'} />}
+ onClick={() => this.updateEffectDirection(PresEffectDirection.Bottom)}
+ />
+ </>
+ )}
+ </div>
+ </>
+ )}
+ {/* 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="presBox-dropdown"
- onClick={action(e => {
- e.stopPropagation();
- this._openEffectDropdown = !this._openEffectDropdown;
- })}
- style={{
- color: SnappingManager.userColor,
- background: SnappingManager.userVariantColor,
- borderBottomLeftRadius: this._openEffectDropdown ? 0 : 5,
- border: this._openEffectDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.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: SnappingManager.userColor,
- background: SnappingManager.userBackgroundColor,
- display: this._openEffectDropdown ? 'grid' : 'none',
- }}
- onPointerDown={e => e.stopPropagation()}>
- {Object.values(PresEffect)
- .filter(v => isNaN(Number(v)))
- .map(presEffect => preseEffect(presEffect))}
- </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 ${SnappingManager.userColor}` }}>
- {StrCast(this.activeItem.presentation_effectDirection)}
- </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={SnappingManager.userColor}
+ />
+ <Toggle
+ formLabel={'Zoom Text Selections'}
+ toggleType={ToggleType.SWITCH}
+ toggleStatus={BoolCast(activeItem.presentation_zoomText)}
+ onClick={() => (activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText))}
+ color={SnappingManager.userColor}
+ />
+ <Button text="Apply to all" type={Type.TERT} color={StrCast(Doc.UserDoc().userVariantColor)} onClick={() => this.applyTo(this.childDocs)} />
</div>
</div>
- </div>
+ </>
);
}
return undefined;
@@ -1995,23 +2474,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>
@@ -2284,6 +2746,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
SnappingManager.SetPropertiesWidth(SnappingManager.PropertiesWidth > 0 ? 0 : 250);
};
+ @action
+ openProperties = () => {
+ // need to also focus slide
+ SnappingManager.SetPropertiesWidth(250);
+ };
+
@computed get toolbar() {
const propIcon = SnappingManager.PropertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left';
const propTitle = SnappingManager.PropertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel';
@@ -2670,7 +3138,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>}>
@@ -2680,6 +3147,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 306b98190..25adfba23 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -197,6 +197,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
* Function to drag and drop the pres element to a diferent location
*/
startDrag = (e: PointerEvent) => {
+ this.presBoxView?.regularSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, true, false);
const miniView: boolean = this.toolbarWidth <= 100;
const activeItem = this.slideDoc;
const dragArray = this.presBoxView?._dragArray ?? [];
@@ -524,6 +525,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..bad1fd8fa
--- /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 SlideEffectProps {
+ // 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 }: SlideEffectProps) {
+ const [springs, api] = useSpring(
+ () => ({
+ from: {
+ x: 0,
+ y: 0,
+ opacity: 0,
+ scale: 1,
+ },
+ config: {
+ tension,
+ friction,
+ 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 2680644ac..3f6c154bb 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -17,6 +17,8 @@ import { DocUtils } from '../../../documents/DocUtils';
import { ObservableReactComponent } from '../../ObservableReactComponent';
import { AnchorMenu } from '../AnchorMenu';
import './GPTPopup.scss';
+import { SettingsManager } from '../../../util/SettingsManager';
+import { SnappingManager } from '../../../util/SnappingManager';
export enum GPTPopupMode {
SUMMARY,
@@ -138,12 +140,9 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
try {
const imageUrls = await gptImageCall(this.imgDesc);
- console.log('Image urls: ', imageUrls);
if (imageUrls && imageUrls[0]) {
const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [imageUrls[0]] });
- console.log('Upload result: ', result);
const source = ClientUtils.prepend(result.accessPaths.agnostic.client);
- console.log('Upload source: ', source);
this.setImgUrls([[imageUrls[0], source]]);
}
} catch (err) {
@@ -203,6 +202,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, {
@@ -313,8 +313,8 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
<div className="btns-wrapper">
{this.done ? (
<>
- <IconButton tooltip="Generate Again" onClick={this.generateSummary} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />
- <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} />
+ <IconButton tooltip="Generate Again" onClick={this.generateSummary} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(SettingsManager.userVariantColor)} />
+ <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(SettingsManager.userVariantColor)} type={Type.TERT} />
</>
) : (
<div className="summarizing">
@@ -325,7 +325,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
onClick={() => {
this.setDone(true);
}}
- color={StrCast(Doc.UserDoc().userVariantColor)}
+ color={StrCast(SettingsManager.userVariantColor)}
type={Type.TERT}
/>
</div>
@@ -378,9 +378,8 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
/>
) : (
<>
- <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} />
- <Button tooltip="Create a graph to visualize the correlation results" text="Visualize" onClick={this.createVisualization} 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(SnappingManager.userVariantColor)} type={Type.TERT} />
+ <Button tooltip="Chat with AI" text="Chat with AI" onClick={this.chatWithAI} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} />
</>
)
) : (
@@ -392,7 +391,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
onClick={() => {
this.setDone(true);
}}
- color={StrCast(Doc.UserDoc().userVariantColor)}
+ color={StrCast(SnappingManager.userVariantColor)}
type={Type.TERT}
/>
</div>
@@ -413,7 +412,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
heading = (headingText: string) => (
<div className="summary-heading">
<label className="summary-text">{headingText}</label>
- {this.loading ? <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} /> : <IconButton color={StrCast(Doc.UserDoc().userVariantColor)} tooltip="close" icon={<CgClose size="16px" />} onClick={() => this.setVisible(false)} />}
+ {this.loading ? <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} /> : <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="close" icon={<CgClose size="16px" />} onClick={() => this.setVisible(false)} />}
</div>
);
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index fe044c035..2d3dddc8b 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1297,10 +1297,12 @@ export namespace Doc {
document.removeEventListener('pointerdown', linkFollowUnhighlight);
document.addEventListener('pointerdown', linkFollowUnhighlight);
if (UnhighlightTimer) clearTimeout(UnhighlightTimer);
+ const presTransition = Number(presentationEffect?.presentation_transition);
+ const duration = isNaN(presTransition) ? 5000 : presTransition;
UnhighlightTimer = window.setTimeout(() => {
linkFollowUnhighlight();
UnhighlightTimer = 0;
- }, 5000);
+ }, duration);
}
export const highlightedDocs = new ObservableSet<Doc>();