From ef5948e183b15009615818e22e9ea9c748abce2c Mon Sep 17 00:00:00 2001 From: Nathan-SR <144961007+Nathan-SR@users.noreply.github.com> Date: Thu, 10 Oct 2024 01:02:51 -0400 Subject: docCreatorMenu refactor work --- .../DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx | 2328 ++++++++++++++++++++ 1 file changed, 2328 insertions(+) create mode 100644 src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx (limited to 'src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx') 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..91c86bef1 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx @@ -0,0 +1,2328 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Colors } from 'browndash-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, FieldType, 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, DocumentOptions } 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'; + +export enum LayoutType { + Stacked = 'stacked', + Grid = 'grid', + Row = 'row', + Column = 'column', + Custom = 'custom', +} + +@observer +export class DocCreatorMenu extends ObservableReactComponent { + 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 => { + 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(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(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 => { + console.log(prompt); + + try { + const res = await gptImageCall(prompt); + + if (res) { + const result = await Networking.PostToServer('/uploadRemoteImage', { sources: res }); + 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: FieldSettings) => { + 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 => { + 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 => { + 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: FieldSettings = 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: FieldSettings = 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 => { + 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: FieldSettings = 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[] = 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 = +
+ 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 - 5,-this._pageY - 35, 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} + /> +
+ + + return ( +
+
+ {rendered} +
+ + +
+
+ + ); + } + + get templatesPreviewContents() { + const renderedTemplates: Doc[] = []; + + const GPTOptions =
; + + // + + return ( +
+ {this._expandedPreview ? ( + this.editingWindow + ) : ( +
+
+
+
Suggested Templates
+ +
+
400 ? 'center' : '' }}> + {this._GPTLoading ? ( +
+ +
+ ) : ( + this._suggestedTemplates + ?.map(doc => ({ icon: ImageCast(doc.icon), doc })) + .filter(info => info.icon && info.doc) + .map(info => ( +
this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(info.doc)))}> + + + +
+ )) + )} +
+
+
+ +
+ {this._GPTOpt ? GPTOptions : null} +
+
+
+
+
+
Your Templates
+ +
+
400 ? 'center' : '' }}> +
this.testTemplate()}> + +
+ {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 ( +
this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(info.doc)))}> + + + +
+ ); + })} +
+
+
+ )} +
+ ); + } + + get savedLayoutsPreviewContents() { + return ( +
+ {this._savedLayouts.map((layout, index) => ( +
this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedSavedLayout(layout)))}> + {this.layoutPreviewContents(87, layout, true, index)} +
+ ))} +
+ ); + } + + @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 ( +
+
+ +
+ func(e.currentTarget.value)} className="docCreatorMenu-input config layout-config" /> +
+ ); + }; + + switch (this._layout.type) { + case LayoutType.Row: + return
{optionInput('arrows-left-right', this.updateXMargin, this._layout.xMargin, '0')}
; + case LayoutType.Column: + return
{optionInput('arrows-up-down', this.updateYMargin, this._layout.yMargin, '1')}
; + case LayoutType.Grid: + return ( +
+ {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)} +
+ ); + 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 ( + //
+ // 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} + // + //
+
+
+ + + {altLayout ? ( + + ) : null} +
+ { +
+ {this._layout.type === LayoutType.Stacked ? ( +
+ All +
+ ) : ( + this.docsToRender.map(num => ( +
this._dataViz?.setSpecialHighlightedRow(num)} + onMouseLeave={() => this._dataViz?.setSpecialHighlightedRow(undefined)} + className="docCreatorMenu-layout-preview-item" + style={{ + width: scaledDown(docWidth), + height: scaledDown(docHeight), + fontSize: fontSize, + }}> + {num} +
+ )) + )} +
+ } +
+ ); + }; + + get optionsMenuContents() { + const layoutEquals = (layout: DataVizTemplateLayout) => {}; //TODO: ADD LATER + + const layoutOption = (option: LayoutType, optStyle?: {}, specialFunc?: Function) => { + return ( +
+ this.setUpButtonClick(e, () => { + specialFunc?.(); + runInAction(() => (this._layout.type = option)); + }) + }> + {option} +
+ ); + }; + + const selectionBox = (width: number, height: number, icon: string, specClass?: string, options?: JSX.Element[], manual?: boolean): JSX.Element => { + return ( +
+
+ +
+ {manual ? ( + + ) : ( + + )} +
+ ); + }; + + const repeatOptions = [0, 1, 2, 3, 4, 5]; + + return ( +
+
+
+
{this._layout.type ? this._layout.type.toUpperCase() : 'Choose Layout'}
+
+ {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` })} +
+
+ +
+ {this._layout.type ? this.layoutConfigOptions : null} + {this._layoutPreview ? this.layoutPreviewContents(this._menuDimensions.width * 0.75) : null} + {selectionBox( + 60, + 20, + 'repeat', + undefined, + repeatOptions.map(num => ) + )} +
+
+ + +
+
+ ); + } + + get dashboardContents() { + const sizes: string[] = ['tiny', 'small', 'medium', 'large', 'huge']; + + const fieldPanel = (field: Col) => { + return ( +
+
+ {`${field.title} Field`} + +
+
+
+
Title
+