aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/DataVizBox/DocCreatorMenu
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/DataVizBox/DocCreatorMenu')
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss1060
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx1438
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx117
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/Field.tsx66
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/FieldUtils.tsx79
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/StaticField.tsx147
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx139
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.tsx752
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx22
9 files changed, 3820 insertions, 0 deletions
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss
new file mode 100644
index 000000000..57f4a1e94
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss
@@ -0,0 +1,1060 @@
+.no-margin {
+ margin-top: 0px !important;
+ margin-bottom: 0px !important;
+ margin-left: 0px !important;
+ margin-right: 0px !important;
+}
+
+.docCreatorMenu-cont {
+ position: absolute;
+ z-index: 1000;
+ // box-shadow: 0px 3px 4px rgba(0, 0, 0, 30%);
+ // background: whitesmoke;
+ // color: black;
+ border-radius: 3px;
+}
+
+.docCreatorMenu-menu {
+ display: flex;
+ flex-direction: row;
+ height: 25px;
+ align-items: flex-end;
+}
+
+.docCreatorMenu-menu-button {
+ width: 25px;
+ height: 25px;
+ background: whitesmoke;
+ background-color: rgb(50, 50, 50);
+ border-radius: 5px;
+ border: 1px solid rgb(180, 180, 180);
+ padding: 0px;
+ font-size: 13px;
+ //box-shadow: 3px 3px rgb(29, 29, 31);
+
+ &:hover {
+ box-shadow: none;
+ }
+
+ &.right{
+ margin-left: 0px;
+ font-size: 12px;
+ }
+
+ &.close-menu {
+ font-size: 12px;
+ width: 18px;
+ height: 18px;
+ font-size: 12px;
+ margin-left: auto;
+ margin-right: 5px;
+ margin-bottom: 3px;
+ }
+
+ &.options {
+ margin-left: 0px;
+ }
+
+ &:hover {
+ background-color: rgb(60, 60, 65);
+ }
+
+ &.top-bar {
+ border-bottom: 25px solid #555;
+ border-left: 12px solid transparent;
+ border-right: 12px solid transparent;
+ // border-top-left-radius: 5px;
+ // border-top-right-radius: 5px;
+ border-radius: 0px;
+ height: 0;
+ width: 50px;
+ }
+
+ &.preview-toggle {
+ margin: 0px;
+ border-top-left-radius: 0px;
+ border-bottom-left-radius: 0px;
+ border-left: 0px;
+ }
+}
+
+.docCreatorMenu-top-buttons-container {
+ position: relative;
+ margin-top: 5px;
+ margin-left: 7px;
+ display: flex;
+ flex-direction: row;
+ align-items: flex-end;
+ width: 150px;
+ height: auto;
+}
+
+.top-button-container {
+ position: relative;
+ width: 52px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &.left {
+ z-index: 3;
+ }
+
+ &.middle {
+ position: absolute;
+ left: 40px;
+ z-index: 2;
+
+ &.selected {
+ z-index: 4;
+ }
+ }
+
+ &.right {
+ position: absolute;
+ left: 80px;
+ z-index: 1;
+
+ &.selected {
+ z-index: 4;
+ }
+ }
+
+ &:hover::before{
+ border-bottom: 20px solid rgb(82, 82, 82);
+ }
+
+ &::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ border-bottom: 20px solid rgb(50, 50, 50);
+ border-left: 12px solid transparent;
+ border-right: 12px solid transparent;
+ height: 0;
+ width: 50px;
+ }
+
+ &::after {
+ content: "";
+ position: absolute;
+ top: -1px;
+ left: -1px;
+ border-bottom: 22px solid rgb(180, 180, 180);
+ border-left: 12px solid transparent;
+ border-right: 12px solid transparent;
+ height: 0;
+ width: 52px;
+ z-index: -1;
+ }
+
+ &.selected::before {
+ border-bottom-color: rgb(67, 119, 214);
+ }
+}
+
+.top-button-content {
+ position: relative;
+ z-index: 1;
+ color: white;
+}
+
+.docCreatorMenu-menu-hr{
+ margin-top: 0px;
+ margin-bottom: 0px;
+ color: rgb(180, 180, 180);
+}
+
+.docCreatorMenu-placement-indicator {
+ position: absolute;
+ z-index: 100000;
+ border-left: solid 3px #9fd7fb;
+ border-top: solid 3px #9fd7fb;
+ width: 25px;
+ height: 25px;
+}
+
+.docCreatorMenu-general-options-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin: 0px;
+ padding: 0px;
+ gap: 5px;
+}
+
+.docCreatorMenu-save-layout-button {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 40px;
+ height: 40px;
+ background-color: rgb(99, 148, 238);
+ border: 2px solid rgb(80, 107, 152);
+ border-radius: 5px;
+ margin-bottom: 20px;
+ font-size: 25px;
+
+ &:hover{
+ background-color: rgb(59, 128, 255);
+ border: 2px solid rgb(53, 80, 127);
+ }
+}
+
+.docCreatorMenu-create-docs-button {
+ width: 40px;
+ height: 40px;
+ background-color: rgb(176, 229, 149);
+ border: 2px solid rgb(126, 219, 80);
+ border-radius: 5px;
+ padding: 0px;
+ font-size: 25px;
+ color: white;
+ flex: 0 0 auto;
+ margin-bottom: 20px; //remove later !!!
+
+ &:hover {
+ background-color: rgb(129, 223, 83);
+ border: 2px solid rgb(80, 185, 28);
+ }
+}
+
+.docCreatorMenu-option-divider {
+ border-top: 1px solid rgb(180, 180, 180);
+ width: 95%;
+ margin-top: 10px;
+ margin-bottom: 10px;
+
+ &.full {
+ width: 100%;
+ }
+}
+
+//------------------------------------------------------------------------------------------------------------------------------------------
+// Resizers CSS
+//--------------------------------------------------------------------------------------------------------------------------------------------
+
+.docCreatorMenu-resizer {
+ position: absolute;
+ background-color: none;
+
+ &.top, &.bottom {
+ height: 10px;
+ cursor: ns-resize;
+ }
+
+ &.right, &.left {
+ width: 10px;
+ cursor: ew-resize;
+ }
+
+ &.topRight, &.topLeft, &.bottomRight, &.bottomLeft {
+ height: 15px;
+ width: 15px;
+ background-color: none;
+ }
+}
+
+//------------------------------------------------------------------------------------------------------------------------------------------
+// DocCreatorMenu templates preview CSS
+//--------------------------------------------------------------------------------------------------------------------------------------------
+
+.docCreatorMenu-templates-view {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ overflow-y: scroll;
+ //align-items: flex-start;
+ margin: 5px;
+ margin-top: 0px;
+ width: calc(100% - 10px);
+ height: calc(100% - 30px);
+ border: 1px solid rgb(180, 180, 180);
+ border-radius: 5px;
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
+
+.docCreatorMenu-preview-container {
+ display: grid;
+ grid-template-columns: repeat(2, 140px);
+ grid-template-rows: 140px;
+ grid-auto-rows: 141px;
+ overflow-y: scroll;
+ margin: 0px;
+ margin-top: 0px;
+ width: 100%;
+ height: 100%;
+}
+
+.docCreatorMenu-expanded-template-preview {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ color: black;
+ position: relative;
+ width: 100%;
+ height: 100%;
+
+ .top-panel{
+ width: 100%;
+ height: 10px;
+ }
+
+ .right-buttons-panel {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ height: 100%;
+ position: absolute;
+ right: 0px;
+ top: 0px;
+ width: 40px;
+ padding: 5px;
+ gap: 2px;
+ }
+}
+
+.docCreatorMenu-preview-window {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 113px;
+ height: 113px;
+ margin-top: 10px;
+ margin-left: 10px;
+ color: none;
+ border: 1px solid rgb(163, 163, 163);
+ border-radius: 5px;
+ box-shadow: 5px 5px rgb(29, 29, 31);
+ flex: 0 0 auto;
+
+ &:hover{
+ background-color: rgb(72, 72, 73);
+ }
+
+ &.empty {
+ font-size: 35px;
+
+ &.GPT {
+ margin-top: 0px;
+ }
+ }
+
+ .option-button {
+ display: none;
+ height: 25px;
+ width: 25px;
+ margin: 0px;
+ background: none;
+ border: 0px;
+ padding: 0px;
+ font-size: 15px;
+ z-index: 1000;
+
+ &.right {
+ position: absolute;
+ bottom: 0px;
+ right: 0px;
+ }
+
+ &.left {
+ position: absolute;
+ bottom: 0px;
+ left: 0px;
+ }
+
+ &.top-left {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ }
+ }
+
+ &:hover .option-button {
+ display: block;
+ }
+
+}
+
+.docCreatorMenu-preview-image{
+ background-color: transparent;
+ height: 100px;
+ width: 100px;
+ display: block;
+ object-fit: contain;
+ border-radius: 5px;
+
+ &.expanded {
+ height: 100%;
+ width: 100%;
+ }
+}
+
+.docCreatorMenu-section {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ position: relative;
+ margin: 0px;
+ margin-top: 0px;
+ margin-bottom: 0px;
+ width: 100%;
+ height: 200;
+ flex: 0 0 auto;
+}
+
+.docCreatorMenu-GPT-options-container {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ position: relative;
+ width: auto;
+ margin: 0px;
+ margin-top: 5px;
+ padding: 0px;
+}
+
+.docCreatorMenu-templates-preview-window {
+ display: flex;
+ flex-direction: row;
+ //justify-content: center;
+ align-items: center;
+ overflow-y: scroll;
+ position: relative;
+ color: black;
+ height: 125px;
+ width: calc(100% - 10px);
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+
+ .loading-spinner {
+ justify-self: center;
+ }
+}
+
+.divvv{
+ width: 200;
+ height: 200;
+ border: solid 1px white;
+}
+
+.docCreatorMenu-section-topbar {
+ position: relative;
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+}
+
+.section-reveal-options {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ margin-right: 0px;
+ margin-left: auto;
+ border: 0px;
+ background: none;
+
+ &.float-right {
+ float: right;
+ }
+}
+
+.docCreatorMenu-section-title {
+ border: 1px solid rgb(163, 163, 163);
+ border-top: 0px;
+ border-left: 0px;
+ border-bottom-right-radius: 5px;
+ font-size: 12px;
+ padding: 2px;
+ padding-left: 3px;
+ padding-right: 3px;
+ margin-bottom: 3px;
+}
+
+.docCreatorMenu-GPT-generate {
+ height: 30px;
+ width: 30px;
+ background-color: rgb(176, 229, 149);
+ border: 1px solid rgb(126, 219, 80);
+ border-radius: 5px;
+ padding: 0px;
+ font-size: 14px;
+ color: white;
+ letter-spacing: 1px;
+ flex: 0 0 auto;
+
+ &:hover {
+ background-color: rgb(129, 223, 83);
+ border: 2px solid rgb(80, 185, 28);
+ }
+}
+
+.docCreatorMenu-GPT-prompt-input {
+ width: 140px;
+ height: 25px;
+ overflow-y: scroll;
+ border: 1px solid rgb(180, 180, 180);
+ background-color: rgb(35, 35, 35);
+ border-radius: 3px;
+ padding-left: 4px;
+}
+
+//------------------------------------------------------------------------------------------------------------------------------------------
+// DocCreatorMenu options CSS
+//--------------------------------------------------------------------------------------------------------------------------------------------
+
+.docCreatorMenu-option-container{
+ display: flex;
+ width: 180px;
+ height: 30px;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ margin-top: 10px;
+ margin-bottom: 10px;
+
+ &.layout{
+ z-index: 5;
+ }
+}
+
+.docCreatorMenu-option-title{
+ display: flex;
+ width: 140px;
+ height: 30px;
+ background: whitesmoke;
+ background-color: rgb(34, 34, 37);
+ border-radius: 5px;
+ border: 1px solid rgb(180, 180, 180);
+ padding: 0px;
+ font-size: 12px;
+ align-items: center;
+ justify-content: center;
+ text-transform: uppercase;
+ cursor: pointer;
+
+ &.spacer {
+ border-left: none;
+ border-right: none;
+ border-radius: 0px;
+ width: auto;
+ text-transform: none;
+
+ &.small {
+ height: 20px;
+ transform: translateY(-5px);
+ }
+ }
+
+ &.config {
+ border-radius: 4px;
+ border-top-right-radius: 0px;
+ border-bottom-right-radius: 0px;
+ width: 30px;
+ border-right: 0px;
+ gap: 3px;
+
+ &.layout-config {
+ height: 20px;
+ transform: translateY(-5px);
+ text-transform: none;
+ padding-left: 2px;
+ }
+
+ &.dimensions {
+ text-transform: none;
+ height: 20px;
+ transform: translateY(-5px);
+ width: 70px;
+ }
+ }
+}
+
+.docCreatorMenu-input {
+ display: flex;
+ height: 30px;
+ background-color: rgb(34, 34, 37);
+ border: 1px solid rgb(180, 180, 180);
+ align-items: center;
+ justify-content: center;
+
+ &.config {
+ border-radius: 4px;
+ margin: 0px;
+ border-top-left-radius: 0px;
+ border-bottom-left-radius: 0px;
+ border-left: 0px;
+ width: 25px;
+
+ &.layout-config {
+ height: 20px;
+ transform: translateY(-5px);
+ }
+
+ &.dimensions {
+ height: 20px;
+ width: 30px;
+ transform: translateY(-5px);
+
+ &.right {
+ border-top-left-radius: 0px;
+ border-bottom-left-radius: 0px;
+ }
+
+ &.left {
+ border-radius: 0px;
+ border-right: 0px;
+ }
+ }
+ }
+}
+
+.docCreatorMenu-configuration-bar {
+ width: 200;
+ gap: 5px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+
+ &.no-gap {
+ gap: 0px;
+ }
+}
+
+.docCreatorMenu-menu-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ overflow-y: scroll;
+ margin: 5px;
+ margin-top: 0px;
+ width: calc(100% - 10px);
+ height: calc(100% - 30px);
+ border: 1px solid rgb(180, 180, 180);
+ border-radius: 5px;
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+
+ .docCreatorMenu-option-container{
+ width: 180px;
+ height: 30px;
+
+ .docCreatorMenu-dropdown-hoverable {
+ width: 140px;
+ height: 30px;
+
+ &:hover .docCreatorMenu-dropdown-content {
+ display: block;
+ }
+
+ &:hover .docCreatorMenu-option-title {
+ border-bottom-left-radius: 0px;
+ border-bottom-right-radius: 0px;
+ }
+
+ .docCreatorMenu-dropdown-content {
+ display: none;
+ min-width: 100px;
+ height: 75px;
+ overflow-y: scroll;
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ border-bottom: 1px solid rgb(180, 180, 180);
+ border-bottom-left-radius: 5px;
+ border-bottom-right-radius: 5px;
+
+ .docCreatorMenu-dropdown-option{
+ display: flex;
+ background-color: rgb(42, 42, 46);
+ border-left: 1px solid rgb(180, 180, 180);
+ border-right: 1px solid rgb(180, 180, 180);
+ border-bottom: 1px solid rgb(180, 180, 180);
+ width: 140px;
+ height: 25px;
+ justify-content: center;
+ justify-items: center;
+ padding-top: 3px;
+
+ &:hover {
+ background-color: rgb(68, 68, 74);
+ cursor: pointer;
+ }
+ }
+ }
+ }
+ }
+}
+
+.docCreatorMenu-layout-preview-window-wrapper {
+ flex: 0 0 auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ color: black;
+ width: calc(100% - 50px);
+ height: calc(100% - 50px);
+ position: relative;
+ border: 1px solid rgb(180, 180, 180);
+ padding: 10px;
+ margin-left: 20px;
+ margin-right: 20px;
+
+ &.loading {
+ width: 100px;
+ height: 100px;
+ border: none;
+ }
+
+ &:hover .docCreatorMenu-zoom-button-container {
+ display: block;
+ }
+
+ .docCreatorMenu-layout-preview-window {
+ padding: 5px;
+ flex: 0 0 auto;
+ overflow: scroll;
+ display: grid;
+ width: 100%;
+ aspect-ratio: 1;
+ //height: auto;
+ // max-width: 240;
+ // max-height: 240;
+ border: 1px solid rgb(180, 180, 180);
+ border-radius: 5px;
+ background-color: rgb(34, 34, 37);
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+
+ &.small {
+ max-width: 100;
+ max-height: 100;
+ }
+
+ .docCreatorMenu-layout-preview-item {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 3px;
+ border: solid 1px lightblue;
+
+ &:hover {
+ border: solid 2px rgb(68, 153, 233);
+ z-index: 2;
+ }
+ }
+ }
+
+ .docCreatorMenu-zoom-button-container {
+ position: absolute;
+ top: 0px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ display: none;
+ z-index: 999;
+ }
+
+ .docCreatorMenu-zoom-button{
+ width: 15px;
+ height: 15px;
+ background: whitesmoke;
+ background-color: rgb(34, 34, 37);
+ border-radius: 3px;
+ border: 1px solid rgb(180, 180, 180);
+ padding: 0px;
+ font-size: 10px;
+ z-index: 6;
+ margin-left: 0px;
+ margin-top: 0px;
+ margin-right: 0px; //225px
+ margin-bottom: 0px;
+ }
+}
+
+//------------------------------------------------------------------------------------------------------------------------------------------
+// DocCreatorMenu dashboard CSS
+//--------------------------------------------------------------------------------------------------------------------------------------------
+
+.docCreatorMenu-dashboard-view {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ overflow-y: hidden;
+ //align-items: flex-start;
+ margin: 5px;
+ margin-top: 0px;
+ width: calc(100% - 10px);
+ height: calc(100% - 30px);
+ border: 1px solid rgb(180, 180, 180);
+ border-radius: 5px;
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+
+ .panels-container {
+ height: 100%;
+ width: 100%;
+ flex-direction: column;
+ justify-content: flex-start;
+ overflow-y: scroll;
+ }
+
+ .topbar {
+ height: 30px;
+ width: 100%;
+ background-color: rgb(50, 50, 50);
+ }
+
+// .field-panel {
+// position: relative;
+// display: flex;
+// // align-items: flex-start;
+// flex-direction: column;
+// gap: 5px;
+// padding: 5px;
+// height: 100px;
+// //width: 100%;
+// border: 1px solid rgb(180, 180, 180);
+// margin: 5px;
+// margin-top: 0px;
+// border-radius: 3px;
+// flex: 0 0 auto;
+
+// .properties-wrapper {
+// display: flex;
+// flex-direction: row;
+// align-items: flex-start;
+// gap: 5px;
+
+// .field-property-container {
+// background-color: rgb(40, 40, 40);
+// border: 1px solid rgb(100, 100, 100);
+// border-radius: 3px;
+// width: 30%;
+// height: 25px;
+// padding-left: 3px;
+// align-items: center;
+// color: whitesmoke;
+// }
+
+// .field-type-selection-container {
+// display: flex;
+// flex-direction: row;
+// align-items: center;
+// background-color: rgb(40, 40, 40);
+// border: 1px solid rgb(100, 100, 100);
+// border-radius: 3px;
+// width: 31%;
+// height: 25px;
+// padding-left: 3px;
+// color: whitesmoke;
+
+// .placeholder {
+// color: gray;
+// }
+
+// &:hover .placeholder {
+// display: none;
+// }
+
+// .bubbles {
+// display: none;
+// }
+
+// .text {
+// margin-top: 5px;
+// margin-bottom: 5px;
+// }
+
+// &:hover .bubbles {
+// display: flex;
+// flex-direction: row;
+// align-items: flex-start;
+// }
+
+// &:hover .type-display {
+// display: none;
+// }
+
+// .bubble {
+// margin: 5px;
+// }
+
+// &:hover .bubble {
+// margin-top: 7px;
+// }
+// }
+// }
+
+// .field-description-container {
+// background-color: rgb(40, 40, 40);
+// border: 1px solid rgb(100, 100, 100);
+// border-radius: 3px;
+// width: 100%;
+// height: 100%;
+// resize: none;
+
+// ::-webkit-scrollbar-track {
+// background: none;
+// }
+// }
+
+// .top-right {
+// position: absolute;
+// top: 0px;
+// right: 0px;
+// }
+// }
+// }
+
+ .field-panel {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ height: 285px;
+ width: calc(100% - 10px);
+ border: 1px solid rgb(180, 180, 180);
+ margin: 5px;
+ margin-top: 0px;
+ margin-bottom: 10px;
+ border-radius: 3px;
+ flex: 0 0 auto;
+ gap: 25px;
+ background-color: rgb(60, 60, 60);
+
+ .top-bar {
+ position: relative;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ border-bottom: 1px solid rgb(180, 180, 180);
+ border-top-right-radius: 5px;
+ border-top-left-radius: 5px;
+ width: 100%;
+ height: 20px;
+ background-color: rgb(50, 50, 50);
+ color: rgb(168, 167, 167);
+
+ .field-title {
+ color: whitesmoke;
+ }
+ }
+
+ .opts-bar {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+
+ .opt-box {
+ border: 1px solid rgb(180, 180, 180);
+ border-radius: 5px;
+ width: 40%;
+ height: 50px;
+ margin-right: 4%;
+ margin-left: 4%;
+ box-shadow: 5px 5px rgb(29, 29, 31);
+ }
+
+ .content {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ height: calc(100% - 20px);
+ width: 100%;
+ background-color: rgb(50, 50, 50);
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+ resize: none;
+
+ .bubbles {
+ display: none;
+ }
+
+ .text {
+ margin-right: 5px;
+ }
+
+ &:hover .bubbles {
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ }
+
+ &:hover .type-display {
+ display: none;
+ }
+
+ .bubble {
+ margin: 3px;
+ }
+ }
+ }
+
+ .sizes-box {
+ width: 88%;
+ height: 60px;
+ border: 1px solid rgb(180, 180, 180);
+ border-radius: 5px;
+ background-color: rgb(50, 50, 50);
+ box-shadow: 5px 5px rgb(29, 29, 31);
+
+ .content {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ height: calc(100% - 20px);
+ width: 100%;
+ background-color: rgb(50, 50, 50);
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+
+ .text {
+ margin-right: 9px;
+ }
+
+ .bubbles {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ }
+
+ .bubble {
+ margin: 3px;
+ margin-right: 4px;
+ }
+ }
+ }
+
+ .desc-box {
+ width: 88%;
+ height: 50px;
+ border: 1px solid rgb(180, 180, 180);
+ border-radius: 5px;
+ background-color: rgb(50, 50, 50);
+ box-shadow: 5px 5px rgb(29, 29, 31);
+
+ .content {
+ height: calc(100% - 20px);
+ width: 100%;
+ background-color: rgb(50, 50, 50);
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+ resize: none;
+
+ }
+ }
+
+ }
+
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx
new file mode 100644
index 000000000..16d588c55
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx
@@ -0,0 +1,1438 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Colors } from 'browndash-components';
+import { action, computed, makeObservable, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import { IDisposer } from 'mobx-utils';
+import * as React from 'react';
+import ReactLoading from 'react-loading';
+import { ClientUtils, returnEmptyFilter, returnFalse, setupMoveUpEvents } from '../../../../../ClientUtils';
+import { emptyFunction } from '../../../../../Utils';
+import { Doc, NumListCast, StrListCast, returnEmptyDoclist } from '../../../../../fields/Doc';
+import { Id } from '../../../../../fields/FieldSymbols';
+import { ImageCast, StrCast } from '../../../../../fields/Types';
+import { ImageField } from '../../../../../fields/URLField';
+import { Networking } from '../../../../Network';
+import { GPTCallType, gptAPICall, gptImageCall } from '../../../../apis/gpt/GPT';
+import { Docs, DocumentOptions } from '../../../../documents/Documents';
+import { DragManager } from '../../../../util/DragManager';
+import { SnappingManager } from '../../../../util/SnappingManager';
+import { UndoManager, undoable } from '../../../../util/UndoManager';
+import { ObservableReactComponent } from '../../../ObservableReactComponent';
+import { CollectionFreeFormView } from '../../../collections/collectionFreeForm/CollectionFreeFormView';
+import { DocumentView, DocumentViewInternal } from '../../DocumentView';
+import { FieldViewProps } from '../../FieldView';
+import { OpenWhere } from '../../OpenWhere';
+import { DataVizBox } from '../DataVizBox';
+import './DocCreatorMenu.scss';
+import { DefaultStyleProvider } from '../../../StyleProvider';
+import { Transform } from '../../../../util/Transform';
+import { TemplateFieldSize, TemplateFieldType, TemplateLayouts } from './TemplateBackend';
+import { TemplateManager } from './TemplateManager';
+import { Template } from './Template';
+import { Field, FieldContentType } from './FieldTypes/Field';
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { Upload } from '../../../../../server/SharedMediaTypes';
+
+export enum LayoutType {
+ FREEFORM = 'Freeform',
+ CAROUSEL = 'Carousel',
+ CAROUSEL3D = '3D Carousel',
+ MASONRY = 'Masonry',
+ CARD = 'Card View',
+}
+
+export interface DataVizTemplateInfo {
+ doc: Doc;
+ layout: { type: LayoutType; xMargin: number; yMargin: number; repeat: number };
+ columns: number;
+ referencePos: { x: number; y: number };
+}
+
+export interface DataVizTemplateLayout {
+ template: Doc;
+ docsNumList: number[];
+ layout: { type: LayoutType; xMargin: number; yMargin: number; repeat: number };
+ columns: number;
+ rows: number;
+}
+
+export type Col = {
+ sizes: TemplateFieldSize[];
+ desc: string;
+ title: string;
+ type: TemplateFieldType;
+ defaultContent?: string;
+};
+
+@observer
+export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> {
+ static Instance: DocCreatorMenu;
+
+ private _disposers: { [name: string]: IDisposer } = {};
+
+ private _ref: HTMLDivElement | null = null;
+
+ private templateManager: TemplateManager;
+
+ @observable _fullyRenderedDocs: Doc[] = [];
+ @observable _renderedDocCollectionPreview: Doc | undefined = undefined;
+ @observable _renderedDocCollection: Doc | undefined = undefined;
+ @observable _docsRendering: boolean = false;
+
+ @observable _userTemplates: {template: Template, doc: Doc}[] = []; //!!! used to keep track of all templates, should be refactored to work with actual templates and not docs
+ @observable _selectedTemplate: Template | undefined = undefined;
+ @observable _currEditingTemplate: Template | undefined = undefined;
+
+ @observable _userCreatedFields: Col[] = [];
+ @observable _selectedCols: { title: string; type: string; desc: string }[] | undefined = [];
+
+ @observable _layout: { type: LayoutType; yMargin: number; xMargin: number; columns?: number; repeat: number } = { type: LayoutType.FREEFORM, yMargin: 10, xMargin: 10, columns: 3, repeat: 0 };
+ @observable _layoutPreviewScale: number = 1;
+ @observable _savedLayouts: DataVizTemplateLayout[] = [];
+ @observable _expandedPreview: Doc | undefined = undefined;
+
+ @observable _suggestedTemplates: Template[] = [];
+ @observable _suggestedTemplatePreviews: {doc: Doc, template: Template}[] = [];
+ @observable _GPTOpt: boolean = false;
+ @observable _callCount: number = 0;
+ @observable _GPTLoading: boolean = false;
+
+ @observable _pageX: number = 0;
+ @observable _pageY: number = 0;
+
+ @observable _hoveredLayoutPreview: number | undefined = undefined;
+ @observable _mouseX: number = -1;
+ @observable _mouseY: number = -1;
+ @observable _startPos?: { x: number; y: number };
+ @observable _shouldDisplay: boolean = false;
+
+ @observable _menuContent: 'templates' | 'options' | 'saved' | 'dashboard' = 'templates';
+ @observable _dragging: boolean = false;
+ @observable _draggingIndicator: boolean = false;
+ @observable _dataViz?: DataVizBox;
+ @observable _interactionLock: boolean | undefined;
+ @observable _snapPt: {x: number, y: number} = {x: 0, y: 0};
+ @observable _resizeHdlId: string = '';
+ @observable _resizing: boolean = false;
+ @observable _offset: { x: number; y: number } = { x: 0, y: 0 };
+ @observable _resizeUndo: UndoManager.Batch | undefined = undefined;
+ @observable _initDimensions: { width: number; height: number; x?: number; y?: number } = { width: 300, height: 400, x: undefined, y: undefined };
+ @observable _menuDimensions: { width: number; height: number } = { width: 400, height: 400 };
+ @observable _editing: boolean = false;
+
+ constructor(props: any) {
+ super(props);
+ makeObservable(this);
+ DocCreatorMenu.Instance = this;
+ this.templateManager = new TemplateManager(TemplateLayouts.allTemplates);
+ }
+
+ @action setDataViz = (dataViz: DataVizBox) => {
+ this._dataViz = dataViz;
+ this._selectedTemplate = undefined;
+ this._renderedDocCollection = undefined;
+ this._renderedDocCollectionPreview = undefined;
+ this._fullyRenderedDocs = [];
+ this._suggestedTemplatePreviews = [];
+ this._suggestedTemplates = [];
+ this._userCreatedFields = [];
+ };
+ @action addUserTemplate = (template: Template) => {
+ this._userTemplates.push({template: template.cloneBase(), doc: template.getRenderedDoc()});
+ };
+ @action removeUserTemplate = (template: Template) => {
+ this._userTemplates = this._userTemplates.filter(info => info.template !== template);
+ }
+ @action updateTemplatePreview = (template: Template) => {
+ template.renderUpdates();
+ const preview = {template: template, doc: template.getRenderedDoc()};
+ this._suggestedTemplatePreviews = this._suggestedTemplatePreviews.map(t => { return t.template === preview.template ? preview : t }); //prettier-ignore
+ this._userTemplates = this._userTemplates.map(t => { return t.template === preview.template ? preview : t }); //prettier-ignore
+ };
+ @action setSuggestedTemplates = (templates: Template[]) => {
+ this._suggestedTemplates = templates;
+ this._suggestedTemplatePreviews = templates.map(template => {return {template: template, doc: template.getRenderedDoc()}}); //prettier-ignore
+ };
+
+ @computed get docsToRender() {
+ return this._selectedTemplate ? NumListCast(this._dataViz?.layoutDoc.dataViz_selectedRows) : [];
+ }
+
+ @computed get rowsCount() {
+ switch (this._layout.type) {
+ case LayoutType.FREEFORM:
+ return Math.ceil(this.docsToRender.length / (this._layout.columns ?? 1)) ?? 0;
+ case LayoutType.CAROUSEL3D:
+ return 1.8;
+ default:
+ return 1;
+ }
+ }
+
+ @computed get columnsCount() {
+ switch (this._layout.type) {
+ case LayoutType.FREEFORM:
+ return this._layout.columns ?? 0;
+ case LayoutType.CAROUSEL3D:
+ return 3;
+ default:
+ return 1;
+ }
+ }
+
+ @computed get selectedFields() {
+ return StrListCast(this._dataViz?.layoutDoc._dataViz_axes);
+ }
+
+ @computed get fieldsInfos(): Col[] {
+ const colInfo = this._dataViz?.colsInfo;
+ return this.selectedFields
+ .map(field => {
+ const fieldInfo = colInfo?.get(field);
+
+ const col: Col = {
+ title: field,
+ type: fieldInfo?.type ?? TemplateFieldType.UNSET,
+ desc: fieldInfo?.desc ?? '',
+ sizes: fieldInfo?.sizes ?? [TemplateFieldSize.MEDIUM],
+ };
+
+ if (fieldInfo?.defaultContent !== undefined) {
+ col.defaultContent = fieldInfo.defaultContent;
+ }
+
+ return col;
+ })
+ .concat(this._userCreatedFields);
+ }
+
+ @computed get canMakeDocs() {
+ return this._selectedTemplate !== undefined && this._layout !== undefined;
+ }
+
+ get bounds(): { t: number; b: number; l: number; r: number } {
+ const rect = this._ref?.getBoundingClientRect();
+ const bounds = { t: rect?.top ?? 0, b: rect?.bottom ?? 0, l: rect?.left ?? 0, r: rect?.right ?? 0 };
+ return bounds;
+ }
+
+ setUpButtonClick = (e: any, func: () => void) => {
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ undoable(clickEv => {
+ clickEv.stopPropagation();
+ clickEv.preventDefault();
+ func();
+ }, 'create docs')
+ );
+ };
+
+ @action
+ onPointerDown = (e: PointerEvent) => {
+ this._mouseX = e.clientX;
+ this._mouseY = e.clientY;
+ };
+
+ @action
+ onPointerUp = (e: PointerEvent) => {
+ if (this._resizing) {
+ this._initDimensions.width = this._menuDimensions.width;
+ this._initDimensions.height = this._menuDimensions.height;
+ this._initDimensions.x = this._pageX;
+ this._initDimensions.y = this._pageY;
+ document.removeEventListener('pointermove', this.onResize);
+ SnappingManager.SetIsResizing(undefined);
+ this._resizing = false;
+ }
+ if (this._dragging) {
+ document.removeEventListener('pointermove', this.onDrag);
+ this._dragging = false;
+ }
+ if (e.button !== 2 && !e.ctrlKey) return;
+ const curX = e.clientX;
+ const curY = e.clientY;
+ if (Math.abs(this._mouseX - curX) > 1 || Math.abs(this._mouseY - curY) > 1) {
+ this._shouldDisplay = false;
+ }
+ };
+
+ componentDidMount() {
+ document.addEventListener('pointerdown', this.onPointerDown, true);
+ document.addEventListener('pointerup', this.onPointerUp);
+ }
+
+ componentWillUnmount() {
+ Object.values(this._disposers).forEach(disposer => disposer?.());
+ document.removeEventListener('pointerdown', this.onPointerDown, true);
+ document.removeEventListener('pointerup', this.onPointerUp);
+ }
+
+ @action
+ toggleDisplay = (x: number, y: number) => {
+ if (this._shouldDisplay) {
+ this._shouldDisplay = false;
+ } else {
+ this._pageX = x;
+ this._pageY = y;
+ this._shouldDisplay = true;
+ }
+ };
+
+ @action
+ closeMenu = () => {
+ this._shouldDisplay = false;
+ };
+
+ @action
+ openMenu = () => {
+ this._shouldDisplay = true;
+ };
+
+ @action
+ onResizePointerDown = (e: React.PointerEvent): void => {
+ this._resizing = true;
+ document.addEventListener('pointermove', this.onResize);
+ SnappingManager.SetIsResizing(DocumentView.Selected().lastElement()?.Document[Id]); // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them
+ e.stopPropagation();
+ const id = (this._resizeHdlId = e.currentTarget.className);
+ const pad = id.includes('Left') || id.includes('Right') ? Number(getComputedStyle(e.target as any).width.replace('px', '')) / 2 : 0;
+ const bounds = e.currentTarget.getBoundingClientRect();
+ this._offset = {
+ x: id.toLowerCase().includes('left') ? bounds.right - e.clientX - pad : bounds.left - e.clientX + pad, //
+ y: id.toLowerCase().includes('top') ? bounds.bottom - e.clientY - pad : bounds.top - e.clientY + pad,
+ };
+ this._resizeUndo = UndoManager.StartBatch('drag resizing');
+ this._snapPt = { x: e.pageX, y: e.pageY };
+ };
+
+ @action
+ onResize = (e: any): boolean => {
+ const dragHdl = this._resizeHdlId.split(' ')[1];
+ const thisPt = DragManager.snapDrag(e, -this._offset.x, -this._offset.y, this._offset.x, this._offset.y);
+
+ const { scale, refPt, transl } = this.getResizeVals(thisPt, dragHdl);
+ !this._interactionLock && runInAction(async () => { // resize selected docs if we're not in the middle of a resize (ie, throttle input events to frame rate)
+ this._interactionLock = true;
+ const scaleAspect = {x: scale.x, y: scale.y};
+ this.resizeView(refPt, scaleAspect, transl); // prettier-ignore
+ await new Promise<boolean | undefined>(res => { setTimeout(() => { res(this._interactionLock = undefined)})});
+ }); // prettier-ignore
+ return true;
+ };
+
+ @action
+ onDrag = (e: any): boolean => {
+ this._pageX = e.pageX - (this._startPos?.x ?? 0);
+ this._pageY = e.pageY - (this._startPos?.y ?? 0);
+ this._initDimensions.x = this._pageX;
+ this._initDimensions.y = this._pageY;
+ return true;
+ };
+
+ getResizeVals = (thisPt: { x: number; y: number }, dragHdl: string) => {
+ const [w, h] = [this._initDimensions.width, this._initDimensions.height];
+ const [moveX, moveY] = [thisPt.x - this._snapPt!.x, thisPt.y - this._snapPt!.y];
+ let vals: { scale: { x: number; y: number }; refPt: [number, number]; transl: { x: number; y: number } };
+ switch (dragHdl) {
+ case 'topLeft': vals = { scale: { x: 1 - moveX / w, y: 1 -moveY / h }, refPt: [this.bounds.r, this.bounds.b], transl: {x: moveX, y: moveY } }; break;
+ case 'topRight': vals = { scale: { x: 1 + moveX / w, y: 1 -moveY / h }, refPt: [this.bounds.l, this.bounds.b], transl: {x: 0, y: moveY } }; break;
+ case 'top': vals = { scale: { x: 1, y: 1 -moveY / h }, refPt: [this.bounds.l, this.bounds.b], transl: {x: 0, y: moveY } }; break;
+ case 'left': vals = { scale: { x: 1 - moveX / w, y: 1 }, refPt: [this.bounds.r, this.bounds.t], transl: {x: moveX, y: 0 } }; break;
+ case 'bottomLeft': vals = { scale: { x: 1 - moveX / w, y: 1 + moveY / h }, refPt: [this.bounds.r, this.bounds.t], transl: {x: moveX, y: 0 } }; break;
+ case 'right': vals = { scale: { x: 1 + moveX / w, y: 1 }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break;
+ case 'bottomRight':vals = { scale: { x: 1 + moveX / w, y: 1 + moveY / h }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break;
+ case 'bottom': vals = { scale: { x: 1, y: 1 + moveY / h }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break;
+ default: vals = { scale: { x: 1, y: 1 }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break;
+ } // prettier-ignore
+ return vals;
+ };
+
+ resizeView = (refPt: number[], scale: { x: number; y: number }, translation: { x: number; y: number }) => {
+ if (this._initDimensions.x === undefined) this._initDimensions.x = this._pageX;
+ if (this._initDimensions.y === undefined) this._initDimensions.y = this._pageY;
+ const { height, width, x, y } = this._initDimensions;
+
+ this._menuDimensions.width = Math.max(300, scale.x * width);
+ this._menuDimensions.height = Math.max(200, scale.y * height);
+ this._pageX = x + translation.x;
+ this._pageY = y + translation.y;
+ };
+
+ async getIcon(doc: Doc) {
+ const docView = DocumentView.getDocumentView(doc);
+ if (docView) {
+ docView.ComponentView?.updateIcon?.();
+ return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 500));
+ }
+ return undefined;
+ }
+
+ @action updateSelectedTemplate = async (template: Template) => {
+ if (this._selectedTemplate === template) {
+ this._selectedTemplate = undefined;
+ return;
+ } else {
+ this._selectedTemplate = template;
+ template.renderUpdates();
+ this._fullyRenderedDocs = await this.createDocsFromTemplate(template) ?? [];
+ this.updateRenderedDocCollection();
+ }
+ };
+
+ @action updateSelectedSavedLayout = (layout: DataVizTemplateLayout) => {
+ this._layout.xMargin = layout.layout.xMargin;
+ this._layout.yMargin = layout.layout.yMargin;
+ this._layout.type = layout.layout.type;
+ this._layout.columns = layout.columns;
+ };
+
+ isSelectedLayout = (layout: DataVizTemplateLayout) => {
+ return this._layout.xMargin === layout.layout.xMargin && this._layout.yMargin === layout.layout.yMargin && this._layout.type === layout.layout.type && this._layout.columns === layout.columns;
+ };
+
+ editTemplate = (doc: Doc) => {
+ DocumentViewInternal.addDocTabFunc(doc, OpenWhere.addRight);
+ DocumentView.DeselectAll();
+ Doc.UnBrushDoc(doc);
+ };
+
+ @action addField = () => {
+ const newFields: Col[] = this._userCreatedFields.concat([{ title: '', type: TemplateFieldType.UNSET, desc: '', sizes: [] }]);
+ this._userCreatedFields = newFields;
+ };
+
+ @action removeField = (field: { title: string; type: string; desc: string }) => {
+ if (this._dataViz?.axes.includes(field.title)) {
+ this._dataViz.selectAxes(this._dataViz.axes.filter(col => col !== field.title));
+ } else {
+ const toRemove = this._userCreatedFields.filter(f => f === field);
+ if (!toRemove) return;
+
+ if (toRemove.length > 1) {
+ while (toRemove.length > 1) {
+ toRemove.pop();
+ }
+ }
+
+ if (this._userCreatedFields.length === 1) {
+ this._userCreatedFields = [];
+ } else {
+ this._userCreatedFields.splice(this._userCreatedFields.indexOf(toRemove[0]), 1);
+ }
+ }
+ };
+
+ @action setColTitle = (column: Col, title: string) => {
+ if (this.selectedFields.includes(column.title)) {
+ this._dataViz?.setColumnTitle(column.title, title);
+ } else {
+ column.title = title;
+ }
+ this.forceUpdate();
+ };
+
+ @action setColType = (column: Col, type: TemplateFieldType) => {
+ if (this.selectedFields.includes(column.title)) {
+ this._dataViz?.setColumnType(column.title, type);
+ } else {
+ column.type = type;
+ }
+ this.forceUpdate();
+ };
+
+ modifyColSizes = (column: Col, size: TemplateFieldSize, valid: boolean) => {
+ if (this.selectedFields.includes(column.title)) {
+ this._dataViz?.modifyColumnSizes(column.title, size, valid);
+ } else {
+ if (!valid && column.sizes.includes(size)) {
+ column.sizes.splice(column.sizes.indexOf(size), 1);
+ } else if (valid && !column.sizes.includes(size)) {
+ column.sizes.push(size);
+ }
+ }
+ this.forceUpdate();
+ };
+
+ setColDesc = (column: Col, desc: string) => {
+ if (this.selectedFields.includes(column.title)) {
+ this._dataViz?.setColumnDesc(column.title, desc);
+ } else {
+ column.desc = desc;
+ }
+ this.forceUpdate();
+ };
+
+ generateGPTImage = async (prompt: string): Promise<string | undefined> => {
+ try {
+ const res = await gptImageCall(prompt);
+
+ if (res) {
+ const result = (await Networking.PostToServer('/uploadRemoteImage', { sources: res })) as Upload.FileInformation[];
+ const source = ClientUtils.prepend(result[0].accessPaths.agnostic.client);
+ return source;
+ }
+ } catch (e) {
+ console.log(e);
+ }
+ };
+
+ /**
+ * Populates a preset template framework with content from a datavizbox or any AI-generated content.
+ * @param template the preloaded template framework being filled in
+ * @param assignments a list of template field numbers (from top to bottom) and their assigned columns from the linked dataviz
+ * @returns a doc containing the fully rendered template
+ */
+ applyGPTContentToTemplate = async (template: Template, assignments: { [field: string]: Col }): Promise<Template | undefined> => {
+
+ const GPTTextCalls = Object.entries(assignments).filter(([str, col]) => col.type === TemplateFieldType.TEXT && this._userCreatedFields.includes(col));
+ const GPTIMGCalls = Object.entries(assignments).filter(([str, col]) => col.type === TemplateFieldType.VISUAL && this._userCreatedFields.includes(col));
+
+ if (GPTTextCalls.length) {
+ const promises = GPTTextCalls.map(([str, col]) => {
+ return this.renderGPTTextCall(template, col, Number(str));
+ });
+
+ await Promise.all(promises);
+ }
+
+ if (GPTIMGCalls.length) {
+ const promises = GPTIMGCalls.map(async ([fieldNum, col]) => {
+ return this.renderGPTImageCall(template, col, Number(fieldNum));
+ });
+
+ await Promise.all(promises);
+ };
+
+ return template;
+ };
+
+ compileFieldDescriptions = (templates: Template[]): string => {
+ let descriptions: string = '';
+ templates.forEach(template => {
+ descriptions += `---------- NEW TEMPLATE TO INCLUDE: The title is: ${template.mainField.getTitle()}. Its fields are: `;
+ descriptions += template.descriptionSummary;
+ });
+
+ return descriptions;
+ };
+
+ compileColDescriptions = (cols: Col[]): string => {
+ let descriptions: string = ' ------------- COL DESCRIPTIONS START HERE:';
+ cols.forEach(col => (descriptions += `{title: ${col.title}, sizes: ${String(col.sizes)}, type: ${col.type}, descreiption: ${col.desc}} `));
+
+ return descriptions;
+ };
+
+ getColByTitle = (title: string) => {
+ return this.fieldsInfos.filter(col => col.title === title)[0];
+ };
+
+ @action
+ assignColsToFields = async (templates: Template[], cols: Col[]): Promise<[Template, { [field: number]: Col }][]> => {
+ const fieldDescriptions: string = this.compileFieldDescriptions(templates);
+ const colDescriptions: string = this.compileColDescriptions(cols);
+
+ const inputText = fieldDescriptions.concat(colDescriptions);
+
+ ++this._callCount;
+ const origCount = this._callCount;
+
+ const prompt: string = `(${origCount}) ${inputText}`;
+
+ this._GPTLoading = true;
+
+ try {
+ const res = await gptAPICall(prompt, GPTCallType.TEMPLATE);
+
+ if (res) {
+ const assignments: { [templateTitle: string]: { [fieldID: string]: string } } = JSON.parse(res);
+ const brokenDownAssignments: [Template, { [fieldID: number]: Col }][] = [];
+
+ Object.entries(assignments).forEach(([tempTitle, assignment]) => {
+ const template = templates.filter(t => t.mainField.getTitle() === tempTitle)[0];
+ if (!template) return;
+ const toObj = Object.entries(assignment).reduce(
+ (a, [fieldID, colTitle]) => {
+ const col = this.getColByTitle(colTitle);
+ if (!this._userCreatedFields.includes(col)){ // do the following for any fields not added by the user; will change in the future, for now only GPT content works with user-added fields
+ const field = template.getFieldByID(Number(fieldID));
+ field.setContent(col.defaultContent ?? '', col.type === TemplateFieldType.VISUAL ? FieldContentType.IMAGE : FieldContentType.STRING);
+ field.setTitle(col.title);
+ } else {
+ a[Number(fieldID)] = this.getColByTitle(colTitle);
+ }
+ return a;
+ },
+ {} as { [field: number]: Col }
+ );
+ brokenDownAssignments.push([template, toObj]);
+ });
+
+ return brokenDownAssignments;
+ }
+ } catch (err) {
+ console.error(err);
+ }
+
+ return [];
+ };
+
+ generatePresetTemplates = async () => {
+ this._dataViz?.updateColDefaults();
+
+ const cols = this.fieldsInfos;
+ const templates = this.templateManager.getValidTemplates(cols);
+
+ const assignments: [Template, { [field: number]: Col }][] = await this.assignColsToFields(templates, cols);
+
+ const renderedTemplatePromises: Promise<Template | undefined>[] = assignments.map(([template, asns]) => this.applyGPTContentToTemplate(template, asns));
+
+ await Promise.all(renderedTemplatePromises);
+
+ setTimeout(() => {
+ this.setSuggestedTemplates(templates);
+ this._GPTLoading = false;
+ });
+ };
+
+ renderGPTImageCall = async (template: Template, col: Col, fieldNumber: number): Promise<boolean> => {
+ const generateAndLoadImage = async (fieldNum: string, column: Col, prompt: string) => {
+ const url = await this.generateGPTImage(prompt);
+ const field: Field = template.getFieldByID(Number(fieldNum));
+
+ field.setContent(url ?? '', FieldContentType.IMAGE);
+ field.setTitle(column.title);
+ };
+
+ const fieldContent: string = template.compiledContent;
+
+ try {
+ const sysPrompt =
+ 'Your job is to create a prompt for an AI image generator to help it generate an image based on existing content in a template and a user prompt. Your prompt should focus heavily on visual elements to help the image generator; avoid unecessary info that might distract it. ONLY INCLUDE THE PROMPT, NO OTHER TEXT OR EXPLANATION. The existing content is as follows: ' +
+ fieldContent +
+ ' **** The user prompt is: ' +
+ col.desc;
+
+ const prompt = await gptAPICall(sysPrompt, GPTCallType.COMPLETEPROMPT);
+
+ await generateAndLoadImage(String(fieldNumber), col, prompt);
+ } catch (e) {
+ console.log(e);
+ }
+ return true;
+ }
+
+ renderGPTTextCall = async (template: Template, col: Col, fieldNum: number): Promise<boolean> => {
+ const wordLimit = (size: TemplateFieldSize) => {
+ switch (size) {
+ case TemplateFieldSize.TINY:
+ return 2;
+ case TemplateFieldSize.SMALL:
+ return 5;
+ case TemplateFieldSize.MEDIUM:
+ return 20;
+ case TemplateFieldSize.LARGE:
+ return 50;
+ case TemplateFieldSize.HUGE:
+ return 100;
+ default:
+ return 10;
+ }
+ };
+
+ const textAssignment = `--- title: ${col.title}, prompt: ${col.desc}, word limit: ${wordLimit(col.sizes[0])} words, assigned field: ${fieldNum} ---`;
+
+ const fieldContent: string = template.compiledContent;
+
+ try {
+ const prompt = fieldContent + textAssignment;
+
+ const res = await gptAPICall(`${++this._callCount}: ${prompt}`, GPTCallType.FILL);
+
+ if (res) {
+ const assignments: { [title: string]: { number: string; content: string } } = JSON.parse(res);
+ Object.entries(assignments).forEach(([title, info]) => {
+ const field: Field = template.getFieldByID(Number(info.number));
+ const column = this.getColByTitle(title);
+
+ field.setContent(info.content ?? '', FieldContentType.STRING);
+ field.setTitle(column.title);
+ });
+ }
+ } catch (err) {
+ console.log(err);
+ }
+
+ return true;
+ }
+
+ createDocsFromTemplate = async (template: Template) => {
+ const dv = this._dataViz;
+
+ if (!dv) return;
+
+ this._docsRendering = true;
+
+ const fields: string[] = Array.from(Object.keys(dv.records[0]));
+ const selectedRows = NumListCast(dv.layoutDoc.dataViz_selectedRows);
+
+ const rowContents: { [title: string]: string }[] = selectedRows.map(row => {
+ const values: { [title: string]: string } = {};
+ fields.forEach(col => {
+ values[col] = dv.records[row][col];
+ });
+
+ return values;
+ });
+
+ const processContent = async (content: {[title: string]: string}) => {
+ const templateCopy = template.cloneBase();
+
+ fields.filter(title => title).forEach(title => {
+ const field = templateCopy.getFieldByTitle(title);
+ if (field === undefined) { return };
+ field.setContent(content[title]);
+ });
+
+ const gptPromises = this._userCreatedFields.filter(field => field.type === TemplateFieldType.TEXT).map(field => {
+ const title = field.title;
+ const templateField = templateCopy.getFieldByTitle(title);
+ if (templateField === undefined) { return };
+ const templatefieldID = templateField.getID;
+
+ return this.renderGPTTextCall(templateCopy, field, templatefieldID);
+ });
+
+ const imagePromises = this._userCreatedFields.filter(field => field.type === TemplateFieldType.VISUAL).map(field => {
+ const title = field.title;
+ const templateField = templateCopy.getFieldByTitle(title);
+ if (templateField === undefined) { return };
+ const templatefieldID = templateField.getID;
+
+ return this.renderGPTImageCall(templateCopy, field, templatefieldID);
+ });
+
+ await Promise.all(gptPromises);
+
+ await Promise.all(imagePromises);
+
+ return templateCopy.getRenderedDoc();
+ };
+
+ const promises = rowContents.map(content => processContent(content));
+
+ const renderedDocs = await Promise.all(promises);
+
+ this._docsRendering = false;
+
+ return renderedDocs;
+ }
+
+
+ addRenderedCollectionToMainview = () => {
+ const collection = this._renderedDocCollection;
+ if (!collection) return;
+ const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView;
+ collection.x = this._pageX - this._menuDimensions.width;
+ collection.y = this._pageY - this._menuDimensions.height;
+ mainCollection.addDocument(collection);
+ this.closeMenu();
+ }
+
+ @action setExpandedView = (template: Template | undefined) => {
+ if (template) {
+ this._currEditingTemplate = template;
+ this._expandedPreview = template.mainField.renderedDoc(); //Docs.Create.FreeformDocument([doc], { _height: NumListCast(doc._height)[0], _width: NumListCast(doc._width)[0], title: ''});
+ } else {
+ this._currEditingTemplate = undefined;
+ this._expandedPreview = undefined;
+ }
+ };
+
+ get editingWindow(){
+ const rendered = !this._expandedPreview ? null :
+ <div className="docCreatorMenu-expanded-template-preview">
+ <DocumentView
+ Document={this._expandedPreview}
+ isContentActive={emptyFunction}
+ addDocument={returnFalse}
+ moveDocument={returnFalse}
+ removeDocument={returnFalse}
+ PanelWidth={() => this._menuDimensions.width - 10}
+ PanelHeight={() => this._menuDimensions.height - 60}
+ ScreenToLocalTransform={() => new Transform(-this._pageX - 5,-this._pageY - 35, 1)}
+ renderDepth={5}
+ whenChildContentsActiveChanged={emptyFunction}
+ focus={emptyFunction}
+ styleProvider={DefaultStyleProvider}
+ addDocTab={DocumentViewInternal.addDocTabFunc}
+ pinToPres={() => undefined}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ fitContentsToBox={returnFalse}
+ fitWidth={returnFalse}
+ />
+ </div>
+
+ return (
+ <div className="docCreatorMenu-expanded-template-preview">
+ <div className="top-panel"/>
+ {rendered}
+ <div className="right-buttons-panel">
+ <button className="docCreatorMenu-menu-button section-reveal-options top-right" onPointerDown={e => this.setUpButtonClick(e, () => {
+ this._currEditingTemplate && this.updateTemplatePreview(this._currEditingTemplate);
+ this.setExpandedView(undefined)}
+ )}>
+ <FontAwesomeIcon icon="minimize" />
+ </button>
+ <button className="docCreatorMenu-menu-button section-reveal-options top-right-lower" onPointerDown={e => this.setUpButtonClick(e, () => {this._currEditingTemplate?.resetToBase(); this.setExpandedView(this._currEditingTemplate);})}>
+ <FontAwesomeIcon icon="arrows-rotate" color="white" />
+ </button>
+ </div>
+ </div>
+ );
+ }
+
+ get templatesPreviewContents() {
+
+ const GPTOptions = <div></div>;
+
+ return (
+ <div className={`docCreatorMenu-templates-view`}>
+ {this._expandedPreview ? (
+ this.editingWindow
+ ) : (
+ <div>
+ <div className="docCreatorMenu-section" style={{ height: this._GPTOpt ? 200 : 200 }}>
+ <div className="docCreatorMenu-section-topbar">
+ <div className="docCreatorMenu-section-title">Suggested Templates</div>
+ <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._menuContent = 'dashboard')))}>
+ <FontAwesomeIcon icon="gear" />
+ </button>
+ </div>
+ <div className="docCreatorMenu-templates-preview-window" style={{ justifyContent: this._GPTLoading || this._menuDimensions.width > 400 ? 'center' : '' }}>
+ {this._GPTLoading ? (
+ <div className="loading-spinner">
+ <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} />
+ </div>
+ ) : (
+ this._suggestedTemplatePreviews
+ .map(({doc, template}) => (
+ <div
+ className="docCreatorMenu-preview-window"
+ key='0'
+ style={{
+ border: this._selectedTemplate === template ? `solid 3px ${Colors.MEDIUM_BLUE}` : '',
+ boxShadow: this._selectedTemplate === template ? `0 0 15px rgba(68, 118, 247, .8)` : '',
+ }}
+ onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(template)))}>
+ <button
+ className="option-button left"
+ onPointerDown={e =>
+ this.setUpButtonClick(e, () => {
+ this.setExpandedView(template);
+ })
+ }>
+ <FontAwesomeIcon icon="magnifying-glass" color="white" />
+ </button>
+ <button className="option-button right" onPointerDown={e => this.setUpButtonClick(e, () => this.addUserTemplate(template))}>
+ <FontAwesomeIcon icon="plus" color="white" />
+ </button>
+ <DocumentView
+ Document={doc}
+ isContentActive={emptyFunction} // !!! should be return false
+ addDocument={returnFalse}
+ moveDocument={returnFalse}
+ removeDocument={returnFalse}
+ PanelWidth={() => this._selectedTemplate === template ? 104 : 111}
+ PanelHeight={() => this._selectedTemplate === template ? 104 : 111}
+ ScreenToLocalTransform={() => new Transform(-this._pageX - 5,-this._pageY - 35, 1)}
+ renderDepth={1}
+ whenChildContentsActiveChanged={emptyFunction}
+ focus={emptyFunction}
+ styleProvider={DefaultStyleProvider}
+ addDocTab={this._props.addDocTab}
+ pinToPres={() => undefined}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ fitContentsToBox={returnFalse}
+ fitWidth={returnFalse}
+ hideDecorations={true}
+ />
+ </div>
+ ))
+ )}
+ </div>
+ <div className="docCreatorMenu-GPT-options">
+ <div className="docCreatorMenu-GPT-options-container">
+ <button className="docCreatorMenu-menu-button" onPointerDown={e => this.setUpButtonClick(e, () => this.generatePresetTemplates())}>
+ <FontAwesomeIcon icon="arrows-rotate" />
+ </button>
+ </div>
+ {this._GPTOpt ? GPTOptions : null}
+ </div>
+ </div>
+ <hr className="docCreatorMenu-option-divider full no-margin" />
+ <div className="docCreatorMenu-section">
+ <div className="docCreatorMenu-section-topbar">
+ <div className="docCreatorMenu-section-title">Your Templates</div>
+ <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, () => (this._GPTOpt = !this._GPTOpt))}>
+ <FontAwesomeIcon icon="gear" />
+ </button>
+ </div>
+ <div className="docCreatorMenu-templates-preview-window" style={{ justifyContent: this._menuDimensions.width > 400 ? 'center' : '' }}>
+ <div className="docCreatorMenu-preview-window empty">
+ <FontAwesomeIcon icon="plus" color="rgb(160, 160, 160)" />
+ </div>
+ {this._userTemplates
+ .map(({template, doc}) => (
+ <div
+ className="docCreatorMenu-preview-window"
+ key='0'
+ style={{
+ border: this._selectedTemplate === template ? `solid 3px ${Colors.MEDIUM_BLUE}` : '',
+ boxShadow: this._selectedTemplate === template ? `0 0 15px rgba(68, 118, 247, .8)` : '',
+ }}
+ onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(template)))}>
+ <button
+ className="option-button left"
+ onPointerDown={e =>
+ this.setUpButtonClick(e, () => {
+ this.setExpandedView(template);
+ })
+ }>
+ <FontAwesomeIcon icon="magnifying-glass" color="white" />
+ </button>
+ <button className="option-button right" onPointerDown={e => this.setUpButtonClick(e, () => this.removeUserTemplate(template))}>
+ <FontAwesomeIcon icon="minus" color="white" />
+ </button>
+ <DocumentView
+ Document={doc}
+ isContentActive={emptyFunction} // !!! should be return false
+ addDocument={returnFalse}
+ moveDocument={returnFalse}
+ removeDocument={returnFalse}
+ PanelWidth={() => this._selectedTemplate === template ? 104 : 111}
+ PanelHeight={() => this._selectedTemplate === template ? 104 : 111}
+ ScreenToLocalTransform={() => new Transform(-this._pageX - 5, -this._pageY - 35, 1)}
+ renderDepth={1}
+ whenChildContentsActiveChanged={emptyFunction}
+ focus={emptyFunction}
+ styleProvider={DefaultStyleProvider}
+ addDocTab={this._props.addDocTab}
+ pinToPres={() => undefined}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ fitContentsToBox={returnFalse}
+ fitWidth={returnFalse}
+ hideDecorations={true}
+ />
+ </div>
+ ))}
+ </div>
+ </div>
+ </div>
+ )}
+ </div>
+ );
+ }
+
+
+ @action updateXMargin = (input: string) => {
+ this._layout.xMargin = Number(input);
+ setTimeout(() => {
+ if (!this._renderedDocCollection || !this._fullyRenderedDocs) return;
+ this.applyLayout(this._renderedDocCollection, this._fullyRenderedDocs);
+ });
+ };
+ @action updateYMargin = (input: string) => {
+ this._layout.yMargin = Number(input);
+ setTimeout(() => {
+ if (!this._renderedDocCollection || !this._fullyRenderedDocs) return;
+ this.applyLayout(this._renderedDocCollection, this._fullyRenderedDocs);
+ });
+ };
+ @action updateColumns = (input: string) => {
+ this._layout.columns = Number(input);
+ this.updateRenderedDocCollection();
+ };
+
+ get layoutConfigOptions() {
+ const optionInput = (icon: string, func: (input: string) => void, def?: number, key?: string, noMargin?: boolean) => {
+ return (
+ <div className="docCreatorMenu-option-container small no-margin" key={key} style={{ marginTop: noMargin ? '0px' : '' }}>
+ <div className="docCreatorMenu-option-title config layout-config">
+ <FontAwesomeIcon icon={icon as IconProp} />
+ </div>
+ <input defaultValue={def} onInput={e => func(e.currentTarget.value)} className="docCreatorMenu-input config layout-config" />
+ </div>
+ );
+ };
+
+ switch (this._layout.type) {
+ case LayoutType.FREEFORM:
+ return (
+ <div className="docCreatorMenu-configuration-bar">
+ {optionInput('arrows-up-down', this.updateYMargin, this._layout.xMargin, '2')}
+ {optionInput('arrows-left-right', this.updateXMargin, this._layout.xMargin, '3')}
+ {optionInput('table-columns', this.updateColumns, this._layout.columns, '4', true)}
+ </div>
+ );
+ default:
+ break;
+ }
+ }
+
+ screenToLocalTransform = () => this._props.ScreenToLocalTransform();
+
+ applyLayout = (collection: Doc, docs: Doc[]) => {
+ const { horizontalSpan, verticalSpan } = this.previewInfo;
+ collection._height = verticalSpan;
+ collection._width = horizontalSpan;
+
+ const layout = this._layout;
+ const columns: number = layout.columns ?? this.columnsCount;
+ const xGap: number = layout.xMargin;
+ const yGap: number = layout.yMargin;
+ // const repeat: number = templateInfo.layout.repeat;
+ const startX: number = -Number(collection._width)/2;
+ const startY: number = -Number(collection._height)/2;
+ const docHeight: number = Number(docs[0]._height);
+ const docWidth: number = Number(docs[0]._width);
+
+ if (columns === 0 || docs.length === 0){
+ return;
+ }
+
+ let i: number = 0;
+ let docsChanged: number = 0;
+ let curX: number = startX;
+ let curY: number = startY;
+
+ while (docsChanged < docs.length) {
+ while (i < columns && docsChanged < docs.length) {
+ docs[docsChanged].x = curX;
+ docs[docsChanged].y = curY;
+ curX += docWidth + xGap;
+ ++docsChanged;
+ ++i;
+ }
+ i = 0;
+ curX = startX;
+ curY += docHeight + yGap;
+ }
+ };
+
+ @computed
+ get previewInfo(){
+ const docHeight: number = Number(this._fullyRenderedDocs[0]._height);
+ const docWidth: number = Number(this._fullyRenderedDocs[0]._width);
+ const layout = this._layout;
+ return {
+ docHeight: docHeight,
+ docWidth: docWidth,
+ horizontalSpan: (docWidth + layout.xMargin) * (this.columnsCount) - layout.xMargin,
+ verticalSpan: (docHeight + layout.yMargin) * (this.rowsCount) - layout.yMargin,
+ }
+ }
+
+ /**
+ * Updates the preview that shows how all docs will be rendered in the chosen collection type.
+ @type the type of collection the docs should render to (ie. freeform, carousel, card)
+ */
+ updateRenderedDocCollection = () => {
+ if (!this._fullyRenderedDocs) return;
+
+ const { horizontalSpan, verticalSpan } = this.previewInfo;
+
+ const collectionFactory = (): (docs: Doc[], options: DocumentOptions) => Doc => {
+ switch (this._layout.type) {
+ case LayoutType.CAROUSEL3D:
+ return Docs.Create.Carousel3DDocument;
+ case LayoutType.FREEFORM:
+ return Docs.Create.FreeformDocument;
+ case LayoutType.CARD:
+ return Docs.Create.CardDeckDocument;
+ case LayoutType.MASONRY:
+ return Docs.Create.MasonryDocument;
+ case LayoutType.CAROUSEL:
+ return Docs.Create.CarouselDocument;
+ default:
+ return Docs.Create.FreeformDocument;
+ }
+ }
+
+ const collection: Doc = collectionFactory()(this._fullyRenderedDocs, {
+ isDefaultTemplateDoc: true,
+ _height: verticalSpan,
+ _width: horizontalSpan,
+ title: 'title',
+ backgroundColor: 'gray',
+ });
+
+ this.applyLayout(collection, this._fullyRenderedDocs);
+
+ this._renderedDocCollection = collection;
+ }
+
+ layoutPreviewContents = () => {
+
+ return this._docsRendering ? (
+ <div className="docCreatorMenu-layout-preview-window-wrapper loading">
+ <div className="loading-spinner">
+ <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} />
+ </div>
+ </div>
+ ) : !this._renderedDocCollection? null : (
+ <div className="docCreatorMenu-layout-preview-window-wrapper">
+ <DocumentView
+ Document={this._renderedDocCollection}
+ isContentActive={emptyFunction}
+ addDocument={returnFalse}
+ moveDocument={returnFalse}
+ removeDocument={returnFalse}
+ PanelWidth={() => this._menuDimensions.width - 80}
+ PanelHeight={() => this._menuDimensions.height - 105}
+ ScreenToLocalTransform={() => new Transform(-this._pageX - 5,-this._pageY - 35, 1)}
+ renderDepth={5}
+ whenChildContentsActiveChanged={emptyFunction}
+ focus={emptyFunction}
+ styleProvider={DefaultStyleProvider}
+ addDocTab={this._props.addDocTab}
+ pinToPres={() => undefined}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ fitContentsToBox={returnFalse}
+ fitWidth={returnFalse}
+ hideDecorations={true}
+ />
+ </div>
+ )
+ };
+
+ get optionsMenuContents() {
+ const layoutOption = (option: LayoutType, optStyle?: object, specialFunc?: () => void) => {
+ return (
+ <div
+ className="docCreatorMenu-dropdown-option"
+ style={optStyle}
+ onPointerDown={e =>
+ this.setUpButtonClick(e, () => {
+ specialFunc?.();
+ runInAction(() => {
+ this._layout.type = option;
+ this.updateRenderedDocCollection();
+ });
+ })
+ }>
+ {option}
+ </div>
+ );
+ };
+
+ const selectionBox = (width: number, height: number, icon: string, specClass?: string, options?: JSX.Element[], manual?: boolean): JSX.Element => {
+ return (
+ <div className="docCreatorMenu-option-container">
+ <div className={`docCreatorMenu-option-title config ${specClass}`} style={{ width: width * 0.4, height: height }}>
+ <FontAwesomeIcon icon={icon as IconProp} />
+ </div>
+ {manual ? (
+ <input className={`docCreatorMenu-input config ${specClass}`} style={{ width: width * 0.6, height: height }} />
+ ) : (
+ <select className={`docCreatorMenu-input config ${specClass}`} style={{ width: width * 0.6, height: height }}>
+ {options}
+ </select>
+ )}
+ </div>
+ );
+ };
+
+ const repeatOptions = [0, 1, 2, 3, 4, 5];
+
+ return (
+ <div className="docCreatorMenu-menu-container">
+ <div className="docCreatorMenu-option-container layout">
+ <div className="docCreatorMenu-dropdown-hoverable">
+ <div className="docCreatorMenu-option-title">{this._layout.type ? this._layout.type.toUpperCase() : 'Choose Layout'}</div>
+ <div className="docCreatorMenu-dropdown-content">
+ {layoutOption(LayoutType.FREEFORM, undefined, () => {
+ if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length));
+ })}
+ {layoutOption(LayoutType.CAROUSEL)}
+ {layoutOption(LayoutType.CAROUSEL3D)}
+ {layoutOption(LayoutType.MASONRY)}
+ </div>
+ </div>
+ </div>
+ {this._layout.type ? this.layoutConfigOptions : null}
+ {this.layoutPreviewContents()}
+ {selectionBox(
+ 60,
+ 20,
+ 'repeat',
+ undefined,
+ repeatOptions.map(num => <option key={num} onPointerDown={() => (this._layout.repeat = num)}>{`${num}x`}</option>)
+ )}
+ <hr className="docCreatorMenu-option-divider" />
+ <div className="docCreatorMenu-general-options-container">
+ <button
+ className="docCreatorMenu-save-layout-button"
+ onPointerDown={e =>
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ undoable(clickEv => {
+ clickEv.stopPropagation();
+ if (!this._selectedTemplate) return;
+ const layout: DataVizTemplateLayout = {
+ template: this._selectedTemplate.getRenderedDoc(),
+ layout: { type: this._layout.type, xMargin: this._layout.xMargin, yMargin: this._layout.yMargin, repeat: 0 },
+ columns: this.columnsCount,
+ rows: this.rowsCount,
+ docsNumList: this.docsToRender,
+ };
+ if (!this._savedLayouts.includes(layout)) {
+ this._savedLayouts.push(layout);
+ }
+ }, 'make docs')
+ )
+ }>
+ <FontAwesomeIcon icon="floppy-disk" />
+ </button>
+ <button
+ className="docCreatorMenu-create-docs-button"
+ style={{ backgroundColor: this.canMakeDocs ? '' : 'rgb(155, 155, 155)', border: this.canMakeDocs ? '' : 'solid 2px rgb(180, 180, 180)' }}
+ onPointerDown={e =>
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ undoable(clickEv => {
+ clickEv.stopPropagation();
+ if (!this._selectedTemplate) return;
+ this.addRenderedCollectionToMainview();
+ }, 'make docs')
+ )
+ }>
+ <FontAwesomeIcon icon="plus" />
+ </button>
+ </div>
+ </div>
+ );
+ }
+
+ get dashboardContents() {
+ const sizes: string[] = ['tiny', 'small', 'medium', 'large', 'huge'];
+
+ const fieldPanel = (field: Col, id: number) => {
+ return (
+ <div className="field-panel" key={id}>
+ <div className="top-bar">
+ <span className="field-title">{`${field.title} Field`}</span>
+ <button className="docCreatorMenu-menu-button section-reveal-options no-margin" onPointerDown={e => this.setUpButtonClick(e, () => this.removeField(field))} style={{ position: 'absolute', right: '0px' }}>
+ <FontAwesomeIcon icon="minus" />
+ </button>
+ </div>
+ <div className="opts-bar">
+ <div className="opt-box">
+ <div className="top-bar"> Title </div>
+ <textarea className="content" style={{ width: '100%', height: 'calc(100% - 20px)' }} value={field.title} placeholder={'Enter title'} onChange={e => this.setColTitle(field, e.target.value)} />
+ </div>
+ <div className="opt-box">
+ <div className="top-bar"> Type </div>
+ <div className="content">
+ <span className="type-display">{field.type === TemplateFieldType.TEXT ? 'Text Field' : field.type === TemplateFieldType.VISUAL ? 'File Field' : ''}</span>
+ <div className="bubbles">
+ <input
+ className="bubble"
+ type="radio"
+ name="type"
+ onClick={() => {
+ this.setColType(field, TemplateFieldType.TEXT);
+ }}
+ />
+ <div className="text">Text</div>
+ <input
+ className="bubble"
+ type="radio"
+ name="type"
+ onClick={() => {
+ this.setColType(field, TemplateFieldType.VISUAL);
+ }}
+ />
+ <div className="text">File</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className="sizes-box">
+ <div className="top-bar"> Valid Sizes </div>
+ <div className="content">
+ <div className="bubbles">
+ {sizes.map(size => (
+ <>
+ <input
+ className="bubble"
+ type="checkbox"
+ name="type"
+ checked={field.sizes.includes(size as TemplateFieldSize)}
+ onChange={e => {
+ this.modifyColSizes(field, size as TemplateFieldSize, e.target.checked);
+ }}
+ />
+ <div className="text">{size}</div>
+ </>
+ ))}
+ </div>
+ </div>
+ </div>
+ <div className="desc-box">
+ <div className="top-bar"> Prompt </div>
+ <textarea
+ className="content"
+ onChange={e => this.setColDesc(field, e.target.value)}
+ defaultValue={field.desc === this._dataViz?.GPTSummary?.get(field.title)?.desc ? '' : field.desc}
+ placeholder={this._dataViz?.GPTSummary?.get(field.title)?.desc ?? 'Add a description/prompt to help with template generation.'}
+ />
+ </div>
+ </div>
+ );
+ };
+
+ return (
+ <div className="docCreatorMenu-dashboard-view">
+ <div className="topbar">
+ <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, this.addField)}>
+ <FontAwesomeIcon icon="plus" />
+ </button>
+ <button className="docCreatorMenu-menu-button section-reveal-options float-right" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._menuContent = 'templates')))}>
+ <FontAwesomeIcon icon="arrow-left" />
+ </button>
+ </div>
+ <div className="panels-container">{this.fieldsInfos.map((field, i) => fieldPanel(field, i))}</div>
+ </div>
+ );
+ }
+
+ get renderSelectedViewType() {
+ switch (this._menuContent) {
+ case 'templates':
+ return this.templatesPreviewContents;
+ case 'options':
+ return this.optionsMenuContents;
+ case 'dashboard':
+ return this.dashboardContents;
+ default:
+ return undefined;
+ }
+ }
+
+ get resizePanes() {
+ const ref = this._ref?.getBoundingClientRect();
+ const height: number = ref?.height ?? 0;
+ const width: number = ref?.width ?? 0;
+
+ return [
+ <div className='docCreatorMenu-resizer top' key='0' onPointerDown={this.onResizePointerDown} style={{width: width, left: 0, top: -7}}/>,
+ <div className='docCreatorMenu-resizer left' key='1' onPointerDown={this.onResizePointerDown} style={{height: height, left: -7, top: 0}}/>,
+ <div className='docCreatorMenu-resizer right' key='2' onPointerDown={this.onResizePointerDown} style={{height: height, left: width - 3, top: 0}}/>,
+ <div className='docCreatorMenu-resizer bottom' key='3' onPointerDown={this.onResizePointerDown} style={{width: width, left: 0, top: height - 3}}/>,
+ <div className='docCreatorMenu-resizer topLeft' key='4' onPointerDown={this.onResizePointerDown} style={{left: -10, top: -10, cursor: 'nwse-resize'}}/>,
+ <div className='docCreatorMenu-resizer topRight' key='5' onPointerDown={this.onResizePointerDown} style={{left: width - 5, top: -10, cursor: 'nesw-resize'}}/>,
+ <div className='docCreatorMenu-resizer bottomLeft' key='6' onPointerDown={this.onResizePointerDown} style={{left: -10, top: height - 5, cursor: 'nesw-resize'}}/>,
+ <div className='docCreatorMenu-resizer bottomRight' key='7' onPointerDown={this.onResizePointerDown} style={{left: width - 5, top: height - 5, cursor: 'nwse-resize'}}/>,
+ ]; //prettier-ignore
+ }
+
+ render() {
+ const topButton = (icon: string, opt: string, func: () => void, tag: string) => {
+ return (
+ <div className={`top-button-container ${tag} ${opt === this._menuContent ? 'selected' : ''}`}>
+ <div
+ className="top-button-content"
+ onPointerDown={e =>
+ this.setUpButtonClick(e, () =>
+ runInAction(() => {
+ func();
+ })
+ )
+ }>
+ <FontAwesomeIcon icon={icon as IconProp} />
+ </div>
+ </div>
+ );
+ };
+
+ const onPreviewSelected = () => {
+ this._menuContent = 'templates';
+ };
+ const onSavedSelected = () => {
+ this._menuContent = 'dashboard';
+ };
+ const onOptionsSelected = () => {
+ this._menuContent = 'options';
+ if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length));
+ };
+
+ return (
+ <div className="docCreatorMenu">
+ {!this._shouldDisplay ? undefined : (
+ <div
+ className="docCreatorMenu-cont"
+ ref={r => (this._ref = r)}
+ style={{
+ display: '',
+ left: this._pageX,
+ top: this._pageY,
+ width: this._menuDimensions.width,
+ height: this._menuDimensions.height,
+ background: SnappingManager.userBackgroundColor,
+ color: SnappingManager.userColor,
+ }}>
+ {this.resizePanes}
+ <div
+ className="docCreatorMenu-menu"
+ onPointerDown={e =>
+ setupMoveUpEvents(
+ this,
+ e,
+ event => {
+ this._dragging = true;
+ this._startPos = { x: 0, y: 0 };
+ this._startPos.x = event.pageX - (this._ref?.getBoundingClientRect().left ?? 0);
+ this._startPos.y = event.pageY - (this._ref?.getBoundingClientRect().top ?? 0);
+ document.addEventListener('pointermove', this.onDrag);
+ return true;
+ },
+ emptyFunction,
+ undoable(clickEv => {
+ clickEv.stopPropagation();
+ }, 'drag menu')
+ )
+ }>
+ <div className="docCreatorMenu-top-buttons-container">
+ {topButton('lightbulb', 'templates', onPreviewSelected, 'left')}
+ {topButton('magnifying-glass', 'options', onOptionsSelected, 'middle')}
+ {topButton('bars', 'saved', onSavedSelected, 'right')}
+ </div>
+ <button className="docCreatorMenu-menu-button close-menu" onPointerDown={e => this.setUpButtonClick(e, this.closeMenu)}>
+ <FontAwesomeIcon icon={'minus'} />
+ </button>
+ </div>
+ {this.renderSelectedViewType}
+ </div>
+ )}
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx
new file mode 100644
index 000000000..c5254c17d
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx
@@ -0,0 +1,117 @@
+import { Doc } from "../../../../../../fields/Doc";
+import { Docs } from "../../../../../documents/Documents";
+import { Field, FieldDimensions, FieldSettings, ViewType } from "./Field";
+import { FieldUtils } from "./FieldUtils";
+import { StaticField } from "./StaticField";
+
+export class DynamicField implements Field {
+ private subfields: Field[] = [];
+
+ private id: number;
+ private settings: FieldSettings;
+ private title: string = '';
+
+ private parent: Field;
+ private dimensions: FieldDimensions;
+
+ constructor(settings: FieldSettings, id: number, parent?: Field) {
+ this.id = id;
+ this.settings = settings;
+ if (settings.title) { this.title = settings.title };
+ if (!parent) {
+ this.parent = this;
+ this.dimensions = {width: this.settings.br[0] - this.settings.tl[0], height: this.settings.br[1] - this.settings.tl[1], coord: {x: this.settings.tl[0], y: this.settings.tl[1]}};
+ } else {
+ this.parent = parent;
+ this.dimensions = FieldUtils.getLocalDimensions({tl: settings.tl, br: settings.br}, this.parent.getDimensions);
+ }
+ this.subfields = this.setupSubfields();
+ }
+
+ setContent = () => { return };
+ getContent = () => { return '' };
+
+ setTitle = (title: string) => { this.title = title };
+ getTitle = () => { return this.title };
+
+ get getSubfields() { return this.subfields };
+ get getAllSubfields() {
+ let fields: Field[] = [];
+ this.subfields?.forEach(field => {
+ fields.push(field);
+ fields = fields.concat(field.getAllSubfields)
+ });
+ return fields;
+ };
+
+ get getDimensions() { return this.dimensions };
+ get getID() { return this.id };
+ get getViewType() { return this.settings.viewType };
+
+ get getDescription(): string {
+ return this.settings.description ?? '';
+ }
+
+ matches = (): Array<number> => {
+ return [];
+ }
+
+ updateRenderedDoc = () => {
+ return new Doc();
+ }
+
+ setupSubfields = (): Field[] => {
+ const fields: Field[] = [];
+ this.settings.subfields?.forEach((fieldSettings, index) => {
+ let field: Field;
+ const type = fieldSettings.viewType;
+
+ const id = Number(String(this.id) + String(index));
+
+ if (type == ViewType.CAROUSEL3D || type === ViewType.FREEFORM) {
+ field = new DynamicField(fieldSettings, id, this);
+ } else {
+ field = new StaticField(fieldSettings, this, id);
+ }
+ fields.push(field);
+ });
+ return fields;
+ }
+
+ applyAttributes = (field: Field) => {
+ field.setTitle(this.title);
+ field.updateRenderedDoc(this.renderedDoc());
+ }
+
+ getChildDimensions = (coords: { tl: [number, number]; br: [number, number] }): FieldDimensions => {
+ const l = (coords.tl[0] * this.dimensions.height) / 2;
+ const t = coords.tl[1] * this.dimensions.width / 2; //prettier-ignore
+ const r = (coords.br[0] * this.dimensions.height) / 2;
+ const b = coords.br[1] * this.dimensions.width / 2; //prettier-ignore
+ const width = r - l;
+ const height = b - t;
+ const coord = { x: l, y: t };
+ return { width, height, coord };
+ };
+
+ renderedDoc = (): Doc => {
+ let doc: Doc;
+ switch (this.settings.viewType) {
+ case ViewType.CAROUSEL3D:
+ doc = Docs.Create.Carousel3DDocument(this.subfields.map(field => field.renderedDoc()), {
+ title: this.title,
+ });
+ FieldUtils.applyBasicOpts(doc, this.dimensions, this.settings);
+ return doc;
+ case ViewType.FREEFORM:
+ doc = Docs.Create.FreeformDocument(this.subfields.map(field => field.renderedDoc()), {
+ title: this.title,
+ });
+ FieldUtils.applyBasicOpts(doc, this.dimensions, this.settings);
+ return doc;
+ default:
+ return new Doc();
+ }
+ }
+
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/Field.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/Field.tsx
new file mode 100644
index 000000000..ea9b566b3
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/Field.tsx
@@ -0,0 +1,66 @@
+import { Doc } from "../../../../../../fields/Doc";
+import { Col } from "../DocCreatorMenu";
+import { TemplateFieldSize, TemplateFieldType } from "../TemplateBackend";
+
+export enum FieldContentType {
+ STRING = 'string',
+ IMAGE = 'image',
+}
+
+export enum ViewType {
+ CAROUSEL3D = 'carousel3d',
+ FREEFORM = 'freeform',
+ STATIC = 'static',
+ DEC = 'decoration'
+}
+
+export type FieldDimensions = {
+ width: number;
+ height: number;
+ coord: {x: number, y: number};
+}
+
+export interface FieldOpts {
+ backgroundColor?: string;
+ color?: string;
+ cornerRounding?: number;
+ borderWidth?: string;
+ borderColor?: string;
+ contentXCentering?: 'h-left' | 'h-center' | 'h-right';
+ contentYCentering?: 'top' | 'center' | 'bottom';
+ opacity?: number;
+ rotation?: number;
+ fontBold?: boolean;
+ fontTransform?: 'uppercase' | 'lowercase';
+ fieldViewType?: 'freeform' | 'stacked';
+}
+
+export type FieldSettings = {
+ tl: [number, number];
+ br: [number, number];
+ opts: FieldOpts;
+ viewType: ViewType;
+ title?: string;
+ subfields?: FieldSettings[];
+ types?: TemplateFieldType[];
+ sizes?: TemplateFieldSize[];
+ description?: string;
+};
+
+export interface Field {
+ getContent: () => string;
+ setContent: (content: string, type?: FieldContentType) => void;
+ getDimensions: FieldDimensions;
+ getSubfields: Field[];
+ getAllSubfields: Field[];
+ getID: number;
+ getViewType: ViewType;
+ getDescription: string;
+ getTitle: () => string;
+ setTitle: (title: string) => void;
+ setupSubfields: () => Field[];
+ applyAttributes: (field: Field) => void;
+ renderedDoc: () => Doc;
+ matches: (cols: Col[]) => number[];
+ updateRenderedDoc: (oldDoc?: Doc) => Doc;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/FieldUtils.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/FieldUtils.tsx
new file mode 100644
index 000000000..3886774d2
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/FieldUtils.tsx
@@ -0,0 +1,79 @@
+import { Doc } from "../../../../../../fields/Doc";
+import { ComputedField, ScriptField } from "../../../../../../fields/ScriptField";
+import { Col } from "../DocCreatorMenu";
+import { TemplateFieldSize, TemplateFieldType, TemplateLayouts } from "../TemplateBackend";
+import { FieldDimensions, FieldSettings } from "./Field";
+
+export class FieldUtils {
+ public static getLocalDimensions = (coords: { tl: [number, number]; br: [number, number] }, parentDimensions: FieldDimensions): FieldDimensions => {
+ const l = (coords.tl[0] * parentDimensions.width) / 2;
+ const t = coords.tl[1] * parentDimensions.height / 2; //prettier-ignore
+ const r = (coords.br[0] * parentDimensions.width) / 2;
+ const b = coords.br[1] * parentDimensions.height / 2; //prettier-ignore
+ const width = r - l;
+ const height = b - t;
+ const coord = { x: l, y: t };
+ return { width, height, coord };
+ };
+
+ public static applyBasicOpts = (doc: Doc, parentDimensions: FieldDimensions, settings: FieldSettings, oldDoc?: Doc) => {
+ const opts = settings.opts;
+ doc.isDefaultTemplateDoc = oldDoc ? oldDoc.isDefaultTemplateDoc : true;
+ doc._layout_hideScroll = oldDoc ? oldDoc._layout_hideScroll : true;
+ doc.x = oldDoc ? oldDoc.x : parentDimensions.coord.x;
+ doc.y = oldDoc ? oldDoc.y : parentDimensions.coord.y;
+ doc._height = oldDoc ? oldDoc.height : parentDimensions.height;
+ doc._width = oldDoc ? oldDoc.width : parentDimensions.width;
+ doc.backgroundColor = oldDoc ? oldDoc.backgroundColor : opts.backgroundColor ?? '';
+ doc._layout_borderRounding = !opts.cornerRounding ? '0px' : ScriptField.MakeFunction(`${opts.cornerRounding} * this.width + 'px'`);
+ doc.borderColor = oldDoc ? oldDoc.borderColor : opts.borderColor;
+ doc.borderWidth = oldDoc ? oldDoc.borderWidth : opts.borderWidth;
+ doc.opacity = oldDoc ? oldDoc.opacity : opts.opacity;
+ doc._rotation = oldDoc ? oldDoc._rotation : opts.rotation;
+ doc.hCentering = oldDoc ? oldDoc.hCentering : opts.contentXCentering;
+ doc.nativeWidth = parentDimensions.width;
+ doc.nativeHeight = parentDimensions.height;
+ doc._layout_nativeDimEditable = true;
+ };
+
+ public static calculateFontSize = (contWidth: number, contHeight: number, text: string, uppercase: boolean): number => {
+ const words: string[] = text.split(/\s+/).filter(Boolean);
+
+ let currFontSize = 1;
+ let rowsCount = 1;
+ let currTextHeight = currFontSize * rowsCount * 2;
+
+ while (currTextHeight <= contHeight) {
+ let wordIndex = 0;
+ let currentRowWidth = 0;
+ let wordsInCurrRow = 0;
+ rowsCount = 1;
+
+ while (wordIndex < words.length) {
+ const word = words[wordIndex];
+ const wordWidth = word.length * currFontSize * 0.7;
+
+ if (currentRowWidth + wordWidth <= contWidth) {
+ currentRowWidth += wordWidth;
+ ++wordsInCurrRow;
+ } else {
+ if (words.length !== 1 && words.length > wordsInCurrRow) {
+ rowsCount++;
+ currentRowWidth = wordWidth;
+ wordsInCurrRow = 1;
+ } else {
+ break;
+ }
+ }
+
+ wordIndex++;
+ }
+
+ currTextHeight = rowsCount * currFontSize * 2;
+
+ currFontSize += 1;
+ }
+
+ return currFontSize - 1;
+ };
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/StaticField.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/StaticField.tsx
new file mode 100644
index 000000000..47b43f051
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/StaticField.tsx
@@ -0,0 +1,147 @@
+import { Doc } from "../../../../../../fields/Doc";
+import { Docs } from "../../../../../documents/Documents";
+import { Col } from "../DocCreatorMenu";
+import { DynamicField } from "./DynamicField";
+import { FieldUtils } from "./FieldUtils";
+import { Field, FieldContentType, FieldDimensions, FieldSettings, ViewType } from "./Field";
+
+export class StaticField {
+ private content: string;
+ private contentType: FieldContentType | undefined;
+ private subfields: Field[] = [];
+ private renderedDocument: Doc;
+
+ private id: number;
+ private title: string = '';
+
+ private settings: FieldSettings;
+
+ private parent: Field;
+ private dimensions: FieldDimensions;
+
+ constructor(settings: FieldSettings, parent: Field, id: number) {
+ this.settings = settings;
+ if (settings.title) { this.title = settings.title };
+ this.id = id;
+ this.parent = parent;
+ this.dimensions = FieldUtils.getLocalDimensions({tl: settings.tl, br: settings.br}, this.parent.getDimensions);
+ this.content = '';
+ this.subfields = this.setupSubfields();
+ this.renderedDocument = this.updateRenderedDoc();
+ };
+
+ get getSubfields(): Field[] { return this.subfields ?? []; };
+
+ get getAllSubfields(): Field[] {
+ let fields: Field[] = [];
+ this.subfields?.forEach(field => {
+ fields.push(field);
+ fields = fields.concat(field.getAllSubfields);
+ });
+ return fields;
+ };
+
+ get getDimensions() { return this.dimensions };
+ get getID() { return this.id };
+ get getViewType() { return this.settings.viewType };
+
+ get getDescription(): string {
+ return this.settings.description ?? '';
+ }
+
+ renderedDoc = () => {
+ return this.renderedDocument;
+ }
+
+ setContent = (newContent: string, type?: FieldContentType) => {
+ this.content = newContent;
+ if (type) this.contentType = type;
+ this.updateRenderedDoc(this.renderedDocument);
+ };
+ getContent() { return this.content };
+
+ setTitle = (title: string) => {
+ this.title = title;
+ this.renderedDocument.title = title;
+ this.updateRenderedDoc(this.renderedDocument);
+ };
+ getTitle = () => { return this.title };
+
+ applyAttributes = (field: Field) => { //!!! can be updated later for more robust clonign; this is all ythat's needed now
+ field.setTitle(this.title);
+ field.setContent('', this.contentType);
+ field.updateRenderedDoc(this.renderedDoc());
+ }
+
+ setupSubfields = (): Field[] => {
+ const fields: Field[] = [];
+ this.settings.subfields?.forEach((fieldSettings, index) => {
+ let field: Field;
+ const type = fieldSettings.viewType;
+
+ const id = Number(String(this.id) + String(index));
+
+ if (type === ViewType.FREEFORM || type === ViewType.CAROUSEL3D) {
+ field = new DynamicField(fieldSettings, id, this);
+ } else {
+ field = new StaticField(fieldSettings, this, id);
+ };
+
+ fields.push(field);
+ });
+ return fields;
+ };
+
+ matches = (cols: Col[]): number[] => {
+ const colMatchesField = (col: Col) => {
+ const isMatch: boolean = (
+ this.settings.sizes?.some(size => col.sizes?.includes(size))
+ && this.settings.types?.includes(col.type))
+ ?? false;
+ return isMatch;
+ }
+
+ const matches: Array<number> = [];
+
+ cols.forEach((col, v) => {
+ if (colMatchesField(col)) {
+ matches.push(v);
+ }
+ });
+
+ return matches;
+ };
+
+ updateRenderedDoc = (oldDoc?: Doc): Doc => {
+ const opts = this.settings.opts;
+
+ if (!this.contentType) { this.contentType = FieldContentType.STRING };
+
+ let doc: Doc;
+
+ switch (this.contentType) {
+ case FieldContentType.STRING:
+ doc = Docs.Create.TextDocument(String(this.content), {
+ title: this.title,
+ text_fontColor: oldDoc ? String(oldDoc.color) : opts.color,
+ contentBold: oldDoc ? Boolean(oldDoc.fontBold) : opts.fontBold,
+ textTransform: oldDoc ? String(oldDoc.fontTransform) : opts.fontTransform,
+ color: oldDoc ? String(oldDoc.color) : opts.color,
+ _text_fontSize: `${FieldUtils.calculateFontSize(this.dimensions.width, this.dimensions.height, String(this.content), true)}`
+ });
+ FieldUtils.applyBasicOpts(doc, this.dimensions, this.settings, oldDoc);
+ break;
+ case FieldContentType.IMAGE:
+ doc = Docs.Create.ImageDocument(String(this.content), {
+ title: this.title,
+ _layout_fitWidth: false,
+ });
+ FieldUtils.applyBasicOpts(doc, this.dimensions, this.settings, oldDoc);
+ break;
+ }
+
+ this.renderedDocument = doc;
+
+ return doc;
+ };
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx
new file mode 100644
index 000000000..0a5097d4a
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx
@@ -0,0 +1,139 @@
+import { Doc, FieldType } from "../../../../../fields/Doc";
+import { Col } from "./DocCreatorMenu";
+import { DynamicField } from "./FieldTypes/DynamicField";
+import { Field, FieldSettings, ViewType } from "./FieldTypes/Field";
+import { } from "./FieldTypes/FieldUtils";
+import { } from "./FieldTypes/StaticField";
+
+export class Template {
+
+ mainField: DynamicField;
+ settings: FieldSettings;
+
+ constructor(templateInfo: FieldSettings) {
+ this.mainField = this.setupMainField(templateInfo);
+ this.settings = templateInfo;
+ }
+
+ get childFields(): Field[] { return this.mainField.getSubfields };
+ get allFields(): Field[] { return this.mainField.getAllSubfields };
+ get contentFields(): Field[] { return this.allFields.filter(field => field.getViewType === ViewType.STATIC) };
+ get doc(){ return this.mainField.renderedDoc(); };
+
+ cloneBase = () => {
+ const clone: Template = new Template(this.settings);
+ clone.allFields.forEach(field => {
+ const matchingField: Field = this.allFields.filter(f => f.getID === field.getID)[0];
+ matchingField.applyAttributes(field);
+ })
+ return clone;
+ }
+
+ getRenderedDoc = () => {
+ const doc: Doc = this.mainField.renderedDoc();
+ this.contentFields.forEach(field => {
+ const title: string = field.getTitle();
+ const val: FieldType = field.getContent() as FieldType;
+ if (!title || !val) return;
+ doc[title] = val;
+ });
+ return doc;
+ }
+
+ getFieldByID = (id: number): Field => {
+ return this.allFields.filter(field => field.getID === id)[0];
+ }
+
+ getFieldByTitle = (title: string) => {
+ return this.allFields.filter(field => field.getTitle() === title)[0];
+ }
+
+ setupMainField = (templateInfo: FieldSettings) => {
+ return new DynamicField(templateInfo, 1);
+ }
+
+ get descriptionSummary(): string {
+ let summary: string = '';
+ this.contentFields.forEach(field => {
+ summary += `--- Field #${field.getID} (title: ${field.getTitle()}): ${field.getDescription ?? ''} ---`;
+ });
+ return summary;
+ }
+
+ get compiledContent(): string {
+ let summary: string = '';
+ this.contentFields.forEach(field => {
+ summary += `--- Field #${field.getID} (title: ${field.getTitle()}): ${field.getContent() ?? ''} ---`;
+ });
+ return summary;
+ }
+
+ renderUpdates = () => {
+ this.allFields.forEach(field => {
+ field.updateRenderedDoc(field.renderedDoc());
+ });
+ };
+
+ resetToBase = () => {
+ this.allFields.forEach(field => {
+ field.updateRenderedDoc();
+ })
+ }
+
+ isValidTemplate = (cols: Col[]) => {
+ const matches: number[][] = this.getMatches(cols);
+ const maxMatches: number = this.maxMatches(matches);
+ return maxMatches === this.contentFields.length;
+ }
+
+ getMatches = (cols: Col[]): number[][] => {
+ const numFields = this.contentFields.length;
+
+ if (cols.length !== numFields) return [];
+
+ const matches: number[][] = Array(numFields)
+ .fill([])
+ .map(() => []);
+
+ this.contentFields.forEach((field, i) => {
+ matches[i] = (field.matches(cols));
+ });
+
+ return matches;
+ }
+
+ maxMatches = (matches: number[][]) => {
+ if (matches.length === 0) return 0;
+
+ const fieldsCt = this.contentFields.length;
+ const used: boolean[] = Array(fieldsCt).fill(false);
+ const mt: number[] = Array(fieldsCt).fill(-1);
+
+ const augmentingPath = (v: number): boolean => {
+ if (used[v]) return false;
+ used[v] = true;
+
+ for (const to of matches[v]) {
+ if (mt[to] === -1 || augmentingPath(mt[to])) {
+ mt[to] = v;
+ return true;
+ }
+ }
+ return false;
+ };
+
+ for (let v = 0; v < fieldsCt; ++v) {
+ used.fill(false);
+ augmentingPath(v);
+ }
+
+ let count: number = 0;
+
+ for (let i = 0; i < fieldsCt; ++i) {
+ if (mt[i] !== -1) ++count;
+ }
+
+ return count;
+ };
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.tsx
new file mode 100644
index 000000000..d3282eda3
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.tsx
@@ -0,0 +1,752 @@
+import { FieldSettings, ViewType } from "./FieldTypes/Field";
+import { } from "./FieldTypes/StaticField";
+
+export enum TemplateFieldType {
+ TEXT = 'text',
+ VISUAL = 'visual',
+ UNSET = 'unset',
+}
+
+export enum TemplateFieldSize {
+ TINY = 'tiny',
+ SMALL = 'small',
+ MEDIUM = 'medium',
+ LARGE = 'large',
+ HUGE = 'huge',
+}
+
+export class TemplateLayouts {
+ public static get allTemplates(): FieldSettings[] {
+ return Object.values(TemplateLayouts);
+ }
+
+ public static FourField001: FieldSettings = {
+ title: 'fourfield001',
+ tl: [0, 0],
+ br: [416, 700],
+ viewType: ViewType.FREEFORM,
+ opts: {
+ backgroundColor: '#C0B887',
+ cornerRounding: .05,
+ //borderColor: '#6B461F',
+ //borderWidth: '12',
+ },
+ subfields: [
+ {
+ viewType: ViewType.STATIC,
+ tl: [-0.95, -1],
+ br: [0.95, -0.85],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY],
+ description: 'A title field for very short text that contextualizes the content.',
+ opts: {
+ backgroundColor: 'transparent',
+ color: '#F1F0E9',
+ contentXCentering: 'h-center',
+ fontBold: true,
+ },
+ },
+ {
+ viewType: ViewType.STATIC,
+ tl: [-0.87, -0.83],
+ br: [0.87, 0.2],
+ types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
+ description: 'The main focus of the template; could be an image, long text, etc.',
+ opts: {
+ cornerRounding: .05,
+ borderColor: '#8F5B25',
+ borderWidth: '6',
+ backgroundColor: '#CECAB9',
+ },
+ },
+ {
+ viewType: ViewType.STATIC,
+ tl: [-0.8, 0.2],
+ br: [0.8, 0.3],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
+ description: 'A caption for field #2, very short text.',
+ opts: {
+ backgroundColor: 'transparent',
+ contentXCentering: 'h-center',
+ color: '#F1F0E9',
+ },
+ },
+ {
+ viewType: ViewType.STATIC,
+ tl: [-0.87, 0.37],
+ br: [0.87, 0.96],
+ types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
+ description: 'A medium-sized field for medium/long text.',
+ opts: {
+ cornerRounding: .05,
+ borderColor: '#8F5B25',
+ borderWidth: '6',
+ backgroundColor: '#CECAB9',
+ },
+ },
+ ],
+ };
+
+ public static FourField002: FieldSettings = {
+ title: 'fourfield002',
+ viewType: ViewType.FREEFORM,
+ tl: [0,0],
+ br: [425, 778],
+ opts: {
+ backgroundColor: '#242425',
+ },
+ subfields: [
+ {
+ viewType: ViewType.STATIC,
+ tl: [-0.83, -0.95],
+ br: [0.83, -0.2],
+ types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE],
+ description: 'A medium to large-sized field suitable for an image or longer text that should be the main focus.',
+ opts: {
+ borderWidth: '8',
+ borderColor: '#F8E71C',
+ backgroundColor: '#242425',
+ color: 'white',
+ },
+ },
+ {
+ viewType: ViewType.STATIC,
+ tl: [-0.65, -0.2],
+ br: [0.65, -0.02],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY],
+ description: 'A tiny field for just a word or two of plain text.',
+ opts: {
+ backgroundColor: 'transparent',
+ color: 'white',
+ contentXCentering: 'h-center',
+ fontTransform: 'uppercase',
+ },
+ },
+ {
+ viewType: ViewType.STATIC,
+ tl: [-0.65, 0],
+ br: [0.65, 0.18],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY],
+ description: 'A tiny field for just a word or two of plain text.',
+ opts: {
+ backgroundColor: 'transparent',
+ color: 'white',
+ contentXCentering: 'h-center',
+ fontTransform: 'uppercase',
+ },
+ },
+ {
+ viewType: ViewType.STATIC,
+ tl: [-0.83, 0.2],
+ br: [0.83, 0.95],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
+ description: 'A medium to large-sized field suitable for longer text that should contextualize field 1.',
+ opts: {
+ borderWidth: '8',
+ borderColor: '#F8E71C',
+ color: 'white',
+ backgroundColor: '#242425',
+ },
+ },
+ {
+ viewType: ViewType.DEC,
+ tl: [-0.8, -0.075],
+ br: [-0.525, 0.075],
+ opts: {
+ backgroundColor: '#F8E71C',
+ rotation: 45,
+ },
+ },
+ {
+ viewType: ViewType.DEC,
+ tl: [-0.3075, -0.0245],
+ br: [-0.2175, 0.0245],
+ opts: {
+ backgroundColor: '#F8E71C',
+ rotation: 45,
+ },
+ },
+ {
+ viewType: ViewType.DEC,
+ tl: [-0.045, -0.0245],
+ br: [0.045, 0.0245],
+ opts: {
+ backgroundColor: '#F8E71C',
+ rotation: 45,
+ },
+ },
+ {
+ viewType: ViewType.DEC,
+ tl: [0.2175, -0.0245],
+ br: [0.3075, 0.0245],
+ opts: {
+ backgroundColor: '#F8E71C',
+ rotation: 45,
+ },
+ },
+ {
+ viewType: ViewType.DEC,
+ tl: [0.525, -0.075],
+ br: [0.8, 0.075],
+ opts: {
+ backgroundColor: '#F8E71C',
+ rotation: 45,
+ },
+ },
+ ],
+ };
+
+ // public static FourField003: TemplateDocInfos = {
+ // title: 'fourfield3',
+ // width: 477,
+ // height: 662,
+ // opts: {
+ // backgroundColor: '#9E9C95'
+ // },
+ // fields: [{
+ // tl: [-.875, -.9],
+ // br: [.875, .7],
+ // types: [TemplateFieldType.VISUAL],
+ // sizes: [TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
+ // description: '',
+ // opts: {
+ // borderWidth: '15',
+ // borderColor: '#E0E0DA',
+ // }
+ // }, {
+ // tl: [-.95, .8],
+ // br: [-.1, .95],
+ // types: [TemplateFieldType.TEXT],
+ // sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
+ // description: '',
+ // opts: {
+ // backgroundColor: 'transparent',
+ // color: 'white',
+ // contentXCentering: 'h-right',
+ // }
+ // }, {
+ // tl: [.1, .8],
+ // br: [.95, .95],
+ // types: [TemplateFieldType.TEXT],
+ // sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
+ // description: '',
+ // opts: {
+ // backgroundColor: 'transparent',
+ // color: 'red',
+ // fontTransform: 'uppercase',
+ // contentXCentering: 'h-left'
+ // }
+ // }, {
+ // tl: [0, -.9],
+ // br: [.85, -.66],
+ // types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL],
+ // sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
+ // description: '',
+ // opts: {
+ // backgroundColor: 'transparent',
+ // contentXCentering: 'h-right'
+ // }
+ // }],
+ // decorations: [{
+ // tl: [-.025, .8],
+ // br: [.025, .95],
+ // opts: {
+ // backgroundColor: '#E0E0DA',
+ // }
+ // }]
+ // };
+
+ public static FourField004: FieldSettings = {
+ title: 'fourfield04',
+ viewType: ViewType.FREEFORM,
+ tl: [0,0],
+ br: [414,583],
+ opts: {
+ backgroundColor: '#6CCAF0',
+ //borderColor: '#1088C3',
+ //borderWidth: '10',
+ },
+ subfields: [
+ {
+ viewType: ViewType.STATIC,
+ tl: [-0.86, -0.92],
+ br: [-0.075, -0.77],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY],
+ description: 'A tiny field for just a word or two of plain text.',
+ opts: {
+ backgroundColor: '#E2B4F5',
+ borderWidth: '9',
+ borderColor: '#9222F1',
+ contentXCentering: 'h-center',
+ },
+ },
+ {
+ viewType: ViewType.STATIC,
+ tl: [0.075, -0.92],
+ br: [0.86, -0.77],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY],
+ description: 'A tiny field for just a word or two of plain text.',
+ opts: {
+ backgroundColor: '#F5B4DD',
+ borderWidth: '9',
+ borderColor: '#E260F3',
+ contentXCentering: 'h-center',
+ },
+ },
+ {
+ viewType: ViewType.STATIC,
+ tl: [-0.81, -0.64],
+ br: [0.81, 0.48],
+ types: [TemplateFieldType.VISUAL],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
+ description: 'A large to huge field for visual content that is the main content of the template.',
+ opts: {
+ borderWidth: '16',
+ borderColor: '#A2BD77',
+ },
+ },
+ {
+ viewType: ViewType.STATIC,
+ tl: [-0.86, 0.6],
+ br: [0.86, 0.92],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE],
+ description: 'A medium to large field for text that describes the visual content above',
+ opts: {
+ borderWidth: '9',
+ borderColor: '#F0D601',
+ backgroundColor: '#F3F57D',
+ },
+ },
+ {
+ viewType: ViewType.DEC,
+ tl: [-0.852, -0.67],
+ br: [0.852, 0.51],
+ opts: {
+ backgroundColor: 'transparent',
+ borderColor: '#007C0C',
+ borderWidth: '10',
+ },
+ },
+ ],
+ };
+
+ public static FourField005: FieldSettings = {
+ title: 'fourfield05',
+ viewType: ViewType.FREEFORM,
+ tl: [0,0],
+ br: [400,550],
+ opts: {
+ backgroundColor: '#95A575',
+ },
+ subfields: [
+ {
+ viewType: ViewType.STATIC,
+ tl: [-0.9, -.925],
+ br: [-.075, -.775],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
+ description: 'A small text field for a title or word(s) that categorize the rest of the content.',
+ opts: {
+ borderColor: '#3B4A2C',
+ borderWidth: '8',
+ contentXCentering: "h-center",
+ backgroundColor: '#B8DC90',
+ },
+ },
+ {
+ viewType: ViewType.STATIC,
+ tl: [.075, -.925],
+ br: [.9, -.775],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
+ description: 'A small text field for a title that categorizes the rest of the content.',
+ opts: {
+ borderColor: '#3B4A2C',
+ borderWidth: '8',
+ contentXCentering: "h-center",
+ backgroundColor: '#B8DC90',
+ },
+ },
+ {
+ viewType: ViewType.DEC,
+ tl: [-.82, -.4],
+ br: [-.5, -.2],
+ opts: {
+ backgroundColor: '#94B058',
+ borderColor: '#3B4A2C',
+ borderWidth: '8',
+ },
+ },
+ {
+ viewType: ViewType.STATIC,
+ tl: [-0.66, -.65],
+ br: [0.66, .25],
+ types: [TemplateFieldType.VISUAL],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE],
+ description: 'A medium to large field in the center of the template, for the main visual content.',
+ opts: {
+ borderColor: '#3B4A2C',
+ borderWidth: '8',
+ backgroundColor: '#B8DC90',
+ },
+ },
+ {
+ viewType: ViewType.STATIC,
+ tl: [-.875, .425],
+ br: [0.875, .925],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE],
+ description: 'A medium to large field at the bottom of the template, for the main text content.',
+ opts: {
+ borderColor: '#3B4A2C',
+ borderWidth: '8',
+ contentXCentering: "h-center",
+ backgroundColor: '#B8DC90',
+ },
+ },
+ {
+ viewType: ViewType.DEC,
+ tl: [-1.1, -.62],
+ br: [-.9, -.5],
+ opts: {
+ backgroundColor: '#7A9D31',
+ borderColor: '#3B4A2C',
+ borderWidth: '8',
+ },
+ },
+ {
+ viewType: ViewType.DEC,
+ tl: [-1.1, 0],
+ br: [-.9, .15],
+ opts: {
+ backgroundColor: '#94B058',
+ borderColor: '#3B4A2C',
+ borderWidth: '8',
+ },
+ },
+ {
+ viewType: ViewType.DEC,
+ tl: [-.93, -.265],
+ br: [-.715, -.125],
+ opts: {
+ backgroundColor: '#728745',
+ borderColor: '#3B4A2C',
+ borderWidth: '8',
+ },
+ },
+ {
+ viewType: ViewType.DEC,
+ tl: [.7, -.45],
+ br: [.85, -.3],
+ opts: {
+ backgroundColor: '#7A9D31',
+ borderColor: '#3B4A2C',
+ borderWidth: '8',
+ },
+ },
+ {
+ viewType: ViewType.DEC,
+ tl: [.8, .03],
+ br: [1.2, .33],
+ opts: {
+ backgroundColor: '#728745',
+ borderColor: '#3B4A2C',
+ borderWidth: '8',
+ },
+ },
+ {
+ viewType: ViewType.DEC,
+ tl: [.875, -.13],
+ br: [1.2, .12],
+ opts: {
+ backgroundColor: '#94B058',
+ borderColor: '#3B4A2C',
+ borderWidth: '8',
+ },
+ },
+ ]
+ }
+
+ public static FourFieldCarousel: FieldSettings = {
+ title: 'title_fourfieldcarousel',
+ viewType: ViewType.FREEFORM,
+ tl:[0,0],
+ br:[500, 600],
+ opts: {
+ backgroundColor: '#DDD3A9',
+ },
+ subfields: [
+ {
+ viewType: ViewType.STATIC,
+ tl: [-0.8, -.9],
+ br: [0.8, -.5],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
+ description: 'A small text field for a title that categorizes the rest of the content.',
+ opts: {
+ borderColor: 'yellow',
+ borderWidth: '8',
+ contentXCentering: "h-center",
+ backgroundColor: 'transparent',
+ },
+ },
+ {
+ viewType: ViewType.CAROUSEL3D,
+ tl: [-0.9, -.3],
+ br: [0.9, .9],
+ opts: {
+ borderColor: 'yellow',
+ borderWidth: '8',
+ backgroundColor: 'transparent',
+ },
+ subfields: [
+ {
+ viewType: ViewType.STATIC,
+ tl: [-.3, -.6],
+ br: [.3, .6],
+ types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
+ description: 'A medium to large field for content that will share central focus with other content in the carousel.',
+ opts: {
+ borderColor: 'yellow',
+ borderWidth: '8',
+ },
+ },
+ {
+ viewType: ViewType.STATIC,
+ tl: [-.3, -.6],
+ br: [.3, .6],
+ types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
+ description: 'A medium to large field for content that will share central focus with other content in the carousel.',
+ opts: {
+ borderColor: 'black',
+ borderWidth: '8',
+ },
+ },
+ {
+ viewType: ViewType.STATIC,
+ tl: [-.3, -.6],
+ br: [.3, .6],
+ types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
+ description: 'A medium to large field for content that will share central focus with other content in the carousel.',
+ opts: {
+ borderColor: 'yellow',
+ borderWidth: '8',
+ },
+ },
+ ]
+ },
+ ]
+ }
+
+ public static ThreeField001: FieldSettings = {
+ title: 'threefield001',
+ viewType: ViewType.FREEFORM,
+ tl: [0,0],
+ br: [575, 770],
+ opts: {
+ backgroundColor: '#DDD3A9',
+ },
+ subfields: [
+ {
+ viewType: ViewType.FREEFORM,
+ tl: [-0.66, -0.747],
+ br: [0.66, 0.247],
+ description: 'A medium to large field for visual content that is the central focus.',
+ opts: {
+ borderColor: 'yellow',
+ borderWidth: '8',
+ backgroundColor: '#DDD3A9',
+ rotation: 45,
+ },
+ subfields: [
+ {
+ viewType: ViewType.STATIC,
+ tl: [-1.25, -1.25],
+ br: [1.25, 1.25],
+ types: [TemplateFieldType.VISUAL],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
+ description: 'A medium to large field for visual content that is the central focus.',
+ opts: {
+ rotation: -45,
+ },
+ },
+ ]
+ },
+ {
+ viewType: ViewType.STATIC,
+ tl: [-0.7, 0.2],
+ br: [0.7, 0.46],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
+ description: 'A very small text field for one to a few words. A good caption for the image.',
+ opts: {
+ backgroundColor: 'transparent',
+ contentXCentering: 'h-center',
+ },
+ },
+ {
+ viewType: ViewType.STATIC,
+ tl: [-0.95, 0.5],
+ br: [0.95, 0.95],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE],
+ description: 'A medium to large text field for a thorough description of the image. ',
+ opts: {
+ backgroundColor: 'transparent',
+ color: 'white',
+ },
+ },
+ {
+ viewType: ViewType.FREEFORM,
+ tl: [0.2, -1.32],
+ br: [1.8, -0.66],
+ opts: {
+ backgroundColor: '#CEB155',
+ rotation: 45,
+ },
+ subfields: [
+ {
+ viewType: ViewType.DEC,
+ tl: [-1, -.7],
+ br: [1, -.625],
+ opts: {
+ backgroundColor: 'yellow',
+ },
+ },
+ ]
+ },
+ {
+ viewType: ViewType.FREEFORM,
+ tl: [-1.8, -1.32],
+ br: [-0.2, -0.66],
+ opts: {
+ backgroundColor: '#CEB155',
+ rotation: 135,
+ },
+ subfields: [
+ {
+ viewType: ViewType.DEC,
+ tl: [-1, -.7],
+ br: [1, -.625],
+ opts: {
+ backgroundColor: 'yellow',
+ },
+ },
+ ]
+ },
+ {
+ viewType: ViewType.FREEFORM,
+ tl: [0.33, 0.75],
+ br: [1.66, 1.25],
+ opts: {
+ backgroundColor: '#CEB155',
+ rotation: 135,
+ },
+ subfields: [
+ {
+ viewType: ViewType.DEC,
+ tl: [-1, -.7],
+ br: [1, -.625],
+ opts: {
+ backgroundColor: 'yellow',
+ },
+ },
+ ]
+ },
+ {
+ viewType: ViewType.FREEFORM,
+ tl: [-1.66, 0.75],
+ br: [-0.33, 1.25],
+ opts: {
+ backgroundColor: '#CEB155',
+ rotation: 45,
+ },
+ subfields: [
+ {
+ viewType: ViewType.DEC,
+ tl: [-1, -.7],
+ br: [1, -.625],
+ opts: {
+ backgroundColor: 'yellow',
+ },
+ },
+ ]
+ },
+ ],
+ };
+
+ public static ThreeField002: FieldSettings = {
+ title: 'threefield002',
+ viewType: ViewType.FREEFORM,
+ tl: [0,0],
+ br: [477, 662],
+ opts: {
+ backgroundColor: '#9E9C95',
+ },
+ subfields: [
+ {
+ viewType: ViewType.STATIC,
+ tl: [-0.875, -0.9],
+ br: [0.875, 0.7],
+ types: [TemplateFieldType.VISUAL],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
+ description: 'A medium to large visual field for the main content of the template',
+ opts: {
+ borderWidth: '15',
+ borderColor: '#E0E0DA',
+ },
+ },
+ {
+ viewType: ViewType.STATIC,
+ tl: [0.1, 0.775],
+ br: [0.95, 0.975],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
+ description: 'A very small text field for one to a few words. The content should represent a general categorization of the image.',
+ opts: {
+ backgroundColor: 'transparent',
+ color: '#AF0D0D',
+ fontTransform: 'uppercase',
+ fontBold: true,
+ contentXCentering: 'h-left',
+ },
+ },
+ {
+ viewType: ViewType.STATIC,
+ tl: [-0.95, 0.775],
+ br: [-0.1, 0.975],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
+ description: 'A very small text field for one to a few words. The content should contextualize field 2.',
+ opts: {
+ backgroundColor: 'transparent',
+ color: 'black',
+ contentXCentering: 'h-right',
+ },
+ },
+ {
+ viewType: ViewType.DEC,
+ tl: [-0.025, 0.8],
+ br: [0.025, 0.95],
+ opts: {
+ backgroundColor: '#E0E0DA',
+ },
+ },
+ ],
+ };
+}
+
+
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx
new file mode 100644
index 000000000..50ae4d72a
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx
@@ -0,0 +1,22 @@
+import { Col } from "./DocCreatorMenu";
+import { FieldSettings } from "./FieldTypes/Field";
+import { Template } from "./Template";
+
+export class TemplateManager {
+
+ templates: Template[] = [];
+
+ constructor(templateSettings: FieldSettings[]) {
+ this.templates = this.initializeTemplates(templateSettings);
+ }
+
+ initializeTemplates = (templateSettings: FieldSettings[]): Template[] => {
+ const initializedTemplates: Template[] = [];
+ templateSettings.forEach(settings => initializedTemplates.push(new Template(settings)));
+ return initializedTemplates;
+ }
+
+ getValidTemplates = (cols: Col[]): Template[] => {
+ return this.templates.filter(template => template.isValidTemplate(cols));
+ }
+} \ No newline at end of file