aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json258
-rw-r--r--package.json5
-rw-r--r--report.20230720.011057.28881.0.001.json1269
-rw-r--r--src/client/Network.ts17
-rw-r--r--src/client/apis/gpt/GPT.ts30
-rw-r--r--src/client/documents/Documents.ts2
-rw-r--r--src/client/theme.ts0
-rw-r--r--src/client/util/DragManager.ts4
-rw-r--r--src/client/util/ReportManager.scss88
-rw-r--r--src/client/util/ReportManager.tsx297
-rw-r--r--src/client/util/SettingsManager.tsx299
-rw-r--r--src/client/util/reportManager/ReportManager.scss364
-rw-r--r--src/client/util/reportManager/ReportManager.tsx409
-rw-r--r--src/client/util/reportManager/ReportManagerComponents.tsx360
-rw-r--r--src/client/util/reportManager/reportManagerSchema.ts877
-rw-r--r--src/client/util/reportManager/reportManagerUtils.ts257
-rw-r--r--src/client/views/MainView.tsx15
-rw-r--r--src/client/views/collections/TabDocView.tsx6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx11
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss12
-rw-r--r--src/client/views/global/globalScripts.ts50
-rw-r--r--src/client/views/nodes/ImageBox.tsx13
-rw-r--r--src/client/views/nodes/LoadingBox.scss4
-rw-r--r--src/client/views/nodes/WebBox.tsx5
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx93
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFill.scss96
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFill.tsx560
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFillButtons.scss4
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx48
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts87
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts11
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts128
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts15
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts9
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts16
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx172
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.scss64
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx195
-rw-r--r--src/client/views/pdf/PDFViewer.tsx4
-rw-r--r--src/client/views/topbar/TopBar.tsx2
-rw-r--r--src/fields/URLField.ts2
-rw-r--r--src/server/ApiManagers/AzureManager.ts67
-rw-r--r--src/server/DashUploadUtils.ts61
43 files changed, 5446 insertions, 840 deletions
diff --git a/package-lock.json b/package-lock.json
index 5f71c488a..c22c34b26 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,121 @@
"@jridgewell/trace-mapping": "^0.3.9"
}
},
+ "@azure/abort-controller": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz",
+ "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==",
+ "requires": {
+ "tslib": "^2.2.0"
+ }
+ },
+ "@azure/core-auth": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.4.0.tgz",
+ "integrity": "sha512-HFrcTgmuSuukRf/EdPmqBrc5l6Q5Uu+2TbuhaKbgaCpP2TfAeiNaQPAadxO+CYBRHGUzIDteMAjFspFLDLnKVQ==",
+ "requires": {
+ "@azure/abort-controller": "^1.0.0",
+ "tslib": "^2.2.0"
+ }
+ },
+ "@azure/core-http": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-3.0.2.tgz",
+ "integrity": "sha512-o1wR9JrmoM0xEAa0Ue7Sp8j+uJvmqYaGoHOCT5qaVYmvgmnZDC0OvQimPA/JR3u77Sz6D1y3Xmk1y69cDU9q9A==",
+ "requires": {
+ "@azure/abort-controller": "^1.0.0",
+ "@azure/core-auth": "^1.3.0",
+ "@azure/core-tracing": "1.0.0-preview.13",
+ "@azure/core-util": "^1.1.1",
+ "@azure/logger": "^1.0.0",
+ "@types/node-fetch": "^2.5.0",
+ "@types/tunnel": "^0.0.3",
+ "form-data": "^4.0.0",
+ "node-fetch": "^2.6.7",
+ "process": "^0.11.10",
+ "tslib": "^2.2.0",
+ "tunnel": "^0.0.6",
+ "uuid": "^8.3.0",
+ "xml2js": "^0.5.0"
+ },
+ "dependencies": {
+ "form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ }
+ },
+ "uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
+ }
+ }
+ },
+ "@azure/core-lro": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.5.3.tgz",
+ "integrity": "sha512-ubkOf2YCnVtq7KqEJQqAI8dDD5rH1M6OP5kW0KO/JQyTaxLA0N0pjFWvvaysCj9eHMNBcuuoZXhhl0ypjod2DA==",
+ "requires": {
+ "@azure/abort-controller": "^1.0.0",
+ "@azure/core-util": "^1.2.0",
+ "@azure/logger": "^1.0.0",
+ "tslib": "^2.2.0"
+ }
+ },
+ "@azure/core-paging": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.5.0.tgz",
+ "integrity": "sha512-zqWdVIt+2Z+3wqxEOGzR5hXFZ8MGKK52x4vFLw8n58pR6ZfKRx3EXYTxTaYxYHc/PexPUTyimcTWFJbji9Z6Iw==",
+ "requires": {
+ "tslib": "^2.2.0"
+ }
+ },
+ "@azure/core-tracing": {
+ "version": "1.0.0-preview.13",
+ "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz",
+ "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==",
+ "requires": {
+ "@opentelemetry/api": "^1.0.1",
+ "tslib": "^2.2.0"
+ }
+ },
+ "@azure/core-util": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.3.2.tgz",
+ "integrity": "sha512-2bECOUh88RvL1pMZTcc6OzfobBeWDBf5oBbhjIhT1MV9otMVWCzpOJkkiKtrnO88y5GGBelgY8At73KGAdbkeQ==",
+ "requires": {
+ "@azure/abort-controller": "^1.0.0",
+ "tslib": "^2.2.0"
+ }
+ },
+ "@azure/logger": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.4.tgz",
+ "integrity": "sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==",
+ "requires": {
+ "tslib": "^2.2.0"
+ }
+ },
+ "@azure/storage-blob": {
+ "version": "12.14.0",
+ "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.14.0.tgz",
+ "integrity": "sha512-g8GNUDpMisGXzBeD+sKphhH5yLwesB4JkHr1U6be/X3F+cAMcyGLPD1P89g2M7wbEtUJWoikry1rlr83nNRBzg==",
+ "requires": {
+ "@azure/abort-controller": "^1.0.0",
+ "@azure/core-http": "^3.0.0",
+ "@azure/core-lro": "^2.2.0",
+ "@azure/core-paging": "^1.1.1",
+ "@azure/core-tracing": "1.0.0-preview.13",
+ "@azure/logger": "^1.0.0",
+ "events": "^3.0.0",
+ "tslib": "^2.2.0"
+ }
+ },
"@babel/code-frame": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz",
@@ -3134,6 +3249,11 @@
"@octokit/openapi-types": "^16.0.0"
}
},
+ "@opentelemetry/api": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz",
+ "integrity": "sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA=="
+ },
"@popperjs/core": {
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz",
@@ -3982,8 +4102,28 @@
"@types/node": {
"version": "10.17.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
- "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==",
- "dev": true
+ "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
+ },
+ "@types/node-fetch": {
+ "version": "2.6.4",
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.4.tgz",
+ "integrity": "sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==",
+ "requires": {
+ "@types/node": "*",
+ "form-data": "^3.0.0"
+ },
+ "dependencies": {
+ "form-data": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
+ "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ }
+ }
+ }
},
"@types/nodemailer": {
"version": "4.6.8",
@@ -4519,6 +4659,14 @@
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
"integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw=="
},
+ "@types/tunnel": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz",
+ "integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/typescript": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/typescript/-/typescript-2.0.0.tgz",
@@ -5538,6 +5686,11 @@
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
},
+ "attr-accept": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
+ "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg=="
+ },
"available-typed-arrays": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
@@ -12755,8 +12908,7 @@
"events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
- "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
- "dev": true
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
},
"eventsource": {
"version": "2.0.2",
@@ -13323,6 +13475,14 @@
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
},
+ "file-selector": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz",
+ "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==",
+ "requires": {
+ "tslib": "^2.4.0"
+ }
+ },
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -23799,6 +23959,16 @@
"prop-types": "^15.8.1"
}
},
+ "react-dropzone": {
+ "version": "14.2.3",
+ "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz",
+ "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==",
+ "requires": {
+ "attr-accept": "^2.2.2",
+ "file-selector": "^0.6.0",
+ "prop-types": "^15.8.1"
+ }
+ },
"react-event-listener": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/react-event-listener/-/react-event-listener-0.6.6.tgz",
@@ -23922,6 +24092,55 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
+ "react-loader-spinner": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-5.3.4.tgz",
+ "integrity": "sha512-G2vw4ssX+RDZ/vfaeva06yfNqyFViv/u+tVZ3kFLy5TKNlNx2DbuwreBSpRtPespQA+VxinxUJsigwLwG9erOg==",
+ "requires": {
+ "react-is": "^18.2.0",
+ "styled-components": "^5.3.5",
+ "styled-tools": "^1.7.2"
+ },
+ "dependencies": {
+ "css-to-react-native": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
+ "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
+ "requires": {
+ "camelize": "^1.0.0",
+ "css-color-keywords": "^1.0.0",
+ "postcss-value-parser": "^4.0.2"
+ }
+ },
+ "postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
+ },
+ "react-is": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+ },
+ "styled-components": {
+ "version": "5.3.11",
+ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz",
+ "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==",
+ "requires": {
+ "@babel/helper-module-imports": "^7.0.0",
+ "@babel/traverse": "^7.4.5",
+ "@emotion/is-prop-valid": "^1.1.0",
+ "@emotion/stylis": "^0.8.4",
+ "@emotion/unitless": "^0.7.4",
+ "babel-plugin-styled-components": ">= 1.12.0",
+ "css-to-react-native": "^3.0.0",
+ "hoist-non-react-statics": "^3.0.0",
+ "shallowequal": "^1.1.0",
+ "supports-color": "^5.5.0"
+ }
+ }
+ }
+ },
"react-loading": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/react-loading/-/react-loading-2.0.3.tgz",
@@ -25008,6 +25227,11 @@
}
}
},
+ "sax": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
+ },
"saxes": {
"version": "3.1.11",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz",
@@ -26323,6 +26547,11 @@
}
}
},
+ "styled-tools": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/styled-tools/-/styled-tools-1.7.2.tgz",
+ "integrity": "sha512-IjLxzM20RMwAsx8M1QoRlCG/Kmq8lKzCGyospjtSXt/BTIIcvgTonaxQAsKnBrsZNwhpHzO9ADx5te0h76ILVg=="
+ },
"stylis": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz",
@@ -27129,6 +27358,11 @@
}
}
},
+ "tunnel": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
+ "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
+ },
"tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@@ -28710,6 +28944,22 @@
"integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==",
"dev": true
},
+ "xml2js": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
+ "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
+ "requires": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "dependencies": {
+ "xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
+ }
+ }
+ },
"xmlbuilder": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.0.0.tgz",
diff --git a/package.json b/package.json
index 4b8444ad8..6204b5c13 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
"start-release": "cross-env RELEASE=true NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev -- src/server/index.ts",
"start": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --debug --transpile-only -- src/server/index.ts",
"oldstart": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --debug -- src/server/index.ts",
- "debug": "cross-env NODE_OPTIONS=--max_old_space_size=8192 ts-node-dev --transpile-only --inspect -- src/server/index.ts",
+ "debug": "cross-env USE_AZURE=false NODE_OPTIONS=--max_old_space_size=8192 ts-node-dev --transpile-only --inspect -- src/server/index.ts",
"monitor": "cross-env MONITORED=true NODE_OPTIONS=--max_old_space_size=4096 ts-node src/server/index.ts",
"build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 webpack --env production",
"test": "mocha -r ts-node/register test/**/*.ts",
@@ -130,6 +130,7 @@
"webpack-hot-middleware": "^2.25.1"
},
"dependencies": {
+ "@azure/storage-blob": "^12.14.0",
"@emotion/react": "^11.11.0",
"@emotion/styled": "^11.11.0",
"@ffmpeg/core": "0.10.0",
@@ -288,9 +289,11 @@
"react-compound-slider": "^2.5.0",
"react-datepicker": "^3.8.0",
"react-dom": "^18.2.0",
+ "react-dropzone": "^14.2.3",
"react-grid-layout": "^1.3.4",
"react-icons": "^4.3.1",
"react-jsx-parser": "^1.29.0",
+ "react-loader-spinner": "^5.3.4",
"react-loading": "^2.0.3",
"react-markdown": "^8.0.3",
"react-measure": "^2.5.2",
diff --git a/report.20230720.011057.28881.0.001.json b/report.20230720.011057.28881.0.001.json
new file mode 100644
index 000000000..5c72c0c91
--- /dev/null
+++ b/report.20230720.011057.28881.0.001.json
@@ -0,0 +1,1269 @@
+
+{
+ "header": {
+ "reportVersion": 1,
+ "event": "Allocation failed - JavaScript heap out of memory",
+ "trigger": "FatalError",
+ "filename": "report.20230720.011057.28881.0.001.json",
+ "dumpEventTime": "2023-07-20T01:10:57Z",
+ "dumpEventTimeStamp": "1689829857721",
+ "processId": 28881,
+ "cwd": "/Users/smallwhale/Desktop/Projects/Dash-Web",
+ "commandLine": [
+ "/Users/smallwhale/.nvm/versions/node/v12.16.0/bin/node",
+ "--max-old-space-size=2048",
+ "/Users/smallwhale/Desktop/Projects/Dash-Web/node_modules/ts-node-dev/lib/wrap.js",
+ "/Users/smallwhale/Desktop/Projects/Dash-Web/node_modules/fork-ts-checker-webpack-plugin/lib/service.js"
+ ],
+ "nodejsVersion": "v12.16.0",
+ "wordSize": 64,
+ "arch": "x64",
+ "platform": "darwin",
+ "componentVersions": {
+ "node": "12.16.0",
+ "v8": "7.8.279.23-node.31",
+ "uv": "1.34.0",
+ "zlib": "1.2.11",
+ "brotli": "1.0.7",
+ "ares": "1.15.0",
+ "modules": "72",
+ "nghttp2": "1.40.0",
+ "napi": "5",
+ "llhttp": "2.0.4",
+ "http_parser": "2.9.3",
+ "openssl": "1.1.1d",
+ "cldr": "35.1",
+ "icu": "64.2",
+ "tz": "2019c",
+ "unicode": "12.1"
+ },
+ "release": {
+ "name": "node",
+ "lts": "Erbium",
+ "headersUrl": "https://nodejs.org/download/release/v12.16.0/node-v12.16.0-headers.tar.gz",
+ "sourceUrl": "https://nodejs.org/download/release/v12.16.0/node-v12.16.0.tar.gz"
+ },
+ "osName": "Darwin",
+ "osRelease": "22.4.0",
+ "osVersion": "Darwin Kernel Version 22.4.0: Mon Mar 6 21:01:02 PST 2023; root:xnu-8796.101.5~3/RELEASE_ARM64_T8112",
+ "osMachine": "x86_64",
+ "cpus": [
+ {
+ "model": "Apple M2",
+ "speed": 2400,
+ "user": 40342390,
+ "nice": 0,
+ "sys": 32818540,
+ "idle": 263532590,
+ "irq": 0
+ },
+ {
+ "model": "Apple M2",
+ "speed": 2400,
+ "user": 37882510,
+ "nice": 0,
+ "sys": 28750670,
+ "idle": 270292020,
+ "irq": 0
+ },
+ {
+ "model": "Apple M2",
+ "speed": 2400,
+ "user": 32806560,
+ "nice": 0,
+ "sys": 24772270,
+ "idle": 279705870,
+ "irq": 0
+ },
+ {
+ "model": "Apple M2",
+ "speed": 2400,
+ "user": 28917140,
+ "nice": 0,
+ "sys": 21505900,
+ "idle": 287180070,
+ "irq": 0
+ },
+ {
+ "model": "Apple M2",
+ "speed": 2400,
+ "user": 52175300,
+ "nice": 0,
+ "sys": 11670040,
+ "idle": 274363170,
+ "irq": 0
+ },
+ {
+ "model": "Apple M2",
+ "speed": 2400,
+ "user": 30406640,
+ "nice": 0,
+ "sys": 8969580,
+ "idle": 298959950,
+ "irq": 0
+ },
+ {
+ "model": "Apple M2",
+ "speed": 2400,
+ "user": 21936370,
+ "nice": 0,
+ "sys": 6476860,
+ "idle": 310114030,
+ "irq": 0
+ },
+ {
+ "model": "Apple M2",
+ "speed": 2400,
+ "user": 16048180,
+ "nice": 0,
+ "sys": 4956530,
+ "idle": 317649610,
+ "irq": 0
+ }
+ ],
+ "networkInterfaces": [
+ {
+ "name": "lo0",
+ "internal": true,
+ "mac": "00:00:00:00:00:00",
+ "address": "127.0.0.1",
+ "netmask": "255.0.0.0",
+ "family": "IPv4"
+ },
+ {
+ "name": "lo0",
+ "internal": true,
+ "mac": "00:00:00:00:00:00",
+ "address": "::1",
+ "netmask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
+ "family": "IPv6",
+ "scopeid": 0
+ },
+ {
+ "name": "lo0",
+ "internal": true,
+ "mac": "00:00:00:00:00:00",
+ "address": "fe80::1",
+ "netmask": "ffff:ffff:ffff:ffff::",
+ "family": "IPv6",
+ "scopeid": 1
+ },
+ {
+ "name": "anpi0",
+ "internal": false,
+ "mac": "42:10:d5:5b:93:9e",
+ "address": "fe80::4010:d5ff:fe5b:939e",
+ "netmask": "ffff:ffff:ffff:ffff::",
+ "family": "IPv6",
+ "scopeid": 4
+ },
+ {
+ "name": "anpi1",
+ "internal": false,
+ "mac": "42:10:d5:5b:93:9f",
+ "address": "fe80::4010:d5ff:fe5b:939f",
+ "netmask": "ffff:ffff:ffff:ffff::",
+ "family": "IPv6",
+ "scopeid": 5
+ },
+ {
+ "name": "en0",
+ "internal": false,
+ "mac": "9c:3e:53:8e:59:0e",
+ "address": "fe80::2b:dde:e859:46b8",
+ "netmask": "ffff:ffff:ffff:ffff::",
+ "family": "IPv6",
+ "scopeid": 12
+ },
+ {
+ "name": "en0",
+ "internal": false,
+ "mac": "9c:3e:53:8e:59:0e",
+ "address": "192.168.1.154",
+ "netmask": "255.255.255.0",
+ "family": "IPv4"
+ },
+ {
+ "name": "en0",
+ "internal": false,
+ "mac": "9c:3e:53:8e:59:0e",
+ "address": "2600:4040:572e:2f00:18fe:e894:e23c:f4c2",
+ "netmask": "ffff:ffff:ffff:ffff::",
+ "family": "IPv6",
+ "scopeid": 0
+ },
+ {
+ "name": "en0",
+ "internal": false,
+ "mac": "9c:3e:53:8e:59:0e",
+ "address": "2600:4040:572e:2f00:c182:d0ad:6e64:1fae",
+ "netmask": "ffff:ffff:ffff:ffff::",
+ "family": "IPv6",
+ "scopeid": 0
+ },
+ {
+ "name": "awdl0",
+ "internal": false,
+ "mac": "b6:6c:9a:3c:a7:93",
+ "address": "fe80::b46c:9aff:fe3c:a793",
+ "netmask": "ffff:ffff:ffff:ffff::",
+ "family": "IPv6",
+ "scopeid": 13
+ },
+ {
+ "name": "llw0",
+ "internal": false,
+ "mac": "b6:6c:9a:3c:a7:93",
+ "address": "fe80::b46c:9aff:fe3c:a793",
+ "netmask": "ffff:ffff:ffff:ffff::",
+ "family": "IPv6",
+ "scopeid": 14
+ },
+ {
+ "name": "utun0",
+ "internal": false,
+ "mac": "00:00:00:00:00:00",
+ "address": "fe80::be27:76ec:8f11:5429",
+ "netmask": "ffff:ffff:ffff:ffff::",
+ "family": "IPv6",
+ "scopeid": 15
+ },
+ {
+ "name": "utun1",
+ "internal": false,
+ "mac": "00:00:00:00:00:00",
+ "address": "fe80::ce81:b1c:bd2c:69e",
+ "netmask": "ffff:ffff:ffff:ffff::",
+ "family": "IPv6",
+ "scopeid": 16
+ },
+ {
+ "name": "utun2",
+ "internal": false,
+ "mac": "00:00:00:00:00:00",
+ "address": "fe80::7707:8801:aef4:1907",
+ "netmask": "ffff:ffff:ffff:ffff::",
+ "family": "IPv6",
+ "scopeid": 17
+ },
+ {
+ "name": "utun3",
+ "internal": false,
+ "mac": "00:00:00:00:00:00",
+ "address": "fe80::a4f8:a3ce:851e:23dd",
+ "netmask": "ffff:ffff:ffff:ffff::",
+ "family": "IPv6",
+ "scopeid": 18
+ },
+ {
+ "name": "utun4",
+ "internal": false,
+ "mac": "00:00:00:00:00:00",
+ "address": "fe80::8d47:6641:b300:bde8",
+ "netmask": "ffff:ffff:ffff:ffff::",
+ "family": "IPv6",
+ "scopeid": 19
+ },
+ {
+ "name": "utun5",
+ "internal": false,
+ "mac": "00:00:00:00:00:00",
+ "address": "fe80::7c36:c665:d5f7:49d7",
+ "netmask": "ffff:ffff:ffff:ffff::",
+ "family": "IPv6",
+ "scopeid": 20
+ },
+ {
+ "name": "utun6",
+ "internal": false,
+ "mac": "00:00:00:00:00:00",
+ "address": "fe80::10d8:db7a:d714:7746",
+ "netmask": "ffff:ffff:ffff:ffff::",
+ "family": "IPv6",
+ "scopeid": 21
+ }
+ ],
+ "host": "Sophies-MBP"
+ },
+ "javascriptStack": {
+ "message": "No stack.",
+ "stack": [
+ "Unavailable."
+ ]
+ },
+ "nativeStack": [
+ {
+ "pc": "0x000000010015c8ca",
+ "symbol": "report::TriggerNodeReport(v8::Isolate*, node::Environment*, char const*, char const*, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&, v8::Local<v8::String>) [/Users/smallwhale/.nvm/versions/node/v12.16.0/bin/node]"
+ },
+ {
+ "pc": "0x0000000100080f3e",
+ "symbol": "node::OnFatalError(char const*, char const*) [/Users/smallwhale/.nvm/versions/node/v12.16.0/bin/node]"
+ },
+ {
+ "pc": "0x0000000100185467",
+ "symbol": "v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/Users/smallwhale/.nvm/versions/node/v12.16.0/bin/node]"
+ },
+ {
+ "pc": "0x0000000100185403",
+ "symbol": "v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/Users/smallwhale/.nvm/versions/node/v12.16.0/bin/node]"
+ },
+ {
+ "pc": "0x000000010030b5f5",
+ "symbol": "v8::internal::Heap::FatalProcessOutOfMemory(char const*) [/Users/smallwhale/.nvm/versions/node/v12.16.0/bin/node]"
+ },
+ {
+ "pc": "0x000000010030ccc4",
+ "symbol": "v8::internal::Heap::RecomputeLimits(v8::internal::GarbageCollector) [/Users/smallwhale/.nvm/versions/node/v12.16.0/bin/node]"
+ },
+ {
+ "pc": "0x0000000100309b37",
+ "symbol": "v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [/Users/smallwhale/.nvm/versions/node/v12.16.0/bin/node]"
+ },
+ {
+ "pc": "0x0000000100307afd",
+ "symbol": "v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/Users/smallwhale/.nvm/versions/node/v12.16.0/bin/node]"
+ },
+ {
+ "pc": "0x00000001003132ba",
+ "symbol": "v8::internal::Heap::AllocateRawWithLightRetry(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/Users/smallwhale/.nvm/versions/node/v12.16.0/bin/node]"
+ },
+ {
+ "pc": "0x0000000100313341",
+ "symbol": "v8::internal::Heap::AllocateRawWithRetryOrFail(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/Users/smallwhale/.nvm/versions/node/v12.16.0/bin/node]"
+ },
+ {
+ "pc": "0x00000001002e065b",
+ "symbol": "v8::internal::Factory::NewFillerObject(int, bool, v8::internal::AllocationType, v8::internal::AllocationOrigin) [/Users/smallwhale/.nvm/versions/node/v12.16.0/bin/node]"
+ },
+ {
+ "pc": "0x0000000100618a18",
+ "symbol": "v8::internal::Runtime_AllocateInYoungGeneration(int, unsigned long*, v8::internal::Isolate*) [/Users/smallwhale/.nvm/versions/node/v12.16.0/bin/node]"
+ },
+ {
+ "pc": "0x0000000100950c19",
+ "symbol": "Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_NoBuiltinExit [/Users/smallwhale/.nvm/versions/node/v12.16.0/bin/node]"
+ },
+ {
+ "pc": "0x00000001008d2291",
+ "symbol": "Builtins_FastNewObject [/Users/smallwhale/.nvm/versions/node/v12.16.0/bin/node]"
+ }
+ ],
+ "javascriptHeap": {
+ "totalMemory": 2152435712,
+ "totalCommittedMemory": 2150538320,
+ "usedMemory": 2139294040,
+ "availableMemory": 47735856,
+ "memoryLimit": 2197815296,
+ "heapSpaces": {
+ "read_only_space": {
+ "memorySize": 262144,
+ "committedMemory": 33088,
+ "capacity": 32808,
+ "used": 32808,
+ "available": 0
+ },
+ "new_space": {
+ "memorySize": 2097152,
+ "committedMemory": 1236968,
+ "capacity": 1047456,
+ "used": 203328,
+ "available": 844128
+ },
+ "old_space": {
+ "memorySize": 1956003840,
+ "committedMemory": 1955767472,
+ "capacity": 1948209048,
+ "used": 1947744360,
+ "available": 464688
+ },
+ "code_space": {
+ "memorySize": 14061568,
+ "committedMemory": 13610624,
+ "capacity": 12121760,
+ "used": 12121760,
+ "available": 0
+ },
+ "map_space": {
+ "memorySize": 1576960,
+ "committedMemory": 1456120,
+ "capacity": 1257120,
+ "used": 1257120,
+ "available": 0
+ },
+ "large_object_space": {
+ "memorySize": 178384896,
+ "committedMemory": 178384896,
+ "capacity": 177931880,
+ "used": 177931880,
+ "available": 0
+ },
+ "code_large_object_space": {
+ "memorySize": 49152,
+ "committedMemory": 49152,
+ "capacity": 2784,
+ "used": 2784,
+ "available": 0
+ },
+ "new_large_object_space": {
+ "memorySize": 0,
+ "committedMemory": 0,
+ "capacity": 1047456,
+ "used": 0,
+ "available": 1047456
+ }
+ }
+ },
+ "resourceUsage": {
+ "userCpuSeconds": 312.946,
+ "kernelCpuSeconds": 137.981,
+ "cpuConsumptionPercent": 62.0257,
+ "maxRss": 2062897119232,
+ "pageFaults": {
+ "IORequired": 658,
+ "IONotRequired": 43579208
+ },
+ "fsActivity": {
+ "reads": 0,
+ "writes": 0
+ }
+ },
+ "libuv": [
+ ],
+ "environmentVariables": {
+ "npm_config_save_dev": "",
+ "npm_config_legacy_bundling": "",
+ "npm_config_dry_run": "",
+ "npm_package_dependencies_translate_google_api": "^1.0.4",
+ "npm_package_dependencies_request": "^2.88.2",
+ "npm_package_dependencies_express_flash": "0.0.2",
+ "npm_package_dependencies__fortawesome_fontawesome_svg_core": "^6.3.0",
+ "NVM_INC": "/Users/smallwhale/.nvm/versions/node/v12.16.0/include/node",
+ "npm_config_viewer": "man",
+ "npm_config_only": "",
+ "npm_config_commit_hooks": "true",
+ "npm_config_browser": "",
+ "npm_package_gitHead": "e11aa60b774d457cb016bb0f375ce092f0a733af",
+ "npm_package_dependencies_webpack_dev_middleware": "^5.3.1",
+ "npm_package_dependencies_webpack_cli": "^4.10.0",
+ "npm_package_devDependencies_prettier": "^2.7.1",
+ "npm_package_devDependencies_awesome_typescript_loader": "^5.2.1",
+ "npm_package_devDependencies__types_archiver": "^3.1.1",
+ "MANPATH": "/Users/smallwhale/.nvm/versions/node/v12.16.0/share/man:/Users/smallwhale/.nvm/versions/node/v18.16.0/share/man:/opt/homebrew/share/man:/usr/share/man:/usr/local/share/man:/Users/smallwhale/.nvm/versions/node/v18.16.0/share/man:/opt/homebrew/share/man::",
+ "npm_config_also": "",
+ "npm_package_dependencies_react_jsx_parser": "^1.29.0",
+ "npm_package_dependencies_mongoose": "^5.13.14",
+ "npm_package_dependencies_connect_flash": "^0.1.1",
+ "npm_package_browser_child_process": "false",
+ "npm_config_sign_git_commit": "",
+ "npm_config_rollback": "true",
+ "npm_package_dependencies_material_ui": "^0.20.2",
+ "npm_package_devDependencies__types_sharp": "^0.23.1",
+ "npm_package_devDependencies__types_passport_local": "^1.0.34",
+ "npm_package_devDependencies__types_dotenv": "^6.1.1",
+ "npm_package_devDependencies__types_cookie_parser": "^1.4.2",
+ "TERM_PROGRAM": "vscode",
+ "NODE": "/Users/smallwhale/.nvm/versions/node/v12.16.0/bin/node",
+ "npm_config_usage": "",
+ "npm_config_audit": "true",
+ "npm_package_dependencies_reveal_js": "^4.3.0",
+ "npm_package_dependencies_process": "^0.11.10",
+ "npm_package_dependencies_pdfjs": "^2.4.7",
+ "npm_package_dependencies_html_to_image": "^0.1.3",
+ "npm_package_devDependencies_file_loader": "^3.0.1",
+ "npm_package_devDependencies__types_express_flash": "0.0.0",
+ "npm_package_scripts_monitor": "cross-env MONITORED=true NODE_OPTIONS=--max_old_space_size=4096 ts-node src/server/index.ts",
+ "INIT_CWD": "/Users/smallwhale/Desktop/Projects/Dash-Web",
+ "npm_package_dependencies_rehype_raw": "^6.1.1",
+ "npm_package_dependencies_react_audio_waveform": "0.0.5",
+ "npm_package_dependencies_path_browserify": "^1.0.1",
+ "npm_package_dependencies_nodemailer": "^5.1.1",
+ "npm_package_dependencies_axios": "^0.19.2",
+ "npm_package_devDependencies_typescript": "^4.7.4",
+ "NVM_CD_FLAGS": "-q",
+ "npm_config_globalignorefile": "/Users/smallwhale/.nvm/versions/node/v12.16.0/etc/npmignore",
+ "npm_package_dependencies_react_grid_layout": "^1.3.4",
+ "npm_package_dependencies_prosemirror_find_replace": "^0.9.0",
+ "npm_package_dependencies_normalize_css": "^8.0.1",
+ "npm_package_devDependencies_mocha": "^5.2.0",
+ "npm_package_devDependencies__types_express_session": "^1.17.5",
+ "TERM": "xterm-256color",
+ "SHELL": "/bin/zsh",
+ "npm_config_shell": "/bin/zsh",
+ "npm_config_maxsockets": "50",
+ "npm_config_init_author_url": "",
+ "npm_package_dependencies_prosemirror_dev_tools": "^3.1.0",
+ "npm_package_dependencies_p_limit": "^2.2.0",
+ "npm_package_dependencies_bson": "^4.6.1",
+ "npm_package_dependencies__types_dom_speech_recognition": "0.0.1",
+ "npm_package_dependencies__emotion_styled": "^11.11.0",
+ "npm_package_devDependencies_style_loader": "^0.23.1",
+ "npm_package_devDependencies__types_react_datepicker": "^3.1.8",
+ "npm_config_shrinkwrap": "true",
+ "npm_config_parseable": "",
+ "npm_config_metrics_registry": "https://registry.npmjs.org/",
+ "npm_package_dependencies_xregexp": "^4.4.1",
+ "npm_package_dependencies_shelljs": "^0.8.5",
+ "npm_package_dependencies_bezier_curve": "^1.0.0",
+ "npm_package_dependencies__mui_icons_material": "^5.11.16",
+ "npm_package_devDependencies_tslint": "^5.20.1",
+ "npm_package_devDependencies__types_react_transition_group": "^4.4.5",
+ "npm_package_scripts_tsc": "tsc",
+ "HOMEBREW_REPOSITORY": "/opt/homebrew",
+ "TMPDIR": "/var/folders/bv/1xck6d7j7bz14bvhgdsllbj40000gn/T/",
+ "npm_config_timing": "",
+ "npm_config_init_license": "ISC",
+ "npm_package_dependencies_socket_io": "^2.5.0",
+ "npm_package_dependencies_probe_image_size": "^4.0.0",
+ "npm_package_dependencies_canvas": "^2.9.3",
+ "npm_package_dependencies__hig_theme_data": "^2.23.1",
+ "npm_package_devDependencies__types_react_select": "^3.1.2",
+ "npm_package_devDependencies__types_prosemirror_model": "^1.16.1",
+ "npm_config_if_present": "",
+ "npm_package_dependencies_typescript_collections": "^1.3.3",
+ "npm_package_dependencies_rimraf": "^3.0.0",
+ "npm_package_dependencies_react_autosuggest": "^9.4.3",
+ "npm_package_dependencies_flexlayout_react": "^0.3.11",
+ "npm_package_dependencies_find_in_files": "^0.5.0",
+ "npm_package_devDependencies__types_chai": "^4.3.0",
+ "TERM_PROGRAM_VERSION": "1.79.2",
+ "npm_package_dependencies_prosemirror_inputrules": "^1.1.3",
+ "npm_package_dependencies_bcrypt_nodejs": "0.0.3",
+ "npm_package_dependencies_async": "^2.6.2",
+ "npm_config_sign_git_tag": "",
+ "npm_config_init_author_email": "",
+ "npm_config_cache_max": "Infinity",
+ "npm_package_dependencies_uuid": "^3.4.0",
+ "npm_package_dependencies_supercluster": "^7.1.4",
+ "npm_package_dependencies_remark_gfm": "^3.0.1",
+ "npm_package_dependencies_connect_mongo": "^2.0.3",
+ "npm_package_dependencies_browser_assert": "^1.2.1",
+ "npm_package_devDependencies_sass_loader": "^7.3.1",
+ "ZDOTDIR": "/Users/smallwhale",
+ "ORIGINAL_XDG_CURRENT_DESKTOP": "undefined",
+ "MallocNanoZone": "0",
+ "npm_config_preid": "",
+ "npm_config_long": "",
+ "npm_config_local_address": "",
+ "npm_config_git_tag_version": "true",
+ "npm_config_cert": "",
+ "npm_package_dependencies_js_datepicker": "^4.6.6",
+ "npm_package_devDependencies__types_webpack_hot_middleware": "^2.25.6",
+ "npm_package_devDependencies__types_mongodb": "^3.6.20",
+ "npm_package_devDependencies__types_mocha": "^5.2.6",
+ "TERM_SESSION_ID": "F2049B14-CDC7-4078-A2C4-82A8C869F43E",
+ "npm_config_registry": "https://registry.npmjs.org/",
+ "npm_config_noproxy": "",
+ "npm_config_fetch_retries": "2",
+ "npm_package_dependencies_react_compound_slider": "^2.5.0",
+ "npm_package_dependencies_prosemirror_history": "^1.2.0",
+ "npm_package_devDependencies__types_react_color": "^2.17.6",
+ "npm_package_devDependencies__types_google_maps_react": "^2.0.5",
+ "npm_package_devDependencies__types_color": "^3.0.3",
+ "npm_package_dependencies_react_dom": "^18.2.0",
+ "npm_package_dependencies_passport_local": "^1.0.0",
+ "npm_package_dependencies__octokit_core": "^4.0.4",
+ "npm_package_devDependencies__types_async": "^2.4.1",
+ "npm_package_scripts_debug": "cross-env NODE_OPTIONS=--max_old_space_size=8192 ts-node-dev --transpile-only --inspect -- src/server/index.ts",
+ "npm_package_scripts_oldstart": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --debug -- src/server/index.ts",
+ "npm_config_versions": "",
+ "npm_config_message": "%s",
+ "npm_config_key": "",
+ "npm_package_readmeFilename": "README.md",
+ "npm_package_dependencies_react_refresh_typescript": "^2.0.7",
+ "npm_package_dependencies_image_size": "^0.7.5",
+ "npm_package_dependencies_html_to_text": "^5.1.1",
+ "npm_package_dependencies_express_validator": "^5.3.1",
+ "npm_package_devDependencies_eslint_plugin_jsx_a11y": "^6.6.0",
+ "npm_package_node_child_process": "empty",
+ "npm_package_dependencies_react_resizable_rotatable_draggable": "^0.2.0",
+ "npm_package_dependencies_got": "^12.0.1",
+ "npm_package_dependencies__types_d3_color": "^2.0.3",
+ "npm_package_devDependencies_webpack": "^5.69.1",
+ "npm_package_devDependencies__types_nodemailer": "^4.6.6",
+ "npm_package_description": "Install Node.js, then, from the project directory, run",
+ "NVM_DIR": "/Users/smallwhale/.nvm",
+ "USER": "smallwhale",
+ "npm_package_dependencies__types_d3_scale": "^3.3.2",
+ "npm_package_devDependencies_dotenv": "^8.6.0",
+ "npm_package_devDependencies__types_react": "^18.0.15",
+ "npm_package_devDependencies__types_prosemirror_transform": "^1.1.5",
+ "npm_package_devDependencies__types_prosemirror_history": "^1.0.3",
+ "npm_package_dependencies_readline": "^1.3.0",
+ "npm_package_dependencies__types_supercluster": "^7.1.0",
+ "COMMAND_MODE": "unix2003",
+ "npm_config_globalconfig": "/Users/smallwhale/.nvm/versions/node/v12.16.0/etc/npmrc",
+ "npm_package_dependencies_depcheck": "^0.9.2",
+ "npm_package_dependencies__types_web": "0.0.53",
+ "npm_config_prefer_online": "",
+ "npm_config_logs_max": "10",
+ "npm_config_always_auth": "",
+ "npm_package_dependencies_react_icons": "^4.3.1",
+ "npm_package_dependencies_passport_google_oauth20": "^2.0.0",
+ "npm_package_devDependencies_webpack_dev_server": "^3.11.3",
+ "npm_package_devDependencies__types_brotli": "^1.3.1",
+ "npm_package_dependencies_url_loader": "^1.1.2",
+ "npm_package_dependencies_stream_browserify": "^3.0.0",
+ "npm_package_dependencies_prosemirror_transform": "^1.3.4",
+ "npm_package_dependencies_lodash": "^4.17.21",
+ "npm_package_dependencies_i": "^0.3.7",
+ "npm_package_devDependencies_tslint_loader": "^3.6.0",
+ "SSH_AUTH_SOCK": "/private/tmp/com.apple.launchd.iMTrb4REX6/Listeners",
+ "npm_package_dependencies_words_to_numbers": "^1.5.1",
+ "npm_package_dependencies_valid_url": "^1.0.9",
+ "npm_package_dependencies_styled_components": "^4.4.1",
+ "npm_package_dependencies_csv_parser": "^3.0.0",
+ "npm_package_dependencies_class_transformer": "^0.2.0",
+ "npm_package_devDependencies_eslint": "^8.36.0",
+ "npm_package_devDependencies__types_prosemirror_inputrules": "^1.0.4",
+ "npm_package_devDependencies__types_express": "^4.17.13",
+ "__CF_USER_TEXT_ENCODING": "0x1F5:0x0:0x0",
+ "npm_execpath": "/Users/smallwhale/.nvm/versions/node/v12.16.0/lib/node_modules/npm/bin/npm-cli.js",
+ "npm_config_global_style": "",
+ "npm_config_cache_lock_retries": "10",
+ "npm_package_dependencies_wikijs": "^6.3.3",
+ "npm_package_dependencies_bluebird": "^3.7.2",
+ "npm_package_devDependencies__types_react_typist": "^2.0.3",
+ "npm_config_update_notifier": "true",
+ "npm_config_cafile": "",
+ "npm_package_dependencies_util": "^0.12.4",
+ "npm_package_dependencies_raw_loader": "^1.0.0",
+ "npm_package_dependencies_https_browserify": "^1.0.0",
+ "npm_package_dependencies_brotli": "^1.3.3",
+ "npm_package_dependencies__mui_material": "^5.13.1",
+ "npm_package_dependencies__fortawesome_react_fontawesome": "^0.2.0",
+ "npm_package_devDependencies__types_passport_google_oauth20": "^2.0.11",
+ "npm_package_dependencies_cors": "^2.8.5",
+ "npm_package_dependencies_bezier_js": "^4.1.1",
+ "npm_package_dependencies__fortawesome_free_brands_svg_icons": "^6.3.0",
+ "npm_config_heading": "npm",
+ "npm_config_audit_level": "low",
+ "npm_package_dependencies_chrome": "^0.1.0",
+ "npm_package_dependencies__react_three_fiber": "^6.2.3",
+ "npm_package_devDependencies_eslint_plugin_prettier": "^4.2.1",
+ "npm_package_devDependencies_copy_webpack_plugin": "^4.6.0",
+ "npm_package_devDependencies__types_react_measure": "^2.0.8",
+ "npm_package_devDependencies__types_react_dom": "^18.0.6",
+ "npm_package_devDependencies__types_mobile_detect": "^1.3.4",
+ "npm_config_searchlimit": "20",
+ "npm_config_read_only": "",
+ "npm_config_offline": "",
+ "npm_config_fetch_retry_mintimeout": "10000",
+ "npm_package_dependencies_react_typist": "^2.0.5",
+ "npm_package_dependencies_mobx_react_devtools": "^6.1.1",
+ "npm_package_dependencies_md5_file": "^5.0.0",
+ "npm_package_dependencies_forever_agent": "^0.6.1",
+ "npm_package_devDependencies__types_xregexp": "^4.4.0",
+ "npm_package_devDependencies__types_typescript": "^2.0.0",
+ "npm_package_devDependencies__types_request": "^2.48.8",
+ "npm_package_devDependencies__types_prosemirror_commands": "^1.0.4",
+ "npm_config_json": "",
+ "npm_config_access": "",
+ "npm_config_argv": "{\"remain\":[],\"cooked\":[\"start\"],\"original\":[\"start\"]}",
+ "npm_package_dependencies__fortawesome_free_solid_svg_icons": "^6.3.0",
+ "npm_package_devDependencies__types_socket_io": "^2.1.13",
+ "PATH": "/Users/smallwhale/.nvm/versions/node/v12.16.0/lib/node_modules/npm/node_modules/npm-lifecycle/node-gyp-bin:/Users/smallwhale/Desktop/Projects/Dash-Web/node_modules/.bin:/Users/smallwhale/.nvm/versions/node/v12.16.0/bin:/Users/smallwhale/.nvm/versions/node/v18.16.0/bin:/Library/Frameworks/Python.framework/Versions/3.11/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/Users/smallwhale/.nvm/versions/node/v18.16.0/bin:/Library/Frameworks/Python.framework/Versions/3.11/bin:/opt/homebrew/bin:/opt/homebrew/sbin",
+ "npm_config_allow_same_version": "",
+ "npm_package_dependencies_webrtc_adapter": "^7.7.1",
+ "npm_package_dependencies_react_reveal": "^1.2.2",
+ "npm_package_dependencies_prosemirror_schema_list": "^1.1.6",
+ "npm_package_dependencies__material_ui_core": "^4.12.3",
+ "npm_package_devDependencies__types_rimraf": "^2.0.5",
+ "npm_package_devDependencies__types_connect_flash": "0.0.34",
+ "npm_config_https_proxy": "",
+ "npm_config_engine_strict": "",
+ "npm_config_description": "true",
+ "npm_package_dependencies_pug": "^2.0.4",
+ "npm_package_dependencies_prosemirror_keymap": "^1.1.5",
+ "npm_package_dependencies_pdfjs_dist": "^2.14.305",
+ "npm_package_dependencies_mobile_detect": "^1.4.5",
+ "npm_package_dependencies_image_size_stream": "^1.1.0",
+ "npm_package_dependencies_golden_layout": "^1.5.9",
+ "npm_package_dependencies_child_process": "^1.0.2",
+ "npm_package_dependencies__types_d3_axis": "^2.1.3",
+ "_": "/Users/smallwhale/Desktop/Projects/Dash-Web/node_modules/.bin/cross-env",
+ "LaunchInstanceID": "57D8446D-0E37-4DF2-8110-E0AA2FF019D8",
+ "npm_config_userconfig": "/Users/smallwhale/.npmrc",
+ "npm_config_init_module": "/Users/smallwhale/.npm-init.js",
+ "npm_package_dependencies__react_google_maps_api": "^2.7.0",
+ "USER_ZDOTDIR": "/Users/smallwhale",
+ "__CFBundleIdentifier": "com.microsoft.VSCode",
+ "npm_config_cidr": "",
+ "npm_package_dependencies_puppeteer": "^3.3.0",
+ "npm_package_dependencies_prosemirror_view": "^1.26.5",
+ "npm_package_dependencies_mongodb": "^3.7.3",
+ "npm_package_dependencies_google_auth_library": "^4.2.4",
+ "npm_package_dependencies_bootstrap": "^4.6.1",
+ "npm_package_devDependencies_eslint_config_airbnb": "^19.0.4",
+ "PWD": "/Users/smallwhale/Desktop/Projects/Dash-Web",
+ "npm_config_user": "501",
+ "npm_config_node_version": "12.16.0",
+ "npm_package_dependencies_node_sass": "^4.14.1",
+ "npm_package_dependencies_howler": "^2.2.3",
+ "npm_package_dependencies_expressjs": "^1.0.1",
+ "npm_package_dependencies_core_js": "^3.28.0",
+ "npm_package_dependencies_browndash_components": "0.0.82",
+ "npm_package_devDependencies_eslint_plugin_react_hooks": "^4.6.0",
+ "npm_package_devDependencies__types_lodash": "^4.14.179",
+ "npm_lifecycle_event": "start",
+ "npm_package_dependencies_react_table": "^6.11.5",
+ "npm_package_dependencies_react_loading": "^2.0.3",
+ "npm_package_dependencies_mobx": "^5.15.7",
+ "npm_package_dependencies_babel": "^6.23.0",
+ "npm_package_devDependencies_jsdom": "^15.2.1",
+ "npm_package_devDependencies_chai": "^4.3.6",
+ "npm_config_save": "true",
+ "npm_config_ignore_prepublish": "",
+ "npm_config_editor": "vi",
+ "npm_config_auth_type": "legacy",
+ "npm_package_dependencies_npm": "^6.14.18",
+ "npm_package_dependencies_node_stream_zip": "^1.15.0",
+ "npm_package_dependencies_image_data_uri": "^2.0.1",
+ "npm_package_scripts_start_release": "cross-env RELEASE=true NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev -- src/server/index.ts",
+ "npm_package_name": "dash",
+ "LANG": "en_US.UTF-8",
+ "npm_config_tag": "latest",
+ "npm_config_script_shell": "",
+ "npm_package_dependencies_query_string": "^6.14.1",
+ "npm_package_dependencies_mobx_utils": "^5.6.2",
+ "npm_package_dependencies_file_saver": "^2.0.5",
+ "npm_package_dependencies_body_parser": "^1.19.2",
+ "npm_package_dependencies__types_reveal": "^3.3.33",
+ "npm_package_devDependencies_eslint_plugin_import": "^2.26.0",
+ "npm_package_devDependencies__types_prosemirror_view": "^1.23.1",
+ "npm_config_progress": "true",
+ "npm_config_global": "",
+ "npm_config_before": "",
+ "npm_package_dependencies_xoauth2": "^1.2.0",
+ "npm_package_dependencies_standard_http_error": "^2.0.1",
+ "npm_package_dependencies_react_loader_spinner": "^5.3.4",
+ "npm_package_dependencies_http_browserify": "^1.7.0",
+ "npm_package_dependencies__types_d3_selection": "^2.0.1",
+ "npm_package_dependencies__hig_flyout": "^1.3.1",
+ "npm_package_devDependencies_fork_ts_checker_webpack_plugin": "^1.6.0",
+ "npm_package_scripts_build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 webpack --env production",
+ "npm_package_scripts_start": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --debug --transpile-only -- src/server/index.ts",
+ "npm_config_searchstaleness": "900",
+ "npm_config_optional": "true",
+ "npm_config_ham_it_up": "",
+ "npm_package_dependencies_sharp": "^0.23.4",
+ "npm_package_dependencies_rc_switch": "^1.9.2",
+ "npm_package_dependencies_googlephotos": "^0.2.5",
+ "npm_package_dependencies_exifr": "^7.1.3",
+ "npm_package_dependencies__types_google_maps": "^3.2.3",
+ "npm_package_dependencies__types_bezier_js": "^4.1.0",
+ "npm_package_dependencies__ffmpeg_core": "0.10.0",
+ "npm_package_devDependencies_ts_loader": "^5.3.3",
+ "npm_package_devDependencies__types_bcrypt_nodejs": "0.0.30",
+ "VSCODE_GIT_ASKPASS_EXTRA_ARGS": "--ms-enable-electron-run-as-node",
+ "XPC_FLAGS": "0x0",
+ "npm_config_save_prod": "",
+ "npm_config_force": "",
+ "npm_config_bin_links": "true",
+ "npm_package_devDependencies__types_youtube": "0.0.39",
+ "npm_config_searchopts": "",
+ "npm_package_dependencies_react_beautiful_dnd": "^13.1.0",
+ "npm_package_dependencies_jszip": "^3.7.1",
+ "npm_package_dependencies_csv_stringify": "^6.3.0",
+ "npm_package_devDependencies__types_react_icons": "^3.0.0",
+ "npm_config_node_gyp": "/Users/smallwhale/.nvm/versions/node/v12.16.0/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js",
+ "npm_config_depth": "Infinity",
+ "npm_package_dependencies_google_maps_react": "^2.0.6",
+ "npm_package_dependencies_express_session": "^1.17.2",
+ "npm_package_devDependencies_eslint_plugin_node": "^11.1.0",
+ "npm_package_devDependencies_eslint_config_prettier": "^8.5.0",
+ "npm_package_main": "index.js",
+ "npm_config_sso_poll_frequency": "500",
+ "npm_config_rebuild_bundle": "true",
+ "npm_package_dependencies_chart_js": "^3.8.0",
+ "npm_package_dependencies__emotion_react": "^11.11.0",
+ "npm_package_devDependencies__types_prosemirror_menu": "^1.0.6",
+ "npm_package_devDependencies__types_prosemirror_keymap": "^1.0.4",
+ "npm_package_devDependencies__types_pdfjs_dist": "^2.10.378",
+ "npm_package_devDependencies__types_exif": "^0.6.3",
+ "npm_package_version": "1.0.0",
+ "XPC_SERVICE_NAME": "0",
+ "npm_config_unicode": "true",
+ "VSCODE_INJECTION": "1",
+ "npm_package_dependencies_typescript_language_server": "^0.4.0",
+ "npm_package_dependencies_prosemirror_model": "^1.18.1",
+ "npm_package_dependencies__ffmpeg_ffmpeg": "0.10.0",
+ "SHLVL": "4",
+ "HOME": "/Users/smallwhale",
+ "npm_config_fetch_retry_maxtimeout": "60000",
+ "npm_package_dependencies_request_promise": "^4.2.6",
+ "npm_package_dependencies_react_markdown": "^8.0.3",
+ "npm_package_dependencies__hig_theme_context": "^2.1.3",
+ "npm_package_devDependencies__types_react_autosuggest": "^9.3.14",
+ "npm_package_devDependencies__types_mongoose": "^5.11.97",
+ "npm_package_devDependencies__types_d3": "^7.4.0",
+ "npm_package_devDependencies__types_animejs": "^2.0.2",
+ "npm_package_scripts_test": "mocha -r ts-node/register test/**/*.ts",
+ "VSCODE_GIT_ASKPASS_MAIN": "/Applications/Visual Studio Code.app/Contents/Resources/app/extensions/git/dist/askpass-main.js",
+ "npm_config_tag_version_prefix": "v",
+ "npm_config_strict_ssl": "true",
+ "npm_config_sso_type": "oauth",
+ "npm_config_scripts_prepend_node_path": "warn-only",
+ "npm_config_save_prefix": "^",
+ "npm_config_loglevel": "notice",
+ "npm_config_ca": "",
+ "npm_package_dependencies_three": "^0.127.0",
+ "npm_package_dependencies_openai": "^3.2.1",
+ "npm_package_dependencies_mobx_react": "^5.4.4",
+ "npm_package_dependencies_cookie_parser": "^1.4.6",
+ "npm_package_dependencies_adm_zip": "^0.4.16",
+ "npm_package_devDependencies_eslint_config_node": "^4.1.0",
+ "npm_config_save_exact": "",
+ "npm_config_group": "20",
+ "npm_config_fetch_retry_factor": "10",
+ "npm_config_dev": "",
+ "npm_package_devDependencies_webpack_hot_middleware": "^2.25.1",
+ "npm_package_devDependencies_cross_env": "^5.2.1",
+ "HOMEBREW_PREFIX": "/opt/homebrew",
+ "npm_config_version": "",
+ "npm_config_prefer_offline": "",
+ "npm_config_cache_lock_stale": "60000",
+ "npm_package_devDependencies__types_prosemirror_state": "^1.2.8",
+ "npm_package_devDependencies__types_body_parser": "^1.19.2",
+ "npm_config_otp": "",
+ "npm_config_cache_min": "10",
+ "npm_package_dependencies_react_color": "^2.19.3",
+ "npm_package_dependencies_d3": "^7.6.1",
+ "npm_package_devDependencies_ts_node": "^10.9.1",
+ "npm_package_devDependencies__types_react_grid_layout": "^1.3.2",
+ "npm_config_searchexclude": "",
+ "npm_config_cache": "/Users/smallwhale/.npm",
+ "npm_package_dependencies_tough_cookie": "^4.0.0",
+ "npm_package_dependencies_googleapis": "^40.0.0",
+ "npm_package_devDependencies__types_valid_url": "^1.0.3",
+ "npm_package_devDependencies__types_passport": "^1.0.9",
+ "npm_package_devDependencies__types_adm_zip": "^0.4.34",
+ "LOGNAME": "smallwhale",
+ "npm_lifecycle_script": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --debug --transpile-only -- src/server/index.ts",
+ "npm_config_color": "true",
+ "npm_package_dependencies_solr_node": "^1.2.1",
+ "npm_package_dependencies_react_transition_group": "^4.4.2",
+ "npm_package_dependencies_iink_js": "^1.5.4",
+ "npm_package_dependencies_html_webpack_plugin": "^5.5.0",
+ "npm_config_proxy": "",
+ "npm_config_package_lock": "true",
+ "npm_package_dependencies_prosemirror_state": "^1.4.1",
+ "npm_package_dependencies_nodemon": "^1.19.4",
+ "npm_package_dependencies_function_plot": "^1.22.8",
+ "npm_package_dependencies_equation_editor_react": "github:bobzel/equation-editor-react#useLocally",
+ "npm_package_devDependencies__types_socket_io_parser": "^3.0.0",
+ "VSCODE_GIT_IPC_HANDLE": "/var/folders/bv/1xck6d7j7bz14bvhgdsllbj40000gn/T/vscode-git-b8c8318074.sock",
+ "npm_config_package_lock_only": "",
+ "npm_config_fund": "true",
+ "npm_package_dependencies_react": "^18.2.0",
+ "npm_package_dependencies_bingmaps_react": "^1.2.10",
+ "npm_package_devDependencies_scss_loader": "0.0.1",
+ "npm_package_devDependencies__types_cookie_session": "^2.0.44",
+ "npm_config_save_optional": "",
+ "npm_package_dependencies_textarea_caret": "^3.1.0",
+ "npm_package_dependencies_react_measure": "^2.5.2",
+ "npm_package_dependencies_exif": "^0.6.0",
+ "NVM_BIN": "/Users/smallwhale/.nvm/versions/node/v12.16.0/bin",
+ "npm_config_ignore_scripts": "",
+ "npm_config_user_agent": "npm/6.13.4 node/v12.16.0 darwin x64",
+ "npm_package_dependencies_react_resizable": "^1.11.1",
+ "npm_package_dependencies_prosemirror_commands": "^1.2.1",
+ "npm_package_dependencies_memorystream": "^0.3.1",
+ "npm_package_dependencies_formidable": "1.2.1",
+ "npm_package_devDependencies__types_uuid": "^3.4.10",
+ "npm_config_cache_lock_wait": "10000",
+ "npm_package_dependencies_socket_io_client": "^2.5.0",
+ "npm_package_dependencies_recharts": "^2.1.12",
+ "npm_package_dependencies_react_chartjs_2": "^4.3.0",
+ "npm_package_dependencies_fluent_ffmpeg": "^2.1.2",
+ "npm_package_dependencies__types_cors": "^2.8.12",
+ "npm_package_devDependencies__types_node": "^10.17.60",
+ "npm_package_devDependencies__types_file_saver": "^2.0.5",
+ "VSCODE_GIT_ASKPASS_NODE": "/Applications/Visual Studio Code.app/Contents/Frameworks/Code Helper (Plugin).app/Contents/MacOS/Code Helper (Plugin)",
+ "GIT_ASKPASS": "/Applications/Visual Studio Code.app/Contents/Resources/app/extensions/git/dist/askpass.sh",
+ "HOMEBREW_CELLAR": "/opt/homebrew/Cellar",
+ "INFOPATH": "/opt/homebrew/share/info:/opt/homebrew/share/info:",
+ "npm_config_production": "",
+ "npm_package_dependencies_jsonschema": "^1.4.0",
+ "npm_package_dependencies_ffmpeg": "0.0.4",
+ "npm_package_dependencies_cookie_session": "^2.0.0",
+ "npm_package_dependencies_color": "^3.2.1",
+ "npm_package_devDependencies__types_webpack": "^4.41.32",
+ "npm_package_devDependencies__types_request_promise": "^4.1.48",
+ "npm_package_devDependencies__types_prosemirror_schema_list": "^1.0.3",
+ "npm_config_send_metrics": "",
+ "npm_config_save_bundle": "",
+ "npm_package_dependencies_web_request": "^1.0.7",
+ "npm_package_dependencies_react_datepicker": "^3.8.0",
+ "npm_package_dependencies_express": "^4.17.3",
+ "npm_package_dependencies_D": "^1.0.0",
+ "npm_package_dependencies__types_formidable": "1.0.31",
+ "npm_package_devDependencies__types_rc_switch": "^1.9.2",
+ "npm_package_devDependencies__types_prosemirror_dev_tools": "^2.1.0",
+ "npm_package_devDependencies__types_jquery": "^3.5.14",
+ "npm_config_umask": "0022",
+ "npm_config_node_options": "",
+ "npm_config_init_version": "1.0.0",
+ "npm_package_dependencies_https": "^1.0.0",
+ "npm_package_dependencies_array_batcher": "^1.2.3",
+ "npm_package_dependencies__fortawesome_free_regular_svg_icons": "^6.3.0",
+ "npm_package_devDependencies__types_shelljs": "^0.8.11",
+ "npm_package_devDependencies__types_libxmljs": "^0.18.7",
+ "npm_package_devDependencies__types_express_validator": "^3.0.0",
+ "npm_package_devDependencies__types_bluebird": "^3.5.36",
+ "npm_config_init_author_name": "",
+ "npm_config_git": "git",
+ "npm_config_scope": "",
+ "npm_package_dependencies_react_select": "^3.2.0",
+ "npm_package_dependencies_pdf_parse": "^1.1.1",
+ "npm_package_dependencies_colors": "^1.4.0",
+ "npm_package_dependencies_archiver": "^3.1.1",
+ "npm_package_devDependencies_css_loader": "^2.1.1",
+ "npm_package_devDependencies__types_socket_io_client": "^1.4.36",
+ "SECURITYSESSIONID": "186a4",
+ "npm_config_unsafe_perm": "true",
+ "npm_config_tmp": "/var/folders/bv/1xck6d7j7bz14bvhgdsllbj40000gn/T",
+ "npm_config_onload_script": "",
+ "npm_package_dependencies_serializr": "^1.5.4",
+ "npm_package_dependencies_fit_curve": "^0.1.7",
+ "npm_package_dependencies__webscopeio_react_textarea_autocomplete": "^4.9.1",
+ "npm_package_dependencies__types_three": "^0.126.2",
+ "npm_package_devDependencies_ts_node_dev": "^2.0.0",
+ "npm_node_execpath": "/Users/smallwhale/.nvm/versions/node/v12.16.0/bin/node",
+ "npm_config_prefix": "/Users/smallwhale/.nvm/versions/node/v12.16.0",
+ "npm_config_link": "",
+ "npm_config_format_package_lock": "true",
+ "npm_package_dependencies_passport": "^0.4.0",
+ "npm_package_devDependencies_eslint_plugin_react": "^7.30.1",
+ "npm_package_devDependencies__types_react_table": "^6.8.9",
+ "npm_package_devDependencies__types_react_reconciler": "^0.26.4",
+ "COLORTERM": "truecolor",
+ "NODE_OPTIONS": "--max_old_space_size=4096",
+ "TS_NODE_DEV": "true",
+ "_CLIENT_OPENAI_KEY": "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",
+ "_CLIENT_GITHUB_ACCESS_TOKEN": "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",
+ "VIPSHOME": "/usr/local/Cellar/vips/8.8.1",
+ "TYPESCRIPT_PATH": "/Users/smallwhale/Desktop/Projects/Dash-Web/node_modules/typescript/lib/typescript.js",
+ "TSCONFIG": "/Users/smallwhale/Desktop/Projects/Dash-Web/tsconfig.json",
+ "COMPILER_OPTIONS": "{}",
+ "TSLINT": "true",
+ "CONTEXT": "/Users/smallwhale/Desktop/Projects/Dash-Web",
+ "TSLINTAUTOFIX": "false",
+ "ESLINT": "false",
+ "ESLINT_OPTIONS": "{}",
+ "WATCH": "",
+ "WORK_DIVISION": "1",
+ "MEMORY_LIMIT": "2048",
+ "CHECK_SYNTACTIC_ERRORS": "false",
+ "USE_INCREMENTAL_API": "true",
+ "VUE": "false"
+ },
+ "userLimits": {
+ "core_file_size_blocks": {
+ "soft": 0,
+ "hard": "unlimited"
+ },
+ "data_seg_size_kbytes": {
+ "soft": "unlimited",
+ "hard": "unlimited"
+ },
+ "file_size_blocks": {
+ "soft": "unlimited",
+ "hard": "unlimited"
+ },
+ "max_locked_memory_bytes": {
+ "soft": "unlimited",
+ "hard": "unlimited"
+ },
+ "max_memory_size_kbytes": {
+ "soft": "unlimited",
+ "hard": "unlimited"
+ },
+ "open_files": {
+ "soft": 1048575,
+ "hard": "unlimited"
+ },
+ "stack_size_bytes": {
+ "soft": 8372224,
+ "hard": 67092480
+ },
+ "cpu_time_seconds": {
+ "soft": "unlimited",
+ "hard": "unlimited"
+ },
+ "max_user_processes": {
+ "soft": 1333,
+ "hard": 2000
+ },
+ "virtual_memory_kbytes": {
+ "soft": "unlimited",
+ "hard": "unlimited"
+ }
+ },
+ "sharedObjects": [
+ "/Users/smallwhale/.nvm/versions/node/v12.16.0/bin/node",
+ "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation",
+ "/usr/lib/libobjc.A.dylib",
+ "/System/Library/PrivateFrameworks/CoreServicesInternal.framework/Versions/A/CoreServicesInternal",
+ "/usr/lib/liboah.dylib",
+ "/usr/lib/libfakelink.dylib",
+ "/usr/lib/libicucore.A.dylib",
+ "/usr/lib/libSystem.B.dylib",
+ "/System/Library/PrivateFrameworks/SoftLinking.framework/Versions/A/SoftLinking",
+ "/usr/lib/libc++abi.dylib",
+ "/usr/lib/libc++.1.dylib",
+ "/usr/lib/system/libcache.dylib",
+ "/usr/lib/system/libcommonCrypto.dylib",
+ "/usr/lib/system/libcompiler_rt.dylib",
+ "/usr/lib/system/libcopyfile.dylib",
+ "/usr/lib/system/libcorecrypto.dylib",
+ "/usr/lib/system/libdispatch.dylib",
+ "/usr/lib/system/libdyld.dylib",
+ "/usr/lib/system/libkeymgr.dylib",
+ "/usr/lib/system/libmacho.dylib",
+ "/usr/lib/system/libquarantine.dylib",
+ "/usr/lib/system/libremovefile.dylib",
+ "/usr/lib/system/libsystem_asl.dylib",
+ "/usr/lib/system/libsystem_blocks.dylib",
+ "/usr/lib/system/libsystem_c.dylib",
+ "/usr/lib/system/libsystem_collections.dylib",
+ "/usr/lib/system/libsystem_configuration.dylib",
+ "/usr/lib/system/libsystem_containermanager.dylib",
+ "/usr/lib/system/libsystem_coreservices.dylib",
+ "/usr/lib/system/libsystem_darwin.dylib",
+ "/usr/lib/system/libsystem_dnssd.dylib",
+ "/usr/lib/system/libsystem_featureflags.dylib",
+ "/usr/lib/system/libsystem_info.dylib",
+ "/usr/lib/system/libsystem_m.dylib",
+ "/usr/lib/system/libsystem_malloc.dylib",
+ "/usr/lib/system/libsystem_networkextension.dylib",
+ "/usr/lib/system/libsystem_notify.dylib",
+ "/usr/lib/system/libsystem_sandbox.dylib",
+ "/usr/lib/system/libsystem_secinit.dylib",
+ "/usr/lib/system/libsystem_kernel.dylib",
+ "/usr/lib/system/libsystem_platform.dylib",
+ "/usr/lib/system/libsystem_pthread.dylib",
+ "/usr/lib/system/libsystem_symptoms.dylib",
+ "/usr/lib/system/libsystem_trace.dylib",
+ "/usr/lib/system/libunwind.dylib",
+ "/usr/lib/system/libxpc.dylib",
+ "/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit",
+ "/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices",
+ "/usr/lib/libDiagnosticMessagesClient.dylib",
+ "/usr/lib/libenergytrace.dylib",
+ "/usr/lib/libbsm.0.dylib",
+ "/usr/lib/libz.1.dylib",
+ "/usr/lib/system/libkxld.dylib",
+ "/System/Library/Frameworks/CFNetwork.framework/Versions/A/CFNetwork",
+ "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/FSEvents",
+ "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/CarbonCore",
+ "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/Metadata.framework/Versions/A/Metadata",
+ "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/OSServices.framework/Versions/A/OSServices",
+ "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SearchKit.framework/Versions/A/SearchKit",
+ "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/AE.framework/Versions/A/AE",
+ "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/LaunchServices",
+ "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/DictionaryServices.framework/Versions/A/DictionaryServices",
+ "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SharedFileList.framework/Versions/A/SharedFileList",
+ "/System/Library/Frameworks/Security.framework/Versions/A/Security",
+ "/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration",
+ "/usr/lib/libapple_nghttp2.dylib",
+ "/usr/lib/libcompression.dylib",
+ "/usr/lib/libnetwork.dylib",
+ "/usr/lib/libsqlite3.dylib",
+ "/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation",
+ "/System/Library/Frameworks/Network.framework/Versions/A/Network",
+ "/usr/lib/libCoreEntitlements.dylib",
+ "/System/Library/PrivateFrameworks/MessageSecurity.framework/Versions/A/MessageSecurity",
+ "/System/Library/PrivateFrameworks/ProtocolBuffer.framework/Versions/A/ProtocolBuffer",
+ "/usr/lib/libMobileGestalt.dylib",
+ "/System/Library/PrivateFrameworks/AppleFSCompression.framework/Versions/A/AppleFSCompression",
+ "/usr/lib/libcoretls.dylib",
+ "/usr/lib/libcoretls_cfhelpers.dylib",
+ "/usr/lib/libpam.2.dylib",
+ "/usr/lib/libxar.1.dylib",
+ "/System/Library/PrivateFrameworks/CoreAutoLayout.framework/Versions/A/CoreAutoLayout",
+ "/System/Library/Frameworks/DiskArbitration.framework/Versions/A/DiskArbitration",
+ "/usr/lib/libarchive.2.dylib",
+ "/usr/lib/libxml2.2.dylib",
+ "/usr/lib/liblangid.dylib",
+ "/System/Library/Frameworks/Combine.framework/Versions/A/Combine",
+ "/usr/lib/swift/libswiftCore.dylib",
+ "/usr/lib/swift/libswiftCoreFoundation.dylib",
+ "/usr/lib/swift/libswiftDarwin.dylib",
+ "/usr/lib/swift/libswiftDispatch.dylib",
+ "/usr/lib/swift/libswiftIOKit.dylib",
+ "/usr/lib/swift/libswiftObjectiveC.dylib",
+ "/usr/lib/swift/libswiftXPC.dylib",
+ "/usr/lib/swift/libswift_Concurrency.dylib",
+ "/usr/lib/swift/libswift_StringProcessing.dylib",
+ "/usr/lib/swift/libswiftos.dylib",
+ "/System/Library/PrivateFrameworks/AppleSystemInfo.framework/Versions/A/AppleSystemInfo",
+ "/System/Library/PrivateFrameworks/IOMobileFramebuffer.framework/Versions/A/IOMobileFramebuffer",
+ "/System/Library/Frameworks/IOSurface.framework/Versions/A/IOSurface",
+ "/usr/lib/libpcap.A.dylib",
+ "/usr/lib/libdns_services.dylib",
+ "/usr/lib/liblzma.5.dylib",
+ "/usr/lib/libbz2.1.0.dylib",
+ "/usr/lib/libiconv.2.dylib",
+ "/usr/lib/libcharset.1.dylib",
+ "/usr/lib/swift/libswift_RegexParser.dylib",
+ "/usr/lib/libheimdal-asn1.dylib",
+ "/usr/lib/libCheckFix.dylib",
+ "/System/Library/PrivateFrameworks/TCC.framework/Versions/A/TCC",
+ "/System/Library/PrivateFrameworks/CoreNLP.framework/Versions/A/CoreNLP",
+ "/System/Library/PrivateFrameworks/MetadataUtilities.framework/Versions/A/MetadataUtilities",
+ "/System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate",
+ "/usr/lib/libmecab.dylib",
+ "/usr/lib/libCRFSuite.dylib",
+ "/usr/lib/libgermantok.dylib",
+ "/usr/lib/libThaiTokenizer.dylib",
+ "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vImage.framework/Versions/A/vImage",
+ "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/vecLib",
+ "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvMisc.dylib",
+ "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvDSP.dylib",
+ "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib",
+ "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libLAPACK.dylib",
+ "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libLinearAlgebra.dylib",
+ "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libSparseBLAS.dylib",
+ "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libQuadrature.dylib",
+ "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBNNS.dylib",
+ "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libSparse.dylib",
+ "/System/Library/PrivateFrameworks/MIL.framework/Versions/A/MIL",
+ "/System/Library/Frameworks/OpenDirectory.framework/Versions/A/Frameworks/CFOpenDirectory.framework/Versions/A/CFOpenDirectory",
+ "/System/Library/Frameworks/OpenDirectory.framework/Versions/A/OpenDirectory",
+ "/System/Library/PrivateFrameworks/APFS.framework/Versions/A/APFS",
+ "/System/Library/Frameworks/SecurityFoundation.framework/Versions/A/SecurityFoundation",
+ "/usr/lib/libutil.dylib",
+ "/System/Library/PrivateFrameworks/InstalledContentLibrary.framework/Versions/A/InstalledContentLibrary",
+ "/System/Library/PrivateFrameworks/CoreServicesStore.framework/Versions/A/CoreServicesStore",
+ "/usr/lib/libapp_launch_measurement.dylib",
+ "/System/Library/PrivateFrameworks/AppleMobileFileIntegrity.framework/Versions/A/AppleMobileFileIntegrity",
+ "/usr/lib/libmis.dylib",
+ "/System/Library/PrivateFrameworks/MobileSystemServices.framework/Versions/A/MobileSystemServices",
+ "/System/Library/PrivateFrameworks/ConfigProfileHelper.framework/Versions/A/ConfigProfileHelper",
+ "/System/Library/PrivateFrameworks/CoreAnalytics.framework/Versions/A/CoreAnalytics",
+ "/System/Library/PrivateFrameworks/AppleSauce.framework/Versions/A/AppleSauce",
+ "/System/Library/PrivateFrameworks/LanguageModeling.framework/Versions/A/LanguageModeling",
+ "/usr/lib/libxslt.1.dylib",
+ "/usr/lib/libcmph.dylib",
+ "/System/Library/PrivateFrameworks/CoreEmoji.framework/Versions/A/CoreEmoji",
+ "/System/Library/PrivateFrameworks/LinguisticData.framework/Versions/A/LinguisticData",
+ "/System/Library/PrivateFrameworks/Lexicon.framework/Versions/A/Lexicon",
+ "/System/Library/PrivateFrameworks/BackgroundTaskManagement.framework/Versions/A/BackgroundTaskManagement",
+ "/usr/lib/libTLE.dylib",
+ "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices",
+ "/System/Library/Frameworks/CoreGraphics.framework/Versions/A/CoreGraphics",
+ "/System/Library/Frameworks/CoreText.framework/Versions/A/CoreText",
+ "/System/Library/Frameworks/ImageIO.framework/Versions/A/ImageIO",
+ "/System/Library/Frameworks/ColorSync.framework/Versions/A/ColorSync",
+ "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/ATS",
+ "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/HIServices",
+ "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/PrintCore.framework/Versions/A/PrintCore",
+ "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/QD.framework/Versions/A/QD",
+ "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ColorSyncLegacy.framework/Versions/A/ColorSyncLegacy",
+ "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/SpeechSynthesis.framework/Versions/A/SpeechSynthesis",
+ "/System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/SkyLight",
+ "/System/Library/PrivateFrameworks/FontServices.framework/libFontParser.dylib",
+ "/System/Library/PrivateFrameworks/RunningBoardServices.framework/Versions/A/RunningBoardServices",
+ "/System/Library/PrivateFrameworks/IOSurfaceAccelerator.framework/Versions/A/IOSurfaceAccelerator",
+ "/System/Library/PrivateFrameworks/WatchdogClient.framework/Versions/A/WatchdogClient",
+ "/System/Library/Frameworks/CoreDisplay.framework/Versions/A/CoreDisplay",
+ "/System/Library/Frameworks/CoreMedia.framework/Versions/A/CoreMedia",
+ "/System/Library/PrivateFrameworks/IOAccelerator.framework/Versions/A/IOAccelerator",
+ "/System/Library/Frameworks/Metal.framework/Versions/A/Metal",
+ "/System/Library/Frameworks/CoreVideo.framework/Versions/A/CoreVideo",
+ "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/MetalPerformanceShaders",
+ "/System/Library/PrivateFrameworks/MultitouchSupport.framework/Versions/A/MultitouchSupport",
+ "/System/Library/Frameworks/QuartzCore.framework/Versions/A/QuartzCore",
+ "/System/Library/Frameworks/VideoToolbox.framework/Versions/A/VideoToolbox",
+ "/System/Library/PrivateFrameworks/BaseBoard.framework/Versions/A/BaseBoard",
+ "/System/Library/PrivateFrameworks/AppleJPEG.framework/Versions/A/AppleJPEG",
+ "/usr/lib/libexpat.1.dylib",
+ "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libPng.dylib",
+ "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libTIFF.dylib",
+ "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libGIF.dylib",
+ "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJP2.dylib",
+ "/usr/lib/libate.dylib",
+ "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJPEG.dylib",
+ "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libRadiance.dylib",
+ "/System/Library/PrivateFrameworks/GPUWrangler.framework/Versions/A/GPUWrangler",
+ "/System/Library/PrivateFrameworks/IOPresentment.framework/Versions/A/IOPresentment",
+ "/System/Library/PrivateFrameworks/DSExternalDisplay.framework/Versions/A/DSExternalDisplay",
+ "/System/Library/PrivateFrameworks/GPUCompiler.framework/Versions/31001/Libraries/libllvm-flatbuffers.dylib",
+ "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCoreFSCache.dylib",
+ "/System/Library/PrivateFrameworks/GPUCompiler.framework/Versions/31001/Libraries/libGPUCompilerUtils.dylib",
+ "/System/Library/PrivateFrameworks/CMCaptureCore.framework/Versions/A/CMCaptureCore",
+ "/usr/lib/libspindump.dylib",
+ "/System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio",
+ "/System/Library/Frameworks/ExtensionFoundation.framework/Versions/A/ExtensionFoundation",
+ "/System/Library/PrivateFrameworks/CoreTime.framework/Versions/A/CoreTime",
+ "/System/Library/PrivateFrameworks/AppServerSupport.framework/Versions/A/AppServerSupport",
+ "/System/Library/PrivateFrameworks/perfdata.framework/Versions/A/perfdata",
+ "/System/Library/PrivateFrameworks/AudioToolboxCore.framework/Versions/A/AudioToolboxCore",
+ "/System/Library/PrivateFrameworks/caulk.framework/Versions/A/caulk",
+ "/usr/lib/libAudioStatistics.dylib",
+ "/System/Library/PrivateFrameworks/SystemPolicy.framework/Versions/A/SystemPolicy",
+ "/usr/lib/libSMC.dylib",
+ "/System/Library/Frameworks/CoreMIDI.framework/Versions/A/CoreMIDI",
+ "/usr/lib/libAudioToolboxUtility.dylib",
+ "/System/Library/PrivateFrameworks/OSAServicesClient.framework/Versions/A/OSAServicesClient",
+ "/usr/lib/libperfcheck.dylib",
+ "/System/Library/PrivateFrameworks/PlugInKit.framework/Versions/A/PlugInKit",
+ "/System/Library/PrivateFrameworks/AssertionServices.framework/Versions/A/AssertionServices",
+ "/System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL",
+ "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGLU.dylib",
+ "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGFXShared.dylib",
+ "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib",
+ "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGLImage.dylib",
+ "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCVMSPluginSupport.dylib",
+ "/usr/lib/libRosetta.dylib",
+ "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCoreVMClient.dylib",
+ "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSCore.framework/Versions/A/MPSCore",
+ "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSImage.framework/Versions/A/MPSImage",
+ "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSNeuralNetwork.framework/Versions/A/MPSNeuralNetwork",
+ "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSMatrix.framework/Versions/A/MPSMatrix",
+ "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSRayIntersector.framework/Versions/A/MPSRayIntersector",
+ "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSNDArray.framework/Versions/A/MPSNDArray",
+ "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/Frameworks/MPSFunctions.framework/Versions/A/MPSFunctions",
+ "/System/Library/PrivateFrameworks/MetalTools.framework/Versions/A/MetalTools",
+ "/System/Library/PrivateFrameworks/AggregateDictionary.framework/Versions/A/AggregateDictionary",
+ "/usr/lib/libIOReport.dylib",
+ "/System/Library/Frameworks/CoreImage.framework/Versions/A/CoreImage",
+ "/System/Library/PrivateFrameworks/PhotosensitivityProcessing.framework/Versions/A/PhotosensitivityProcessing",
+ "/System/Library/Frameworks/OpenCL.framework/Versions/A/OpenCL",
+ "/System/Library/PrivateFrameworks/GraphVisualizer.framework/Versions/A/GraphVisualizer",
+ "/System/Library/PrivateFrameworks/FontServices.framework/Versions/A/FontServices",
+ "/System/Library/Frameworks/UniformTypeIdentifiers.framework/Versions/A/UniformTypeIdentifiers",
+ "/System/Library/PrivateFrameworks/OTSVG.framework/Versions/A/OTSVG",
+ "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/Resources/libFontRegistry.dylib",
+ "/System/Library/PrivateFrameworks/FontServices.framework/libhvf.dylib",
+ "/System/Library/PrivateFrameworks/FontServices.framework/libXTFontStaticRegistryData.dylib",
+ "/System/Library/PrivateFrameworks/VideoToolboxParavirtualizationSupport.framework/Versions/A/VideoToolboxParavirtualizationSupport",
+ "/System/Library/PrivateFrameworks/AppleVA.framework/Versions/A/AppleVA",
+ "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATSUI.framework/Versions/A/ATSUI",
+ "/usr/lib/libcups.2.dylib",
+ "/System/Library/Frameworks/Kerberos.framework/Versions/A/Kerberos",
+ "/System/Library/Frameworks/GSS.framework/Versions/A/GSS",
+ "/usr/lib/libresolv.9.dylib",
+ "/System/Library/PrivateFrameworks/Heimdal.framework/Versions/A/Heimdal",
+ "/System/Library/Frameworks/Kerberos.framework/Versions/A/Libraries/libHeimdalProxy.dylib",
+ "/System/Library/PrivateFrameworks/CommonAuth.framework/Versions/A/CommonAuth",
+ "/System/Library/Frameworks/AVFAudio.framework/Versions/A/AVFAudio",
+ "/System/Library/PrivateFrameworks/AXCoreUtilities.framework/Versions/A/AXCoreUtilities",
+ "/System/Library/Frameworks/AudioToolbox.framework/Versions/A/AudioToolbox",
+ "/System/Library/PrivateFrameworks/AudioSession.framework/Versions/A/AudioSession",
+ "/System/Library/Frameworks/IOBluetooth.framework/Versions/A/IOBluetooth",
+ "/System/Library/PrivateFrameworks/MediaExperience.framework/Versions/A/MediaExperience",
+ "/System/Library/PrivateFrameworks/AudioSession.framework/libSessionUtility.dylib",
+ "/System/Library/PrivateFrameworks/AudioResourceArbitration.framework/Versions/A/AudioResourceArbitration",
+ "/System/Library/PrivateFrameworks/PowerLog.framework/Versions/A/PowerLog",
+ "/System/Library/Frameworks/CoreData.framework/Versions/A/CoreData",
+ "/System/Library/Frameworks/CoreBluetooth.framework/Versions/A/CoreBluetooth",
+ "/System/Library/Frameworks/AudioUnit.framework/Versions/A/AudioUnit",
+ "/System/Library/PrivateFrameworks/CoreUtils.framework/Versions/A/CoreUtils",
+ "/System/Library/PrivateFrameworks/CoreUtilsExtras.framework/Versions/A/CoreUtilsExtras",
+ "/System/Library/PrivateFrameworks/IO80211.framework/Versions/A/IO80211",
+ "/System/Library/PrivateFrameworks/MobileKeyBag.framework/Versions/A/MobileKeyBag",
+ "/System/Library/PrivateFrameworks/MallocStackLogging.framework/Versions/A/MallocStackLogging"
+ ]
+} \ No newline at end of file
diff --git a/src/client/Network.ts b/src/client/Network.ts
index d606b9854..39bf69e32 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -5,7 +5,7 @@ import { Upload } from '../server/SharedMediaTypes';
/**
* Networking is repsonsible for connecting the client to the server. Networking
* mainly provides methods that the client can use to begin the process of
- * interacting with the server, such as fetching or uploading files.
+ * interacting with the server, such as fetching or uploading files.
*/
export namespace Networking {
export async function FetchFromServer(relativeRoute: string) {
@@ -25,9 +25,9 @@ export namespace Networking {
/**
* FileGuidPair attaches a guid to a file that is being uploaded,
* allowing the client to track the upload progress.
- *
+ *
* When files are dragged to the canvas, the overWriteDoc's ID is
- * used as the guid. Otherwise, a new guid is generated.
+ * used as the guid. Otherwise, a new guid is generated.
*/
export interface FileGuidPair {
file: File;
@@ -38,9 +38,10 @@ export namespace Networking {
* with the mapping of guid to files as parameters.
*
* @param fileguidpairs the files and corresponding guids to be uploaded to the server
+ * @param browndash whether the endpoint should be invoked on the browndash server
* @returns the response as a json from the server
*/
- export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(fileguidpairs: FileGuidPair | FileGuidPair[]): Promise<Upload.FileResponse<T>[]> {
+ export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(fileguidpairs: FileGuidPair | FileGuidPair[], browndash?: boolean): Promise<Upload.FileResponse<T>[]> {
const formData = new FormData();
if (Array.isArray(fileguidpairs)) {
if (!fileguidpairs.length) {
@@ -57,17 +58,19 @@ export namespace Networking {
])
);
}
- // If the fileguidpair has a guid to use (From the overwriteDoc) use that guid. Otherwise, generate a new guid.
+ // If the fileguidpair has a guid to use (From the overwriteDoc) use that guid. Otherwise, generate a new guid.
fileguidpairs.forEach(fileguidpair => formData.append(fileguidpair.guid ?? Utils.GenerateGuid(), fileguidpair.file));
} else {
- // Handle the case where fileguidpairs is a single file.
+ // Handle the case where fileguidpairs is a single file.
formData.append(fileguidpairs.guid ?? Utils.GenerateGuid(), fileguidpairs.file);
}
const parameters = {
method: 'POST',
body: formData,
};
- const response = await fetch('/uploadFormData', parameters);
+
+ const endpoint = browndash ? '[insert endpoint allowing local => browndash]' : '/uploadFormData';
+ const response = await fetch(endpoint, parameters);
return response.json();
}
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 4b3960902..6b4106f56 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -14,7 +14,7 @@ type GPTCallOpts = {
};
const callTypeMap: { [type: string]: GPTCallOpts } = {
- summary: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: 'Summarize this text briefly: ' },
+ 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: '' },
};
@@ -39,7 +39,7 @@ const gptAPICall = async (inputText: string, callType: GPTCallType) => {
temperature: opts.temp,
prompt: `${opts.prompt}${inputText}`,
});
- console.log(response.data.choices[0]);
+ // console.log(response.data.choices[0]);
return response.data.choices[0].text;
} catch (err) {
console.log(err);
@@ -47,7 +47,7 @@ const gptAPICall = async (inputText: string, callType: GPTCallType) => {
}
};
-const gptImageCall = async (prompt: string) => {
+const gptImageCall = async (prompt: string, n?: number) => {
try {
const configuration = new Configuration({
apiKey: process.env.OPENAI_KEY,
@@ -55,33 +55,15 @@ const gptImageCall = async (prompt: string) => {
const openai = new OpenAIApi(configuration);
const response = await openai.createImage({
prompt: prompt,
- n: 1,
+ n: n ?? 1,
size: '1024x1024',
});
- return response.data.data[0].url;
+ return response.data.data.map(data => data.url);
+ // return response.data.data[0].url;
} catch (err) {
console.error(err);
return;
}
};
-// const gptEditCall = async (selectedText: string, fullText: string) => {
-// try {
-// const configuration = new Configuration({
-// apiKey: process.env.OPENAI_KEY,
-// });
-// const openai = new OpenAIApi(configuration);
-// const response = await openai.createCompletion({
-// model: 'text-davinci-003',
-// max_tokens: 256,
-// temperature: 0.1,
-// prompt: `Replace the phrase ${selectedText} inside of ${fullText}.`,
-// });
-// return response.data.choices[0].text.trim();
-// } catch (err) {
-// console.log(err);
-// return 'Error connecting with API.';
-// }
-// };
-
export { gptAPICall, gptImageCall, GPTCallType };
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 5ef033e35..f3f645ca2 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1731,7 +1731,7 @@ export namespace DocUtils {
return;
}
const full = { ...options, _width: 400, title: name };
- const pathname = Utils.prepend(result.accessPaths.agnostic.client);
+ const pathname = result.accessPaths.agnostic.client;
const doc = await DocUtils.DocumentFromType(type, pathname, full, overwriteDoc);
if (doc) {
const proto = Doc.GetProto(doc);
diff --git a/src/client/theme.ts b/src/client/theme.ts
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/client/theme.ts
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 85101fcab..ac948c7c7 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -195,7 +195,7 @@ export namespace DragManager {
}
// drag a document and drop it (or make an embed/copy on drop)
- export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions, dropEvent?: () => any) {
+ export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions, onDropCompleted?: (e?: DragCompleteEvent) => any) {
const addAudioTag = (dropDoc: any) => {
dropDoc && !dropDoc.author_date && (dropDoc.author_date = new DateField());
dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(() => dropDoc);
@@ -203,7 +203,7 @@ export namespace DragManager {
};
const finishDrag = async (e: DragCompleteEvent) => {
const docDragData = e.docDragData;
- dropEvent?.(); // glr: optional additional function to be called - in this case with presentation trails
+ onDropCompleted?.(e); // glr: optional additional function to be called - in this case with presentation trails
if (docDragData && !docDragData.droppedDocuments.length) {
docDragData.dropAction = dragData.userDropAction || dragData.dropAction;
docDragData.droppedDocuments = (
diff --git a/src/client/util/ReportManager.scss b/src/client/util/ReportManager.scss
deleted file mode 100644
index 5a2f2fcad..000000000
--- a/src/client/util/ReportManager.scss
+++ /dev/null
@@ -1,88 +0,0 @@
-@import '../views/global/globalCssVariables';
-
-.issue-list-wrapper {
- position: relative;
- min-width: 250px;
- background-color: $light-blue;
- overflow-y: scroll;
-}
-
-.issue-list {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 5px;
- margin: 5px;
- border-radius: 5px;
- border: 1px solid grey;
- background-color: lightgoldenrodyellow;
-}
-
-// issue should pop up when the user hover over the issue
-.issue-list:hover {
- box-shadow: 2px;
- cursor: pointer;
- border: 3px solid #252b33;
-}
-
-.issue-content {
- background-color: white;
- padding: 10px;
- flex: 1 1 auto;
- overflow-y: scroll;
-}
-
-.issue-title {
- font-size: 20px;
- font-weight: 600;
- color: black;
-}
-
-.issue-body {
- padding: 0 10px;
- width: 100%;
- text-align: left;
-}
-
-.issue-body > * {
- margin-top: 5px;
-}
-
-.issue-body img,
-.issue-body video {
- display: block;
- max-width: 100%;
-}
-
-.report-issue-fab {
- position: fixed;
- bottom: 20px;
- right: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
-}
-
-.loading-center {
- margin: auto 0;
-}
-
-.settings-content label {
- margin-top: 10px;
-}
-
-.report-disclaimer {
- font-size: 8px;
- color: grey;
- padding-right: 50px;
- font-style: italic;
- text-align: left;
-}
-
-.flex-select {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 10px;
-}
diff --git a/src/client/util/ReportManager.tsx b/src/client/util/ReportManager.tsx
deleted file mode 100644
index 89c17e42f..000000000
--- a/src/client/util/ReportManager.tsx
+++ /dev/null
@@ -1,297 +0,0 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, runInAction } from 'mobx';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import { ColorState, SketchPicker } from 'react-color';
-import { Doc } from '../../fields/Doc';
-import { Id } from '../../fields/FieldSymbols';
-import { BoolCast, Cast, StrCast } from '../../fields/Types';
-import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils';
-import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
-import { DocServer } from '../DocServer';
-import { Networking } from '../Network';
-import { MainViewModal } from '../views/MainViewModal';
-import { FontIconBox } from '../views/nodes/FontIconBox/FontIconBox';
-import { DragManager } from './DragManager';
-import { GroupManager } from './GroupManager';
-import './SettingsManager.scss';
-import './ReportManager.scss';
-import { undoBatch } from './UndoManager';
-import { Octokit } from "@octokit/core";
-import { CheckBox } from '../views/search/CheckBox';
-import ReactLoading from 'react-loading';
-import ReactMarkdown from 'react-markdown';
-import rehypeRaw from 'rehype-raw';
-import remarkGfm from 'remark-gfm';
-const higflyout = require('@hig/flyout');
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
-
-@observer
-export class ReportManager extends React.Component<{}> {
- public static Instance: ReportManager;
- @observable private isOpen = false;
-
- private octokit: Octokit;
-
- @observable public issues: any[] = [];
- @action setIssues = action((issues: any[]) => { this.issues = issues; });
-
- // undefined is the default - null is if the user is making an issue
- @observable public selectedIssue: any = undefined;
- @action setSelectedIssue = action((issue: any) => { this.selectedIssue = issue; });
-
- // only get the open issues
- @observable public shownIssues = this.issues.filter(issue => issue.state === 'open');
-
- public updateIssueSearch = action((query: string = '') => {
- if (query === '') {
- this.shownIssues = this.issues.filter(issue => issue.state === 'open');
- return;
- }
- this.shownIssues = this.issues.filter(issue => issue.title.toLowerCase().includes(query.toLowerCase()));
- });
-
- constructor(props: {}) {
- super(props);
- ReportManager.Instance = this;
-
- this.octokit = new Octokit({
- auth: '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'
- });
- }
-
- public close = action(() => (this.isOpen = false));
- public open = action(() => {
- if (this.issues.length === 0) {
- // load in the issues if not already loaded
- this.getAllIssues()
- .then(issues => {
- this.setIssues(issues);
- this.updateIssueSearch();
- })
- .catch(err => console.log(err));
- }
- (this.isOpen = true)
- });
-
- @observable private bugTitle = '';
- @action setBugTitle = action((title: string) => { this.bugTitle = title; });
- @observable private bugDescription = '';
- @action setBugDescription = action((description: string) => { this.bugDescription = description; });
- @observable private bugType = '';
- @action setBugType = action((type: string) => { this.bugType = type; });
- @observable private bugPriority = '';
- @action setBugPriority = action((priortiy: string) => { this.bugPriority = priortiy; });
-
- // private toGithub = false;
- // will always be set to true - no alterntive option yet
- private toGithub = true;
-
- private formatTitle = (title: string, userEmail: string) => `${title} - ${userEmail.replace('@brown.edu', '')}`;
-
- public async getAllIssues() : Promise<any[]> {
- const res = await this.octokit.request('GET /repos/{owner}/{repo}/issues', {
- owner: 'brown-dash',
- repo: 'Dash-Web',
- });
-
- // 200 status means success
- if (res.status === 200) {
- return res.data;
- } else {
- throw new Error('Error getting issues');
- }
- }
-
- // turns an upload link into a servable link
- // ex:
- // C: /Users/dash/Documents/GitHub/Dash-Web/src/server/public/files/images/upload_8008dbc4b6424fbff14da7345bb32eb2.png
- // -> http://localhost:1050/files/images/upload_8008dbc4b6424fbff14da7345bb32eb2_l.png
- private fileLinktoServerLink = (fileLink: string) => {
- const serverUrl = 'https://browndash.com/';
-
- const regex = 'public'
- const publicIndex = fileLink.indexOf(regex) + regex.length;
-
- const finalUrl = `${serverUrl}${fileLink.substring(publicIndex + 1).replace('.', '_l.')}`;
- return finalUrl;
- }
-
- public async reportIssue() {
- if (this.bugTitle === '' || this.bugDescription === ''
- || this.bugType === '' || this.bugPriority === '') {
- alert('Please fill out all required fields to report an issue.');
- return;
- }
-
- if (this.toGithub) {
-
- const formattedLinks = (this.fileLinks ?? []).map(this.fileLinktoServerLink)
-
- const req = await this.octokit.request('POST /repos/{owner}/{repo}/issues', {
- owner: 'brown-dash',
- repo: 'Dash-Web',
- title: this.formatTitle(this.bugTitle, Doc.CurrentUserEmail),
- body: `${this.bugDescription} \n\nfiles:\n${formattedLinks.join('\n')}`,
- labels: [
- 'from-dash-app',
- this.bugType,
- this.bugPriority
- ]
- });
-
- // 201 status means success
- if (req.status !== 201) {
- alert('Error creating issue on github.');
- // on error, don't close the modal
- return;
- }
- }
- else {
- // if not going to github issues, not sure what to do yet...
- }
-
- // if we're down here, then we're good to go. reset the fields.
- this.setBugTitle('');
- this.setBugDescription('');
- // this.toGithub = false;
- this.setFileLinks([]);
- this.setBugType('');
- this.setBugPriority('');
- this.close();
- }
-
- @observable public fileLinks: any = [];
- @action setFileLinks = action((links: any) => { this.fileLinks = links; });
-
- private getServerPath = (link: any) => { return link.result.accessPaths.agnostic.server }
-
- private uploadFiles = (input: any) => {
- // keep null while uploading
- this.setFileLinks(null);
- // upload the files to the server
- if (input.files && input.files.length !== 0) {
- const fileArray: File[] = Array.from(input.files);
- (Networking.UploadFilesToServer(fileArray.map(file =>({file})))).then(links => {
- console.log('finshed uploading', links.map(this.getServerPath));
- this.setFileLinks((links ?? []).map(this.getServerPath));
- })
- }
-
- }
-
-
- private renderIssue = (issue: any) => {
-
- const isReportingIssue = issue === null;
-
- return isReportingIssue ?
- // report issue
- (<div className="settings-content">
- <h3 style={{ 'textDecoration': 'underline'}}>Report an Issue</h3>
- <label>Please leave a title for the bug.</label><br />
- <input type="text" placeholder='title' onChange={(e) => this.setBugTitle(e.target.value)} required/>
- <br />
- <label>Please leave a description for the bug and how it can be recreated.</label>
- <textarea placeholder='description' onChange={(e) => this.setBugDescription(e.target.value)} required/>
- <br />
- {/* {<label>Send to github issues? </label>
- <input type="checkbox" onChange={(e) => this.toGithub = e.target.checked} />
- <br /> } */}
-
- <label>Please label the issue</label>
- <div className='flex-select'>
- <select name="bugType" onChange={e => this.bugType = e.target.value}>
- <option value="" disabled selected>Type</option>
- <option value="bug">Bug</option>
- <option value="cosmetic">Poor Design or Cosmetic</option>
- <option value="documentation">Poor Documentation</option>
- </select>
-
- <select name="bigPriority" onChange={e => this.bugPriority = e.target.value}>
- <option value="" disabled selected>Priority</option>
- <option value="priority-low">Low</option>
- <option value="priority-medium">Medium</option>
- <option value="priority-high">High</option>
- </select>
- </div>
-
-
- <div>
- <label>Upload media that shows the bug (optional)</label>
- <input type="file" name="file" multiple accept='audio/*, video/*, image/*' onChange={e => this.uploadFiles(e.target)}/>
- </div>
- <br />
-
- <button onClick={() => this.reportIssue()} disabled={this.fileLinks === null} style={{ backgroundColor: this.fileLinks === null ? 'grey' : '' }}>{this.fileLinks === null ? 'Uploading...' : 'Submit'}</button>
- </div>)
- :
- // view issue
- (
- <div className='issue-container'>
- <h5 style={{'textAlign': "left"}}><a href={issue.html_url} target="_blank">Issue #{issue.number}</a></h5>
- <div className='issue-title'>
- {issue.title}
- </div>
- <ReactMarkdown children={issue.body} className='issue-body' linkTarget={"_blank"} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} />
- </div>
- );
- }
-
- private showReportIssueScreen = () => {
- this.setSelectedIssue(null);
- }
-
- private closeReportIssueScreen = () => {
- this.setSelectedIssue(undefined);
- }
-
- private get reportInterface() {
-
- const isReportingIssue = this.selectedIssue === null;
-
- return (
- <div className="settings-interface">
- <div className='issue-list-wrapper'>
- <h3>Current Issues</h3>
- <input type="text" placeholder='search issues' onChange={(e => this.updateIssueSearch(e.target.value))}></input><br />
- {this.issues.length === 0 ? <ReactLoading className='loading-center'/> : this.shownIssues.map(issue => <div className='issue-list' key={issue.number} onClick={() => this.setSelectedIssue(issue)}>{issue.title}</div>)}
-
- {/* <div className="settings-user">
- <button onClick={() => this.getAllIssues().then(issues => this.issues = issues)}>Poll Issues</button>
- </div> */}
- </div>
-
- <div className="close-button" onClick={this.close}>
- <FontAwesomeIcon icon={'times'} color="black" size={'lg'} />
- </div>
-
- <div className="issue-content" style={{'paddingTop' : this.selectedIssue === undefined ? '50px' : 'inherit'}}>
- {this.selectedIssue === undefined ? "no issue selected" : this.renderIssue(this.selectedIssue)}
- </div>
-
- <div className='report-issue-fab'>
- <span className='report-disclaimer' hidden={!isReportingIssue}>Note: issue reporting is not anonymous.</span>
- <button
- onClick={() => isReportingIssue ? this.closeReportIssueScreen() : this.showReportIssueScreen()}
- >{isReportingIssue ? 'Cancel' : 'Report New Issue'}</button>
- </div>
-
-
- </div>
- );
- }
-
- render() {
- return (
- <MainViewModal
- contents={this.reportInterface}
- isDisplayed={this.isOpen}
- interactive={true}
- closeOnExternalClick={this.close}
- dialogueBoxStyle={{ width: 'auto', height: '500px', background: Cast(Doc.UserDoc().userColor, 'string', null) }}
- />
- );
- }
-}
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index b6df5f26a..b8e327968 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -17,8 +17,8 @@ import { GroupManager } from './GroupManager';
import './SettingsManager.scss';
import { undoBatch } from './UndoManager';
import { Button, ColorPicker, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from 'browndash-components';
-import { BsGoogle } from 'react-icons/bs'
-import { FaFillDrip, FaPalette } from 'react-icons/fa'
+import { BsGoogle } from 'react-icons/bs';
+import { FaFillDrip, FaPalette } from 'react-icons/fa';
const higflyout = require('@hig/flyout');
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -26,8 +26,10 @@ export const Flyout = higflyout.default;
export enum ColorScheme {
Dark = 'Dark',
Light = 'Light',
+ CoolBlue = 'Cool Blue',
+ Cupcake = 'Cupcake',
System = 'Match System',
- Custom = 'Custom'
+ Custom = 'Custom',
}
export enum freeformScrollMode {
@@ -78,15 +80,15 @@ export class SettingsManager extends React.Component<{}> {
};
@computed get userColor() {
- return StrCast(Doc.UserDoc().userColor)
+ return StrCast(Doc.UserDoc().userColor);
}
@computed get userVariantColor() {
- return StrCast(Doc.UserDoc().userVariantColor)
+ return StrCast(Doc.UserDoc().userVariantColor);
}
@computed get userBackgroundColor() {
- return StrCast(Doc.UserDoc().userBackgroundColor)
+ return StrCast(Doc.UserDoc().userBackgroundColor);
}
@undoBatch selectUserMode = action((mode: string) => (Doc.noviceMode = mode === 'Novice'));
@@ -117,14 +119,24 @@ export class SettingsManager extends React.Component<{}> {
Doc.UserDoc().userTheme = scheme;
switch (scheme) {
case ColorScheme.Light:
- this.switchUserColor("#323232")
- this.switchUserBackgroundColor("#DFDFDF")
- this.switchUserVariantColor("#BDDDF5")
+ this.switchUserColor('#323232');
+ this.switchUserBackgroundColor('#DFDFDF');
+ this.switchUserVariantColor('#BDDDF5');
break;
case ColorScheme.Dark:
- this.switchUserColor("#DFDFDF")
- this.switchUserBackgroundColor("#323232")
- this.switchUserVariantColor("#4476F7")
+ this.switchUserColor('#DFDFDF');
+ this.switchUserBackgroundColor('#323232');
+ this.switchUserVariantColor('#4476F7');
+ break;
+ case ColorScheme.CoolBlue:
+ this.switchUserColor('#ADEAFF');
+ this.switchUserBackgroundColor('#060A15');
+ this.switchUserVariantColor('#3C51FF');
+ break;
+ case ColorScheme.Cupcake:
+ this.switchUserColor('#3BC7FF');
+ this.switchUserBackgroundColor('#fffdf7');
+ this.switchUserVariantColor('#FFD7F3');
break;
case ColorScheme.Custom:
break;
@@ -138,34 +150,32 @@ export class SettingsManager extends React.Component<{}> {
});
@computed get colorsContent() {
-
-
- const colorSchemes = [ColorScheme.Light, ColorScheme.Dark, ColorScheme.Custom, ColorScheme.System];
- const schemeMap = ['Light', 'Dark', 'Custom', 'Match System'];
+ const colorSchemes = [ColorScheme.Light, ColorScheme.Dark, ColorScheme.Cupcake, ColorScheme.CoolBlue, ColorScheme.Custom, ColorScheme.System];
+ const schemeMap = ['Light', 'Dark', 'Cupcake', 'Cool Blue', 'Custom', 'Match System'];
const userTheme = StrCast(Doc.UserDoc().userTheme);
return (
- <div style={{width: '100%'}}>
- <Dropdown
- formLabel='Theme'
+ <div style={{ width: '100%' }}>
+ <Dropdown
+ formLabel="Theme"
size={Size.SMALL}
type={Type.TERT}
selectedVal={userTheme}
- setSelectedVal={(scheme) => this.changeColorScheme(scheme as string)}
- items={colorSchemes.map((scheme, i) => (
- {
- text: schemeMap[i],
- val: scheme
- }
- ))}
+ setSelectedVal={scheme => this.changeColorScheme(scheme as string)}
+ items={colorSchemes.map((scheme, i) => ({
+ text: schemeMap[i],
+ val: scheme,
+ }))}
dropdownType={DropdownType.SELECT}
color={this.userColor}
fillWidth
/>
- {userTheme === ColorScheme.Custom && <Group formLabel='Custom Theme'>
- <ColorPicker tooltip={'User Color'} color={this.userColor} type={Type.SEC} icon={<FaFillDrip/>} selectedColor={this.userColor} setSelectedColor={this.switchUserColor}/>
- <ColorPicker tooltip={'User Background Color'} color={this.userColor} type={Type.SEC} icon={<FaPalette/>} selectedColor={this.userBackgroundColor} setSelectedColor={this.switchUserBackgroundColor}/>
- <ColorPicker tooltip={'User Variant Color'} color={this.userColor} type={Type.SEC} icon={<FaPalette/>} selectedColor={this.userVariantColor} setSelectedColor={this.switchUserVariantColor}/>
- </Group>}
+ {userTheme === ColorScheme.Custom && (
+ <Group formLabel="Custom Theme">
+ <ColorPicker tooltip={'User Color'} color={this.userColor} type={Type.SEC} icon={<FaFillDrip />} selectedColor={this.userColor} setSelectedColor={this.switchUserColor} />
+ <ColorPicker tooltip={'User Background Color'} color={this.userColor} type={Type.SEC} icon={<FaPalette />} selectedColor={this.userBackgroundColor} setSelectedColor={this.switchUserBackgroundColor} />
+ <ColorPicker tooltip={'User Variant Color'} color={this.userColor} type={Type.SEC} icon={<FaPalette />} selectedColor={this.userVariantColor} setSelectedColor={this.switchUserVariantColor} />
+ </Group>
+ )}
</div>
);
}
@@ -173,64 +183,59 @@ export class SettingsManager extends React.Component<{}> {
@computed get formatsContent() {
return (
<div className="prefs-content">
- <Toggle
- formLabel={'Show document header'}
- formLabelPlacement={'right'}
- toggleType={ToggleType.SWITCH}
- onClick={e => (Doc.UserDoc().layout_showTitle = Doc.UserDoc().layout_showTitle ? undefined : 'author_date')}
- toggleStatus={Doc.UserDoc().layout_showTitle !== undefined} size={Size.XSMALL}
+ <Toggle
+ formLabel={'Show document header'}
+ formLabelPlacement={'right'}
+ toggleType={ToggleType.SWITCH}
+ onClick={e => (Doc.UserDoc().layout_showTitle = Doc.UserDoc().layout_showTitle ? undefined : 'author_date')}
+ toggleStatus={Doc.UserDoc().layout_showTitle !== undefined}
+ size={Size.XSMALL}
color={this.userColor}
-
/>
- <Toggle
- formLabel={'Show Full Toolbar'}
- formLabelPlacement={'right'}
- toggleType={ToggleType.SWITCH}
- onClick={e => (Doc.UserDoc()['documentLinksButton-fullMenu'] = !Doc.UserDoc()['documentLinksButton-fullMenu'])}
- toggleStatus={BoolCast(Doc.UserDoc()['documentLinksButton-fullMenu'])}
+ <Toggle
+ formLabel={'Show Full Toolbar'}
+ formLabelPlacement={'right'}
+ toggleType={ToggleType.SWITCH}
+ onClick={e => (Doc.UserDoc()['documentLinksButton-fullMenu'] = !Doc.UserDoc()['documentLinksButton-fullMenu'])}
+ toggleStatus={BoolCast(Doc.UserDoc()['documentLinksButton-fullMenu'])}
size={Size.XSMALL}
color={this.userColor}
-
/>
- <Toggle
- formLabel={'Show Button Labels'}
- formLabelPlacement={'right'}
- toggleType={ToggleType.SWITCH}
- onClick={e => FontIconBox.SetShowLabels(!FontIconBox.GetShowLabels())}
- toggleStatus={FontIconBox.GetShowLabels()}
+ <Toggle
+ formLabel={'Show Button Labels'}
+ formLabelPlacement={'right'}
+ toggleType={ToggleType.SWITCH}
+ onClick={e => FontIconBox.SetShowLabels(!FontIconBox.GetShowLabels())}
+ toggleStatus={FontIconBox.GetShowLabels()}
size={Size.XSMALL}
color={this.userColor}
-
/>
- <Toggle
- formLabel={'Recognize Ink Gestures'}
- formLabelPlacement={'right'}
- toggleType={ToggleType.SWITCH}
- onClick={e => FontIconBox.SetRecognizeGestures(!FontIconBox.GetRecognizeGestures())}
- toggleStatus={FontIconBox.GetRecognizeGestures()}
+ <Toggle
+ formLabel={'Recognize Ink Gestures'}
+ formLabelPlacement={'right'}
+ toggleType={ToggleType.SWITCH}
+ onClick={e => FontIconBox.SetRecognizeGestures(!FontIconBox.GetRecognizeGestures())}
+ toggleStatus={FontIconBox.GetRecognizeGestures()}
size={Size.XSMALL}
color={this.userColor}
-
/>
- <Toggle
- formLabel={'Hide Labels In Ink Shapes'}
- formLabelPlacement={'right'}
- toggleType={ToggleType.SWITCH}
- onClick={e => (Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels)}
- toggleStatus={BoolCast(Doc.UserDoc().activeInkHideTextLabels)}
+ <Toggle
+ formLabel={'Hide Labels In Ink Shapes'}
+ formLabelPlacement={'right'}
+ toggleType={ToggleType.SWITCH}
+ onClick={e => (Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels)}
+ toggleStatus={BoolCast(Doc.UserDoc().activeInkHideTextLabels)}
size={Size.XSMALL}
color={this.userColor}
-
/>
- <Toggle
- formLabel={'Open Ink Docs in Lightbox'}
- formLabelPlacement={'right'}
- toggleType={ToggleType.SWITCH}
- onClick={e => (Doc.UserDoc().openInkInLightbox = !Doc.UserDoc().openInkInLightbox)}
- toggleStatus={BoolCast(Doc.UserDoc().openInkInLightbox)}
+ <Toggle
+ formLabel={'Open Ink Docs in Lightbox'}
+ formLabelPlacement={'right'}
+ toggleType={ToggleType.SWITCH}
+ onClick={e => (Doc.UserDoc().openInkInLightbox = !Doc.UserDoc().openInkInLightbox)}
+ toggleStatus={BoolCast(Doc.UserDoc().openInkInLightbox)}
size={Size.XSMALL}
color={this.userColor}
-
/>
</div>
);
@@ -262,29 +267,23 @@ export class SettingsManager extends React.Component<{}> {
<div className="tab-column-content">
{/* <NumberInput/> */}
<Group formLabel={'Default Font'}>
- <NumberDropdown
- color={this.userColor}
- numberDropdownType={'input'}
- min={0} max={50} step={2}
- type={Type.TERT}
- number={0}
- unit={"px"}
- setNumber={() => {}}
- />
- <Dropdown
- items={fontFamilies.map((val) => {
+ <NumberDropdown color={this.userColor} numberDropdownType={'input'} min={0} max={50} step={2} type={Type.TERT} number={0} unit={'px'} setNumber={() => {}} />
+ <Dropdown
+ items={fontFamilies.map(val => {
return {
text: val,
val: val,
style: {
- fontFamily: val
- }
- }
- })}
+ fontFamily: val,
+ },
+ };
+ })}
dropdownType={DropdownType.SELECT}
type={Type.TERT}
selectedVal={StrCast(Doc.UserDoc().fontFamily)}
- setSelectedVal={(val) => {this.changeFontFamily(val as string)}}
+ setSelectedVal={val => {
+ this.changeFontFamily(val as string);
+ }}
color={this.userColor}
fillWidth
/>
@@ -313,33 +312,12 @@ export class SettingsManager extends React.Component<{}> {
@computed get passwordContent() {
return (
<div className="password-content">
- <EditableText placeholder="Current password"
- type={Type.SEC}
- color={this.userColor}
- val={""}
- setVal={val => this.changeVal(val as string, 'curr')}
- fillWidth
- password
- />
- <EditableText placeholder="New password"
- type={Type.SEC}
- color={this.userColor}
- val={""}
- setVal={val => this.changeVal(val as string, 'new')}
- fillWidth
- password
- />
- <EditableText placeholder="Confirm new password"
- type={Type.SEC}
- color={this.userColor}
- val={""}
- setVal={val => this.changeVal(val as string, 'conf')}
- fillWidth
- password
- />
+ <EditableText placeholder="Current password" type={Type.SEC} color={this.userColor} val={''} setVal={val => this.changeVal(val as string, 'curr')} fillWidth password />
+ <EditableText placeholder="New password" type={Type.SEC} color={this.userColor} val={''} setVal={val => this.changeVal(val as string, 'new')} fillWidth password />
+ <EditableText placeholder="Confirm new password" type={Type.SEC} color={this.userColor} val={''} setVal={val => this.changeVal(val as string, 'conf')} fillWidth password />
{!this.passwordResultText ? null : <div className={`${this.passwordResultText.startsWith('Error') ? 'error' : 'success'}-text`}>{this.passwordResultText}</div>}
- <Button type={Type.SEC} text={'Forgot Password'} color={this.userColor}/>
- <Button type={Type.TERT} text={'Submit'} onClick={this.changePassword} color={this.userColor}/>
+ <Button type={Type.SEC} text={'Forgot Password'} color={this.userColor} />
+ <Button type={Type.TERT} text={'Submit'} onClick={this.changePassword} color={this.userColor} />
</div>
);
}
@@ -347,7 +325,7 @@ export class SettingsManager extends React.Component<{}> {
@computed get accountOthersContent() {
return (
<div className="account-others-content">
- <Button type={Type.TERT} text={'Connect to Google'} iconPlacement='left' icon={<BsGoogle/>} onClick={() => this.googleAuthorize()}/>
+ <Button type={Type.TERT} text={'Connect to Google'} iconPlacement="left" icon={<BsGoogle />} onClick={() => this.googleAuthorize()} />
</div>
);
}
@@ -377,59 +355,56 @@ export class SettingsManager extends React.Component<{}> {
<div className="tab-column">
<div className="tab-column-title">Modes</div>
<div className="tab-column-content">
- <Dropdown
- formLabel={"Mode"}
+ <Dropdown
+ formLabel={'Mode'}
items={[
{
text: 'Novice',
description: 'Novice mode is a user-friendly setting designed to cater to those who are new to Dash',
- val: "Novice"
+ val: 'Novice',
},
{
text: 'Developer',
- description: 'Developer mode is an advanced setting that grants you greater control and access to the underlying mechanics and tools of a software or system. Developer mode is still under development as there are experimental features.',
- val: "Developer"
+ description:
+ 'Developer mode is an advanced setting that grants you greater control and access to the underlying mechanics and tools of a software or system. Developer mode is still under development as there are experimental features.',
+ val: 'Developer',
},
- ]}
+ ]}
selectedVal={Doc.noviceMode ? 'Novice' : 'Developer'}
- setSelectedVal={(val) => {this.selectUserMode(val as string)}}
+ setSelectedVal={val => {
+ this.selectUserMode(val as string);
+ }}
dropdownType={DropdownType.SELECT}
type={Type.TERT}
- placement='bottom-start'
+ placement="bottom-start"
color={this.userColor}
fillWidth
/>
- <Toggle
- formLabel={'Playground Mode'}
- toggleType={ToggleType.SWITCH}
- toggleStatus={this.playgroundMode}
- onClick={this.playgroundModeToggle}
- color={this.userColor}
- />
+ <Toggle formLabel={'Playground Mode'} toggleType={ToggleType.SWITCH} toggleStatus={this.playgroundMode} onClick={this.playgroundModeToggle} color={this.userColor} />
</div>
<div className="tab-column-title" style={{ marginTop: 20, marginBottom: 10 }}>
Freeform Navigation
</div>
<div className="tab-column-content">
- <Dropdown
- formLabel={"Scroll Mode"}
+ <Dropdown
+ formLabel={'Scroll Mode'}
items={[
{
text: 'Scroll to Pan',
description: 'Scrolling pans canvas, shift + scrolling zooms',
- val: freeformScrollMode.Pan
+ val: freeformScrollMode.Pan,
},
{
text: 'Scroll to Zoom',
description: 'Scrolling zooms canvas',
- val: freeformScrollMode.Zoom
+ val: freeformScrollMode.Zoom,
},
- ]}
+ ]}
selectedVal={StrCast(Doc.UserDoc().freeformScrollMode)}
- setSelectedVal={(val) => this.setFreeformScrollMode(val as string)}
+ setSelectedVal={val => this.setFreeformScrollMode(val as string)}
dropdownType={DropdownType.SELECT}
type={Type.TERT}
- placement='bottom-start'
+ placement="bottom-start"
color={this.userColor}
/>
</div>
@@ -437,18 +412,8 @@ export class SettingsManager extends React.Component<{}> {
<div className="tab-column">
<div className="tab-column-title">Permissions</div>
<div className="tab-column-content">
- <Button
- text={"Manage Groups"}
- type={Type.TERT}
- onClick={() => GroupManager.Instance?.open()}
- color={this.userColor}
- />
- <Toggle
- toggleType={ToggleType.SWITCH}
- formLabel={"Default access private"}
- color={this.userColor}
- toggleStatus={BoolCast(Doc.defaultAclPrivate)}
- onClick={action(() => (Doc.defaultAclPrivate = !Doc.defaultAclPrivate))}/>
+ <Button text={'Manage Groups'} type={Type.TERT} onClick={() => GroupManager.Instance?.open()} color={this.userColor} />
+ <Toggle toggleType={ToggleType.SWITCH} formLabel={'Default access private'} color={this.userColor} toggleStatus={BoolCast(Doc.defaultAclPrivate)} onClick={action(() => (Doc.defaultAclPrivate = !Doc.defaultAclPrivate))} />
</div>
</div>
</div>
@@ -470,45 +435,35 @@ export class SettingsManager extends React.Component<{}> {
<div className="settings-panel" style={{ background: this.userColor }}>
<div className="settings-tabs">
{tabs.map(tab => {
- const isActive = this.activeTab === tab.title
+ const isActive = this.activeTab === tab.title;
return (
- <div key={tab.title}
+ <div
+ key={tab.title}
style={{
background: isActive ? this.userBackgroundColor : this.userColor,
color: isActive ? this.userColor : this.userBackgroundColor,
}}
- className={'tab-control ' + (isActive ? 'active' : 'inactive')}
- onClick={action(() => (this.activeTab = tab.title))
- }>
+ className={'tab-control ' + (isActive ? 'active' : 'inactive')}
+ onClick={action(() => (this.activeTab = tab.title))}>
{tab.title}
</div>
- )
+ );
})}
</div>
<div className="settings-user">
- <div className="settings-username"
- style={{color: this.userBackgroundColor}}
- >{Doc.CurrentUserEmail}</div>
- <Button
- text={Doc.GuestDashboard ? 'Exit' : 'Log Out'}
- type={Type.TERT}
- color={this.userVariantColor}
- onClick={() => window.location.assign(Utils.prepend('/logout'))}
- />
+ <div className="settings-username" style={{ color: this.userBackgroundColor }}>
+ {Doc.CurrentUserEmail}
+ </div>
+ <Button text={Doc.GuestDashboard ? 'Exit' : 'Log Out'} type={Type.TERT} color={this.userVariantColor} onClick={() => window.location.assign(Utils.prepend('/logout'))} />
</div>
</div>
-
<div className="close-button">
- <Button
- icon={<FontAwesomeIcon icon={'times'} size={'lg'} />}
- onClick={this.close}
- color={this.userColor}
- />
+ <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={this.close} color={this.userColor} />
</div>
- <div className="settings-content" style={{color: this.userColor, background: this.userBackgroundColor}}>
+ <div className="settings-content" style={{ color: this.userColor, background: this.userBackgroundColor }}>
{tabs.map(tab => (
<div key={tab.title} className={'tab-section ' + (this.activeTab === tab.title ? 'active' : 'inactive')}>
{tab.ele}
diff --git a/src/client/util/reportManager/ReportManager.scss b/src/client/util/reportManager/ReportManager.scss
new file mode 100644
index 000000000..cd6a1d934
--- /dev/null
+++ b/src/client/util/reportManager/ReportManager.scss
@@ -0,0 +1,364 @@
+@import '../../views/global/globalCssVariables';
+
+// header
+
+.report-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .header-btns {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ }
+
+ h2 {
+ margin: 0;
+ padding: 0;
+ font-size: 24px;
+ }
+}
+
+.report-header-vertical {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 4px;
+
+ h2 {
+ margin: 0;
+ padding: 0;
+ padding-bottom: 8px;
+ font-size: 24px;
+ }
+}
+
+// Report
+
+.report-issue {
+ width: 450px;
+ min-width: 300px;
+ padding: 16px;
+ padding-top: 32px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ // background-color: #ffffff;
+ text-align: left;
+ position: relative;
+
+ .report-label {
+ font-size: 14px;
+ font-weight: 400;
+ }
+
+ .report-section {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .report-textarea {
+ border: none;
+ outline: none;
+ width: 100%;
+ height: 80px;
+ padding: 8px;
+ resize: vertical;
+ background: transparent;
+ transition: border 0.3s ease;
+ }
+
+ .report-selects {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 16px;
+ background-color: transparent;
+
+ .report-select {
+ padding: 8px;
+ background-color: transparent;
+
+ .report-opt {
+ padding: 8px;
+ }
+ }
+ }
+}
+
+.report-input {
+ border: none;
+ outline: none;
+ border-bottom: 1px solid;
+ padding: 8px;
+ padding-left: 0;
+ transition: all 0.2s ease;
+ background: transparent;
+
+ &:hover {
+ // border-bottom-color: $text-gray;
+ }
+ &:focus {
+ // border-bottom-color: #4476f7;
+ }
+}
+
+// View issues
+
+.view-issues {
+ width: 75vw;
+ min-width: 500px;
+ display: flex;
+ gap: 16px;
+ height: 100%;
+ overflow-x: auto;
+
+ video::-webkit-media-controls {
+ display: flex !important;
+ }
+
+ .left {
+ flex: 1;
+ height: 100%;
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ text-align: left;
+ position: relative;
+
+ .issues {
+ padding-top: 24px;
+ position: relative;
+ flex-grow: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+ }
+
+ .right {
+ position: relative;
+ flex: 1;
+ padding: 16px;
+ min-width: 300px;
+ height: 100%;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ }
+}
+
+// Issue
+
+.issue-card {
+ cursor: pointer;
+ padding: 16px;
+ border: 1px solid;
+ transition: all 0.1s ease;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ border-radius: 8px;
+ transition: all 0.2s ease;
+
+ .issue-top {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding-bottom: 8px;
+ }
+
+ .issue-label {
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 400;
+ padding: 0;
+ margin: 0;
+ }
+
+ .issue-title {
+ font-size: 16px;
+ font-weight: 500;
+ padding: 0;
+ margin: 0;
+ }
+}
+
+// Dropzone
+
+.dropzone {
+ padding: 2rem;
+ border-radius: 0.5rem;
+ border: 2px dashed;
+
+ .dropzone-instructions {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+
+ p {
+ text-align: center;
+ }
+ }
+}
+
+.file-list {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ font-size: 14px;
+ width: 100%;
+ overflow-x: auto;
+ list-style-type: none;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+
+ .file-name {
+ padding: 8px 12px;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ white-space: nowrap;
+ }
+}
+
+// Detailed issue view
+
+.issue-view {
+ height: 100%;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ text-align: left;
+ position: relative;
+ overflow: auto;
+
+ .issue-label {
+ .issue-link {
+ cursor: pointer;
+ color: #4476f7;
+ }
+ }
+
+ .issue-title {
+ font-size: 24px;
+ margin: 0;
+ padding: 0;
+ }
+
+ .issue-date {
+ font-size: 14px;
+ }
+
+ .issue-content {
+ font-size: 14px;
+ }
+}
+
+// tags flex lists
+
+.issues-filters {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ .issues-filter {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ white-space: nowrap;
+ overflow-x: auto;
+ }
+}
+
+.issue-tags {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ white-space: nowrap;
+ overflow-x: auto;
+}
+
+// Media previews
+
+.report-media-wrapper {
+ position: relative;
+ cursor: pointer;
+
+ .close-btn {
+ position: absolute;
+ top: 2px;
+ right: 2px;
+ opacity: 0;
+ }
+
+ .report-media-content {
+ position: relative;
+ display: inline block;
+
+ video::-webkit-media-controls {
+ display: flex !important;
+ }
+ }
+
+ .report-media-content::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5); /* Adjust the opacity as desired */
+ opacity: 0;
+ transition: opacity 0.3s ease; /* Transition for smooth effect */
+ pointer-events: none;
+
+ video::-webkit-media-controls {
+ pointer-events: all;
+ }
+ }
+
+ &:hover {
+ .report-media-content::after {
+ opacity: 1;
+ }
+
+ .close-btn {
+ opacity: 1;
+ }
+ }
+}
+
+.report-audio-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+@media (max-width: 1100px) {
+ .report-header {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 2rem;
+ }
+}
+
+// Tag styling
+
+.report-tag {
+ box-sizing: border-box;
+ padding: 4px 10px;
+ font-size: 10px;
+ border-radius: 32px;
+ transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
+}
diff --git a/src/client/util/reportManager/ReportManager.tsx b/src/client/util/reportManager/ReportManager.tsx
new file mode 100644
index 000000000..f20c2baaa
--- /dev/null
+++ b/src/client/util/reportManager/ReportManager.tsx
@@ -0,0 +1,409 @@
+import * as React from 'react';
+import v4 = require('uuid/v4');
+import '.././SettingsManager.scss';
+import './ReportManager.scss';
+import Dropzone from 'react-dropzone';
+import ReactLoading from 'react-loading';
+import { action, observable } from 'mobx';
+import { BsX, BsArrowsAngleExpand, BsArrowsAngleContract } from 'react-icons/bs';
+import { CgClose } from 'react-icons/cg';
+import { AiOutlineUpload } from 'react-icons/ai';
+import { HiOutlineArrowLeft } from 'react-icons/hi';
+import { Issue } from './reportManagerSchema';
+import { observer } from 'mobx-react';
+import { Doc } from '../../../fields/Doc';
+import { MainViewModal } from '../../views/MainViewModal';
+import { Octokit } from '@octokit/core';
+import { Button, Dropdown, DropdownType, IconButton, Type } from 'browndash-components';
+import { BugType, FileData, Priority, ReportForm, ViewState, bugDropdownItems, darkColors, emptyReportForm, formatTitle, getAllIssues, isDarkMode, lightColors, passesTagFilter, priorityDropdownItems, uploadFilesToServer } from './reportManagerUtils';
+import { Filter, FormInput, FormTextArea, IssueCard, IssueView, Tag } from './ReportManagerComponents';
+import { StrCast } from '../../../fields/Types';
+import { MdRefresh } from 'react-icons/md';
+const higflyout = require('@hig/flyout');
+export const { anchorPoints } = higflyout;
+export const Flyout = higflyout.default;
+
+/**
+ * Class for reporting and viewing Github issues within the app.
+ */
+@observer
+export class ReportManager extends React.Component<{}> {
+ public static Instance: ReportManager;
+ @observable private isOpen = false;
+
+ @observable private query = '';
+ @action private setQuery = (q: string) => {
+ this.query = q;
+ };
+
+ private octokit: Octokit;
+
+ @observable viewState: ViewState = ViewState.VIEW;
+ @action private setViewState = (state: ViewState) => {
+ this.viewState = state;
+ };
+ @observable submitting: boolean = false;
+ @action private setSubmitting = (submitting: boolean) => {
+ this.submitting = submitting;
+ };
+
+ @observable fetchingIssues: boolean = false;
+ @action private setFetchingIssues = (fetching: boolean) => {
+ this.fetchingIssues = fetching;
+ };
+
+ @observable
+ public shownIssues: Issue[] = [];
+ @action setShownIssues = action((issues: Issue[]) => {
+ this.shownIssues = issues;
+ });
+
+ @observable
+ public priorityFilter: Priority | null = null;
+ @action setPriorityFilter = action((priority: Priority | null) => {
+ this.priorityFilter = priority;
+ });
+
+ @observable
+ public bugFilter: BugType | null = null;
+ @action setBugFilter = action((bug: BugType | null) => {
+ this.bugFilter = bug;
+ });
+
+ @observable selectedIssue: Issue | undefined = undefined;
+ @action setSelectedIssue = action((issue: Issue | undefined) => {
+ this.selectedIssue = issue;
+ });
+
+ @observable rightExpanded: boolean = false;
+ @action setRightExpanded = action((expanded: boolean) => {
+ this.rightExpanded = expanded;
+ });
+
+ // Form state
+ @observable private formData: ReportForm = emptyReportForm;
+ @action setFormData = action((newData: ReportForm) => {
+ this.formData = newData;
+ });
+
+ public close = action(() => (this.isOpen = false));
+ public open = action(async () => {
+ this.isOpen = true;
+ if (this.shownIssues.length === 0) {
+ this.updateIssues();
+ }
+ });
+
+ @action updateIssues = action(async () => {
+ this.setFetchingIssues(true);
+ try {
+ const issues = (await getAllIssues(this.octokit)) as Issue[];
+ this.setShownIssues(issues.filter(issue => issue.state === 'open' && !issue.pull_request));
+ } catch (err) {
+ console.log(err);
+ }
+ this.setFetchingIssues(false);
+ });
+
+ constructor(props: {}) {
+ super(props);
+ ReportManager.Instance = this;
+
+ // initializing Github connection
+ this.octokit = new Octokit({
+ auth: process.env.GITHUB_ACCESS_TOKEN,
+ });
+ }
+
+ /**
+ * Sends a request to Github to report a new issue with the form data.
+ * @returns nothing
+ */
+ public async reportIssue(): Promise<void> {
+ if (this.formData.title === '' || this.formData.description === '') {
+ alert('Please fill out all required fields to report an issue.');
+ return;
+ }
+ let formattedLinks: string[] = [];
+ this.setSubmitting(true);
+ if (this.formData.mediaFiles.length > 0) {
+ const links = await uploadFilesToServer(this.formData.mediaFiles);
+ console.log(links);
+ if (!links) {
+ return;
+ }
+ formattedLinks = links;
+ }
+
+ const req = await this.octokit.request('POST /repos/{owner}/{repo}/issues', {
+ owner: 'brown-dash',
+ repo: 'Dash-Web',
+ title: formatTitle(this.formData.title, Doc.CurrentUserEmail),
+ body: `${this.formData.description} ${formattedLinks.length > 0 ? `\n\nFiles:\n${formattedLinks.join('\n')}` : ''}`,
+ labels: ['from-dash-app', this.formData.type, this.formData.priority],
+ });
+
+ // 201 status means success
+ if (req.status !== 201) {
+ alert('Error creating issue on github.');
+ return;
+ }
+
+ // Reset fields
+ this.setFormData(emptyReportForm);
+ this.setSubmitting(false);
+ await this.updateIssues();
+ alert('Successfully submitted issue.');
+ }
+
+ /**
+ * Handles file upload.
+ *
+ * @param files uploaded files
+ */
+ private onDrop = (files: File[]) => {
+ this.setFormData({ ...this.formData, mediaFiles: [...this.formData.mediaFiles, ...files.map(file => ({ _id: v4(), file }))] });
+ };
+
+ /**
+ * Gets a JSX element to render a media preview
+ * @param fileData file data
+ * @returns JSX element of a piece of media (image, video, audio)
+ */
+ private getMediaPreview = (fileData: FileData): JSX.Element => {
+ const file = fileData.file;
+ const mimeType = file.type;
+ const preview = URL.createObjectURL(file);
+
+ if (mimeType.startsWith('image/')) {
+ return (
+ <div key={fileData._id} className="report-media-wrapper">
+ <div className="report-media-content">
+ <img height={100} alt={`Preview of ${file.name}`} src={preview} style={{ display: 'block' }} />
+ </div>
+ <div className="close-btn">
+ <IconButton icon={<BsX color="#ffffff" />} onClick={() => this.setFormData({ ...this.formData, mediaFiles: this.formData.mediaFiles.filter(f => f._id !== fileData._id) })} />
+ </div>
+ </div>
+ );
+ } else if (mimeType.startsWith('video/')) {
+ return (
+ <div key={fileData._id} className="report-media-wrapper">
+ <div className="report-media-content">
+ <video className="report-default-video" controls style={{ height: '100px', width: 'auto', display: 'block' }}>
+ <source src={preview} type="video/mp4" />
+ Your browser does not support the video tag.
+ </video>
+ </div>
+ <div className="close-btn">
+ <IconButton icon={<BsX color="#ffffff" />} onClick={() => this.setFormData({ ...this.formData, mediaFiles: this.formData.mediaFiles.filter(f => f._id !== fileData._id) })} />
+ </div>
+ </div>
+ );
+ } else if (mimeType.startsWith('audio/')) {
+ return (
+ <div key={fileData._id} className="report-audio-wrapper">
+ <audio src={preview} controls />
+ <div className="close-btn">
+ <IconButton icon={<BsX color="#ffffff" />} onClick={() => this.setFormData({ ...this.formData, mediaFiles: this.formData.mediaFiles.filter(f => f._id !== fileData._id) })} />
+ </div>
+ </div>
+ );
+ }
+ return <></>;
+ };
+
+ /**
+ * @returns the component that dispays all issues
+ */
+ private viewIssuesComponent = () => {
+ const darkMode = isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor));
+ const colors = darkMode ? darkColors : lightColors;
+
+ return (
+ <div className="view-issues" style={{ backgroundColor: StrCast(Doc.UserDoc().userBackgroundColor), color: colors.text }}>
+ <div className="left" style={{ display: this.rightExpanded ? 'none' : 'flex' }}>
+ <div className="report-header">
+ <h2 style={{ color: colors.text }}>Open Issues</h2>
+ <div className="header-btns">
+ <IconButton color={StrCast(Doc.UserDoc().userColor)} tooltip="refresh" icon={<MdRefresh size="16px" />} onClick={this.updateIssues} />
+ <Button
+ type={Type.TERT}
+ color={StrCast(Doc.UserDoc().userVariantColor)}
+ text="Report Issue"
+ onClick={() => {
+ this.setViewState(ViewState.CREATE);
+ }}
+ />
+ </div>
+ </div>
+ <FormInput value={this.query} placeholder="Filter by query..." onChange={this.setQuery} />
+ <div className="issues-filters">
+ <Filter items={Object.values(Priority)} activeValue={this.priorityFilter} setActiveValue={p => this.setPriorityFilter(p)} />
+ <Filter items={Object.values(BugType)} activeValue={this.bugFilter} setActiveValue={b => this.setBugFilter(b)} />
+ </div>
+ <div className="issues">
+ {this.fetchingIssues ? (
+ <div style={{ flexGrow: 1, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
+ <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userColor)} width={50} height={50} />
+ </div>
+ ) : (
+ this.shownIssues
+ .filter(issue => issue.title.toLowerCase().includes(this.query))
+ .filter(issue => passesTagFilter(issue, this.priorityFilter, this.bugFilter))
+ .map(issue => (
+ <IssueCard
+ key={issue.number}
+ issue={issue}
+ onSelect={() => {
+ this.setSelectedIssue(issue);
+ }}
+ />
+ ))
+ )}
+ </div>
+ </div>
+ <div className="right">{this.selectedIssue ? <IssueView key={this.selectedIssue.number} issue={this.selectedIssue} /> : <div>No issue selected</div>} </div>
+ <div style={{ position: 'absolute', top: '8px', right: '8px', display: 'flex', gap: '16px' }}>
+ <IconButton
+ color={StrCast(Doc.UserDoc().userColor)}
+ tooltip={this.rightExpanded ? 'Minimize right side' : 'Expand right side'}
+ icon={this.rightExpanded ? <BsArrowsAngleContract size="16px" /> : <BsArrowsAngleExpand size="16px" />}
+ onClick={e => {
+ e.stopPropagation();
+ this.setRightExpanded(!this.rightExpanded);
+ }}
+ />
+ <IconButton color={StrCast(Doc.UserDoc().userColor)} tooltip="close" icon={<CgClose size="16px" />} onClick={this.close} />
+ </div>
+ </div>
+ );
+ };
+
+ /**
+ * @returns the form component for submitting issues
+ */
+ private reportIssueComponent = () => {
+ const darkMode = isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor));
+ const colors = darkMode ? darkColors : lightColors;
+
+ return (
+ <div className="report-issue" style={{ color: colors.text }}>
+ <div className="report-header-vertical">
+ <Button
+ type={Type.PRIM}
+ color={StrCast(Doc.UserDoc().userColor)}
+ text="back to view"
+ icon={<HiOutlineArrowLeft />}
+ iconPlacement="left"
+ onClick={() => {
+ this.setViewState(ViewState.VIEW);
+ }}
+ />
+ <h2>Report an Issue</h2>
+ </div>
+ <div className="report-section">
+ <label className="report-label">Please provide a title for the bug</label>
+ <FormInput value={this.formData.title} placeholder="Title..." onChange={val => this.setFormData({ ...this.formData, title: val })} />
+ </div>
+ <div className="report-section">
+ <label className="report-label">Please leave a description for the bug and how it can be recreated</label>
+ <FormTextArea value={this.formData.description} placeholder="Description..." onChange={val => this.setFormData({ ...this.formData, description: val })} />
+ </div>
+ <div className="report-selects">
+ <Dropdown
+ color={StrCast(Doc.UserDoc().userColor)}
+ formLabel={'Type'}
+ items={bugDropdownItems}
+ selectedVal={this.formData.type}
+ setSelectedVal={val => {
+ if (typeof val === 'string') this.setFormData({ ...this.formData, type: val as BugType });
+ }}
+ dropdownType={DropdownType.SELECT}
+ type={Type.TERT}
+ fillWidth
+ />
+ <Dropdown
+ color={StrCast(Doc.UserDoc().userColor)}
+ formLabel={'Priority'}
+ items={priorityDropdownItems}
+ selectedVal={this.formData.priority}
+ setSelectedVal={val => {
+ if (typeof val === 'string') this.setFormData({ ...this.formData, priority: val as Priority });
+ }}
+ dropdownType={DropdownType.SELECT}
+ type={Type.TERT}
+ fillWidth
+ />
+ </div>
+ <Dropzone
+ onDrop={this.onDrop}
+ accept={{
+ 'image/*': ['.png', '.jpg', '.jpeg', '.gif'],
+ 'video/*': ['.mp4', '.mpeg', '.webm', '.mov'],
+ 'audio/mpeg': ['.mp3'],
+ 'audio/wav': ['.wav'],
+ 'audio/ogg': ['.ogg'],
+ }}>
+ {({ getRootProps, getInputProps }) => (
+ <div {...getRootProps({ className: 'dropzone' })} style={{ borderColor: isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.border : lightColors.border }}>
+ <input {...getInputProps()} />
+ <div className="dropzone-instructions">
+ <AiOutlineUpload size={25} />
+ <p>Drop or select media that shows the bug (optional)</p>
+ </div>
+ </div>
+ )}
+ </Dropzone>
+ {this.formData.mediaFiles.length > 0 && <ul className="file-list">{this.formData.mediaFiles.map(file => this.getMediaPreview(file))}</ul>}
+ {this.submitting ? (
+ <Button
+ text="Submit"
+ type={Type.TERT}
+ color={StrCast(Doc.UserDoc().userVariantColor)}
+ icon={<ReactLoading type="spin" color={'#ffffff'} width={20} height={20} />}
+ iconPlacement="right"
+ onClick={() => {
+ this.reportIssue();
+ }}
+ />
+ ) : (
+ <Button
+ text="Submit"
+ type={Type.TERT}
+ color={StrCast(Doc.UserDoc().userVariantColor)}
+ onClick={() => {
+ this.reportIssue();
+ }}
+ />
+ )}
+ <div style={{ position: 'absolute', top: '4px', right: '4px' }}>
+ <IconButton color={StrCast(Doc.UserDoc().userColor)} tooltip="close" icon={<CgClose size={'16px'} />} onClick={this.close} />
+ </div>
+ </div>
+ );
+ };
+
+ /**
+ * @returns the component rendered to the modal
+ */
+ private reportComponent = () => {
+ if (this.viewState === ViewState.VIEW) {
+ return this.viewIssuesComponent();
+ } else {
+ return this.reportIssueComponent();
+ }
+ };
+
+ render() {
+ return (
+ <MainViewModal
+ contents={this.reportComponent()}
+ isDisplayed={this.isOpen}
+ interactive={true}
+ closeOnExternalClick={this.close}
+ dialogueBoxStyle={{ width: 'auto', minWidth: '300px', height: '85vh', maxHeight: '90vh', background: StrCast(Doc.UserDoc().userBackgroundColor), borderRadius: '8px' }}
+ />
+ );
+ }
+}
diff --git a/src/client/util/reportManager/ReportManagerComponents.tsx b/src/client/util/reportManager/ReportManagerComponents.tsx
new file mode 100644
index 000000000..8f882c7f2
--- /dev/null
+++ b/src/client/util/reportManager/ReportManagerComponents.tsx
@@ -0,0 +1,360 @@
+import * as React from 'react';
+import { Issue } from './reportManagerSchema';
+import { darkColors, dashBlue, getLabelColors, isDarkMode, lightColors } from './reportManagerUtils';
+import ReactMarkdown from 'react-markdown';
+import rehypeRaw from 'rehype-raw';
+import remarkGfm from 'remark-gfm';
+import { StrCast } from '../../../fields/Types';
+import { Doc } from '../../../fields/Doc';
+
+/**
+ * Mini helper components for the report component.
+ */
+
+interface FilterProps<T> {
+ items: T[];
+ activeValue: T | null;
+ setActiveValue: (val: T | null) => void;
+}
+
+// filter ui for issues (horizontal list of tags)
+export const Filter = <T extends string>({ items, activeValue, setActiveValue }: FilterProps<T>) => {
+ // establishing theme
+ const darkMode = isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor));
+ const colors = darkMode ? darkColors : lightColors;
+ const isTagDarkMode = isDarkMode(StrCast(Doc.UserDoc().userColor));
+ const activeTagTextColor = isTagDarkMode ? darkColors.text : lightColors.text;
+
+ return (
+ <div className="issues-filter">
+ <Tag
+ text={'All'}
+ onClick={() => {
+ setActiveValue(null);
+ }}
+ fontSize="12px"
+ backgroundColor={activeValue === null ? StrCast(Doc.UserDoc().userColor) : 'transparent'}
+ color={activeValue === null ? activeTagTextColor : colors.textGrey}
+ borderColor={activeValue === null ? StrCast(Doc.UserDoc().userColor) : colors.border}
+ border
+ />
+ {items.map(item => {
+ return (
+ <Tag
+ key={item}
+ text={item}
+ onClick={() => {
+ setActiveValue(item);
+ }}
+ fontSize="12px"
+ backgroundColor={activeValue === item ? StrCast(Doc.UserDoc().userColor) : 'transparent'}
+ color={activeValue === item ? activeTagTextColor : colors.textGrey}
+ border
+ borderColor={activeValue === item ? StrCast(Doc.UserDoc().userColor) : colors.border}
+ />
+ );
+ })}
+ </div>
+ );
+};
+
+interface IssueCardProps {
+ issue: Issue;
+ onSelect: () => void;
+}
+
+// Component for the issue cards list on the left
+export const IssueCard = ({ issue, onSelect }: IssueCardProps) => {
+ const [textColor, setTextColor] = React.useState('');
+ const [bgColor, setBgColor] = React.useState('transparent');
+ const [borderColor, setBorderColor] = React.useState('transparent');
+
+ const resetColors = () => {
+ const darkMode = isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor));
+ const colors = darkMode ? darkColors : lightColors;
+ setTextColor(colors.text);
+ setBorderColor(colors.border);
+ setBgColor('transparent');
+ };
+
+ const handlePointerOver = () => {
+ const darkMode = isDarkMode(StrCast(Doc.UserDoc().userColor));
+ setTextColor(darkMode ? darkColors.text : lightColors.text);
+ setBorderColor(StrCast(Doc.UserDoc().userColor));
+ setBgColor(StrCast(Doc.UserDoc().userColor));
+ };
+
+ React.useEffect(() => {
+ resetColors();
+ }, []);
+
+ return (
+ <div className="issue-card" onClick={onSelect} style={{ color: textColor, backgroundColor: bgColor, borderColor: borderColor }} onPointerOver={handlePointerOver} onPointerOut={resetColors}>
+ <div className="issue-top">
+ <label className="issue-label">#{issue.number}</label>
+ <div className="issue-tags">
+ {issue.labels.map(label => {
+ const labelString = typeof label === 'string' ? label : label.name ?? '';
+ const colors = getLabelColors(labelString);
+ return <Tag key={labelString} text={labelString} backgroundColor={colors[0]} color={colors[1]} />;
+ })}
+ </div>
+ </div>
+ <h3 className="issue-title">{issue.title}</h3>
+ </div>
+ );
+};
+
+interface IssueViewProps {
+ issue: Issue;
+}
+
+// Detailed issue view that displays on the right
+export const IssueView = ({ issue }: IssueViewProps) => {
+ const [issueBody, setIssueBody] = React.useState('');
+
+ // Parses the issue body into a formatted markdown (main functionality is replacing urls with tags)
+ const parseBody = async (body: string) => {
+ const imgTagRegex = /<img\b[^>]*\/?>/;
+ const videoTagRegex = /<video\b[^>]*\/?>/;
+ const audioTagRegex = /<audio\b[^>]*\/?>/;
+
+ const fileRegex = /https:\/\/browndash\.com\/files/;
+ const parts = body.split('\n');
+
+ const modifiedParts = await Promise.all(
+ parts.map(async part => {
+ if (imgTagRegex.test(part) || videoTagRegex.test(part) || audioTagRegex.test(part)) {
+ return `\n${await parseFileTag(part)}\n`;
+ } else if (fileRegex.test(part)) {
+ const tag = await parseDashFiles(part);
+ return tag;
+ } else {
+ return part;
+ }
+ })
+ );
+
+ setIssueBody(modifiedParts.join('\n'));
+ };
+
+ // Extracts the src from an image tag and either returns the raw url if not accessible or a new image tag
+ const parseFileTag = async (tag: string): Promise<string> => {
+ const regex = /src="([^"]+)"/;
+ let url = '';
+ const match = tag.match(regex);
+ if (!match) return tag;
+ url = match[1];
+ if (!url) return tag;
+
+ const mimeType = url.split('.').pop();
+ if (!mimeType) return tag;
+
+ switch (mimeType) {
+ // image
+ case '.jpg':
+ case '.png':
+ case '.jpeg':
+ case '.gif':
+ return await getDisplayedFile(url, 'image');
+ // video
+ case '.mp4':
+ case '.mpeg':
+ case '.webm':
+ case '.mov':
+ return await getDisplayedFile(url, 'video');
+ //audio
+ case '.mp3':
+ case '.wav':
+ case '.ogg':
+ return await getDisplayedFile(url, 'audio');
+ }
+ return tag;
+ };
+
+ // Returns the corresponding HTML tag for a src url
+ const parseDashFiles = async (url: string) => {
+ const dashImgRegex = /https:\/\/browndash\.com\/files[/\\]images/;
+ const dashVideoRegex = /https:\/\/browndash\.com\/files[/\\]videos/;
+ const dashAudioRegex = /https:\/\/browndash\.com\/files[/\\]audio/;
+
+ if (dashImgRegex.test(url)) {
+ return await getDisplayedFile(url, 'image');
+ } else if (dashVideoRegex.test(url)) {
+ return await getDisplayedFile(url, 'video');
+ } else if (dashAudioRegex.test(url)) {
+ return await getDisplayedFile(url, 'audio');
+ } else {
+ return url;
+ }
+ };
+
+ const getDisplayedFile = async (url: string, fileType: 'image' | 'video' | 'audio'): Promise<string> => {
+ switch (fileType) {
+ case 'image':
+ const imgValid = await isImgValid(url);
+ if (!imgValid) return `\n${url} (This image could not be loaded)\n`;
+ return `\n${url}\n<img width="100%" alt="Issue asset" src=${url} />\n`;
+ case 'video':
+ const videoValid = await isVideoValid(url);
+ if (!videoValid) return `\n${url} (This video could not be loaded)\n`;
+ return `\n${url}\n<video class="report-default-video" width="100%" controls alt="Issue asset" src=${url} />\n`;
+ case 'audio':
+ const audioValid = await isAudioValid(url);
+ if (!audioValid) return `\n${url} (This audio could not be loaded)\n`;
+ return `\n${url}\n<audio src=${url} controls />\n`;
+ }
+ };
+
+ // Loads an image and returns a promise that resolves as whether the image is valid or not
+ const isImgValid = (src: string): Promise<boolean> => {
+ const imgElement = document.createElement('img');
+ const validPromise: Promise<boolean> = new Promise(resolve => {
+ imgElement.addEventListener('load', () => resolve(true));
+ imgElement.addEventListener('error', () => resolve(false));
+ });
+ imgElement.src = src;
+ return validPromise;
+ };
+
+ // Loads a video and returns a promise that resolves as whether the video is valid or not
+ const isVideoValid = (src: string): Promise<boolean> => {
+ const videoElement = document.createElement('video');
+ const validPromise: Promise<boolean> = new Promise(resolve => {
+ videoElement.addEventListener('loadeddata', () => resolve(true));
+ videoElement.addEventListener('error', () => resolve(false));
+ });
+ videoElement.src = src;
+ return validPromise;
+ };
+
+ // Loads audio and returns a promise that resolves as whether the audio is valid or not
+ const isAudioValid = (src: string): Promise<boolean> => {
+ const audioElement = document.createElement('audio');
+ const validPromise: Promise<boolean> = new Promise(resolve => {
+ audioElement.addEventListener('loadeddata', () => resolve(true));
+ audioElement.addEventListener('error', () => resolve(false));
+ });
+ audioElement.src = src;
+ return validPromise;
+ };
+
+ // Called on mount to parse the body
+ React.useEffect(() => {
+ setIssueBody('Loading...');
+ parseBody((issue.body as string) ?? '');
+ }, [issue]);
+
+ return (
+ <div className="issue-view">
+ <span className="issue-label">
+ Issue{' '}
+ <a className="issue-link" href={issue.html_url} target="_blank">
+ #{issue.number}
+ </a>
+ </span>
+ <h2 className="issue-title">{issue.title}</h2>
+ <div className="issue-date">
+ Opened on {new Date(issue.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} {issue.user?.login && `by ${issue.user?.login}`}
+ </div>
+ {issue.labels.length > 0 && (
+ <div>
+ <div className="issue-tags">
+ {issue.labels.map(label => {
+ const labelString = typeof label === 'string' ? label : label.name ?? '';
+ const colors = getLabelColors(labelString);
+ return <Tag key={labelString} text={labelString} backgroundColor={colors[0]} color={colors[1]} fontSize="12px" />;
+ })}
+ </div>
+ </div>
+ )}
+ <ReactMarkdown children={issueBody} className="issue-content" linkTarget={'_blank'} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} />
+ </div>
+ );
+};
+
+interface TagProps {
+ text: string;
+ fontSize?: string;
+ color?: string;
+ backgroundColor?: string;
+ borderColor?: string;
+ border?: boolean;
+ onClick?: () => void;
+}
+
+// Small tag for labels of the issue
+export const Tag = ({ text, color, backgroundColor, fontSize, border, borderColor, onClick }: TagProps) => {
+ return (
+ <div
+ onClick={onClick ?? (() => {})}
+ className="report-tag"
+ style={{ color: color ?? '#ffffff', backgroundColor: backgroundColor ?? '#347bff', cursor: onClick ? 'pointer' : 'auto', fontSize: fontSize ?? '10px', border: border ? '1px solid' : 'none', borderColor: borderColor ?? '#94a3b8' }}>
+ {text}
+ </div>
+ );
+};
+
+interface FormInputProps {
+ value: string;
+ placeholder: string;
+ onChange: (val: string) => void;
+}
+export const FormInput = ({ value, placeholder, onChange }: FormInputProps) => {
+ const [inputBorderColor, setInputBorderColor] = React.useState('');
+
+ return (
+ <input
+ className="report-input"
+ style={{ borderBottom: `1px solid ${inputBorderColor}` }}
+ value={value}
+ type="text"
+ placeholder={placeholder}
+ onChange={e => onChange(e.target.value)}
+ required
+ onPointerOver={() => {
+ if (inputBorderColor === dashBlue) return;
+ setInputBorderColor(isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.textGrey : lightColors.textGrey);
+ }}
+ onPointerOut={() => {
+ if (inputBorderColor === dashBlue) return;
+ setInputBorderColor(isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.border : lightColors.border);
+ }}
+ onFocus={() => {
+ setInputBorderColor(dashBlue);
+ }}
+ onBlur={() => {
+ setInputBorderColor(isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.border : lightColors.border);
+ }}
+ />
+ );
+};
+
+export const FormTextArea = ({ value, placeholder, onChange }: FormInputProps) => {
+ const [textAreaBorderColor, setTextAreaBorderColor] = React.useState('');
+
+ return (
+ <textarea
+ className="report-textarea"
+ value={value}
+ placeholder={placeholder}
+ onChange={e => onChange(e.target.value)}
+ required
+ style={{ border: `1px solid ${textAreaBorderColor}` }}
+ onPointerOver={() => {
+ if (textAreaBorderColor === dashBlue) return;
+ setTextAreaBorderColor(isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.textGrey : lightColors.textGrey);
+ }}
+ onPointerOut={() => {
+ if (textAreaBorderColor === dashBlue) return;
+ setTextAreaBorderColor(isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.border : lightColors.border);
+ }}
+ onFocus={() => {
+ setTextAreaBorderColor(dashBlue);
+ }}
+ onBlur={() => {
+ setTextAreaBorderColor(isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.border : lightColors.border);
+ }}
+ />
+ );
+};
diff --git a/src/client/util/reportManager/reportManagerSchema.ts b/src/client/util/reportManager/reportManagerSchema.ts
new file mode 100644
index 000000000..9a1c7c3e9
--- /dev/null
+++ b/src/client/util/reportManager/reportManagerSchema.ts
@@ -0,0 +1,877 @@
+/**
+ * Issue interface schema from Github.
+ */
+export interface Issue {
+ active_lock_reason?: null | string;
+ assignee: null | PurpleSimpleUser;
+ assignees?: AssigneeElement[] | null;
+ /**
+ * How the author is associated with the repository.
+ */
+ author_association: AuthorAssociation;
+ /**
+ * Contents of the issue
+ */
+ body?: null | string;
+ body_html?: string;
+ body_text?: string;
+ closed_at: Date | null;
+ closed_by?: null | FluffySimpleUser;
+ comments: number;
+ comments_url: string;
+ created_at: Date;
+ draft?: boolean;
+ events_url: string;
+ html_url: string;
+ id: number;
+ /**
+ * Labels to associate with this issue; pass one or more label names to replace the set of
+ * labels on this issue; send an empty array to clear all labels from the issue; note that
+ * the labels are silently dropped for users without push access to the repository
+ */
+ labels: Array<LabelObject | string>;
+ labels_url: string;
+ locked: boolean;
+ milestone: null | Milestone;
+ node_id: string;
+ /**
+ * Number uniquely identifying the issue within its repository
+ */
+ number: number;
+ performed_via_github_app?: null | GitHubApp;
+ pull_request?: PullRequest;
+ reactions?: ReactionRollup;
+ /**
+ * A repository on GitHub.
+ */
+ repository?: Repository;
+ repository_url: string;
+ /**
+ * State of the issue; either 'open' or 'closed'
+ */
+ state: string;
+ /**
+ * The reason for the current state
+ */
+ state_reason?: StateReason | null;
+ timeline_url?: string;
+ /**
+ * Title of the issue
+ */
+ title: string;
+ updated_at: Date;
+ /**
+ * URL for the issue
+ */
+ url: string;
+ user: null | TentacledSimpleUser;
+ [property: string]: any;
+}
+
+/**
+ * A GitHub user.
+ */
+export interface PurpleSimpleUser {
+ avatar_url: string;
+ email?: null | string;
+ events_url: string;
+ followers_url: string;
+ following_url: string;
+ gists_url: string;
+ gravatar_id: null | string;
+ html_url: string;
+ id: number;
+ login: string;
+ name?: null | string;
+ node_id: string;
+ organizations_url: string;
+ received_events_url: string;
+ repos_url: string;
+ site_admin: boolean;
+ starred_at?: string;
+ starred_url: string;
+ subscriptions_url: string;
+ type: string;
+ url: string;
+ [property: string]: any;
+}
+
+/**
+ * A GitHub user.
+ */
+export interface AssigneeElement {
+ avatar_url: string;
+ email?: null | string;
+ events_url: string;
+ followers_url: string;
+ following_url: string;
+ gists_url: string;
+ gravatar_id: null | string;
+ html_url: string;
+ id: number;
+ login: string;
+ name?: null | string;
+ node_id: string;
+ organizations_url: string;
+ received_events_url: string;
+ repos_url: string;
+ site_admin: boolean;
+ starred_at?: string;
+ starred_url: string;
+ subscriptions_url: string;
+ type: string;
+ url: string;
+ [property: string]: any;
+}
+
+/**
+ * How the author is associated with the repository.
+ */
+export enum AuthorAssociation {
+ Collaborator = 'COLLABORATOR',
+ Contributor = 'CONTRIBUTOR',
+ FirstTimeContributor = 'FIRST_TIME_CONTRIBUTOR',
+ FirstTimer = 'FIRST_TIMER',
+ Mannequin = 'MANNEQUIN',
+ Member = 'MEMBER',
+ None = 'NONE',
+ Owner = 'OWNER',
+}
+
+/**
+ * A GitHub user.
+ */
+export interface FluffySimpleUser {
+ avatar_url: string;
+ email?: null | string;
+ events_url: string;
+ followers_url: string;
+ following_url: string;
+ gists_url: string;
+ gravatar_id: null | string;
+ html_url: string;
+ id: number;
+ login: string;
+ name?: null | string;
+ node_id: string;
+ organizations_url: string;
+ received_events_url: string;
+ repos_url: string;
+ site_admin: boolean;
+ starred_at?: string;
+ starred_url: string;
+ subscriptions_url: string;
+ type: string;
+ url: string;
+ [property: string]: any;
+}
+
+export interface LabelObject {
+ color?: null | string;
+ default?: boolean;
+ description?: null | string;
+ id?: number;
+ name?: string;
+ node_id?: string;
+ url?: string;
+ [property: string]: any;
+}
+
+/**
+ * A collection of related issues and pull requests.
+ */
+export interface Milestone {
+ closed_at: Date | null;
+ closed_issues: number;
+ created_at: Date;
+ creator: null | MilestoneSimpleUser;
+ description: null | string;
+ due_on: Date | null;
+ html_url: string;
+ id: number;
+ labels_url: string;
+ node_id: string;
+ /**
+ * The number of the milestone.
+ */
+ number: number;
+ open_issues: number;
+ /**
+ * The state of the milestone.
+ */
+ state: State;
+ /**
+ * The title of the milestone.
+ */
+ title: string;
+ updated_at: Date;
+ url: string;
+ [property: string]: any;
+}
+
+/**
+ * A GitHub user.
+ */
+export interface MilestoneSimpleUser {
+ avatar_url: string;
+ email?: null | string;
+ events_url: string;
+ followers_url: string;
+ following_url: string;
+ gists_url: string;
+ gravatar_id: null | string;
+ html_url: string;
+ id: number;
+ login: string;
+ name?: null | string;
+ node_id: string;
+ organizations_url: string;
+ received_events_url: string;
+ repos_url: string;
+ site_admin: boolean;
+ starred_at?: string;
+ starred_url: string;
+ subscriptions_url: string;
+ type: string;
+ url: string;
+ [property: string]: any;
+}
+
+/**
+ * The state of the milestone.
+ */
+export enum State {
+ Closed = 'closed',
+ Open = 'open',
+}
+
+/**
+ * GitHub apps are a new way to extend GitHub. They can be installed directly on
+ * organizations and user accounts and granted access to specific repositories. They come
+ * with granular permissions and built-in webhooks. GitHub apps are first class actors
+ * within GitHub.
+ */
+export interface GitHubApp {
+ client_id?: string;
+ client_secret?: string;
+ created_at: Date;
+ description: null | string;
+ /**
+ * The list of events for the GitHub app
+ */
+ events: string[];
+ external_url: string;
+ html_url: string;
+ /**
+ * Unique identifier of the GitHub app
+ */
+ id: number;
+ /**
+ * The number of installations associated with the GitHub app
+ */
+ installations_count?: number;
+ /**
+ * The name of the GitHub app
+ */
+ name: string;
+ node_id: string;
+ owner: null | GitHubAppSimpleUser;
+ pem?: string;
+ /**
+ * The set of permissions for the GitHub app
+ */
+ permissions: GitHubAppPermissions;
+ /**
+ * The slug name of the GitHub app
+ */
+ slug?: string;
+ updated_at: Date;
+ webhook_secret?: null | string;
+ [property: string]: any;
+}
+
+/**
+ * A GitHub user.
+ */
+export interface GitHubAppSimpleUser {
+ avatar_url: string;
+ email?: null | string;
+ events_url: string;
+ followers_url: string;
+ following_url: string;
+ gists_url: string;
+ gravatar_id: null | string;
+ html_url: string;
+ id: number;
+ login: string;
+ name?: null | string;
+ node_id: string;
+ organizations_url: string;
+ received_events_url: string;
+ repos_url: string;
+ site_admin: boolean;
+ starred_at?: string;
+ starred_url: string;
+ subscriptions_url: string;
+ type: string;
+ url: string;
+ [property: string]: any;
+}
+
+/**
+ * The set of permissions for the GitHub app
+ */
+export interface GitHubAppPermissions {
+ checks?: string;
+ contents?: string;
+ deployments?: string;
+ issues?: string;
+ metadata?: string;
+}
+
+export interface PullRequest {
+ diff_url: null | string;
+ html_url: null | string;
+ merged_at?: Date | null;
+ patch_url: null | string;
+ url: null | string;
+ [property: string]: any;
+}
+
+export interface ReactionRollup {
+ '+1': number;
+ '-1': number;
+ confused: number;
+ eyes: number;
+ heart: number;
+ hooray: number;
+ laugh: number;
+ rocket: number;
+ total_count: number;
+ url: string;
+ [property: string]: any;
+}
+
+/**
+ * A repository on GitHub.
+ */
+export interface Repository {
+ /**
+ * Whether to allow Auto-merge to be used on pull requests.
+ */
+ allow_auto_merge?: boolean;
+ /**
+ * Whether to allow forking this repo
+ */
+ allow_forking?: boolean;
+ /**
+ * Whether to allow merge commits for pull requests.
+ */
+ allow_merge_commit?: boolean;
+ /**
+ * Whether to allow rebase merges for pull requests.
+ */
+ allow_rebase_merge?: boolean;
+ /**
+ * Whether to allow squash merges for pull requests.
+ */
+ allow_squash_merge?: boolean;
+ /**
+ * Whether or not a pull request head branch that is behind its base branch can always be
+ * updated even if it is not required to be up to date before merging.
+ */
+ allow_update_branch?: boolean;
+ /**
+ * Whether anonymous git access is enabled for this repository
+ */
+ anonymous_access_enabled?: boolean;
+ archive_url: string;
+ /**
+ * Whether the repository is archived.
+ */
+ archived: boolean;
+ assignees_url: string;
+ blobs_url: string;
+ branches_url: string;
+ clone_url: string;
+ collaborators_url: string;
+ comments_url: string;
+ commits_url: string;
+ compare_url: string;
+ contents_url: string;
+ contributors_url: string;
+ created_at: Date | null;
+ /**
+ * The default branch of the repository.
+ */
+ default_branch: string;
+ /**
+ * Whether to delete head branches when pull requests are merged
+ */
+ delete_branch_on_merge?: boolean;
+ deployments_url: string;
+ description: null | string;
+ /**
+ * Returns whether or not this repository disabled.
+ */
+ disabled: boolean;
+ downloads_url: string;
+ events_url: string;
+ fork: boolean;
+ forks: number;
+ forks_count: number;
+ forks_url: string;
+ full_name: string;
+ git_commits_url: string;
+ git_refs_url: string;
+ git_tags_url: string;
+ git_url: string;
+ /**
+ * Whether discussions are enabled.
+ */
+ has_discussions?: boolean;
+ /**
+ * Whether downloads are enabled.
+ */
+ has_downloads: boolean;
+ /**
+ * Whether issues are enabled.
+ */
+ has_issues: boolean;
+ has_pages: boolean;
+ /**
+ * Whether projects are enabled.
+ */
+ has_projects: boolean;
+ /**
+ * Whether the wiki is enabled.
+ */
+ has_wiki: boolean;
+ homepage: null | string;
+ hooks_url: string;
+ html_url: string;
+ /**
+ * Unique identifier of the repository
+ */
+ id: number;
+ /**
+ * Whether this repository acts as a template that can be used to generate new repositories.
+ */
+ is_template?: boolean;
+ issue_comment_url: string;
+ issue_events_url: string;
+ issues_url: string;
+ keys_url: string;
+ labels_url: string;
+ language: null | string;
+ languages_url: string;
+ license: null | LicenseSimple;
+ master_branch?: string;
+ /**
+ * The default value for a merge commit message.
+ *
+ * - `PR_TITLE` - default to the pull request's title.
+ * - `PR_BODY` - default to the pull request's body.
+ * - `BLANK` - default to a blank commit message.
+ */
+ merge_commit_message?: MergeCommitMessage;
+ /**
+ * The default value for a merge commit title.
+ *
+ * - `PR_TITLE` - default to the pull request's title.
+ * - `MERGE_MESSAGE` - default to the classic title for a merge message (e.g., Merge pull
+ * request #123 from branch-name).
+ */
+ merge_commit_title?: MergeCommitTitle;
+ merges_url: string;
+ milestones_url: string;
+ mirror_url: null | string;
+ /**
+ * The name of the repository.
+ */
+ name: string;
+ network_count?: number;
+ node_id: string;
+ notifications_url: string;
+ open_issues: number;
+ open_issues_count: number;
+ organization?: null | RepositorySimpleUser;
+ /**
+ * A GitHub user.
+ */
+ owner: OwnerObject;
+ permissions?: RepositoryPermissions;
+ /**
+ * Whether the repository is private or public.
+ */
+ private: boolean;
+ pulls_url: string;
+ pushed_at: Date | null;
+ releases_url: string;
+ /**
+ * The size of the repository. Size is calculated hourly. When a repository is initially
+ * created, the size is 0.
+ */
+ size: number;
+ /**
+ * The default value for a squash merge commit message:
+ *
+ * - `PR_BODY` - default to the pull request's body.
+ * - `COMMIT_MESSAGES` - default to the branch's commit messages.
+ * - `BLANK` - default to a blank commit message.
+ */
+ squash_merge_commit_message?: SquashMergeCommitMessage;
+ /**
+ * The default value for a squash merge commit title:
+ *
+ * - `PR_TITLE` - default to the pull request's title.
+ * - `COMMIT_OR_PR_TITLE` - default to the commit's title (if only one commit) or the pull
+ * request's title (when more than one commit).
+ */
+ squash_merge_commit_title?: SquashMergeCommitTitle;
+ ssh_url: string;
+ stargazers_count: number;
+ stargazers_url: string;
+ starred_at?: string;
+ statuses_url: string;
+ subscribers_count?: number;
+ subscribers_url: string;
+ subscription_url: string;
+ svn_url: string;
+ tags_url: string;
+ teams_url: string;
+ temp_clone_token?: string;
+ template_repository?: null | TemplateRepository;
+ topics?: string[];
+ trees_url: string;
+ updated_at: Date | null;
+ url: string;
+ /**
+ * Whether a squash merge commit can use the pull request title as default. **This property
+ * has been deprecated. Please use `squash_merge_commit_title` instead.
+ */
+ use_squash_pr_title_as_default?: boolean;
+ /**
+ * The repository visibility: public, private, or internal.
+ */
+ visibility?: string;
+ watchers: number;
+ watchers_count: number;
+ /**
+ * Whether to require contributors to sign off on web-based commits
+ */
+ web_commit_signoff_required?: boolean;
+ [property: string]: any;
+}
+
+/**
+ * License Simple
+ */
+export interface LicenseSimple {
+ html_url?: string;
+ key: string;
+ name: string;
+ node_id: string;
+ spdx_id: null | string;
+ url: null | string;
+ [property: string]: any;
+}
+
+/**
+ * The default value for a merge commit message.
+ *
+ * - `PR_TITLE` - default to the pull request's title.
+ * - `PR_BODY` - default to the pull request's body.
+ * - `BLANK` - default to a blank commit message.
+ */
+export enum MergeCommitMessage {
+ Blank = 'BLANK',
+ PRBody = 'PR_BODY',
+ PRTitle = 'PR_TITLE',
+}
+
+/**
+ * The default value for a merge commit title.
+ *
+ * - `PR_TITLE` - default to the pull request's title.
+ * - `MERGE_MESSAGE` - default to the classic title for a merge message (e.g., Merge pull
+ * request #123 from branch-name).
+ */
+export enum MergeCommitTitle {
+ MergeMessage = 'MERGE_MESSAGE',
+ PRTitle = 'PR_TITLE',
+}
+
+/**
+ * A GitHub user.
+ */
+export interface RepositorySimpleUser {
+ avatar_url: string;
+ email?: null | string;
+ events_url: string;
+ followers_url: string;
+ following_url: string;
+ gists_url: string;
+ gravatar_id: null | string;
+ html_url: string;
+ id: number;
+ login: string;
+ name?: null | string;
+ node_id: string;
+ organizations_url: string;
+ received_events_url: string;
+ repos_url: string;
+ site_admin: boolean;
+ starred_at?: string;
+ starred_url: string;
+ subscriptions_url: string;
+ type: string;
+ url: string;
+ [property: string]: any;
+}
+
+/**
+ * A GitHub user.
+ */
+export interface OwnerObject {
+ avatar_url: string;
+ email?: null | string;
+ events_url: string;
+ followers_url: string;
+ following_url: string;
+ gists_url: string;
+ gravatar_id: null | string;
+ html_url: string;
+ id: number;
+ login: string;
+ name?: null | string;
+ node_id: string;
+ organizations_url: string;
+ received_events_url: string;
+ repos_url: string;
+ site_admin: boolean;
+ starred_at?: string;
+ starred_url: string;
+ subscriptions_url: string;
+ type: string;
+ url: string;
+ [property: string]: any;
+}
+
+export interface RepositoryPermissions {
+ admin: boolean;
+ maintain?: boolean;
+ pull: boolean;
+ push: boolean;
+ triage?: boolean;
+ [property: string]: any;
+}
+
+/**
+ * The default value for a squash merge commit message:
+ *
+ * - `PR_BODY` - default to the pull request's body.
+ * - `COMMIT_MESSAGES` - default to the branch's commit messages.
+ * - `BLANK` - default to a blank commit message.
+ */
+export enum SquashMergeCommitMessage {
+ Blank = 'BLANK',
+ CommitMessages = 'COMMIT_MESSAGES',
+ PRBody = 'PR_BODY',
+}
+
+/**
+ * The default value for a squash merge commit title:
+ *
+ * - `PR_TITLE` - default to the pull request's title.
+ * - `COMMIT_OR_PR_TITLE` - default to the commit's title (if only one commit) or the pull
+ * request's title (when more than one commit).
+ */
+export enum SquashMergeCommitTitle {
+ CommitOrPRTitle = 'COMMIT_OR_PR_TITLE',
+ PRTitle = 'PR_TITLE',
+}
+
+export interface TemplateRepository {
+ allow_auto_merge?: boolean;
+ allow_merge_commit?: boolean;
+ allow_rebase_merge?: boolean;
+ allow_squash_merge?: boolean;
+ allow_update_branch?: boolean;
+ archive_url?: string;
+ archived?: boolean;
+ assignees_url?: string;
+ blobs_url?: string;
+ branches_url?: string;
+ clone_url?: string;
+ collaborators_url?: string;
+ comments_url?: string;
+ commits_url?: string;
+ compare_url?: string;
+ contents_url?: string;
+ contributors_url?: string;
+ created_at?: string;
+ default_branch?: string;
+ delete_branch_on_merge?: boolean;
+ deployments_url?: string;
+ description?: string;
+ disabled?: boolean;
+ downloads_url?: string;
+ events_url?: string;
+ fork?: boolean;
+ forks_count?: number;
+ forks_url?: string;
+ full_name?: string;
+ git_commits_url?: string;
+ git_refs_url?: string;
+ git_tags_url?: string;
+ git_url?: string;
+ has_downloads?: boolean;
+ has_issues?: boolean;
+ has_pages?: boolean;
+ has_projects?: boolean;
+ has_wiki?: boolean;
+ homepage?: string;
+ hooks_url?: string;
+ html_url?: string;
+ id?: number;
+ is_template?: boolean;
+ issue_comment_url?: string;
+ issue_events_url?: string;
+ issues_url?: string;
+ keys_url?: string;
+ labels_url?: string;
+ language?: string;
+ languages_url?: string;
+ /**
+ * The default value for a merge commit message.
+ *
+ * - `PR_TITLE` - default to the pull request's title.
+ * - `PR_BODY` - default to the pull request's body.
+ * - `BLANK` - default to a blank commit message.
+ */
+ merge_commit_message?: MergeCommitMessage;
+ /**
+ * The default value for a merge commit title.
+ *
+ * - `PR_TITLE` - default to the pull request's title.
+ * - `MERGE_MESSAGE` - default to the classic title for a merge message (e.g., Merge pull
+ * request #123 from branch-name).
+ */
+ merge_commit_title?: MergeCommitTitle;
+ merges_url?: string;
+ milestones_url?: string;
+ mirror_url?: string;
+ name?: string;
+ network_count?: number;
+ node_id?: string;
+ notifications_url?: string;
+ open_issues_count?: number;
+ owner?: Owner;
+ permissions?: TemplateRepositoryPermissions;
+ private?: boolean;
+ pulls_url?: string;
+ pushed_at?: string;
+ releases_url?: string;
+ size?: number;
+ /**
+ * The default value for a squash merge commit message:
+ *
+ * - `PR_BODY` - default to the pull request's body.
+ * - `COMMIT_MESSAGES` - default to the branch's commit messages.
+ * - `BLANK` - default to a blank commit message.
+ */
+ squash_merge_commit_message?: SquashMergeCommitMessage;
+ /**
+ * The default value for a squash merge commit title:
+ *
+ * - `PR_TITLE` - default to the pull request's title.
+ * - `COMMIT_OR_PR_TITLE` - default to the commit's title (if only one commit) or the pull
+ * request's title (when more than one commit).
+ */
+ squash_merge_commit_title?: SquashMergeCommitTitle;
+ ssh_url?: string;
+ stargazers_count?: number;
+ stargazers_url?: string;
+ statuses_url?: string;
+ subscribers_count?: number;
+ subscribers_url?: string;
+ subscription_url?: string;
+ svn_url?: string;
+ tags_url?: string;
+ teams_url?: string;
+ temp_clone_token?: string;
+ topics?: string[];
+ trees_url?: string;
+ updated_at?: string;
+ url?: string;
+ use_squash_pr_title_as_default?: boolean;
+ visibility?: string;
+ watchers_count?: number;
+ [property: string]: any;
+}
+
+export interface Owner {
+ avatar_url?: string;
+ events_url?: string;
+ followers_url?: string;
+ following_url?: string;
+ gists_url?: string;
+ gravatar_id?: string;
+ html_url?: string;
+ id?: number;
+ login?: string;
+ node_id?: string;
+ organizations_url?: string;
+ received_events_url?: string;
+ repos_url?: string;
+ site_admin?: boolean;
+ starred_url?: string;
+ subscriptions_url?: string;
+ type?: string;
+ url?: string;
+ [property: string]: any;
+}
+
+export interface TemplateRepositoryPermissions {
+ admin?: boolean;
+ maintain?: boolean;
+ pull?: boolean;
+ push?: boolean;
+ triage?: boolean;
+ [property: string]: any;
+}
+
+export enum StateReason {
+ Completed = 'completed',
+ NotPlanned = 'not_planned',
+ Reopened = 'reopened',
+}
+
+/**
+ * A GitHub user.
+ */
+export interface TentacledSimpleUser {
+ avatar_url: string;
+ email?: null | string;
+ events_url: string;
+ followers_url: string;
+ following_url: string;
+ gists_url: string;
+ gravatar_id: null | string;
+ html_url: string;
+ id: number;
+ login: string;
+ name?: null | string;
+ node_id: string;
+ organizations_url: string;
+ received_events_url: string;
+ repos_url: string;
+ site_admin: boolean;
+ starred_at?: string;
+ starred_url: string;
+ subscriptions_url: string;
+ type: string;
+ url: string;
+ [property: string]: any;
+}
diff --git a/src/client/util/reportManager/reportManagerUtils.ts b/src/client/util/reportManager/reportManagerUtils.ts
new file mode 100644
index 000000000..d8344220f
--- /dev/null
+++ b/src/client/util/reportManager/reportManagerUtils.ts
@@ -0,0 +1,257 @@
+// Final file url reference: "https://browndash.com/files/images/upload_cb31bc0fda59c96ca14193ec494f80cf_o.jpg" />
+
+import { Octokit } from '@octokit/core';
+import { Networking } from '../../Network';
+import { Issue } from './reportManagerSchema';
+
+// enums and interfaces
+
+export enum ViewState {
+ VIEW,
+ CREATE,
+}
+
+export enum Priority {
+ HIGH = 'priority-high',
+ MEDIUM = 'priority-medium',
+ LOW = 'priority-low',
+}
+
+export enum BugType {
+ BUG = 'bug',
+ COSMETIC = 'cosmetic',
+ DOCUMENTATION = 'documentation',
+ ENHANCEMENT = 'enhancement',
+}
+
+export interface FileData {
+ _id: string;
+ file: File;
+}
+
+export interface ReportForm {
+ title: string;
+ description: string;
+ type: BugType;
+ priority: Priority;
+ mediaFiles: FileData[];
+}
+
+export type ReportFormKey = keyof ReportForm;
+
+export const emptyReportForm = {
+ title: '',
+ description: '',
+ type: BugType.BUG,
+ priority: Priority.MEDIUM,
+ mediaFiles: [],
+};
+
+// interfacing with Github
+
+/**
+ * Fetches issues from Github.
+ * @returns array of all issues
+ */
+export const getAllIssues = async (octokit: Octokit): Promise<any[]> => {
+ const res = await octokit.request('GET /repos/{owner}/{repo}/issues', {
+ owner: 'brown-dash',
+ repo: 'Dash-Web',
+ per_page: 80,
+ });
+
+ // 200 status means success
+ if (res.status === 200) {
+ return res.data;
+ } else {
+ throw new Error('Error getting issues');
+ }
+};
+
+/**
+ * Formats issue title.
+ *
+ * @param title title of issue
+ * @param userEmail email of issue submitter
+ * @returns formatted title
+ */
+export const formatTitle = (title: string, userEmail: string): string => `${title} - ${userEmail.replace('@brown.edu', '')}`;
+
+// uploading
+
+// turns an upload link -> server link
+// ex:
+// C: /Users/dash/Documents/GitHub/Dash-Web/src/server/public/files/images/upload_8008dbc4b6424fbff14da7345bb32eb2.png
+// -> https://browndash.com/files/images/upload_8008dbc4b6424fbff14da7345bb32eb2_l.png
+export const fileLinktoServerLink = (fileLink: string): string => {
+ const serverUrl = 'https://browndash.com/';
+
+ const regex = 'public';
+ const publicIndex = fileLink.indexOf(regex) + regex.length;
+
+ const finalUrl = `${serverUrl}${fileLink.substring(publicIndex + 1).replace('.', '_l.')}`;
+ return finalUrl;
+};
+
+/**
+ * Gets the server file path.
+ *
+ * @param link response from file upload
+ * @returns server file path
+ */
+export const getServerPath = (link: any): string => {
+ return link.result.accessPaths.agnostic.server as string;
+};
+
+/**
+ * Uploads media files to the server.
+ * @returns the server paths or undefined on error
+ */
+export const uploadFilesToServer = async (mediaFiles: FileData[]): Promise<string[] | undefined> => {
+ try {
+ // need to always upload to browndash
+ const links = await Networking.UploadFilesToServer(
+ mediaFiles.map(file => ({ file: file.file })),
+ true
+ );
+ return (links ?? []).map(getServerPath).map(fileLinktoServerLink);
+ } catch (err) {
+ if (err instanceof Error) {
+ alert(err.message);
+ } else {
+ alert(err);
+ }
+ }
+};
+
+// helper functions
+
+/**
+ * Returns when the issue passes the current filters.
+ *
+ * @param issue issue to check
+ * @returns boolean indicating whether the issue passes the current filters
+ */
+export const passesTagFilter = (issue: Issue, priorityFilter: string | null, bugFilter: string | null) => {
+ let passesPriority = true;
+ let passesBug = true;
+ if (priorityFilter) {
+ passesPriority = issue.labels.some(label => {
+ if (typeof label === 'string') {
+ return label === priorityFilter;
+ } else {
+ return label.name === priorityFilter;
+ }
+ });
+ }
+ if (bugFilter) {
+ passesBug = issue.labels.some(label => {
+ if (typeof label === 'string') {
+ return label === bugFilter;
+ } else {
+ return label.name === bugFilter;
+ }
+ });
+ }
+ return passesPriority && passesBug;
+};
+
+// sets and lists
+
+export const prioritySet = new Set(Object.values(Priority));
+export const bugSet = new Set(Object.values(BugType));
+
+export const priorityDropdownItems = [
+ {
+ text: 'Low',
+ val: Priority.LOW,
+ },
+ {
+ text: 'Medium',
+ val: Priority.MEDIUM,
+ },
+ {
+ text: 'High',
+ val: Priority.HIGH,
+ },
+];
+
+export const bugDropdownItems = [
+ {
+ text: 'Bug',
+ val: BugType.BUG,
+ },
+ {
+ text: 'Poor Design or Cosmetic',
+ val: BugType.COSMETIC,
+ },
+ {
+ text: 'Documentation',
+ val: BugType.DOCUMENTATION,
+ },
+ {
+ text: 'New feature or request',
+ val: BugType.ENHANCEMENT,
+ },
+];
+
+// colors
+
+// [bgColor, color]
+export const priorityColors: { [key: string]: string[] } = {
+ 'priority-low': ['#d4e0ff', '#000000'],
+ 'priority-medium': ['#6a91f6', '#ffffff'],
+ 'priority-high': ['#003cd5', '#ffffff'],
+};
+
+// [bgColor, color]
+export const bugColors: { [key: string]: string[] } = {
+ bug: ['#fe6d6d', '#ffffff'],
+ cosmetic: ['#c650f4', '#ffffff'],
+ documentation: ['#36acf0', '#ffffff'],
+ enhancement: ['#36d4f0', '#ffffff'],
+};
+
+export const getLabelColors = (label: string): string[] => {
+ if (prioritySet.has(label as Priority)) {
+ return priorityColors[label];
+ } else if (bugSet.has(label as BugType)) {
+ return bugColors[label];
+ }
+ return ['#0f73f6', '#ffffff'];
+};
+
+const hexToRgb = (hex: string) => {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result
+ ? {
+ r: parseInt(result[1], 16),
+ g: parseInt(result[2], 16),
+ b: parseInt(result[3], 16),
+ }
+ : {
+ r: 0,
+ g: 0,
+ b: 0,
+ };
+};
+
+// function that returns whether text should be light on the given bg color
+export const isDarkMode = (bgHex: string): boolean => {
+ const { r, g, b } = hexToRgb(bgHex);
+ return r * 0.299 + g * 0.587 + b * 0.114 <= 186;
+};
+
+export const lightColors = {
+ text: '#000000',
+ textGrey: '#5c5c5c',
+ border: '#b8b8b8',
+};
+
+export const darkColors = {
+ text: '#ffffff',
+ textGrey: '#d6d6d6',
+ border: '#717171',
+};
+
+export const dashBlue = '#4476f7';
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 86e8bab13..41c8dd19d 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -21,7 +21,7 @@ import { DocumentManager } from '../util/DocumentManager';
import { GroupManager } from '../util/GroupManager';
import { HistoryUtil } from '../util/History';
import { Hypothesis } from '../util/HypothesisUtils';
-import { ReportManager } from '../util/ReportManager';
+import { ReportManager } from '../util/reportManager/ReportManager';
import { RTFMarkup } from '../util/RTFMarkup';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
import { SelectionManager } from '../util/SelectionManager';
@@ -66,6 +66,8 @@ import { PreviewCursor } from './PreviewCursor';
import { PropertiesView } from './PropertiesView';
import { DashboardStyleProvider, DefaultStyleProvider } from './StyleProvider';
import { TopBar } from './topbar/TopBar';
+import GenerativeFill from './nodes/generativeFill/GenerativeFill';
+import { GPTPopup } from './pdf/GPTPopup/GPTPopup';
const _global = (window /* browser */ || global) /* node */ as any;
@observer
@@ -73,6 +75,15 @@ export class MainView extends React.Component {
public static Instance: MainView;
public static Live: boolean = false;
private _docBtnRef = React.createRef<HTMLDivElement>();
+ // for ai image editor
+ @observable public imageEditorOpen: boolean = false;
+ @action public setImageEditorOpen = (open: boolean) => (this.imageEditorOpen = open);
+ @observable public imageEditorSource: string = '';
+ @action public setImageEditorSource = (source: string) => (this.imageEditorSource = source);
+ @observable public imageRootDoc: Doc | undefined;
+ @action public setImageRootDoc = (doc: Doc) => (this.imageRootDoc = doc);
+ @observable public addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined;
+
@observable public LastButton: Opt<Doc>;
@observable private _windowWidth: number = 0;
@observable private _windowHeight: number = 0;
@@ -1021,6 +1032,8 @@ export class MainView extends React.Component {
<InkTranscription />
{this.snapLines}
<LightboxView key="lightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} />
+ <GPTPopup key="gptpopup" />
+ <GenerativeFill imageEditorOpen={this.imageEditorOpen} imageEditorSource={this.imageEditorSource} imageRootDoc={this.imageRootDoc} addDoc={this.addDoc} />
{/* <NewLightboxView key="newLightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> */}
</div>
);
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 67b7b39dd..1ff5688f9 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -137,7 +137,11 @@ export class TabDocView extends React.Component<TabDocViewProps> {
setupMoveUpEvents(
this,
e,
- e => !e.defaultPrevented && DragManager.StartDocumentDrag([iconWrap], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY),
+ e =>
+ !e.defaultPrevented &&
+ DragManager.StartDocumentDrag([iconWrap], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY, undefined, () => {
+ CollectionDockingView.CloseSplit(doc);
+ }),
returnFalse,
action(e => {
if (this.view) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index f1d98d22a..747ab9249 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -21,6 +21,8 @@ export interface CollectionFreeFormLinkViewProps {
LinkDocs: Doc[];
}
+// props.screentolocatransform
+
@observer
export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
@observable _opacity: number = 0;
@@ -235,8 +237,12 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
bActive,
textX,
textY,
- pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13],
- pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13],
+ // fully connected
+ pt1,
+ pt2,
+ // this code adds space between links
+ // pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13],
+ // pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13],
};
}
@@ -294,6 +300,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
className="collectionfreeformlinkview-linkLine"
style={{ pointerEvents: 'visibleStroke', opacity: this._opacity, stroke, strokeWidth }}
onClick={this.onClickLine}
+ // d={`M ${pt1[0]} ${pt1[1]} L ${pt2[0]} ${pt2[1]}`}
d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`}
markerEnd={link.link_displayArrow ? `url(#${link[Id] + 'arrowhead'})` : ''}
/>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
index cb5cef29c..4ada1731f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
@@ -1,11 +1,13 @@
-.collectionfreeformlinksview-svgCanvas{
+// TODO: change z-index to -1 when a modal is active?
+
+.collectionfreeformlinksview-svgCanvas {
position: absolute;
top: 0;
left: 0;
- width: 100%;
+ width: 100%;
height: 100%;
pointer-events: none;
- }
- .collectionfreeformlinksview-container {
+}
+.collectionfreeformlinksview-container {
pointer-events: none;
- } \ No newline at end of file
+}
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index b906065a0..2ea8a7846 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -1,26 +1,28 @@
-import { Colors } from "browndash-components";
-import { runInAction, action } from "mobx";
-import { aggregateBounds } from "../../../Utils";
-import { Doc } from "../../../fields/Doc";
-import { Width, Height } from "../../../fields/DocSymbols";
-import { InkTool } from "../../../fields/InkField";
-import { Cast, StrCast, NumCast, BoolCast } from "../../../fields/Types";
-import { WebField } from "../../../fields/URLField";
-import { GestureUtils } from "../../../pen-gestures/GestureUtils";
-import { LinkManager } from "../../util/LinkManager";
-import { ScriptingGlobals } from "../../util/ScriptingGlobals";
-import { SelectionManager } from "../../util/SelectionManager";
-import { UndoManager } from "../../util/UndoManager";
-import { GestureOverlay } from "../GestureOverlay";
-import { InkTranscription } from "../InkTranscription";
-import { ActiveFillColor, SetActiveFillColor, ActiveIsInkMask, SetActiveIsInkMask, ActiveInkWidth, SetActiveInkWidth, ActiveInkColor, SetActiveInkColor } from "../InkingStroke";
-import { CollectionFreeFormView } from "../collections/collectionFreeForm";
-import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
-import { WebBox } from "../nodes/WebBox";
-import { RichTextMenu } from "../nodes/formattedText/RichTextMenu";
-import { DocumentType } from "../../documents/DocumentTypes";
+import { Colors } from 'browndash-components';
+import { runInAction, action } from 'mobx';
+import { aggregateBounds } from '../../../Utils';
+import { Doc } from '../../../fields/Doc';
+import { Width, Height } from '../../../fields/DocSymbols';
+import { InkTool } from '../../../fields/InkField';
+import { Cast, StrCast, NumCast, BoolCast } from '../../../fields/Types';
+import { WebField } from '../../../fields/URLField';
+import { GestureUtils } from '../../../pen-gestures/GestureUtils';
+import { LinkManager } from '../../util/LinkManager';
+import { ScriptingGlobals } from '../../util/ScriptingGlobals';
+import { SelectionManager } from '../../util/SelectionManager';
+import { UndoManager } from '../../util/UndoManager';
+import { GestureOverlay } from '../GestureOverlay';
+import { InkTranscription } from '../InkTranscription';
+import { ActiveFillColor, SetActiveFillColor, ActiveIsInkMask, SetActiveIsInkMask, ActiveInkWidth, SetActiveInkWidth, ActiveInkColor, SetActiveInkColor } from '../InkingStroke';
+import { CollectionFreeFormView } from '../collections/collectionFreeForm';
+import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
+import { WebBox } from '../nodes/WebBox';
+import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
+import { DocumentType } from '../../documents/DocumentTypes';
-ScriptingGlobals.add(function IsNoneSelected() { return SelectionManager.Views().length <= 0; }, "are no document selected");
+ScriptingGlobals.add(function IsNoneSelected() {
+ return SelectionManager.Views().length <= 0;
+}, 'are no document selected');
// toggle: Set overlay status of selected document
ScriptingGlobals.add(function setView(view: string) {
@@ -52,7 +54,7 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b
if (contentFrameNumber !== undefined) {
CollectionFreeFormDocumentView.setStringValues(contentFrameNumber, dv.rootDoc, { fieldKey: color });
} else {
- console.log('setting color to: ', color)
+ console.log('setting color to: ', color);
dv.rootDoc['_' + fieldKey] = color;
}
});
@@ -119,7 +121,7 @@ ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'grid' | 'snapli
]);
if (checkResult) {
- console.log(attr, map.get(attr)?.checkResult(selected))
+ console.log(attr, map.get(attr)?.checkResult(selected));
return map.get(attr)?.checkResult(selected);
}
const batch = map.get(attr)?.waitForRender ? UndoManager.StartBatch('set freeform attribute') : { end: () => {} };
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 909a420fe..4ce359f3f 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -38,6 +38,7 @@ import './ImageBox.scss';
import { PinProps, PresBox } from './trails';
import React = require('react');
import Color = require('color');
+import { MainView } from '../MainView';
export const pageSchema = createSchema({
googlePhotosUrl: 'string',
@@ -248,6 +249,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' });
funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' });
funcs.push({ description: 'Copy path', event: () => Utils.CopyText(this.choosePath(field.url)), icon: 'copy' });
+ funcs.push({
+ description: 'Open Image Editor',
+ event: action(() => {
+ MainView.Instance.setImageEditorOpen(true);
+ MainView.Instance.setImageEditorSource(this.choosePath(field.url));
+ MainView.Instance.addDoc = this.props.addDocument;
+ MainView.Instance.imageRootDoc = this.rootDoc;
+ }),
+ icon: 'pencil-alt',
+ });
if (!Doc.noviceMode) {
funcs.push({ description: 'Export to Google Photos', event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: 'caret-square-right' });
@@ -295,7 +306,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
choosePath(url: URL) {
const lower = url.href.toLowerCase();
if (url.protocol === 'data') return url.href;
- if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href);
+ if (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf("dashblobstore") === -1) return Utils.CorsProxy(url.href);
if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return `/assets/unknown-file-icon-hi.png`;
const ext = extname(url.href);
diff --git a/src/client/views/nodes/LoadingBox.scss b/src/client/views/nodes/LoadingBox.scss
index 4c3b8dabe..d4a7e18f2 100644
--- a/src/client/views/nodes/LoadingBox.scss
+++ b/src/client/views/nodes/LoadingBox.scss
@@ -12,6 +12,10 @@
text-overflow: ellipsis;
max-width: 80%;
text-align: center;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ align-items: center;
}
}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 34a1229ba..5cdd6b5f2 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -354,6 +354,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._layout_scrollTop) * scale);
// Changing which document to add the annotation to (the currently selected WebBox)
GPTPopup.Instance.setSidebarId(`${this.props.fieldKey}_${this._urlHash}_sidebar`);
+ const anchor = this._getAnchor(undefined, false);
+ if (anchor) {
+ console.log(anchor);
+ GPTPopup.Instance.setTargetAnchor(anchor);
+ }
GPTPopup.Instance.addDoc = this.sidebarAddDocument;
}
}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index a0eb328a1..9f4483e8d 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -70,6 +70,7 @@ import { schema } from './schema_rts';
import { SummaryView } from './SummaryView';
import applyDevTools = require('prosemirror-dev-tools');
import React = require('react');
+import { GPTPopup, GPTPopupMode } from '../../pdf/GPTPopup/GPTPopup';
const translateGoogleApi = require('translate-google-api');
export const GoogleRef = 'googleDocId';
type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void;
@@ -896,57 +897,85 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
this._downX = this._downY = Number.NaN;
};
- animateRes = (resIndex: number) => {
- if (resIndex < this.gptRes.length) {
- this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + this.gptRes[resIndex];
+ animateRes = (resIndex: number, newText: string) => {
+ if (resIndex < newText.length) {
+ const marks = this._editorView?.state.storedMarks ?? [];
+ // if (!marks) return;
+ this._editorView?.dispatch(this._editorView.state.tr.setStoredMarks(marks).insertText(newText[resIndex]).setStoredMarks(marks));
setTimeout(() => {
- this.animateRes(resIndex + 1);
+ this.animateRes(resIndex + 1, newText);
}, 20);
}
};
+ // breakupdictiation for reference
+ // prepareForTyping
+ // stored marks - userMark and timestamp, setStoredMarks
+ // the._editorView.dispatch(state.tr.setSelection(updated).insertText('\n, to))
+
askGPT = action(async () => {
+ // const state = this._editorView?.state;
+ // if (!state) return;
+ // const to = state.selection.to;
+ // const updated = TextSelection.create(state.doc, to, to);
+ // this._editorView?.dispatch(state.tr.setSelection(updated).insertText('\n', to));
+ // this._editorView?.dispatch(this._editorView.state.tr.setStoredMarks(marks).insertText('\nTesting').setStoredMarks(marks));
+ // console.log('After ', this._editorView?.state.storedMarks);
try {
let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION);
- if (res) {
- this.gptRes = res;
- this.animateRes(0);
+ if (!res) {
+ console.error('GPT call failed');
+ this.animateRes(0, 'Something went wrong.');
+ } else {
+ this.animateRes(0, res);
}
} catch (err) {
- console.log(err);
- this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + 'Something went wrong';
+ console.error('GPT call failed');
+ this.animateRes(0, 'Something went wrong.');
}
});
generateImage = async () => {
console.log('Generate image from text: ', (this.dataDoc.text as RichTextField)?.Text);
+ GPTPopup.Instance?.setImgTargetDoc(this.rootDoc);
+ GPTPopup.Instance.setImgUrls([]);
+ GPTPopup.Instance.setMode(GPTPopupMode.IMAGE);
+ GPTPopup.Instance.setVisible(true);
+ GPTPopup.Instance.addToCollection = this.props.addDocument;
+ GPTPopup.Instance.setLoading(true);
+
try {
- let image_url = await gptImageCall((this.dataDoc.text as RichTextField)?.Text);
- if (image_url) {
- const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [image_url] });
+ // make this support multiple images
+ let image_urls = await gptImageCall((this.dataDoc.text as RichTextField)?.Text);
+ console.log(image_urls);
+ if (image_urls) {
+ const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [image_urls[0]] });
const source = Utils.prepend(result.accessPaths.agnostic.client);
- const newDoc = Docs.Create.ImageDocument(source, {
- x: NumCast(this.rootDoc.x) + NumCast(this.layoutDoc._width) + 10,
- y: NumCast(this.rootDoc.y),
- _height: 200,
- _width: 200,
- data_nativeWidth: result.nativeWidth,
- data_nativeHeight: result.nativeHeight,
- });
- if (Doc.IsInMyOverlay(this.rootDoc)) {
- newDoc.overlayX = this.rootDoc.x;
- newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height);
- Doc.AddToMyOverlay(newDoc);
- } else {
- this.props.addDocument?.(newDoc);
- }
- // Create link between prompt and image
- DocUtils.MakeLink(this.rootDoc, newDoc, { link_relationship: 'Image Prompt' });
+ GPTPopup.Instance.setImgUrls([source]);
+
+ // const newDoc = Docs.Create.ImageDocument(source, {
+ // x: NumCast(this.rootDoc.x) + NumCast(this.layoutDoc._width) + 10,
+ // y: NumCast(this.rootDoc.y),
+ // _height: 200,
+ // _width: 200,
+ // data_nativeWidth: result.nativeWidth,
+ // data_nativeHeight: result.nativeHeight,
+ // });
+ // if (Doc.IsInMyOverlay(this.rootDoc)) {
+ // newDoc.overlayX = this.rootDoc.x;
+ // newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height);
+ // Doc.AddToMyOverlay(newDoc);
+ // } else {
+ // this.props.addDocument?.(newDoc);
+ // }
+ // // Create link between prompt and image
+ // DocUtils.MakeLink(this.rootDoc, newDoc, { link_relationship: 'Image Prompt' });
}
} catch (err) {
console.log(err);
return '';
}
+ GPTPopup.Instance.setLoading(false);
};
breakupDictation = () => {
@@ -1243,8 +1272,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
// Accessing editor and text doc for gpt assisted text edits
if (this._editorView && selected) {
- AnchorMenu.Instance?.setEditorView(this._editorView);
- AnchorMenu.Instance?.setTextDoc(this.dataDoc);
+ console.log('Setting');
+ GPTPopup.Instance?.setTextAnchor(this.getAnchor(false));
+ // AnchorMenu.Instance?.setEditorView(this._editorView);
+ // AnchorMenu.Instance?.setTextDoc(this.dataDoc);
}
}),
{ fireImmediately: true }
diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.scss b/src/client/views/nodes/generativeFill/GenerativeFill.scss
new file mode 100644
index 000000000..b1e570cf1
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/GenerativeFill.scss
@@ -0,0 +1,96 @@
+$navHeight: 5rem;
+$canvasSize: 1024px;
+$scale: 0.5;
+
+.generativeFillContainer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 9999;
+ height: 100vh;
+ width: 100vw;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+
+ .generativeFillControls {
+ flex-shrink: 0;
+ height: $navHeight;
+ background-color: #ffffff;
+ z-index: 999;
+ width: 100%;
+ display: flex;
+ gap: 3rem;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid #c7cdd0;
+ padding: 0 2rem;
+
+ h1 {
+ font-size: 1.5rem;
+ }
+ }
+
+ .drawingArea {
+ cursor: none;
+ touch-action: none;
+ position: relative;
+ flex-grow: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ background-color: #f0f4f6;
+
+ canvas {
+ display: block;
+ position: absolute;
+ transform-origin: 50% 50%;
+ }
+
+ .pointer {
+ pointer-events: none;
+ position: absolute;
+ border-radius: 50%;
+ width: 50px;
+ height: 50px;
+ border: 1px solid #ffffff;
+ transform: translate(-50%, -50%);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .innerPointer {
+ width: 100%;
+ height: 100%;
+ border: 1px solid #000000;
+ border-radius: 50%;
+ }
+ }
+
+ .iconContainer {
+ position: absolute;
+ top: 2rem;
+ left: 2rem;
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ }
+
+ .editsBox {
+ position: absolute;
+ top: 2rem;
+ right: 2rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+
+ img {
+ transition: all 0.2s ease-in-out;
+ &:hover {
+ opacity: 0.8;
+ }
+ }
+ }
+ }
+}
diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
new file mode 100644
index 000000000..2b3cfb920
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
@@ -0,0 +1,560 @@
+import './GenerativeFill.scss';
+import React = require('react');
+import { useEffect, useRef, useState } from 'react';
+import { APISuccess, ImageUtility } from './generativeFillUtils/ImageHandler';
+import { BrushHandler } from './generativeFillUtils/BrushHandler';
+import { IconButton, TextField } from '@mui/material';
+import { CursorData, Point } from './generativeFillUtils/generativeFillInterfaces';
+import { activeColor, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './generativeFillUtils/generativeFillConstants';
+import { PointerHandler } from './generativeFillUtils/PointerHandler';
+import { BsBrush, BsEraser, BsX } from 'react-icons/bs';
+import { AiOutlineUpload } from 'react-icons/ai';
+import { CiUndo, CiRedo } from 'react-icons/ci';
+import Buttons from './GenerativeFillButtons';
+import { EditableText } from 'browndash-components';
+import { MainView } from '../../MainView';
+import { Doc } from '../../../../fields/Doc';
+import { Networking } from '../../../Network';
+import { Utils } from '../../../../Utils';
+import { DocUtils, Docs } from '../../../documents/Documents';
+import { DocCast, NumCast } from '../../../../fields/Types';
+import { CollectionDockingView } from '../../collections/CollectionDockingView';
+import { OpenWhere, OpenWhereMod } from '../DocumentView';
+import { Oval } from 'react-loader-spinner';
+
+/**
+ * For images not 1024x1024 fill in the rest in solid black, or a
+ * reflected version of the image.
+ */
+
+/**
+ * TODO: Look into img onload, sometimes the canvas doesn't update properly
+ *
+ * Ref:
+ *
+ *
+ * CollectionDockingView.AddSplit(Doc.MakeCopy(DocCast(Doc.UserDoc().emptyPane)), OpenWhereMod.right);
+ * CollectionDockingView.AddSplit(newCollection,OpenWhere.inParent)
+ * mind mapping
+ * this.props.addDocTab();
+ */
+
+enum BrushStyle {
+ ADD,
+ SUBTRACT,
+ MARQUEE,
+}
+
+interface ImageEdit {
+ imgElement: HTMLImageElement;
+ parent: ImageEdit;
+ children: ImageEdit[];
+}
+
+interface GenerativeFillProps {
+ imageEditorOpen: boolean;
+ imageEditorSource: string;
+ imageRootDoc: Doc | undefined;
+ addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined;
+}
+
+const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc }: GenerativeFillProps) => {
+ const canvasRef = useRef<HTMLCanvasElement>(null);
+ const canvasBackgroundRef = useRef<HTMLCanvasElement>(null);
+ const drawingAreaRef = useRef<HTMLDivElement>(null);
+ const fileRef = useRef<HTMLInputElement>(null);
+ const [cursorData, setCursorData] = useState<CursorData>({
+ x: 0,
+ y: 0,
+ width: 150,
+ });
+ const [isBrushing, setIsBrushing] = useState(false);
+ const [canvasScale, setCanvasScale] = useState(0.5);
+ const [edits, setEdits] = useState<string[]>([]);
+ const [brushStyle, setBrushStyle] = useState<BrushStyle>(BrushStyle.ADD);
+ const [input, setInput] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [saveLoading, setSaveLoading] = useState(false);
+ // the current image in the main canvas
+ const currImg = useRef<HTMLImageElement | null>(null);
+ // the unedited version of each generation (parent)
+ const originalImg = useRef<HTMLImageElement | null>(null);
+ // stores history of data urls
+ const undoStack = useRef<string[]>([]);
+ // stores redo stack
+ const redoStack = useRef<string[]>([]);
+
+ // early stage properly, likely will get rid of
+ const freeformPosition = useRef<number[]>([0, 0]);
+
+ // which urls were already saved to canvas
+ const savedSrcs = useRef<string[]>([]);
+
+ // references to keep track of tree structure
+ const newCollectionRef = useRef<Doc | null>(null);
+ const parentDoc = useRef<Doc | null>(null);
+ const childrenDocs = useRef<Doc[]>([]);
+
+ // Undo and Redo
+ const handleUndo = () => {
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx || !currImg.current || !canvasRef.current) return;
+
+ const target = undoStack.current[undoStack.current.length - 1];
+ if (!target) {
+ ImageUtility.drawImgToCanvas(currImg.current, canvasRef);
+ } else {
+ redoStack.current = [...redoStack.current, canvasRef.current.toDataURL()];
+ const img = new Image();
+ img.src = target;
+ ImageUtility.drawImgToCanvas(img, canvasRef);
+ undoStack.current = undoStack.current.slice(0, -1);
+ }
+ };
+
+ const handleRedo = () => {
+ // TODO: handle undo as well
+ const target = redoStack.current[redoStack.current.length - 1];
+ if (!target) {
+ } else {
+ const img = new Image();
+ img.src = target;
+ ImageUtility.drawImgToCanvas(img, canvasRef);
+ redoStack.current = redoStack.current.slice(0, -1);
+ }
+ };
+
+ const handleReset = () => {
+ if (!canvasRef.current || !currImg.current) return;
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx) return;
+ ctx.clearRect(0, 0, canvasSize, canvasSize);
+ undoStack.current = [];
+ redoStack.current = [];
+ ImageUtility.drawImgToCanvas(currImg.current, canvasRef, true);
+ };
+
+ // initiate brushing
+ const handlePointerDown = (e: React.PointerEvent) => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx) return;
+
+ undoStack.current = [...undoStack.current, canvasRef.current.toDataURL()];
+ redoStack.current = [];
+
+ setIsBrushing(true);
+ const { x, y } = PointerHandler.getPointRelativeToElement(canvas, e, canvasScale);
+
+ BrushHandler.brushCircleOverlay(x, y, cursorData.width / 2 / canvasScale, ctx, eraserColor, brushStyle === BrushStyle.SUBTRACT);
+ };
+
+ // stop brushing, push to undo stack
+ const handlePointerUp = (e: React.PointerEvent) => {
+ const ctx = ImageUtility.getCanvasContext(canvasBackgroundRef);
+ if (!ctx) return;
+ if (!isBrushing) return;
+ setIsBrushing(false);
+ };
+
+ // handles brushing on pointer movement
+ useEffect(() => {
+ if (!isBrushing) return;
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx) return;
+
+ const handlePointerMove = (e: PointerEvent) => {
+ const currPoint = PointerHandler.getPointRelativeToElement(canvas, e, canvasScale);
+ const lastPoint: Point = {
+ x: currPoint.x - e.movementX / canvasScale,
+ y: currPoint.y - e.movementY / canvasScale,
+ };
+ BrushHandler.createBrushPathOverlay(lastPoint, currPoint, cursorData.width / 2 / canvasScale, ctx, eraserColor, brushStyle === BrushStyle.SUBTRACT);
+ };
+
+ drawingAreaRef.current?.addEventListener('pointermove', handlePointerMove);
+ return () => {
+ drawingAreaRef.current?.removeEventListener('pointermove', handlePointerMove);
+ };
+ }, [isBrushing]);
+
+ // first load
+ useEffect(() => {
+ console.log('first load');
+ if (!imageEditorSource || imageEditorSource === '') return;
+ const img = new Image();
+ img.src = imageEditorSource;
+ ImageUtility.drawImgToCanvas(img, canvasRef);
+ currImg.current = img;
+ originalImg.current = img;
+ freeformPosition.current = [0, 0];
+
+ return () => {
+ console.log('cleanup');
+ newCollectionRef.current = null;
+ parentDoc.current = null;
+ childrenDocs.current = [];
+ currImg.current = null;
+ originalImg.current = null;
+ freeformPosition.current = [0, 0];
+ undoStack.current = [];
+ redoStack.current = [];
+ };
+ }, [canvasRef, imageEditorSource]);
+
+ // handles brush sizing
+ useEffect(() => {
+ const handleKeyPress = (e: KeyboardEvent) => {
+ if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ e.stopPropagation();
+ setCursorData(data => ({ ...data, width: data.width + 5 }));
+ } else if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ e.stopPropagation();
+ setCursorData(data => (data.width >= 20 ? { ...data, width: data.width - 5 } : data));
+ }
+ };
+ window.addEventListener('keydown', handleKeyPress);
+ return () => window.removeEventListener('keydown', handleKeyPress);
+ }, []);
+
+ // handle pinch zoom
+ useEffect(() => {
+ const handlePinch = (e: WheelEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const delta = e.deltaY;
+ const scaleFactor = delta > 0 ? 0.98 : 1.02; // Adjust the scale factor as per your requirement
+ setCanvasScale(prevScale => prevScale * scaleFactor);
+ };
+
+ drawingAreaRef.current?.addEventListener('wheel', handlePinch, {
+ passive: false,
+ });
+ return () => drawingAreaRef.current?.removeEventListener('wheel', handlePinch);
+ }, [drawingAreaRef]);
+
+ // updates the current position of the cursor
+ const updateCursorData = (e: React.PointerEvent) => {
+ const drawingArea = drawingAreaRef.current;
+ if (!drawingArea) return;
+ const { x, y } = PointerHandler.getPointRelativeToElement(drawingArea, e, 1);
+ setCursorData(data => ({
+ ...data,
+ x,
+ y,
+ }));
+ };
+
+ // File upload
+ const uploadImg = (e: React.ChangeEvent<HTMLInputElement>) => {
+ if (e.target.files) {
+ const file = e.target.files[0];
+ const image = new Image();
+ const imgUrl = URL.createObjectURL(file);
+ image.src = imgUrl;
+ ImageUtility.drawImgToCanvas(image, canvasRef);
+ currImg.current = image;
+ }
+ };
+
+ // Get AI Edit
+ const getEdit = async () => {
+ const img = currImg.current;
+ if (!img) return;
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx) return;
+ setLoading(true);
+ // need to adjust later
+ try {
+ const maskBlob = await ImageUtility.canvasToBlob(canvas);
+ const imgBlob = await ImageUtility.canvasToBlob(ImageUtility.getCanvasImg(img));
+ const res = await ImageUtility.getEdit(imgBlob, maskBlob, input !== '' ? input + ' in the same style' : 'Fill in the image in the same style', 2);
+ // const res = await ImageUtility.mockGetEdit(img.src);
+
+ // create first image
+ if (!newCollectionRef.current) {
+ if (!(originalImg.current && imageRootDoc)) return;
+ console.log('creating first image');
+ // create new collection and add it to the view
+ newCollectionRef.current = Docs.Create.FreeformDocument([], {
+ x: NumCast(imageRootDoc.x) + NumCast(imageRootDoc._width) + offsetX,
+ y: NumCast(imageRootDoc.y),
+ _width: newCollectionSize,
+ _height: newCollectionSize,
+ title: 'Image edit collection',
+ });
+ DocUtils.MakeLink(imageRootDoc, newCollectionRef.current, { link_relationship: 'Image Edit Version History', link_displayLine: false });
+ // add the doc to the main freeform
+ addDoc?.(newCollectionRef.current);
+ await createNewImgDoc(originalImg.current, true);
+ } else {
+ parentDoc.current = childrenDocs.current[childrenDocs.current.length - 1];
+ childrenDocs.current = [];
+ }
+
+ originalImg.current = currImg.current;
+
+ const { urls } = res as APISuccess;
+ const image = new Image();
+ image.src = urls[0];
+ setEdits(urls);
+ ImageUtility.drawImgToCanvas(image, canvasRef);
+ currImg.current = image;
+ onSave();
+ freeformPosition.current[0] += 1;
+ freeformPosition.current[1] = 0;
+ } catch (err) {
+ console.log(err);
+ }
+ setLoading(false);
+ };
+
+ // adjusts all the img positions to be aligned
+ const adjustImgPositions = () => {
+ if (!parentDoc.current) return;
+ const startY = NumCast(parentDoc.current.y);
+ const len = childrenDocs.current.length;
+ console.log(len);
+ let initialYPositions: number[] = [];
+ for (let i = 0; i < len; i++) {
+ initialYPositions.push(startY + i * offsetDistanceY);
+ }
+ childrenDocs.current.forEach((doc, i) => {
+ if (len % 2 === 1) {
+ doc.y = initialYPositions[i] - Math.floor(len / 2) * offsetDistanceY;
+ } else {
+ doc.y = initialYPositions[i] - (len / 2 - 1 / 2) * offsetDistanceY;
+ }
+ });
+ };
+
+ // creates a new image document and returns its reference
+ const createNewImgDoc = async (img: HTMLImageElement, firstDoc: boolean): Promise<Doc | undefined> => {
+ if (!newCollectionRef.current || !imageRootDoc) return;
+ const src = img.src;
+ const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [src] });
+ const source = Utils.prepend(result.accessPaths.agnostic.client);
+
+ if (firstDoc) {
+ const x = 0;
+ const initialY = 0;
+ console.log('first doc');
+
+ const newImg = Docs.Create.ImageDocument(source, {
+ x: x,
+ y: initialY,
+ _height: freeformRenderSize,
+ _width: freeformRenderSize,
+ data_nativeWidth: result.nativeWidth,
+ data_nativeHeight: result.nativeHeight,
+ });
+
+ Doc.AddDocToList(newCollectionRef.current, undefined, newImg);
+ parentDoc.current = newImg;
+ return newImg;
+ } else {
+ if (!parentDoc.current) return;
+ const x = NumCast(parentDoc.current.x) + freeformRenderSize + offsetX;
+ // dummy position
+ console.log('creating child elements');
+ const initialY = 0;
+
+ const newImg = Docs.Create.ImageDocument(source, {
+ x: x,
+ y: initialY,
+ _height: freeformRenderSize,
+ _width: freeformRenderSize,
+ data_nativeWidth: result.nativeWidth,
+ data_nativeHeight: result.nativeHeight,
+ });
+
+ childrenDocs.current.push(newImg);
+ DocUtils.MakeLink(parentDoc.current, newImg, { link_relationship: 'Image Edit', link_displayLine: true });
+ adjustImgPositions();
+
+ Doc.AddDocToList(newCollectionRef.current, undefined, newImg);
+ return newImg;
+ }
+ };
+
+ // need to maybe call on every img click, not just when the save btn is clicked
+ const onSave = async () => {
+ setSaveLoading(true);
+ if (!currImg.current || !originalImg.current || !imageRootDoc) return;
+ try {
+ console.log('creating another image');
+ await createNewImgDoc(currImg.current, false);
+ } catch (err) {
+ console.log(err);
+ }
+ setSaveLoading(false);
+ };
+
+ const handleViewClose = () => {
+ // if (newCollectionRef.current) {
+ // CollectionDockingView.AddSplit(newCollectionRef.current, OpenWhereMod.right);
+ // }
+ MainView.Instance.setImageEditorOpen(false);
+ MainView.Instance.setImageEditorSource('');
+ setEdits([]);
+ };
+
+ return (
+ <div className="generativeFillContainer" style={{ display: imageEditorOpen ? 'flex' : 'none' }}>
+ <div className="generativeFillControls">
+ <h1>Generative Fill</h1>
+ <div style={{ display: 'flex', alignItems: 'center', gap: '1.5rem' }}>
+ <Buttons canvasRef={canvasRef} currImg={currImg} getEdit={getEdit} loading={loading} onSave={onSave} onReset={handleReset} />
+ <IconButton onClick={handleViewClose}>
+ <BsX color={activeColor} />
+ </IconButton>
+ </div>
+ </div>
+ {/* Main canvas for editing */}
+ <div
+ className="drawingArea" // this only works if pointerevents: none is set on the custom pointer
+ ref={drawingAreaRef}
+ onPointerOver={updateCursorData}
+ onPointerMove={updateCursorData}
+ onPointerDown={handlePointerDown}
+ onPointerUp={handlePointerUp}>
+ <canvas ref={canvasRef} width={canvasSize} height={canvasSize} style={{ transform: `scale(${canvasScale})` }} />
+ <canvas ref={canvasBackgroundRef} width={canvasSize} height={canvasSize} style={{ transform: `scale(${canvasScale})` }} />
+ <div
+ className="pointer"
+ style={{
+ left: cursorData.x,
+ top: cursorData.y,
+ width: cursorData.width,
+ height: cursorData.width,
+ }}>
+ <div className="innerPointer"></div>
+ </div>
+ {/* Icons */}
+ <div className="iconContainer">
+ <input ref={fileRef} type="file" accept="image/*" onChange={uploadImg} style={{ display: 'none' }} />
+ <IconButton
+ onClick={() => {
+ if (fileRef.current) {
+ fileRef.current.click();
+ }
+ }}>
+ <AiOutlineUpload />
+ </IconButton>
+ <IconButton
+ onClick={() => {
+ setBrushStyle(BrushStyle.ADD);
+ }}>
+ <BsBrush color={brushStyle === BrushStyle.ADD ? activeColor : 'inherit'} />
+ </IconButton>
+ {/* Undo and Redo */}
+ <IconButton
+ onPointerDown={e => {
+ e.stopPropagation();
+ handleUndo();
+ }}
+ onPointerUp={e => {
+ e.stopPropagation();
+ }}>
+ <CiUndo />
+ </IconButton>
+ <IconButton
+ onPointerDown={e => {
+ e.stopPropagation();
+ handleRedo();
+ }}
+ onPointerUp={e => {
+ e.stopPropagation();
+ }}>
+ <CiRedo />
+ </IconButton>
+ </div>
+ {/* Edits thumbnails*/}
+ <div className="editsBox">
+ {edits.map((edit, i) => (
+ <img
+ key={i}
+ width={100}
+ height={100}
+ src={edit}
+ onClick={() => {
+ const img = new Image();
+ img.src = edit;
+ ImageUtility.drawImgToCanvas(img, canvasRef);
+ currImg.current = img;
+ onSave();
+ }}
+ />
+ ))}
+ {/* Original img thumbnail */}
+ {edits.length > 0 && (
+ <div style={{ position: 'relative' }}>
+ <label
+ style={{
+ position: 'absolute',
+ bottom: 10,
+ left: 10,
+ color: '#ffffff',
+ fontSize: '0.8rem',
+ letterSpacing: '1px',
+ textTransform: 'uppercase',
+ }}>
+ Original
+ </label>
+ <img
+ width={100}
+ height={100}
+ src={originalImg.current?.src}
+ style={{ cursor: 'pointer' }}
+ onClick={() => {
+ if (!originalImg.current) return;
+ const img = new Image();
+ img.src = originalImg.current.src;
+ ImageUtility.drawImgToCanvas(img, canvasRef);
+ currImg.current = img;
+ }}
+ />
+ <div
+ style={{
+ position: 'absolute',
+ top: 10,
+ right: 10,
+ }}>
+ {saveLoading && <Oval height={20} width={20} color="#ffffff" visible={true} ariaLabel="oval-loading" secondaryColor="#ffffff89" strokeWidth={3} strokeWidthSecondary={3} />}
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ <div>
+ <TextField
+ value={input}
+ onChange={e => setInput(e.target.value)}
+ disabled={isBrushing}
+ type="text"
+ label="Prompt"
+ placeholder="Prompt..."
+ InputLabelProps={{ style: { fontSize: '1.5rem' } }}
+ inputProps={{ style: { fontSize: '1.5rem' } }}
+ sx={{
+ backgroundColor: '#ffffff',
+ position: 'absolute',
+ bottom: '1rem',
+ transform: 'translateX(calc(50vw - 50%))',
+ width: 'calc(100vw - 4rem)',
+ scale: 1.2,
+ }}
+ />
+ </div>
+ </div>
+ );
+};
+
+export default GenerativeFill;
diff --git a/src/client/views/nodes/generativeFill/GenerativeFillButtons.scss b/src/client/views/nodes/generativeFill/GenerativeFillButtons.scss
new file mode 100644
index 000000000..0180ef904
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/GenerativeFillButtons.scss
@@ -0,0 +1,4 @@
+.generativeFillBtnContainer {
+ display: flex;
+ gap: 1rem;
+}
diff --git a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx
new file mode 100644
index 000000000..53c6cec84
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx
@@ -0,0 +1,48 @@
+import { Button } from '@mui/material';
+import { ImageUtility } from './generativeFillUtils/ImageHandler';
+import { canvasSize } from './generativeFillUtils/generativeFillConstants';
+import { Oval } from 'react-loader-spinner';
+import './GenerativeFillButtons.scss';
+import React = require('react');
+import { Doc } from '../../../../fields/Doc';
+
+interface ButtonContainerProps {
+ canvasRef: React.RefObject<HTMLCanvasElement>;
+ currImg: React.MutableRefObject<HTMLImageElement | null>;
+ getEdit: () => Promise<void>;
+ loading: boolean;
+ onSave: () => Promise<void>;
+ onReset: () => void;
+}
+
+const Buttons = ({ canvasRef, currImg, loading, getEdit, onSave, onReset }: ButtonContainerProps) => {
+ const handleSave = () => {
+ onSave();
+ };
+
+ return (
+ <div className="generativeFillBtnContainer">
+ <Button onClick={onReset}>Reset</Button>
+ {/* <Button onClick={handleSave}>Save</Button> */}
+ {/* <Button
+ onClick={() => {
+ if (!canvasRef.current) return;
+ ImageUtility.downloadImageCanvas('/assets/firefly.png');
+ }}>
+ Download Original
+ </Button> */}
+ <Button
+ variant="contained"
+ onClick={() => {
+ getEdit();
+ }}>
+ <span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
+ Get Edit
+ {loading && <Oval height={20} width={20} color="#ffffff" visible={true} ariaLabel="oval-loading" secondaryColor="#ffffff89" strokeWidth={3} strokeWidthSecondary={3} />}
+ </span>
+ </Button>
+ </div>
+ );
+};
+
+export default Buttons;
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts
new file mode 100644
index 000000000..c2716e083
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts
@@ -0,0 +1,87 @@
+import { GenerativeFillMathHelpers } from "./GenerativeFillMathHelpers";
+import { eraserColor } from "./generativeFillConstants";
+import { Point } from "./generativeFillInterfaces";
+
+export class BrushHandler {
+ static brushCircle = (
+ x: number,
+ y: number,
+ brushRadius: number,
+ ctx: CanvasRenderingContext2D
+ ) => {
+ ctx.globalCompositeOperation = "destination-out";
+ ctx.shadowColor = "#ffffffeb";
+ ctx.shadowBlur = 5;
+ ctx.beginPath();
+ ctx.arc(x, y, brushRadius, 0, 2 * Math.PI);
+ ctx.fill();
+ ctx.closePath();
+ };
+
+ static brushCircleOverlay = (
+ x: number,
+ y: number,
+ brushRadius: number,
+ ctx: CanvasRenderingContext2D,
+ fillColor: string,
+ erase: boolean
+ ) => {
+ ctx.globalCompositeOperation = "destination-out";
+ // ctx.globalCompositeOperation = erase ? "destination-out" : "source-over";
+ ctx.fillStyle = fillColor;
+ ctx.shadowColor = eraserColor;
+ ctx.shadowBlur = 5;
+ ctx.beginPath();
+ ctx.arc(x, y, brushRadius, 0, 2 * Math.PI);
+ ctx.fill();
+ ctx.closePath();
+ };
+
+ static createBrushPath = (
+ startPoint: Point,
+ endPoint: Point,
+ brushRadius: number,
+ ctx: CanvasRenderingContext2D
+ ) => {
+ const dist = GenerativeFillMathHelpers.distanceBetween(
+ startPoint,
+ endPoint
+ );
+
+ for (let i = 0; i < dist; i += 5) {
+ const s = i / dist;
+ BrushHandler.brushCircle(
+ startPoint.x * (1 - s) + endPoint.x * s,
+ startPoint.y * (1 - s) + endPoint.y * s,
+ brushRadius,
+ ctx
+ );
+ }
+ };
+
+ static createBrushPathOverlay = (
+ startPoint: Point,
+ endPoint: Point,
+ brushRadius: number,
+ ctx: CanvasRenderingContext2D,
+ fillColor: string,
+ erase: boolean
+ ) => {
+ const dist = GenerativeFillMathHelpers.distanceBetween(
+ startPoint,
+ endPoint
+ );
+
+ for (let i = 0; i < dist; i += 5) {
+ const s = i / dist;
+ BrushHandler.brushCircleOverlay(
+ startPoint.x * (1 - s) + endPoint.x * s,
+ startPoint.y * (1 - s) + endPoint.y * s,
+ brushRadius,
+ ctx,
+ fillColor,
+ erase
+ );
+ }
+ };
+}
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts
new file mode 100644
index 000000000..027b99a52
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts
@@ -0,0 +1,11 @@
+import { Point } from "./generativeFillInterfaces";
+
+export class GenerativeFillMathHelpers {
+ // math helpers
+ static distanceBetween = (p1: Point, p2: Point) => {
+ return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
+ };
+ static angleBetween = (p1: Point, p2: Point) => {
+ return Math.atan2(p2.x - p1.x, p2.y - p1.y);
+ };
+}
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts
new file mode 100644
index 000000000..48055903c
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts
@@ -0,0 +1,128 @@
+import { RefObject } from 'react';
+import { canvasSize } from './generativeFillConstants';
+
+export interface APISuccess {
+ status: 'success';
+ urls: string[];
+}
+
+export interface APIError {
+ status: 'error';
+ message: string;
+}
+
+export class ImageUtility {
+ static canvasToBlob = (canvas: HTMLCanvasElement): Promise<Blob> => {
+ return new Promise(resolve => {
+ canvas.toBlob(blob => {
+ if (blob) {
+ resolve(blob);
+ }
+ }, 'image/png');
+ });
+ };
+
+ static getEdit = async (imgBlob: Blob, maskBlob: Blob, prompt: string, n?: number): Promise<APISuccess | APIError> => {
+ const apiUrl = 'https://api.openai.com/v1/images/edits';
+ const fd = new FormData();
+ fd.append('image', imgBlob, 'image.png');
+ fd.append('mask', maskBlob, 'mask.png');
+ fd.append('prompt', prompt);
+ fd.append('size', '1024x1024');
+ fd.append('n', n ? JSON.stringify(n) : '1');
+ fd.append('response_format', 'b64_json');
+
+ try {
+ const res = await fetch(apiUrl, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.OPENAI_KEY}`,
+ },
+ body: fd,
+ });
+ const data = await res.json();
+ console.log(data.data);
+ return {
+ status: 'success',
+ urls: (data.data as { b64_json: string }[]).map(data => `data:image/png;base64,${data.b64_json}`),
+ };
+ } catch (err) {
+ console.log(err);
+ return { status: 'error', message: 'API error.' };
+ }
+ };
+
+ static mockGetEdit = async (mockSrc: string): Promise<APISuccess | APIError> => {
+ return {
+ status: 'success',
+ urls: [mockSrc, mockSrc, mockSrc],
+ };
+ };
+
+ static getCanvasContext = (canvasRef: RefObject<HTMLCanvasElement>): CanvasRenderingContext2D | null => {
+ if (!canvasRef.current) return null;
+ const ctx = canvasRef.current.getContext('2d');
+ if (!ctx) return null;
+ return ctx;
+ };
+
+ static downloadCanvas = (canvas: HTMLCanvasElement) => {
+ const url = canvas.toDataURL();
+ const downloadLink = document.createElement('a');
+ downloadLink.href = url;
+ downloadLink.download = 'canvas';
+
+ downloadLink.click();
+ downloadLink.remove();
+ };
+
+ static downloadImageCanvas = (imgUrl: string) => {
+ const img = new Image();
+ img.src = imgUrl;
+ img.onload = () => {
+ const canvas = document.createElement('canvas');
+ canvas.width = canvasSize;
+ canvas.height = canvasSize;
+ const ctx = canvas.getContext('2d');
+ ctx?.drawImage(img, 0, 0, canvasSize, canvasSize);
+
+ this.downloadCanvas(canvas);
+ };
+ };
+
+ static drawImgToCanvas = (img: HTMLImageElement, canvasRef: React.RefObject<HTMLCanvasElement>, loaded?: boolean) => {
+ if (loaded) {
+ const ctx = this.getCanvasContext(canvasRef);
+ if (!ctx) return;
+ ctx.globalCompositeOperation = 'source-over';
+ const scale = Math.max(canvasSize / img.width, canvasSize / img.height);
+ const width = img.width * scale;
+ const height = img.height * scale;
+ ctx.clearRect(0, 0, canvasSize, canvasSize);
+ ctx.drawImage(img, 0, 0, width, height);
+ } else {
+ img.onload = () => {
+ const ctx = this.getCanvasContext(canvasRef);
+ if (!ctx) return;
+ ctx.globalCompositeOperation = 'source-over';
+ const scale = Math.max(canvasSize / img.width, canvasSize / img.height);
+ const width = img.width * scale;
+ const height = img.height * scale;
+ ctx.clearRect(0, 0, canvasSize, canvasSize);
+ ctx.drawImage(img, 0, 0, width, height);
+ };
+ }
+ };
+
+ // The image must be loaded!
+ static getCanvasImg = (img: HTMLImageElement): HTMLCanvasElement => {
+ const canvas = document.createElement('canvas');
+ canvas.width = canvasSize;
+ canvas.height = canvasSize;
+ const ctx = canvas.getContext('2d');
+ ctx?.clearRect(0, 0, canvasSize, canvasSize);
+ ctx?.drawImage(img, 0, 0, canvasSize, canvasSize);
+
+ return canvas;
+ };
+}
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts
new file mode 100644
index 000000000..9e620ad11
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts
@@ -0,0 +1,15 @@
+import { Point } from "./generativeFillInterfaces";
+
+export class PointerHandler {
+ static getPointRelativeToElement = (
+ element: HTMLElement,
+ e: React.PointerEvent | PointerEvent,
+ scale: number
+ ): Point => {
+ const boundingBox = element.getBoundingClientRect();
+ return {
+ x: (e.clientX - boundingBox.x) / scale,
+ y: (e.clientY - boundingBox.y) / scale,
+ };
+ };
+}
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts
new file mode 100644
index 000000000..5a8d33742
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts
@@ -0,0 +1,9 @@
+// constants
+export const canvasSize = 1024;
+export const freeformRenderSize = 300;
+export const offsetDistanceY = freeformRenderSize + 200;
+export const offsetX = 200;
+export const newCollectionSize = 1000;
+
+export const activeColor = '#1976d2';
+export const eraserColor = '#e1e9ec';
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts
new file mode 100644
index 000000000..9b9b9d3c2
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts
@@ -0,0 +1,16 @@
+// interfaces
+export interface CursorData {
+ x: number;
+ y: number;
+ width: number;
+}
+
+export interface Point {
+ x: number;
+ y: number;
+}
+
+export enum BrushMode {
+ ADD,
+ SUBTRACT,
+}
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 07b2afd91..dd8cf7002 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -48,9 +48,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable public Status: 'marquee' | 'annotation' | '' = '';
// GPT additions
- @observable private GPTpopupText: string = '';
- @observable private loadingGPT: boolean = false;
- @observable private showGPTPopup: boolean = false;
@observable private GPTMode: GPTPopupMode = GPTPopupMode.SUMMARY;
@observable private selectedText: string = '';
@observable private editorView?: EditorView;
@@ -59,25 +56,11 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
private selectionRange: number[] | undefined;
@action
- setGPTPopupVis = (vis: boolean) => {
- this.showGPTPopup = vis;
- };
- @action
setGPTMode = (mode: GPTPopupMode) => {
this.GPTMode = mode;
};
@action
- setGPTPopupText = (txt: string) => {
- this.GPTpopupText = txt;
- };
-
- @action
- setLoading = (loading: boolean) => {
- this.loadingGPT = loading;
- };
-
- @action
setHighlightRange(r: number[] | undefined) {
this.highlightRange = r;
}
@@ -130,19 +113,12 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
componentDidMount() {
this._disposer2 = reaction(
() => this._opacity,
- opacity => {
- if (!opacity) {
- this.setGPTPopupVis(false);
- this.setGPTPopupText('');
- }
- },
+ opacity => {},
{ fireImmediately: true }
);
this._disposer = reaction(
() => SelectionManager.Views().slice(),
selected => {
- this.setGPTPopupVis(false);
- this.setGPTPopupText('');
AnchorMenu.Instance.fadeOut(true);
}
);
@@ -153,23 +129,22 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
* @param e pointer down event
*/
gptSummarize = async (e: React.PointerEvent) => {
+ GPTPopup.Instance.setVisible(true);
this.setHighlightRange(undefined);
- this.setGPTPopupVis(true);
- this.setGPTMode(GPTPopupMode.SUMMARY);
- this.setLoading(true);
+ GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY);
+ GPTPopup.Instance.setLoading(true);
try {
const res = await gptAPICall(this.selectedText, GPTCallType.SUMMARY);
if (res) {
- this.setGPTPopupText(res);
+ GPTPopup.Instance.setText(res);
} else {
- this.setGPTPopupText('Something went wrong.');
+ GPTPopup.Instance.setText('Something went wrong.');
}
} catch (err) {
console.error(err);
}
-
- this.setLoading(false);
+ GPTPopup.Instance.setLoading(false);
};
/**
@@ -184,9 +159,9 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
const fullText = state.doc.textBetween(0, this.editorView.state.doc.content.size, ' \n');
const selectedText = state.doc.textBetween(sel.from, sel.to);
- this.setGPTPopupVis(true);
+ GPTPopup.Instance.setVisible(true);
this.setGPTMode(GPTPopupMode.EDIT);
- this.setLoading(true);
+ GPTPopup.Instance.setLoading(true);
try {
let res = await gptAPICall(selectedText, GPTCallType.EDIT);
@@ -196,16 +171,20 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
const resultText = fullText.slice(0, sel.from - 1) + res + fullText.slice(sel.to - 1);
if (res) {
- this.setGPTPopupText(resultText);
+ GPTPopup.Instance.setText(resultText);
this.setHighlightRange([sel.from - 1, sel.from - 1 + res.length]);
} else {
- this.setGPTPopupText('Something went wrong.');
+ GPTPopup.Instance.setText('Something went wrong.');
}
} catch (err) {
console.error(err);
}
- this.setLoading(false);
+ GPTPopup.Instance.setLoading(false);
+ };
+
+ gptImage = async () => {
+ console.log(this.GetAnchor(undefined, false));
};
/**
@@ -253,21 +232,18 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
};
@computed get highlighter() {
- return <Group>
- <IconButton
- icon={<FontAwesomeIcon icon="highlighter" style={{ transition: 'transform 0.1s', transform: 'rotate(-45deg)' }} />}
- tooltip={'Click to Highlight'}
- onClick={this.highlightClicked}
- colorPicker={this.highlightColor}
- color={StrCast(Doc.UserDoc().userColor)}
- />
- <ColorPicker
- colorPickerType={'github'}
- selectedColor={this.highlightColor}
- setSelectedColor={color => this.changeHighlightColor(color)}
- size={Size.XSMALL}
- />
- </Group>
+ return (
+ <Group>
+ <IconButton
+ icon={<FontAwesomeIcon icon="highlighter" style={{ transition: 'transform 0.1s', transform: 'rotate(-45deg)' }} />}
+ tooltip={'Click to Highlight'}
+ onClick={this.highlightClicked}
+ colorPicker={this.highlightColor}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
+ <ColorPicker colorPickerType={'github'} selectedColor={this.highlightColor} setSelectedColor={color => this.changeHighlightColor(color)} size={Size.XSMALL} />
+ </Group>
+ );
}
@action changeHighlightColor = (color: string) => {
@@ -312,12 +288,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
this.Status === 'marquee' ? (
<>
{this.highlighter}
- <IconButton
- tooltip={'Drag to Place Annotation'}
- onPointerDown={this.pointerDown}
- icon={<FontAwesomeIcon icon="comment-alt"/>}
- color={StrCast(Doc.UserDoc().userColor)}
- />
+ <IconButton tooltip={'Drag to Place Annotation'} onPointerDown={this.pointerDown} icon={<FontAwesomeIcon icon="comment-alt" />} color={StrCast(Doc.UserDoc().userColor)} />
{/* GPT Summarize icon only shows up when text is highlighted, not on marquee selection*/}
{AnchorMenu.Instance.StartCropDrag === unimplementedFunction && this.canSummarize() && (
<Tooltip key="gpt" title={<div className="dash-tooltip">Summarize with AI</div>}>
@@ -326,78 +297,33 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
</button>
</Tooltip>
)}
- <GPTPopup
- key="gptpopup"
- visible={this.showGPTPopup}
- text={this.GPTpopupText}
- highlightRange={this.highlightRange}
- loading={this.loadingGPT}
- callSummaryApi={this.gptSummarize}
- callEditApi={this.gptEdit}
- replaceText={this.replaceText}
- mode={this.GPTMode}
- />
{AnchorMenu.Instance.OnAudio === unimplementedFunction ? null : (
- <IconButton
- tooltip={'Click to Record Annotation'}
- onPointerDown={this.audioDown}
- icon={<FontAwesomeIcon icon="microphone" />}
- color={StrCast(Doc.UserDoc().userColor)}
- />
- )}
- {this.canEdit() && (
- <IconButton
- tooltip={'AI edit suggestions'}
- onPointerDown={this.gptEdit}
- icon={<FontAwesomeIcon icon="pencil-alt" />}
- color={StrCast(Doc.UserDoc().userColor)}
- />
+ <IconButton tooltip={'Click to Record Annotation'} onPointerDown={this.audioDown} icon={<FontAwesomeIcon icon="microphone" />} color={StrCast(Doc.UserDoc().userColor)} />
)}
- <Popup
- tooltip='Find document to link to selected text'
- type={Type.PRIM}
- icon={<FontAwesomeIcon icon={'search'} />}
- popup={<LinkPopup key="popup" linkCreateAnchor={this.onMakeAnchor} />}
- color={StrCast(Doc.UserDoc().userColor)}
- />
+ {/* Removed text editing for now, not quite ready */}
+ {/* {this.canEdit() && <IconButton tooltip={'AI edit suggestions'} onPointerDown={this.gptEdit} icon={<FontAwesomeIcon icon="pencil-alt" />} color={StrCast(Doc.UserDoc().userColor)} />} */}
+ {<IconButton tooltip={'Generate DALL-E Image'} onPointerDown={this.gptImage} icon={<FontAwesomeIcon icon="image" />} color={StrCast(Doc.UserDoc().userColor)} />}
+ <Popup tooltip="Find document to link to selected text" type={Type.PRIM} icon={<FontAwesomeIcon icon={'search'} />} popup={<LinkPopup key="popup" linkCreateAnchor={this.onMakeAnchor} />} color={StrCast(Doc.UserDoc().userColor)} />
{AnchorMenu.Instance.StartCropDrag === unimplementedFunction ? null : (
- <IconButton
- tooltip={'Click/Drag to create cropped image'}
- onPointerDown={this.cropDown}
- icon={<FontAwesomeIcon icon="image"/>}
- color={StrCast(Doc.UserDoc().userColor)}
- />
+ <IconButton tooltip={'Click/Drag to create cropped image'} onPointerDown={this.cropDown} icon={<FontAwesomeIcon icon="image" />} color={StrCast(Doc.UserDoc().userColor)} />
)}
</>
) : (
<>
- {this.Delete !== returnFalse && <IconButton
- tooltip={'Remove Link Anchor'}
- onPointerDown={this.Delete}
- icon={<FontAwesomeIcon icon="trash-alt" />}
- color={StrCast(Doc.UserDoc().userColor)}
- />}
- {this.PinToPres !== returnFalse && <IconButton
- tooltip={'Pin to Presentation'}
- onPointerDown={this.PinToPres}
- icon={<FontAwesomeIcon icon="map-pin" />}
- color={StrCast(Doc.UserDoc().userColor)}
- />}
- {this.ShowTargetTrail !== returnFalse && <IconButton
- tooltip={'Show Linked Trail'}
- onPointerDown={this.ShowTargetTrail}
- icon={<FontAwesomeIcon icon="taxi" />}
- color={StrCast(Doc.UserDoc().userColor)}
- />}
- {this.IsTargetToggler !== returnFalse && <Toggle
- tooltip={'Make target visibility toggle on click'}
- type={Type.PRIM}
- toggleType={ToggleType.BUTTON}
- toggleStatus={this.IsTargetToggler()}
- onClick={this.MakeTargetToggle}
- icon={<FontAwesomeIcon icon="thumbtack" />}
- color={StrCast(Doc.UserDoc().userColor)}
- />}
+ {this.Delete !== returnFalse && <IconButton tooltip={'Remove Link Anchor'} onPointerDown={this.Delete} icon={<FontAwesomeIcon icon="trash-alt" />} color={StrCast(Doc.UserDoc().userColor)} />}
+ {this.PinToPres !== returnFalse && <IconButton tooltip={'Pin to Presentation'} onPointerDown={this.PinToPres} icon={<FontAwesomeIcon icon="map-pin" />} color={StrCast(Doc.UserDoc().userColor)} />}
+ {this.ShowTargetTrail !== returnFalse && <IconButton tooltip={'Show Linked Trail'} onPointerDown={this.ShowTargetTrail} icon={<FontAwesomeIcon icon="taxi" />} color={StrCast(Doc.UserDoc().userColor)} />}
+ {this.IsTargetToggler !== returnFalse && (
+ <Toggle
+ tooltip={'Make target visibility toggle on click'}
+ type={Type.PRIM}
+ toggleType={ToggleType.BUTTON}
+ toggleStatus={this.IsTargetToggler()}
+ onClick={this.MakeTargetToggle}
+ icon={<FontAwesomeIcon icon="thumbtack" />}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
+ )}
</>
);
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss
index 44413ede7..478b7d4ba 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.scss
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss
@@ -6,12 +6,6 @@ $button: #5b97ff;
$highlightedText: #82e0ff;
.summary-box {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- background-color: #ffffff;
- box-shadow: 0 2px 5px #7474748d;
- color: $textgrey;
position: fixed;
bottom: 10px;
right: 10px;
@@ -21,9 +15,16 @@ $highlightedText: #82e0ff;
padding: 15px;
padding-bottom: 0;
z-index: 999;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ background-color: #ffffff;
+ box-shadow: 0 2px 5px #7474748d;
+ color: $textgrey;
.summary-heading {
display: flex;
+ justify-content: space-between;
align-items: center;
border-bottom: 1px solid $greyborder;
padding-bottom: 5px;
@@ -110,6 +111,57 @@ $highlightedText: #82e0ff;
}
}
+.image-content-wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+ padding-bottom: 16px;
+
+ .img-wrapper {
+ position: relative;
+ cursor: pointer;
+
+ .img-container {
+ position: relative;
+
+ img {
+ position: relative;
+ }
+ }
+
+ .img-container::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ }
+
+ .btn-container {
+ position: absolute;
+ right: 8px;
+ bottom: 8px;
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ }
+
+ &:hover {
+ .img-container::after {
+ opacity: 1;
+ }
+
+ .btn-container {
+ opacity: 1;
+ }
+ }
+ }
+}
+
// Typist CSS
.Typist .Cursor {
display: inline-block;
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 8bd060d4f..aeee90d16 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -1,82 +1,194 @@
import React = require('react');
+import './GPTPopup.scss';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, observable } from 'mobx';
import { observer } from 'mobx-react';
import ReactLoading from 'react-loading';
import Typist from 'react-typist';
import { Doc } from '../../../../fields/Doc';
-import { Docs } from '../../../documents/Documents';
-import './GPTPopup.scss';
+import { DocUtils, Docs } from '../../../documents/Documents';
+import { Button, IconButton, Type } from 'browndash-components';
+import { NumCast, StrCast } from '../../../../fields/Types';
+import { CgClose } from 'react-icons/cg';
export enum GPTPopupMode {
SUMMARY,
EDIT,
+ IMAGE,
}
-interface GPTPopupProps {
- visible: boolean;
- text: string;
- loading: boolean;
- mode: GPTPopupMode;
- callSummaryApi: (e: React.PointerEvent) => Promise<void>;
- callEditApi: (e: React.PointerEvent) => Promise<void>;
- replaceText: (replacement: string) => void;
- highlightRange?: number[];
-}
+interface GPTPopupProps {}
@observer
export class GPTPopup extends React.Component<GPTPopupProps> {
static Instance: GPTPopup;
@observable
- private done: boolean = false;
+ public visible: boolean = false;
+ @action
+ public setVisible = (vis: boolean) => {
+ this.visible = vis;
+ };
@observable
- private sidebarId: string = '';
+ public loading: boolean = false;
+ @action
+ public setLoading = (loading: boolean) => {
+ this.loading = loading;
+ };
+ @observable
+ public text: string = '';
+ @action
+ public setText = (text: string) => {
+ this.text = text;
+ };
+
+ @observable
+ public imageUrls: string[] = [];
+ @action
+ public setImgUrls = (imgs: string[]) => {
+ this.imageUrls = imgs;
+ };
+ @observable
+ public mode: GPTPopupMode = GPTPopupMode.SUMMARY;
+ @action
+ public setMode = (mode: GPTPopupMode) => {
+ this.mode = mode;
+ };
+
+ @observable
+ public highlightRange: number[] = [];
+ @action callSummaryApi = () => {};
+ @action callEditApi = () => {};
+ @action replaceText = (replacement: string) => {};
+
+ @observable
+ private done: boolean = false;
@action
public setDone = (done: boolean) => {
this.done = done;
};
+
+ // change what can be a ref into a ref
+ @observable
+ private sidebarId: string = '';
@action
public setSidebarId = (id: string) => {
this.sidebarId = id;
};
+ // pdfs and webpages
+ @observable
+ private targetAnchor: Doc | undefined;
+ @action
+ public setTargetAnchor = (anchor: Doc) => {
+ this.targetAnchor = anchor;
+ };
+
+ @observable
+ private imgTargetDoc: Doc | undefined;
+ @action
+ public setImgTargetDoc = (anchor: Doc) => {
+ this.imgTargetDoc = anchor;
+ };
+
+ @observable
+ private textAnchor: Doc | undefined;
+ @action
+ public setTextAnchor = (anchor: Doc) => {
+ this.textAnchor = anchor;
+ };
+
public addDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean = () => false;
+ public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
/**
* Transfers the summarization text to a sidebar annotation text document.
*/
private transferToText = () => {
- const newDoc = Docs.Create.TextDocument(this.props.text.trim(), {
+ const newDoc = Docs.Create.TextDocument(this.text.trim(), {
_width: 200,
_height: 50,
_layout_fitWidth: true,
_layout_autoHeight: true,
});
this.addDoc(newDoc, this.sidebarId);
+ if (this.targetAnchor) {
+ DocUtils.MakeLink(newDoc, this.targetAnchor, {
+ link_relationship: 'GPT Summary',
+ });
+ }
};
+ /**
+ * Transfers the image urls to actual image docs
+ */
+ private transferToImage = (source: string) => {
+ console.log('Text Anchor', this.textAnchor);
+ console.log('Whole doc anchor', this.imgTargetDoc);
+ const textAnchor = this.textAnchor ?? this.imgTargetDoc;
+ if (!textAnchor) return;
+ const newDoc = Docs.Create.ImageDocument(source, {
+ x: NumCast(textAnchor.x) + NumCast(textAnchor._width) + 10,
+ y: NumCast(textAnchor.y),
+ _height: 200,
+ _width: 200,
+ data_nativeWidth: 1024,
+ data_nativeHeight: 1024,
+ });
+ if (Doc.IsInMyOverlay(textAnchor)) {
+ newDoc.overlayX = textAnchor.x;
+ newDoc.overlayY = NumCast(textAnchor.y) + NumCast(textAnchor._height);
+ Doc.AddToMyOverlay(newDoc);
+ } else {
+ this.addToCollection?.(newDoc);
+ }
+ // Create link between prompt and image
+ DocUtils.MakeLink(textAnchor, newDoc, { link_relationship: 'Image Prompt' });
+ };
+
+ private getPreviewUrl = (source: string) => source.split('.').join('_m.');
+
constructor(props: GPTPopupProps) {
super(props);
GPTPopup.Instance = this;
}
componentDidUpdate = () => {
- if (this.props.loading) {
+ if (this.loading) {
this.setDone(false);
}
};
+ imageBox = () => {
+ return (
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
+ {this.heading('GENERATED IMAGE')}
+ <div className="image-content-wrapper">
+ {this.imageUrls.map(rawSrc => (
+ <div className="img-wrapper">
+ <div className="img-container">
+ <img key={rawSrc} src={this.getPreviewUrl(rawSrc)} width={150} height={150} alt="dalle generation" />
+ </div>
+ <div className="btn-container">
+ <Button text="Save Image" onClick={() => this.transferToImage(rawSrc)} color={StrCast(Doc.UserDoc().userColor)} type={Type.TERT} />
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+ };
+
summaryBox = () => (
<>
<div>
{this.heading('SUMMARY')}
<div className="content-wrapper">
- {!this.props.loading &&
+ {!this.loading &&
(!this.done ? (
<Typist
- key={this.props.text}
+ key={this.text}
avgTypingDelay={15}
cursor={{ hideWhenDone: true }}
onTypingDone={() => {
@@ -84,39 +196,32 @@ export class GPTPopup extends React.Component<GPTPopupProps> {
this.setDone(true);
}, 500);
}}>
- {this.props.text}
+ {this.text}
</Typist>
) : (
- this.props.text
+ this.text
))}
</div>
</div>
- {!this.props.loading && (
+ {!this.loading && (
<div className="btns-wrapper">
{this.done ? (
<>
- <button className="icon-btn" onPointerDown={e => this.props.callSummaryApi(e)}>
- <FontAwesomeIcon icon="redo-alt" size="lg" />
- </button>
- <button
- className="text-btn"
- onClick={e => {
- this.transferToText();
- }}>
- Transfer to Text
- </button>
+ <IconButton tooltip="Generate Again" onClick={this.callSummaryApi} 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} />
</>
) : (
<div className="summarizing">
<span>Summarizing</span>
<ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} />
- <button
- className="btn-secondary"
- onClick={e => {
+ <Button
+ text="Stop Animation"
+ onClick={() => {
this.setDone(true);
- }}>
- Stop Animation
- </button>
+ }}
+ color={StrCast(Doc.UserDoc().userVariantColor)}
+ type={Type.TERT}
+ />
</div>
)}
</div>
@@ -125,7 +230,7 @@ export class GPTPopup extends React.Component<GPTPopupProps> {
);
editBox = () => {
- const hr = this.props.highlightRange;
+ const hr = this.highlightRange;
return (
<>
<div>
@@ -133,22 +238,22 @@ export class GPTPopup extends React.Component<GPTPopupProps> {
<div className="content-wrapper">
{hr && (
<div>
- {this.props.text.slice(0, hr[0])} <span className="highlighted-text">{this.props.text.slice(hr[0], hr[1])}</span> {this.props.text.slice(hr[1])}
+ {this.text.slice(0, hr[0])} <span className="highlighted-text">{this.text.slice(hr[0], hr[1])}</span> {this.text.slice(hr[1])}
</div>
)}
</div>
</div>
- {hr && !this.props.loading && (
+ {hr && !this.loading && (
<>
<div className="btns-wrapper">
<>
- <button className="icon-btn" onPointerDown={e => this.props.callEditApi(e)}>
+ <button className="icon-btn" onPointerDown={this.callEditApi}>
<FontAwesomeIcon icon="redo-alt" size="lg" />
</button>
<button
className="text-btn"
onClick={e => {
- this.props.replaceText(this.props.text);
+ this.replaceText(this.text);
}}>
Replace Text
</button>
@@ -174,14 +279,14 @@ export class GPTPopup extends React.Component<GPTPopupProps> {
heading = (headingText: string) => (
<div className="summary-heading">
<label className="summary-text">{headingText}</label>
- {this.props.loading && <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} />}
+ {this.loading ? <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} /> : <IconButton color={StrCast(Doc.UserDoc().userColor)} tooltip="close" icon={<CgClose size="16px" />} onClick={() => this.setVisible(false)} />}
</div>
);
render() {
return (
- <div className="summary-box" style={{ display: this.props.visible ? 'flex' : 'none' }}>
- {this.props.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.editBox()}
+ <div className="summary-box" style={{ display: this.visible ? 'flex' : 'none' }}>
+ {this.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : this.editBox()}
</div>
);
}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 1319a236d..c9fee4813 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -424,6 +424,10 @@ export class PDFViewer extends React.Component<IViewerProps> {
// Changing which document to add the annotation to (the currently selected PDF)
GPTPopup.Instance.setSidebarId('data_sidebar');
+ const anchor = this._getAnchor(undefined, false);
+ if (anchor) {
+ GPTPopup.Instance.setTargetAnchor(anchor);
+ }
GPTPopup.Instance.addDoc = this.props.sidebarAddDoc;
};
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index ec4252eb8..cb8eda9de 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -10,7 +10,7 @@ import { StrCast } from '../../../fields/Types';
import { GetEffectiveAcl } from '../../../fields/util';
import { DocumentManager } from '../../util/DocumentManager';
import { PingManager } from '../../util/PingManager';
-import { ReportManager } from '../../util/ReportManager';
+import { ReportManager } from '../../util/reportManager/ReportManager';
import { ServerStats } from '../../util/ServerStats';
import { SettingsManager } from '../../util/SettingsManager';
import { SharingManager } from '../../util/SharingManager';
diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts
index 8ac20b1e5..8db4d6003 100644
--- a/src/fields/URLField.ts
+++ b/src/fields/URLField.ts
@@ -25,7 +25,7 @@ export abstract class URLField extends ObjectField {
constructor(url: URL | string) {
super();
if (typeof url === 'string') {
- url = url.startsWith('http') ? new URL(url) : new URL(url, window.location.origin);
+ url = (url.startsWith('http') || url.startsWith('https')) ? new URL(url) : new URL(url, window.location.origin);
}
this.url = url;
}
diff --git a/src/server/ApiManagers/AzureManager.ts b/src/server/ApiManagers/AzureManager.ts
new file mode 100644
index 000000000..12bb98ad0
--- /dev/null
+++ b/src/server/ApiManagers/AzureManager.ts
@@ -0,0 +1,67 @@
+import { ContainerClient, BlobServiceClient } from "@azure/storage-blob";
+import * as fs from "fs";
+import { Readable, Stream } from "stream";
+const AZURE_STORAGE_CONNECTION_STRING = process.env.AZURE_STORAGE_CONNECTION_STRING;
+
+export class AzureManager {
+ private _containerClient: ContainerClient;
+ private _blobServiceClient: BlobServiceClient;
+ private static _instance: AzureManager | undefined;
+
+ public static CONTAINER_NAME = "dashmedia";
+ public static STORAGE_ACCOUNT_NAME = "dashblobstore";
+
+ constructor() {
+ if (!AZURE_STORAGE_CONNECTION_STRING) {
+ throw new Error("Azure Storage Connection String Not Found");
+ }
+ this._blobServiceClient = BlobServiceClient.fromConnectionString(AZURE_STORAGE_CONNECTION_STRING);
+ this._containerClient = this.BlobServiceClient.getContainerClient(AzureManager.CONTAINER_NAME);
+ }
+
+ public static get Instance() {
+ return this._instance = this._instance ?? new AzureManager();
+ }
+
+ public get BlobServiceClient() {
+ return this._blobServiceClient;
+ }
+
+ public get ContainerClient() {
+ return this._containerClient;
+ }
+
+ public static UploadBlob(filename: string, filepath: string, filetype: string) {
+ const blockBlobClient = this.Instance.ContainerClient.getBlockBlobClient(filename);
+ const blobOptions = { blobHTTPHeaders: { blobContentType: filetype }};
+ const stream = fs.createReadStream(filepath);
+ return blockBlobClient.uploadStream(stream, undefined, undefined, blobOptions);
+ }
+
+ public static UploadBlobStream(stream: Readable, filename: string, filetype: string) {
+ const blockBlobClient = this.Instance.ContainerClient.getBlockBlobClient(filename);
+ const blobOptions = { blobHTTPHeaders: { blobContentType: filetype }};
+ return blockBlobClient.uploadStream(stream, undefined, undefined, blobOptions);
+ }
+
+ public static DeleteBlob(filename: string) {
+ const blockBlobClient = this.Instance.ContainerClient.getBlockBlobClient(filename);
+ return blockBlobClient.deleteIfExists();
+ }
+
+ public static async GetBlobs() {
+ const foundBlobs = [];
+ for await (const blob of this.Instance.ContainerClient.listBlobsFlat()) {
+ console.log(`${blob.name}`);
+
+ const blobItem = {
+ url : `https://${AzureManager.STORAGE_ACCOUNT_NAME}.blob.core.windows.net/${AzureManager.CONTAINER_NAME}/${blob.name}`,
+ name : blob.name
+ }
+
+ foundBlobs.push(blobItem);
+ }
+
+ return foundBlobs;
+ }
+}
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index eaaac4e6d..bff60568b 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -6,7 +6,7 @@ import { createReadStream, createWriteStream, existsSync, readFileSync, rename,
import * as path from 'path';
import { basename } from 'path';
import * as sharp from 'sharp';
-import { Stream } from 'stream';
+import { Readable, Stream } from 'stream';
import { filesDirectory, publicDirectory } from '.';
import { Opt } from '../fields/Doc';
import { ParsedPDF } from '../server/PdfTypes';
@@ -17,6 +17,8 @@ import { resolvedServerUrl } from './server_Initialization';
import { AcceptableMedia, Upload } from './SharedMediaTypes';
import request = require('request-promise');
import formidable = require('formidable');
+import { AzureManager } from './ApiManagers/AzureManager';
+import axios from 'axios';
const spawn = require('child_process').spawn;
const { exec } = require('child_process');
const parse = require('pdf-parse');
@@ -42,6 +44,10 @@ function isLocal() {
return /Dash-Web[0-9]*[\\\/]src[\\\/]server[\\\/]public[\\\/](.*)/;
}
+function usingAzure(){
+ return process.env.USE_AZURE === 'true';
+}
+
export namespace DashUploadUtils {
export interface Size {
width: number;
@@ -61,6 +67,9 @@ export namespace DashUploadUtils {
const size = 'content-length';
const type = 'content-type';
+ const BLOBSTORE_URL = process.env.BLOBSTORE_URL;
+ const RESIZE_FUNCTION_URL = process.env.RESIZE_FUNCTION_URL;
+
const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; //TODO:glr
export async function concatVideos(filePaths: string[]): Promise<Upload.AccessPathInfo> {
@@ -182,6 +191,7 @@ export namespace DashUploadUtils {
}
export async function upload(file: File, overwriteGuid?: string): Promise<Upload.FileResponse> {
+ const isAzureOn = usingAzure();
const { type, path, name } = file;
const types = type?.split('/') ?? [];
uploadProgress.set(overwriteGuid ?? name, 'uploading'); // If the client sent a guid it uses to track upload progress, use that guid. Otherwise, use the file's name.
@@ -478,17 +488,48 @@ export namespace DashUploadUtils {
};
}
+ /**
+ * UploadInspectedImage() takes an image with its metadata. If Azure is being used, this method will call the Azure function
+ * to execute the resizing. If Azure is not used, the function will begin to resize the image.
+ *
+ * @param metadata metadata object from InspectImage()
+ * @param filename the name of the file
+ * @param prefix the prefix to use, which will be set to '' if none is provided.
+ * @param cleanUp a boolean indicating if the files should be deleted after upload. True by default.
+ * @returns the accessPaths for the resized files.
+ */
export const UploadInspectedImage = async (metadata: Upload.InspectionResults, filename?: string, prefix = '', cleanUp = true): Promise<Upload.ImageInformation> => {
const { requestable, source, ...remaining } = metadata;
const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}.${remaining.contentType.split('/')[1].toLowerCase()}`;
const { images } = Directory;
const information: Upload.ImageInformation = {
accessPaths: {
- agnostic: getAccessPaths(images, resolved),
+ agnostic: usingAzure() ? {
+ client: BLOBSTORE_URL + `/${filename}`,
+ server: BLOBSTORE_URL + `/${filename}`
+ } : getAccessPaths(images, resolved)
},
...metadata,
};
- const writtenFiles = await outputResizedImages(() => request(requestable), resolved, pathToDirectory(Directory.images));
+ let writtenFiles: { [suffix: string] : string};
+
+ if (usingAzure()) {
+ if (!RESIZE_FUNCTION_URL) {
+ throw new Error("Resize function URL not provided.");
+ }
+
+ try {
+ const response = await axios.post(RESIZE_FUNCTION_URL, {
+ url: requestable
+ });
+ writtenFiles = response.data.writtenFiles;
+ } catch (err) {
+ console.error(err);
+ writtenFiles = {};
+ }
+ } else {
+ writtenFiles = await outputResizedImages(() => request(requestable), resolved, pathToDirectory(Directory.images));
+ }
for (const suffix of Object.keys(writtenFiles)) {
information.accessPaths[suffix] = getAccessPaths(images, writtenFiles[suffix]);
}
@@ -533,6 +574,15 @@ export namespace DashUploadUtils {
force: true,
};
+ /**
+ * outputResizedImages takes in a readable stream and resizes the images according to the sizes defined at the top of this file.
+ *
+ * The new images will be saved to the server with the corresponding prefixes.
+ * @param streamProvider a Stream of the image to process, taken from the /parsed_files location
+ * @param outputFileName the basename (No suffix) of the outputted file.
+ * @param outputDirectory the directory to output to, usually Directory.Images
+ * @returns a map with suffixes as keys and resized filenames as values.
+ */
export async function outputResizedImages(streamProvider: () => Stream | Promise<Stream>, outputFileName: string, outputDirectory: string) {
const writtenFiles: { [suffix: string]: string } = {};
for (const { resizer, suffix } of resizers(path.extname(outputFileName))) {
@@ -549,6 +599,11 @@ export namespace DashUploadUtils {
return writtenFiles;
}
+ /**
+ * define the resizers to use
+ * @param ext the extension
+ * @returns an array of resizer functions from sharp
+ */
function resizers(ext: string): DashUploadUtils.ImageResizer[] {
return [
{ suffix: SizeSuffix.Original },