aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/apis/gpt/GPT.ts6
-rw-r--r--src/client/views/MainView.tsx2
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx106
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx2368
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss (renamed from src/client/views/nodes/DataVizBox/DocCreatorMenu.scss)30
-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
13 files changed, 2789 insertions, 2483 deletions
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 6d9bc1d06..29b6ab989 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -104,7 +104,7 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
model: 'gpt-4-turbo',
maxTokens: 512,
temp: 0.5,
- prompt: 'You will be given a list of field descriptions for multiple templates in the format {field #0: “description”}{field #1: “description”}{...}, and a list of column descriptions in the format {“title”: “description”}{...}. Your job is to match columns with fields based on their descriptions. Your output should be in the following JSON format: {“Template title”:{“#”: “title”, “#”: “title”, “#”: “title” …}, “Template title”:{“#”: “title”, “#”: “title”, “#”: “title” …}} where “Template title” represents the template, # represents the field # and “title” the title of the column assigned to it. A filled out example might look like {“fivefield2”:{“0”:”Name”, “1”:”Image”, “2”:”Caption”, “3”:”Position”, “4”:”Stats”}, “fivefield3”:{0:”Image”, 1:”Name”, 2:”Caption”, 3:”Stats”, 4:”Position”}. Include one object for each template. IT IS VERY IMPORTANT THAT YOU ONLY INCLUDE TEXT IN THE FORMAT ABOVE, WITH NO ADDITIONS WHATSOEVER. Do not include extraneous ‘#’ characters, ‘column’ for columns, or ‘template’ for templates: ONLY THE TITLES AND NUMBERS. There should never be one column assigned to more than one field (ie. if the “name” column is assigned to field 1, it can’t be assigned to any other fields) . Do this for each template whose fields are described. The descriptions are as follows:',
+ prompt: 'You will be given a list of field descriptions for one or more templates in the format {field #0: “description”}{field #1: “description”}{...}, and a list of column descriptions in the format {“title”: “description”}{...}. Your job is to match columns with fields based on their descriptions. Your output should be in the following JSON format: {“template_title”:{“#”: “title”, “#”: “title”, “#”: “title” …}, “template_title”:{“#”: “title”, “#”: “title”, “#”: “title” …}} where “template_title” is the templates title as specified in the description provided, # represents the field # and “title” the title of the column assigned to it. A filled out example might look like {“fivefield2”:{“0”:”Name”, “1”:”Image”, “2”:”Caption”, “3”:”Position”, “4”:”Stats”}, “fivefield3”:{0:”Image”, 1:”Name”, 2:”Caption”, 3:”Stats”, 4:”Position”}. Include one object for each template. IT IS VERY IMPORTANT THAT YOU ONLY INCLUDE TEXT IN THE FORMAT ABOVE, WITH NO ADDITIONS WHATSOEVER. Do not include extraneous ‘#’ characters, ‘column’ for columns, or ‘template’ for templates: ONLY THE TITLES AND NUMBERS. There should never be one column assigned to more than one field (ie. if the “name” column is assigned to field 1, it can’t be assigned to any other fields) . Do this for each template whose fields are described. The descriptions are as follows:',
},
vizsum: {
model: 'gpt-4-turbo',
@@ -119,12 +119,12 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
prompt: 'Your job is to provide structured information on columns in a dataset based on example rows. You will categorize each column in two ways: by type and size. The size categories are as follows: tiny (one or two words), small (a sentence/multiple words), medium (a few sentences), large (a longer paragraph), and huge (a very long or multiple paragraphs). The type categories are as follows: visual (links/file paths to images, pdfs, maps, or any other visual media type), and text (plain text that isn’t a link/file path). Visual media should be assumed to have size “medium” “large” or “huge”. You will give your responses in JSON format, like so: {“title (of column)”:{“type”:”text”, “size”:”small”}, “title (of column)”:{“type”:”visual”, “size”:”medium”}, …}. DO NOT INCLUDE ANY OTHER TEXT, ONLY THE JSON.',
},
fill: {
- model: 'gpt-4-turbo',
+ model: 'gpt-4o',
maxTokens: 512,
temp: 0.5,
prompt: 'Your job is to generate content for fields based on a user prompt and background context given to you. You will be given the content of the other fields present in the format: ---- Field # (field title): content ---- Field # (field title): content ----- (etc.) You will be given info on the columns to generate for in the format ---- title: , prompt: , word limit: , assigned field: ----. For each column, based on the prompt, word limit, and the context of existing fields, you should generate a short response in the following JSON format: {“___”(where ___ is the title from the column description with no additions): {“number”:”#” (where # is the assigned field of the column), “content”:”response” (where response is your response to the prompt in the column info)}}. Here’s another example of the format with only one column: {“position”: {“number”:”2”, “content”:”*your response goes here*”}}. ONLY INCLUDE THE JSON TEXT WITH NO OTHER ADDED TEXT. YOUR RESPONSE MUST BE VALID JSON. The word limit for each column applies only to that column’s response. Do not include speculation or information that you can’t glean from your factual knowledge or the content of the other fields (no description of images you can’t see, for example). You should include one object per column you are provided info on.',
},
- completeprompt: { model: 'gpt-4-turbo', maxTokens: 512, temp: 0.5, prompt: 'Your prompt is as follows:' },
+ completeprompt: { model: 'gpt-4o', maxTokens: 512, temp: 0.5, prompt: 'Your prompt is as follows:' },
draw: {
model: 'gpt-4o',
maxTokens: 1024,
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index cc7c1a42b..fddc0e40c 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -59,7 +59,7 @@ import { ImageLabelHandler } from './collections/collectionFreeForm/ImageLabelHa
import { MarqueeOptionsMenu } from './collections/collectionFreeForm/MarqueeOptionsMenu';
import { CollectionLinearView } from './collections/collectionLinear';
import { LinkMenu } from './linking/LinkMenu';
-import { DocCreatorMenu } from './nodes/DataVizBox/DocCreatorMenu';
+import { DocCreatorMenu } from './nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu';
import { SchemaCSVPopUp } from './nodes/DataVizBox/SchemaCSVPopUp';
import { DocButtonState } from './nodes/DocumentLinksButton';
import { DocumentView, DocumentViewInternal } from './nodes/DocumentView';
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index fa3ab73a7..d5e37b3b5 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -32,11 +32,12 @@ import { DocumentView } from '../DocumentView';
import { FieldView, FieldViewProps } from '../FieldView';
import { FocusViewOptions } from '../FocusViewOptions';
import './DataVizBox.scss';
-import { Col, DataVizTemplateInfo, DocCreatorMenu, LayoutType, TemplateFieldSize, TemplateFieldType } from './DocCreatorMenu';
+import { Col, DataVizTemplateInfo, DocCreatorMenu, LayoutType} from './DocCreatorMenu/DocCreatorMenu';
import { Histogram } from './components/Histogram';
import { LineChart } from './components/LineChart';
import { PieChart } from './components/PieChart';
import { TableBox } from './components/TableBox';
+import { TemplateFieldSize, TemplateFieldType } from './DocCreatorMenu/TemplateBackend';
export enum DataVizView {
TABLE = 'table',
@@ -171,7 +172,6 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const colInfo = this.colsInfo.get(colTitle);
if (colInfo) {
colInfo.title = newTitle;
- console.log(colInfo.title);
} else {
this.colsInfo.set(colTitle, { title: newTitle, desc: '', type: TemplateFieldType.UNSET, sizes: [] });
}
@@ -510,7 +510,6 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
openDocCreatorMenu = (x: number, y: number) => {
DocCreatorMenu.Instance.toggleDisplay(x, y);
DocCreatorMenu.Instance.setDataViz(this);
- DocCreatorMenu.Instance.setTemplateDocs(this.getPossibleTemplates());
};
specificContextMenu = (e: React.MouseEvent) => {
@@ -622,107 +621,6 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
};
- getPossibleTemplates = (): Doc[] => {
- const linkedDocs: Doc[] = LinkManager.Instance.getAllRelatedLinks(this.Document).map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document)));
- const linkedCollections: Doc[] = linkedDocs.filter(doc => doc.type === 'config').map(doc => DocCast(doc.annotationOn));
- const isColumnTitle = (title: string): boolean => {
- const colTitles: string[] = Object.keys(this.records[0]);
- for (let i = 0; i < colTitles.length; ++i) {
- if (colTitles[i] === title) {
- return true;
- }
- }
- return false;
- };
- const isValidTemplate = (collection: Doc) => {
- const childDocs = DocListCast(collection[Doc.LayoutFieldKey(collection)]);
- for (let i = 0; i < childDocs.length; ++i) {
- if (isColumnTitle(String(childDocs[i].title))) return true;
- }
- return false;
- };
- return linkedCollections.filter(col => isValidTemplate(col));
- };
-
- ApplyTemplateTo = (templateDoc: Doc, target: Doc, targetKey: string, titleTarget: string | undefined) => {
- if (!Doc.AreProtosEqual(target[targetKey] as Doc, templateDoc)) {
- if (target.resolvedDataDoc) {
- target[targetKey] = new PrefetchProxy(templateDoc);
- } else {
- titleTarget && (Doc.GetProto(target).title = titleTarget);
- const setDoc = [AclAdmin, AclEdit, AclAugment].includes(GetEffectiveAcl(Doc.GetProto(target))) ? Doc.GetProto(target) : target;
- setDoc[targetKey] = new PrefetchProxy(templateDoc);
- }
- }
- return target;
- };
-
- applyLayout = (templateInfo: DataVizTemplateInfo, docs: Doc[]) => {
- if (templateInfo.layout.type === LayoutType.Stacked) return;
- const columns: number = templateInfo.columns;
- const xGap: number = templateInfo.layout.xMargin;
- const yGap: number = templateInfo.layout.yMargin;
- // const repeat: number = templateInfo.layout.repeat;
- const startX: number = templateInfo.referencePos.x;
- const startY: number = templateInfo.referencePos.y;
- const templWidth = Number(templateInfo.doc._width);
- const templHeight = Number(templateInfo.doc._height);
-
- 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 += templWidth + xGap;
- ++docsChanged;
- ++i;
- }
-
- i = 0;
- curX = startX;
- curY += templHeight + yGap;
- }
- };
-
- // @action addSavedLayout = (layout: DataVizTemplateLayout) => {
- // const saved = Cast(this.layoutDoc.dataViz_savedTemplates, listSpec('RefField'));
-
- // }
-
- @action
- createDocsFromTemplate = (templateInfo: DataVizTemplateInfo) => {
- if (!templateInfo.doc) return;
- const mainCollection = this.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView;
- const fields: string[] = Array.from(Object.keys(this.records[0]));
- const selectedRows = NumListCast(this.layoutDoc.dataViz_selectedRows);
- const docs: Doc[] = selectedRows.map(row => {
- const values: string[] = [];
- fields.forEach(col => values.push(this.records[row][col]));
-
- const proto = new Doc();
- proto.author = ClientUtils.CurrentUserEmail();
- values.forEach((val, i) => {
- proto[fields[i]] = val as FieldType;
- });
-
- const target = Doc.MakeDelegate(proto);
- const targetKey = StrCast(templateInfo.doc!.layout_fieldKey, 'layout');
- const applied = this.ApplyTemplateTo(templateInfo.doc!, target, targetKey, templateInfo.doc!.title + `${row}`);
- target.layout_fieldKey = targetKey;
-
- //this.applyImagesTo(target, fields);
- return applied;
- });
-
- docs.forEach(doc => mainCollection.addDocument(doc));
-
- this.applyLayout(templateInfo, docs);
- };
-
/**
* creates a new dataviz document filter from this one
* it appears to the right of this document, with the
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx
deleted file mode 100644
index 4d9173211..000000000
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx
+++ /dev/null
@@ -1,2368 +0,0 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Colors } from '@dash/components';
-import { action, computed, makeObservable, observable, reaction, 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, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
-import { emptyFunction } from '../../../../Utils';
-import { Doc, NumListCast, StrListCast, returnEmptyDoclist } from '../../../../fields/Doc';
-import { Id } from '../../../../fields/FieldSymbols';
-import { Cast, DocCast, ImageCast, StrCast } from '../../../../fields/Types';
-import { ImageField } from '../../../../fields/URLField';
-import { Networking } from '../../../Network';
-import { GPTCallType, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
-import { Docs } from '../../../documents/Documents';
-import { DragManager } from '../../../util/DragManager';
-import { MakeTemplate } from '../../../util/DropConverter';
-import { SnappingManager } from '../../../util/SnappingManager';
-import { UndoManager, undoable } from '../../../util/UndoManager';
-import { LightboxView } from '../../LightboxView';
-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, returnEmptyDocViewList } from '../../StyleProvider';
-import { Transform } from '../../../util/Transform';
-import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { Upload } from '../../../../server/SharedMediaTypes';
-
-export enum LayoutType {
- Stacked = 'stacked',
- Grid = 'grid',
- Row = 'row',
- Column = 'column',
- Custom = 'custom',
-}
-
-@observer
-export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> {
- static Instance: DocCreatorMenu;
-
- private _disposers: { [name: string]: IDisposer } = {};
-
- private _ref: HTMLDivElement | null = null;
-
- @observable _templateDocs: Doc[] = [];
- @observable _selectedTemplate: Doc | undefined = undefined;
- @observable _columns: Col[] = [];
- @observable _selectedCols: { title: string; type: string; desc: string }[] | undefined = [];
-
- @observable _layout: { type: LayoutType; yMargin: number; xMargin: number; columns?: number; repeat: number } = { type: LayoutType.Grid, yMargin: 0, xMargin: 0, repeat: 0 };
- @observable _layoutPreview: boolean = true;
- @observable _layoutPreviewScale: number = 1;
- @observable _savedLayouts: DataVizTemplateLayout[] = [];
- @observable _expandedPreview: { icon: ImageField; doc: Doc } | undefined = undefined;
-
- @observable _suggestedTemplates: Doc[] = [];
- @observable _GPTOpt: boolean = false;
- @observable _userPrompt: string = '';
- @observable _callCount: number = 0;
- @observable _GPTLoading: boolean = false;
-
- @observable _pageX: number = 0;
- @observable _pageY: number = 0;
- @observable _indicatorX: number | undefined = undefined;
- @observable _indicatorY: number | undefined = undefined;
-
- @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: any;
- @observable _snapPt: any;
- @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;
- //setTimeout(() => this.generateTemplates(''));
- }
-
- @action setDataViz = (dataViz: DataVizBox) => {
- this._dataViz = dataViz;
- };
- @action setTemplateDocs = (docs: Doc[]) => {
- this._templateDocs = docs.map(doc => (doc.annotationOn ? DocCast(doc.annotationOn) : doc));
- };
- @action setGSuggestedTemplates = (docs: Doc[]) => {
- this._suggestedTemplates = docs;
- };
-
- @computed get docsToRender() {
- return this._selectedTemplate ? NumListCast(this._dataViz?.layoutDoc.dataViz_selectedRows) : [];
- }
-
- @computed get rowsCount() {
- switch (this._layout.type) {
- case LayoutType.Row:
- case LayoutType.Stacked:
- return 1;
- case LayoutType.Column:
- return this.docsToRender.length;
- case LayoutType.Grid:
- return Math.ceil(this.docsToRender.length / (this._layout.columns ?? 1)) ?? 0;
- default:
- return 0;
- }
- }
-
- @computed get columnsCount() {
- switch (this._layout.type) {
- case LayoutType.Row:
- return this.docsToRender.length;
- case LayoutType.Column:
- case LayoutType.Stacked:
- return 1;
- case LayoutType.Grid:
- return this._layout.columns ?? 0;
- default:
- return 0;
- }
- }
-
- @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._columns);
- }
-
- @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: Function) => {
- 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);
- this._disposers.templates = reaction(
- () => this._templateDocs.slice(),
- docs => this.updateIcons(docs)
- );
- this._disposers.gpt = reaction(
- () => this._suggestedTemplates.slice(),
- docs => this.updateIcons(docs)
- );
- //this._disposers.columns = reaction(() => this._dataViz?.layoutDoc._dataViz_axes, () => {this.generateTemplates('')})
- this._disposers.lightbox = reaction(
- () => LightboxView.LightboxDoc(),
- doc => {
- // NOTE: bcz; commented this out because the doc creator would appear everytime I close out of the lightbox
- // doc ? this._shouldDisplay && this.closeMenu() : !this._shouldDisplay && this.openMenu();
- }
- );
- //this._disposers.fields = reaction(() => this._dataViz?.axes, cols => this._selectedCols = cols?.map(col => { return {title: col, type: '', desc: ''}}))
- }
-
- componentWillUnmount() {
- Object.values(this._disposers).forEach(disposer => disposer?.());
- document.removeEventListener('pointerdown', this.onPointerDown, true);
- document.removeEventListener('pointerup', this.onPointerUp);
- }
-
- updateIcons = (docs: Doc[]) => {
- console.log('called');
- docs.map(this.getIcon);
- };
-
- @action
- updateSelectedCols = (cols: string[]) => {
- this._selectedCols;
- };
-
- @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 = () => {
- const allTemplates = this._templateDocs.concat(this._suggestedTemplates);
- this._shouldDisplay = true;
- this.updateIcons(allTemplates);
- };
-
- @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<any>(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 }) => {
- const refCent = [refPt[0], refPt[1]]; // fixed reference point for resize (ie, a point that doesn't move)
- 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 = (template: Doc) => {
- if (this._selectedTemplate === template) {
- this._selectedTemplate = undefined;
- return;
- } else {
- this._selectedTemplate = template;
- MakeTemplate(template);
- }
- };
-
- @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;
- };
-
- @action
- generateTemplates = async (inputText: string) => {
- ++this._callCount;
- const origCount = this._callCount;
-
- let prompt: string = `(#${origCount}) Please generate for the fields:`;
- this.selectedFields?.forEach(field => (prompt += ` ${field},`));
- prompt += ` (-----NOT A FIELD-----) Additional prompt: ${inputText}`;
-
- this._GPTLoading = true;
-
- try {
- const res = await gptAPICall(prompt, GPTCallType.TEMPLATE);
-
- if (res && this._callCount === origCount) {
- this._suggestedTemplates = [];
- const templates: { template_type: string; fieldVals: { title: string; tlx: string; tly: string; brx: string; bry: string }[] }[] = JSON.parse(res);
- this.createGeneratedTemplates(templates, 500, 500);
- }
- } catch (err) {
- console.error(err);
- }
- };
-
- @action
- createGeneratedTemplates = (layouts: { template_type: string; fieldVals: { title: string; tlx: string; tly: string; brx: string; bry: string }[] }[], tempWidth: number, tempHeight: number) => {
- const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView;
- const GPTTemplates: Doc[] = [];
-
- layouts.forEach(layout => {
- const fields: Doc[] = layout.fieldVals.map(field => {
- const left: number = (Number(field.tlx) * tempWidth) / 2;
- const top: number = Number(field.tly) * tempHeight / 2; //prettier-ignore
- const right: number = (Number(field.brx) * tempWidth) / 2;
- const bottom: number = Number(field.bry) * tempHeight / 2; //prettier-ignore
- const height = bottom - top;
- const width = right - left;
- const doc = !field.title.includes('$$')
- ? Docs.Create.TextDocument('', { _height: height, _width: width, title: field.title, x: left, y: top, text_fontSize: `${height / 2}` })
- : Docs.Create.ImageDocument('', { _height: height, _width: width, title: field.title.replace(/\$\$/g, ''), x: left, y: top });
- return doc;
- });
-
- const template = Docs.Create.FreeformDocument(fields, { _height: tempHeight, _width: tempWidth, title: layout.template_type, x: 400000, y: 400000 });
-
- mainCollection.addDocument(template);
-
- GPTTemplates.push(template);
- });
-
- setTimeout(() => {
- this.setGSuggestedTemplates(GPTTemplates); /*GPTTemplates.forEach(template => mainCollection.removeDocument(template))*/
- }, 100);
-
- this.forceUpdate();
- };
-
- editTemplate = (doc: Doc) => {
- //this.closeMenu();
- DocumentViewInternal.addDocTabFunc(doc, OpenWhere.addRight);
- DocumentView.DeselectAll();
- Doc.UnBrushDoc(doc);
- };
-
- removeTemplate = (doc: Doc) => {
- this._templateDocs.splice(this._templateDocs.indexOf(doc), 1);
- };
-
- testTemplate = async () => {
- this.updateIcons(this._suggestedTemplates.slice());
- this.forceUpdate();
-
- // try {
- // const res = await gptImageCall('Image of panda eating a cookie');
-
- // if (res) {
- // const result = await Networking.PostToServer('/uploadRemoteImage', { sources: res });
-
- // console.log(result);
- // }
- // } catch (e) {
- // console.log(e);
- // }
- };
-
- @action addField = () => {
- const newFields: Col[] = this._columns.concat([{ title: '', type: TemplateFieldType.UNSET, desc: '', sizes: [] }]);
- this._columns = 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._columns.filter(f => f === field);
- if (!toRemove) return;
-
- if (toRemove.length > 1) {
- while (toRemove.length > 1) {
- toRemove.pop();
- }
- }
-
- if (this._columns.length === 1) {
- this._columns = [];
- } else {
- this._columns.splice(this._columns.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> => {
- console.log(prompt);
-
- 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);
- }
- };
-
- matchesForTemplate = (template: TemplateDocInfos, cols: Col[]): number[][] => {
- const colMatchesField = (col: Col, field: Field) => {
- return field.sizes?.some(size => col.sizes?.includes(size)) && field.types?.includes(col.type);
- };
-
- const matches: number[][] = Array(template.fields.length)
- .fill([])
- .map(() => []);
-
- template.fields.forEach((field, i) => {
- cols.forEach((col, v) => {
- if (colMatchesField(col, field)) {
- matches[i].push(v);
- }
- });
- });
-
- return matches;
- };
-
- maxMatches = (fieldsCt: number, matches: number[][]) => {
- 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;
- };
-
- findValidTemplates = (cols: Col[], templates: TemplateDocInfos[]) => {
- let validTemplates: any[] = [];
- templates.forEach(template => {
- const numFields = template.fields.length;
- if (!(numFields === cols.length)) return;
- const matches = this.matchesForTemplate(template, cols);
- if (this.maxMatches(numFields, matches) === numFields) {
- validTemplates.push(template.title);
- }
- });
-
- validTemplates = validTemplates.map(title => TemplateLayouts.getTemplateByTitle(title));
-
- return validTemplates;
- };
-
- // createColumnField = (template: TemplateDocInfos, field: Field, column: Col): Doc => {
-
- // if (field.subfields) {
- // const doc = FieldFuncs.FreeformField({
- // tl: field.tl,
- // br: field.br },
- // template.height,
- // template.width,
- // column.title,
- // '',
- // field.opts
- // );
-
- // field.subfields[1].forEach(f => {
- // const fDoc = ()
- // })
-
- // }
-
- // return new Doc;
- // }
-
- /**
- * 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
- */
- fillPresetTemplate = async (template: TemplateDocInfos, assignments: { [field: string]: Col }): Promise<Doc> => {
- 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 renderTextCalls = async (): Promise<Doc[]> => {
- const rendered: Doc[] = [];
-
- if (GPTTextCalls.length) {
- try {
- const prompt = fieldContent + GPTTextAssignment;
-
- const res = await gptAPICall(prompt, GPTCallType.FILL);
-
- if (res) {
- const assignments: { [title: string]: { number: string; content: string } } = JSON.parse(res);
- //console.log('assignments', GPTAssignments, 'assignment string', GPTAssignmentString, 'field content', fieldContent, 'response', res, 'assignments', assignments);
- Object.entries(assignments).forEach(([title, info]) => {
- const field: Field = template.fields[Number(info.number)];
- const col = this.getColByTitle(title);
-
- const doc = FieldUtils.TextField(
- {
- tl: field.tl,
- br: field.br,
- },
- template.height,
- template.width,
- col.title,
- info.content ?? '',
- field.opts
- );
-
- rendered.push(doc);
- });
- }
- } catch (err) {
- console.log(err);
- }
- }
-
- return rendered;
- };
-
- const createGeneratedImage = async (fieldNum: string, col: Col, prompt: string) => {
- const url = await this.generateGPTImage(prompt);
- const field: Field = template.fields[Number(fieldNum)];
- const doc = FieldUtils.ImageField(
- {
- tl: field.tl,
- br: field.br,
- },
- template.height,
- template.width,
- col.title,
- url ?? '',
- field.opts
- );
-
- return doc;
- };
-
- const renderImageCalls = async (): Promise<Doc[]> => {
- const rendered: Doc[] = [];
- const calls = GPTIMGCalls;
-
- if (calls.length) {
- try {
- const renderedImages: Doc[] = await Promise.all(
- calls.map(async ([fieldNum, col]) => {
- 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);
- console.log(sysPrompt, prompt);
-
- return createGeneratedImage(fieldNum, col, prompt);
- })
- );
-
- const renderedTemplates: Doc[] = await Promise.all(renderedImages);
- renderedTemplates.forEach(doc => rendered.push(doc));
- } catch (e) {
- console.log(e);
- }
- }
-
- return rendered;
- };
-
- const fields: Doc[] = [];
-
- const GPTAssignments = Object.entries(assignments).filter(([f, col]) => this._columns.includes(col));
- const nonGPTAssignments: [string, Col][] = Object.entries(assignments).filter(a => !GPTAssignments.includes(a));
- const GPTTextCalls = GPTAssignments.filter(([str, col]) => col.type === TemplateFieldType.TEXT);
- const GPTIMGCalls = GPTAssignments.filter(([str, col]) => col.type === TemplateFieldType.VISUAL);
-
- const stringifyGPTInfo = (calls: [string, Col][]): string => {
- let string: string = '*** COLUMN INFO:';
- calls.forEach(([fieldNum, col]) => {
- string += `--- title: ${col.title}, prompt: ${col.desc}, word limit: ${wordLimit(col.sizes[0])} words, assigned field: ${fieldNum} ---`;
- });
- return (string += ' ***');
- };
-
- const GPTTextAssignment = stringifyGPTInfo(GPTTextCalls);
-
- let fieldContent: string = '';
-
- Object.entries(nonGPTAssignments).forEach(([f, strCol]) => {
- const field: Field = template.fields[Number(f)];
- const col = strCol[1];
-
- const doc = (col.type === TemplateFieldType.VISUAL ? FieldUtils.ImageField : FieldUtils.TextField)(
- {
- tl: field.tl,
- br: field.br,
- },
- template.height,
- template.width,
- col.title,
- col.defaultContent ?? '',
- field.opts
- );
-
- fieldContent += `--- Field #${f} (title: ${col.title}): ${col.defaultContent ?? ''} ---`;
-
- fields.push(doc);
- });
-
- template.decorations.forEach(dec => {
- const doc = FieldUtils.FreeformField(
- {
- tl: dec.tl,
- br: dec.br,
- },
- template.height,
- template.width,
- '',
- '',
- dec.opts
- );
-
- fields.push(doc);
- });
-
- const createMainDoc = (): Doc => {
- const main = Docs.Create.FreeformDocument(fields, {
- _height: template.height,
- _width: template.width,
- title: template.title,
- backgroundColor: template.opts.backgroundColor,
- _layout_borderRounding: `${template.opts.cornerRounding}px` ?? '0px',
- borderWidth: template.opts.borderWidth,
- borderColor: template.opts.borderColor,
- x: 40000,
- y: 40000,
- });
-
- const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView;
- mainCollection.addDocument(main);
-
- return main;
- };
-
- const textCalls = await renderTextCalls();
- const imageCalls = await renderImageCalls();
-
- textCalls.forEach(doc => {
- fields.push(doc);
- });
- imageCalls.forEach(doc => {
- fields.push(doc);
- });
-
- return createMainDoc();
- };
-
- compileFieldDescriptions = (templates: TemplateDocInfos[]): string => {
- let descriptions: string = '';
- templates.forEach(template => {
- descriptions += `---------- NEW TEMPLATE TO INCLUDE: Description of template ${template.title}'s fields: `;
- template.fields.forEach((field, index) => {
- descriptions += `{Field #${index}: ${field.description}} `;
- });
- });
-
- 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: TemplateDocInfos[], cols: Col[]): Promise<[TemplateDocInfos, { [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;
-
- let prompt: string = `(${origCount}) ${inputText}`;
-
- this._GPTLoading = true;
-
- try {
- const res = await gptAPICall(prompt, GPTCallType.TEMPLATE);
-
- if (res && this._callCount === origCount) {
- const assignments: { [templateTitle: string]: { [field: string]: string } } = JSON.parse(res);
- const brokenDownAssignments: [TemplateDocInfos, { [field: number]: Col }][] = [];
-
- Object.entries(assignments).forEach(([tempTitle, assignment]) => {
- const template = TemplateLayouts.getTemplateByTitle(tempTitle);
- if (!template) return;
- const toObj = Object.entries(assignment).reduce(
- (a, [fieldNum, colTitle]) => {
- a[Number(fieldNum)] = 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.findValidTemplates(cols, TemplateLayouts.allTemplates);
-
- const assignments: [TemplateDocInfos, { [field: number]: Col }][] = await this.assignColsToFields(templates, cols);
-
- const renderedTemplatePromises: Promise<Doc>[] = assignments.map(([template, assignments]) => this.fillPresetTemplate(template, assignments));
-
- const renderedTemplates: Doc[] = await Promise.all(renderedTemplatePromises);
-
- setTimeout(() => {
- this.setGSuggestedTemplates(renderedTemplates);
- this._GPTLoading = false;
- });
- };
-
- @action setExpandedView = (info: { icon: ImageField; doc: Doc } | undefined) => {
- if (info) {
- const doc = info.doc;
- const wrapper: Doc = Docs.Create.FreeformDocument([info.doc], { _height: NumListCast(doc._height)[0], _width: NumListCast(doc._width)[0], title: '' });
- const newInfo = { icon: new ImageField(''), doc: wrapper };
- this._expandedPreview = newInfo;
- } else {
- this._expandedPreview = info;
- }
- };
-
- get editingWindow() {
- const doc = this._expandedPreview?.doc ?? new Doc();
- const rendered = (
- <div className="docCreatorMenu-expanded-template-preview">
- <CollectionFreeFormView
- Document={this._expandedPreview!.doc}
- docViewPath={returnEmptyDocViewList}
- childLayoutTemplate={() => Cast(doc.childLayoutTemplate, Doc, null)}
- isContentActive={emptyFunction}
- isAnyChildContentActive={() => true}
- select={emptyFunction}
- isSelected={returnFalse}
- fieldKey={Doc.LayoutFieldKey(doc)}
- addDocument={returnFalse}
- moveDocument={returnFalse}
- removeDocument={returnFalse}
- PanelWidth={() => this._menuDimensions.width - 10}
- PanelHeight={() => this._menuDimensions.height - 60}
- ScreenToLocalTransform={() => new Transform(-this._pageX, -this._pageY, 1)}
- renderDepth={5}
- whenChildContentsActiveChanged={emptyFunction}
- focus={emptyFunction}
- styleProvider={DefaultStyleProvider}
- addDocTab={this._props.addDocTab}
- // eslint-disable-next-line no-use-before-define
- pinToPres={() => undefined}
- childFilters={returnEmptyFilter}
- childFiltersByRanges={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- fitContentsToBox={returnTrue}
- xPadding={0}
- yPadding={0}
- />
- </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._expandedPreview && this.updateIcons(this._suggestedTemplates.slice());
- this.setExpandedView(undefined);
- })
- }>
- <FontAwesomeIcon icon="minimize" />
- </button>
- <button className="docCreatorMenu-menu-button section-reveal-options top-right-lower" onPointerDown={e => this.setUpButtonClick(e, () => this._expandedPreview && this._templateDocs.push(this._expandedPreview.doc))}>
- <FontAwesomeIcon icon="plus" color="white" />
- </button>
- </div>
- </div>
- );
- }
-
- get templatesPreviewContents() {
- const renderedTemplates: Doc[] = [];
-
- const GPTOptions = <div></div>;
-
- //<img className='docCreatorMenu-preview-image expanded' src={this._expandedPreview.icon!.url.href.replace(".png", "_o.png")} />
-
- 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._suggestedTemplates
- ?.map(doc => ({ icon: ImageCast(doc.icon), doc }))
- .filter(info => info.icon && info.doc)
- .map(info => (
- <div
- className="docCreatorMenu-preview-window"
- style={{
- border: this._selectedTemplate === info.doc ? `solid 3px ${Colors.MEDIUM_BLUE}` : '',
- boxShadow: this._selectedTemplate === info.doc ? `0 0 15px rgba(68, 118, 247, .8)` : '',
- }}
- onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(info.doc)))}>
- <button
- className="option-button left"
- onPointerDown={e =>
- this.setUpButtonClick(e, () => {
- this.setExpandedView(info);
- })
- }>
- <FontAwesomeIcon icon="magnifying-glass" color="white" />
- </button>
- <button className="option-button right" onPointerDown={e => this.setUpButtonClick(e, () => this._templateDocs.push(info.doc))}>
- <FontAwesomeIcon icon="plus" color="white" />
- </button>
- <img className="docCreatorMenu-preview-image" src={info.icon!.url.href.replace('.png', '_o.png')} />
- </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" onPointerDown={e => this.testTemplate()}>
- <FontAwesomeIcon icon="plus" color="rgb(160, 160, 160)" />
- </div>
- {this._templateDocs
- .map(doc => ({ icon: ImageCast(doc.icon), doc }))
- .filter(info => info.icon && info.doc)
- .map(info => {
- if (renderedTemplates.includes(info.doc)) return undefined;
- renderedTemplates.push(info.doc);
- return (
- <div
- className="docCreatorMenu-preview-window"
- style={{
- border: this._selectedTemplate === info.doc ? `solid 3px ${Colors.MEDIUM_BLUE}` : '',
- boxShadow: this._selectedTemplate === info.doc ? `0 0 15px rgba(68, 118, 247, .8)` : '',
- }}
- onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(info.doc)))}>
- <button
- className="option-button left"
- onPointerDown={e =>
- this.setUpButtonClick(e, () => {
- this.editTemplate(info.doc);
- })
- }>
- <FontAwesomeIcon icon="pencil" color="black" />
- </button>
- <button
- className="option-button right"
- onPointerDown={e =>
- this.setUpButtonClick(e, () => {
- this.removeTemplate(info.doc);
- })
- }>
- <FontAwesomeIcon icon="trash" color="black" />
- </button>
- <img className="docCreatorMenu-preview-image" src={info.icon!.url.href.replace('.png', '_o.png')} />
- </div>
- );
- })}
- </div>
- </div>
- </div>
- )}
- </div>
- );
- }
-
- get savedLayoutsPreviewContents() {
- return (
- <div className="docCreatorMenu-preview-container">
- {this._savedLayouts.map((layout, index) => (
- <div
- className="docCreatorMenu-preview-window"
- style={{
- border: this.isSelectedLayout(layout) ? `solid 3px ${Colors.MEDIUM_BLUE}` : '',
- boxShadow: this.isSelectedLayout(layout) ? `0 0 15px rgba(68, 118, 247, .8)` : '',
- }}
- onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedSavedLayout(layout)))}>
- {this.layoutPreviewContents(87, layout, true, index)}
- </div>
- ))}
- </div>
- );
- }
-
- @action updateXMargin = (input: string) => {
- this._layout.xMargin = Number(input);
- };
- @action updateYMargin = (input: string) => {
- this._layout.yMargin = Number(input);
- };
- @action updateColumns = (input: string) => {
- this._layout.columns = Number(input);
- };
-
- get layoutConfigOptions() {
- const optionInput = (icon: string, func: Function, 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 any} />
- </div>
- <input defaultValue={def} onInput={e => func(e.currentTarget.value)} className="docCreatorMenu-input config layout-config" />
- </div>
- );
- };
-
- switch (this._layout.type) {
- case LayoutType.Row:
- return <div className="docCreatorMenu-configuration-bar">{optionInput('arrows-left-right', this.updateXMargin, this._layout.xMargin, '0')}</div>;
- case LayoutType.Column:
- return <div className="docCreatorMenu-configuration-bar">{optionInput('arrows-up-down', this.updateYMargin, this._layout.yMargin, '1')}</div>;
- case LayoutType.Grid:
- 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>
- );
- case LayoutType.Stacked:
- return null;
- default:
- break;
- }
- }
-
- // doc = () => {
- // return Docs.Create.FreeformDocument([], { _height: 200, _width: 200, title: 'title'});
- // }
-
- screenToLocalTransform = () => this._props.ScreenToLocalTransform();
-
- layoutPreviewContents = (outerSpan: number, altLayout?: DataVizTemplateLayout, small: boolean = false, id?: number) => {
- const doc: Doc | undefined = altLayout ? altLayout.template : this._selectedTemplate;
- if (!doc) return;
-
- const layout = altLayout ? altLayout.layout : this._layout;
-
- const docWidth: number = Number(doc._width);
- const docHeight: number = Number(doc._height);
- const horizontalSpan: number = (docWidth + layout.xMargin) * (altLayout ? altLayout.columns : this.columnsCount) - layout.xMargin;
- const verticalSpan: number = (docHeight + layout.yMargin) * (altLayout ? altLayout.rows : this.rowsCount) - layout.yMargin;
- const largerSpan: number = horizontalSpan > verticalSpan ? horizontalSpan : verticalSpan;
- const scaledDown = (input: number) => {
- return input / ((largerSpan / outerSpan) * this._layoutPreviewScale);
- };
- const fontSize = Math.min(scaledDown(docWidth / 3), scaledDown(docHeight / 3));
-
- return (
- // <div className='divvv' style={{width: 100, height: 100, border: `1px solid white`}}>
- // <CollectionFreeFormView
- // // eslint-disable-next-line react/jsx-props-no-spreading
- // {...this._props}
- // Document={new Doc()}
- // isContentActive={returnFalse}
- // setContentViewBox={emptyFunction}
- // NativeWidth={() => 100}
- // NativeHeight={() => 100}
- // pointerEvents={SnappingManager.IsDragging ? returnAll : returnNone}
- // isAnnotationOverlay
- // isAnnotationOverlayScrollable
- // childDocumentsActive={returnFalse}
- // fieldKey={this._props.fieldKey + '_annotations'}
- // dropAction={dropActionType.move}
- // select={emptyFunction}
- // addDocument={returnFalse}
- // removeDocument={returnFalse}
- // moveDocument={returnFalse}
- // renderDepth={this._props.renderDepth + 1}>
- // {null}
- // </CollectionFreeFormView>
- // </div>
- <div className="docCreatorMenu-layout-preview-window-wrapper" id={String(id) ?? undefined}>
- <div className="docCreatorMenu-zoom-button-container">
- <button className="docCreatorMenu-zoom-button" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._layoutPreviewScale *= 1.25)))}>
- <FontAwesomeIcon icon={'minus'} />
- </button>
- <button className="docCreatorMenu-zoom-button zoom-in" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._layoutPreviewScale *= 0.75)))}>
- <FontAwesomeIcon icon={'plus'} />
- </button>
- {altLayout ? (
- <button className="docCreatorMenu-zoom-button trash" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this._savedLayouts.splice(this._savedLayouts.indexOf(altLayout), 1)))}>
- <FontAwesomeIcon icon={'trash'} />
- </button>
- ) : null}
- </div>
- {
- <div
- id={String(id) ?? undefined}
- className={`docCreatorMenu-layout-preview-window ${small ? 'small' : ''}`}
- style={{
- gridTemplateColumns: `repeat(${altLayout ? altLayout.columns : this.columnsCount}, ${scaledDown(docWidth)}px`,
- gridTemplateRows: `${scaledDown(docHeight)}px`,
- gridAutoRows: `${scaledDown(docHeight)}px`,
- rowGap: `${scaledDown(layout.yMargin)}px`,
- columnGap: `${scaledDown(layout.xMargin)}px`,
- }}>
- {this._layout.type === LayoutType.Stacked ? (
- <div
- className="docCreatorMenu-layout-preview-item"
- style={{
- width: scaledDown(docWidth),
- height: scaledDown(docHeight),
- fontSize: fontSize,
- }}>
- All
- </div>
- ) : (
- this.docsToRender.map(num => (
- <div
- onMouseEnter={() => this._dataViz?.setSpecialHighlightedRow(num)}
- onMouseLeave={() => this._dataViz?.setSpecialHighlightedRow(undefined)}
- className="docCreatorMenu-layout-preview-item"
- style={{
- width: scaledDown(docWidth),
- height: scaledDown(docHeight),
- fontSize: fontSize,
- }}>
- {num}
- </div>
- ))
- )}
- </div>
- }
- </div>
- );
- };
-
- get optionsMenuContents() {
- const layoutEquals = (layout: DataVizTemplateLayout) => {}; //TODO: ADD LATER
-
- const layoutOption = (option: LayoutType, optStyle?: {}, specialFunc?: Function) => {
- return (
- <div
- className="docCreatorMenu-dropdown-option"
- style={optStyle}
- onPointerDown={e =>
- this.setUpButtonClick(e, () => {
- specialFunc?.();
- runInAction(() => (this._layout.type = option));
- })
- }>
- {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 any} />
- </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.Stacked)}
- {layoutOption(LayoutType.Grid, undefined, () => {
- if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length));
- })}
- {layoutOption(LayoutType.Row)}
- {layoutOption(LayoutType.Column)}
- {layoutOption(LayoutType.Custom, { borderBottom: `0px` })}
- </div>
- </div>
- <button className="docCreatorMenu-menu-button preview-toggle" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._layoutPreview = !this._layoutPreview)))}>
- <FontAwesomeIcon icon={this._layoutPreview ? 'minus' : 'magnifying-glass'} />
- </button>
- </div>
- {this._layout.type ? this.layoutConfigOptions : null}
- {this._layoutPreview ? this.layoutPreviewContents(this._menuDimensions.width * 0.75) : null}
- {selectionBox(
- 60,
- 20,
- 'repeat',
- undefined,
- repeatOptions.map(num => <option onPointerDown={e => (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,
- 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;
- const templateInfo: DataVizTemplateInfo = { doc: this._selectedTemplate, layout: this._layout, referencePos: { x: this._pageX + 450, y: this._pageY }, columns: this.columnsCount };
- this._dataViz?.createDocsFromTemplate(templateInfo);
- }, 'make docs')
- )
- }>
- <FontAwesomeIcon icon="plus" />
- </button>
- </div>
- </div>
- );
- }
-
- get dashboardContents() {
- const sizes: string[] = ['tiny', 'small', 'medium', 'large', 'huge'];
-
- const fieldPanel = (field: Col) => {
- return (
- <div className="field-panel">
- <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)' }} defaultValue={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 => fieldPanel(field))}</div>
- </div>
- );
- }
-
- get renderSelectedViewType() {
- switch (this._menuContent) {
- case 'templates':
- return this.templatesPreviewContents;
- case 'options':
- return this.optionsMenuContents;
- case 'saved':
- return this.savedLayoutsPreviewContents;
- 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' onPointerDown={this.onResizePointerDown} style={{width: width, left: 0, top: -7}}/>,
- <div className='docCreatorMenu-resizer right' onPointerDown={this.onResizePointerDown} style={{height: height, left: width - 3, top: 0}}/>,
- <div className='docCreatorMenu-resizer bottom' onPointerDown={this.onResizePointerDown} style={{width: width, left: 0, top: height - 3}}/>,
- <div className='docCreatorMenu-resizer left' onPointerDown={this.onResizePointerDown} style={{height: height, left: -7, top: 0}}/>,
- <div className='docCreatorMenu-resizer topRight' onPointerDown={this.onResizePointerDown} style={{left: width - 5, top: -10, cursor: 'nesw-resize'}}/>,
- <div className='docCreatorMenu-resizer topLeft' onPointerDown={this.onResizePointerDown} style={{left: -10, top: -10, cursor: 'nwse-resize'}}/>,
- <div className='docCreatorMenu-resizer bottomRight' onPointerDown={this.onResizePointerDown} style={{left: width - 5, top: height - 5, cursor: 'nwse-resize'}}/>,
- <div className='docCreatorMenu-resizer bottomLeft' onPointerDown={this.onResizePointerDown} style={{left: -10, top: height - 5, cursor: 'nesw-resize'}}/>
- ]; //prettier-ignore
- }
-
- render() {
- const topButton = (icon: string, opt: string, func: Function, 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 any} />
- </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,
- e => {
- this._dragging = true;
- this._startPos = { x: 0, y: 0 };
- this._startPos.x = e.pageX - (this._ref?.getBoundingClientRect().left ?? 0);
- this._startPos.y = e.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('table-cells', 'templates', onPreviewSelected, 'left')}
- {topButton('bars', 'options', onOptionsSelected, 'middle')}
- {topButton('floppy-disk', '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>
- );
- }
-}
-
-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 enum TemplateFieldType {
- TEXT = 'text',
- VISUAL = 'visual',
- UNSET = 'unset',
-}
-
-export enum TemplateFieldSize {
- TINY = 'tiny',
- SMALL = 'small',
- MEDIUM = 'medium',
- LARGE = 'large',
- HUGE = 'huge',
-}
-
-export type Col = {
- sizes: TemplateFieldSize[];
- desc: string;
- title: string;
- type: TemplateFieldType;
- defaultContent?: string;
-};
-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;
- //animation?: boolean;
- fontBold?: boolean;
- fontTransform?: 'uppercase' | 'lowercase';
- fieldViewType?: 'freeform' | 'stacked';
-}
-
-export type Field = {
- tl: [number, number];
- br: [number, number];
- opts: FieldOpts;
- subfields?: Field[];
- types?: TemplateFieldType[];
- sizes?: TemplateFieldSize[];
- isDecoration?: boolean;
- description?: string;
-};
-
-// class ContentField implements Field {
-// tl: [number, number];
-// br: [number, number];
-// opts: FieldOpts;
-// subfields?: Field[];
-// types?: TemplateFieldType[];
-// sizes?: TemplateFieldSize[];
-// description?: string;
-
-// constructor( tl: [number, number], br: [number, number],
-// opts: FieldOpts, subfields?: Field[],
-// types?: TemplateFieldType[],
-// sizes?: TemplateFieldSize[],
-// description?: string) {
-// this.tl = tl;
-// this.br = br;
-// this.opts = opts;
-// this.subfields = subfields;
-// this.types = types;
-// this.sizes = sizes;
-// this.description = description;
-// }
-
-// render = (content: any): Doc => {
-// return new Doc;
-// }
-// }
-
-type DecorationField = Field;
-
-type InkDecoration = {};
-
-type TemplateDecorations = Field | InkDecoration;
-
-interface TemplateOpts extends FieldOpts {}
-
-export interface TemplateDocInfos {
- title: string;
- height: number;
- width: number;
- opts: TemplateOpts;
- fields: Field[];
- decorations: Field[];
-}
-
-export class TemplateLayouts {
- public static get allTemplates(): TemplateDocInfos[] {
- return Object.values(TemplateLayouts).filter(value => typeof value === 'object' && value !== null && 'title' in value) as TemplateDocInfos[];
- }
-
- public static getTemplateByTitle = (title: string): TemplateDocInfos | undefined => {
- switch (title) {
- case 'fourfield1':
- return TemplateLayouts.FourField001;
- case 'fourfield2':
- return TemplateLayouts.FourField002;
- // case 'fourfield3':
- // return TemplateLayouts.FourField003;
- case 'fourfield4':
- return TemplateLayouts.FourField004;
- case 'threefield1':
- return TemplateLayouts.ThreeField001;
- case 'threefield2':
- return TemplateLayouts.ThreeField002;
- default:
- break;
- }
-
- return undefined;
- };
-
- public static FourField001: TemplateDocInfos = {
- title: 'fourfield1',
- width: 416,
- height: 700,
- opts: {
- backgroundColor: '#C0B887',
- cornerRounding: 20,
- borderColor: '#6B461F',
- borderWidth: '12',
- },
- fields: [
- {
- 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,
- },
- },
- {
- 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: 20,
- borderColor: '#8F5B25',
- borderWidth: '6',
- backgroundColor: '#CECAB9',
- },
- },
- {
- 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 to short text that contextualizes the content of field #2',
- opts: {
- backgroundColor: 'transparent',
- contentXCentering: 'h-center',
- color: '#F1F0E9',
- },
- },
- {
- 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: 15,
- borderColor: '#8F5B25',
- borderWidth: '6',
- backgroundColor: '#CECAB9',
- },
- },
- ],
- decorations: [],
- };
-
- public static FourField002: TemplateDocInfos = {
- title: 'fourfield2',
- width: 425,
- height: 778,
- opts: {
- backgroundColor: '#242425',
- },
- fields: [
- {
- 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',
- },
- },
- {
- 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',
- },
- },
- {
- 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',
- },
- },
- {
- tl: [-0.83, 0.2],
- br: [0.83, 0.95],
- types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL],
- sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
- description: 'A medium to large-sized field suitable for an image or longer text that should be the main focus, or share focus with field 1.',
- opts: {
- borderWidth: '8',
- borderColor: '#F8E71C',
- color: 'white',
- backgroundColor: '#242425',
- },
- },
- ],
- decorations: [
- {
- tl: [-0.8, -0.075],
- br: [-0.525, 0.075],
- opts: {
- backgroundColor: '#F8E71C',
- rotation: 45,
- },
- },
- {
- tl: [-0.3075, -0.0245],
- br: [-0.2175, 0.0245],
- opts: {
- backgroundColor: '#F8E71C',
- rotation: 45,
- },
- },
- {
- tl: [-0.045, -0.0245],
- br: [0.045, 0.0245],
- opts: {
- backgroundColor: '#F8E71C',
- rotation: 45,
- },
- },
- {
- tl: [0.2175, -0.0245],
- br: [0.3075, 0.0245],
- opts: {
- backgroundColor: '#F8E71C',
- rotation: 45,
- },
- },
- {
- 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: TemplateDocInfos = {
- title: 'fourfield4',
- width: 414,
- height: 583,
- opts: {
- backgroundColor: '#6CCAF0',
- borderColor: '#1088C3',
- borderWidth: '10',
- },
- fields: [
- {
- 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',
- },
- },
- {
- 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',
- },
- },
- {
- 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',
- },
- },
- {
- 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',
- },
- },
- ],
- decorations: [
- {
- tl: [-0.852, -0.67],
- br: [0.852, 0.51],
- opts: {
- backgroundColor: 'transparent',
- borderColor: '#007C0C',
- borderWidth: '10',
- },
- },
- ],
- };
-
- public static ThreeField001: TemplateDocInfos = {
- title: 'threefield1',
- width: 575,
- height: 770,
- opts: {
- backgroundColor: '#DDD3A9',
- },
- fields: [
- {
- tl: [-0.66, -0.747],
- br: [0.66, 0.247],
- 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: {
- borderColor: 'yellow',
- borderWidth: '8',
- rotation: 45,
- },
- },
- {
- 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',
- },
- },
- {
- 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',
- },
- },
- ],
- decorations: [
- {
- tl: [0.2, -1.32],
- br: [1.8, -0.66],
- opts: {
- backgroundColor: '#CEB155',
- rotation: 45,
- },
- },
- {
- tl: [-1.8, -1.32],
- br: [-0.2, -0.66],
- opts: {
- backgroundColor: '#CEB155',
- rotation: 135,
- },
- },
- {
- tl: [0.33, 0.75],
- br: [1.66, 1.25],
- opts: {
- backgroundColor: '#CEB155',
- rotation: 135,
- },
- },
- {
- tl: [-1.66, 0.75],
- br: [-0.33, 1.25],
- opts: {
- backgroundColor: '#CEB155',
- rotation: 45,
- },
- },
- ],
- };
-
- public static ThreeField002: TemplateDocInfos = {
- title: 'threefield2',
- width: 477,
- height: 662,
- opts: {
- backgroundColor: '#9E9C95',
- },
- fields: [
- {
- 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',
- },
- },
- {
- 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',
- },
- },
- {
- 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',
- },
- },
- ],
- decorations: [
- {
- tl: [-0.025, 0.8],
- br: [0.025, 0.95],
- opts: {
- backgroundColor: '#E0E0DA',
- },
- },
- ],
- };
-}
-
-export class FieldUtils {
- public static contentFields = (fields: Field[]) => {
- let toRet: Field[] = [];
- fields.forEach(field => {
- if (!field.isDecoration) {
- toRet.push(field);
- }
- toRet = toRet.concat(FieldUtils.contentFields(field.subfields ?? []));
- });
-
- return toRet;
- };
-
- 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.5;
- //console.log(wordWidth)
-
- 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;
- //console.log(rowsCount, currFontSize, currTextHeight)
-
- currFontSize += 1;
- }
-
- return currFontSize - 1;
- };
-
- private static getDimensions = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number): { width: number; height: number; coord: { x: number; y: number } } => {
- const l = (coords.tl[0] * parentHeight) / 2;
- const t = coords.tl[1] * parentWidth / 2; //prettier-ignore
- const r = (coords.br[0] * parentHeight) / 2;
- const b = coords.br[1] * parentWidth / 2; //prettier-ignore
- const width = r - l;
- const height = b - t;
- const coord = { x: l, y: t };
- //console.log(coords, parentWidth, parentHeight, height);
- return { width, height, coord };
- };
-
- public static FreeformField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, content: string, opts: FieldOpts) => {
- const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight);
-
- const docWithBasicOpts = Docs.Create.FreeformDocument([], {
- isDefaultTemplateDoc: true,
- _height: height,
- _width: width,
- title: title,
- x: coord.x,
- y: coord.y,
- backgroundColor: opts.backgroundColor ?? '',
- _layout_borderRounding: `${opts.cornerRounding ?? 0}px`,
- borderColor: opts.borderColor,
- borderWidth: opts.borderWidth,
- opacity: opts.opacity,
- hCentering: opts.contentXCentering,
- _rotation: opts.rotation,
- });
-
- return docWithBasicOpts;
- };
-
- public static TextField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, content: string, opts: FieldOpts) => {
- const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight);
-
- const docWithBasicOpts = Docs.Create.TextDocument(content, {
- isDefaultTemplateDoc: true,
- _height: height,
- _width: width,
- title: title,
- x: coord.x,
- y: coord.y,
- text_fontSize: `${FieldUtils.calculateFontSize(width, height, content, true)}`,
- backgroundColor: opts.backgroundColor ?? '',
- text_fontColor: opts.color,
- contentBold: opts.fontBold,
- text_transform: opts.fontTransform,
- color: opts.color,
- _layout_borderRounding: `${opts.cornerRounding ?? 0}px`,
- borderColor: opts.borderColor,
- borderWidth: opts.borderWidth,
- opacity: opts.opacity,
- hCentering: opts.contentXCentering,
- _rotation: opts.rotation,
- });
-
- docWithBasicOpts._layout_hideScroll = true;
-
- return docWithBasicOpts;
- };
-
- public static ImageField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, content: string, opts: FieldOpts) => {
- const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight);
-
- const doc = Docs.Create.ImageDocument(content, {
- isDefaultTemplateDoc: true,
- _height: height,
- _width: width,
- title: title,
- x: coord.x,
- y: coord.y,
- _layout_fitWidth: false,
- backgroundColor: opts.backgroundColor ?? '',
- _layout_borderRounding: `${opts.cornerRounding ?? 0}px`,
- borderColor: opts.borderColor,
- borderWidth: opts.borderWidth,
- opacity: opts.opacity,
- _rotation: opts.rotation,
- });
-
- //setTimeout(() => {doc._height = height; doc._width = width}, 10);
-
- return doc;
- };
-
- public static CarouselField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, fields: Doc[]) => {
- const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight);
-
- const doc = Docs.Create.Carousel3DDocument(fields, { _height: height, _width: width, title: title, x: coord.x, y: coord.y, text_fontSize: `${height / 2}` });
-
- return doc;
- };
-}
-
-// public static FourField002: TemplateDocInfos = {
-// width: 450,
-// height: 600,
-// fields: [{
-// tl: [-.6, -.9],
-// br: [.6, -.8],
-// types: [FieldType.TEXT],
-// sizes: [FieldSize.TINY]
-// }, {
-// tl: [-.9, -.7],
-// br: [.9, .2],
-// types: [FieldType.TEXT, FieldType.VISUAL],
-// sizes: [FieldSize.MEDIUM, FieldSize.LARGE, FieldSize.HUGE]
-// }, {
-// tl: [-.9, .3],
-// br: [-.05, .9],
-// types: [FieldType.TEXT],
-// sizes: [FieldSize.TINY]
-// }, {
-// tl: [.05, .3],
-// br: [.9, .9],
-// types: [FieldType.TEXT, FieldType.VISUAL],
-// sizes: [FieldSize.MEDIUM, FieldSize.LARGE, FieldSize.HUGE]
-// }]
-// };
-
-// public static TwoFieldPlusCarousel: TemplateDocInfos = {
-// width: 500,
-// height: 600,
-// fields: [{
-// tl: [-.9, -.99],
-// br: [.9, -.7],
-// types: [FieldType.TEXT],
-// sizes: [FieldSize.TINY]
-// }, {
-// tl: [-.9, -.65],
-// br: [.9, .35],
-// types: [],
-// sizes: []
-// }, {
-// tl: [-.9, .4],
-// br: [.9, .95],
-// types: [FieldType.TEXT],
-// sizes: [FieldSize.TINY]
-// }]
-// };
-// }
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu.scss b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss
index 4ea904b8e..57f4a1e94 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu.scss
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss
@@ -7,7 +7,7 @@
.docCreatorMenu-cont {
position: absolute;
- z-index: 100000;
+ z-index: 1000;
// box-shadow: 0px 3px 4px rgba(0, 0, 0, 30%);
// background: whitesmoke;
// color: black;
@@ -45,9 +45,10 @@
font-size: 12px;
width: 18px;
height: 18px;
- border-radius: 2px;
font-size: 12px;
margin-left: auto;
+ margin-right: 5px;
+ margin-bottom: 3px;
}
&.options {
@@ -292,6 +293,7 @@
display: flex;
flex-direction: column;
justify-content: flex-start;
+ color: black;
position: relative;
width: 100%;
height: 100%;
@@ -324,6 +326,7 @@
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);
@@ -350,6 +353,7 @@
border: 0px;
padding: 0px;
font-size: 15px;
+ z-index: 1000;
&.right {
position: absolute;
@@ -422,6 +426,7 @@
align-items: center;
overflow-y: scroll;
position: relative;
+ color: black;
height: 125px;
width: calc(100% - 10px);
-ms-overflow-style: none;
@@ -524,8 +529,6 @@
background: whitesmoke;
background-color: rgb(34, 34, 37);
border-radius: 5px;
- border-top-right-radius: 0px;
- border-bottom-right-radius: 0px;
border: 1px solid rgb(180, 180, 180);
padding: 0px;
font-size: 12px;
@@ -634,6 +637,8 @@
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;
@@ -686,13 +691,24 @@
}
.docCreatorMenu-layout-preview-window-wrapper {
+ flex: 0 0 auto;
display: flex;
justify-content: center;
align-items: center;
- width: 85%;
- height: auto;
+ color: black;
+ width: calc(100% - 50px);
+ height: calc(100% - 50px);
position: relative;
- padding: 0px;
+ 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;
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