diff options
author | bobzel <zzzman@gmail.com> | 2025-03-06 16:17:47 -0500 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2025-03-06 16:17:47 -0500 |
commit | 5ad858090f3006631062877d90120e3cc505fada (patch) | |
tree | 9f87a8e1e7098a1025f6f4aac332dbc854db5be3 /src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx | |
parent | 9c2a7c14fd9d0e44609aab30c6323583162009db (diff) | |
parent | adaa107aac8558fa6f46e6ba1263c650c212d506 (diff) |
Merge branch 'master' into aarav_edit
Diffstat (limited to 'src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx')
-rw-r--r-- | src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx | 1458 |
1 files changed, 1458 insertions, 0 deletions
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..64416c26d --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx @@ -0,0 +1,1458 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Colors } from '@dash/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 { 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; +}; + +interface DocCreateMenuProps { + addDocTab: (doc: Doc | Doc[], where: OpenWhere) => boolean; +} + +@observer +export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> { + // eslint-disable-next-line no-use-before-define + 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: DocCreateMenuProps) { + 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: React.PointerEvent, 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 HTMLElement).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: PointerEvent): 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: PointerEvent): 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(([, col]) => col.type === TemplateFieldType.TEXT && this._userCreatedFields.includes(col)); + const GPTIMGCalls = Object.entries(assignments).filter(([, 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; + } + } + + 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> + ); + } +} |