diff options
Diffstat (limited to 'src/client/views')
29 files changed, 2608 insertions, 1844 deletions
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index b884eb8c8..67e8078ba 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -65,6 +65,8 @@ import { PresBox, PresSlideBox } from './nodes/trails'; import { FaceRecognitionHandler } from './search/FaceRecognitionHandler'; import { SearchBox } from './search/SearchBox'; import { StickerPalette } from './smartdraw/StickerPalette'; +import { TemplateField } from './nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField'; +import { TemplateFieldUtils } from './nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateFieldUtils'; import { ScrapbookBox } from './nodes/scrapbook/ScrapbookBox'; dotenv.config(); @@ -101,6 +103,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' }; new PingManager(); new KeyManager(); new FaceRecognitionHandler(); + TemplateField.CreateField = TemplateFieldUtils.CreateField; // set the init function for fields // initialize plugins and classes that require plugins CollectionDockingView.Init(TabDocView); diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateManager.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateManager.ts new file mode 100644 index 000000000..6fcca7e30 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateManager.ts @@ -0,0 +1,118 @@ +import { action, makeAutoObservable } from 'mobx'; +import { Col } from '../DocCreatorMenu'; +import { FieldSettings, TemplateField } from '../TemplateFieldTypes/TemplateField'; +import { Template } from '../Template'; +import { Doc, NumListCast } from '../../../../../../fields/Doc'; +import { DataVizBox } from '../../DataVizBox'; +import { TemplateFieldType } from '../TemplateBackend'; +import { TemplateMenuAIUtils } from './TemplateMenuAIUtils'; + +export type Conditional = { + field: string; + operator: '=' | '>' | '<' | 'contains'; + condition: string; + target: string; + attribute: string; + value: string; +} + +export class TemplateManager { + + templates: Template[] = []; + + conditionalFieldLogic: Record<string, Conditional[]> = {}; + + constructor(templateSettings: FieldSettings[]) { + makeAutoObservable(this); + this.templates = this.initializeTemplates(templateSettings); + } + + initializeTemplates = (templateSettings: FieldSettings[]) => templateSettings.map(settings => { + return new Template(settings)}); + + getValidTemplates = (cols: Col[]) => this.templates.filter(template => template.isValidTemplate(cols)); + + addTemplate = (newTemplate: Template) => this.templates.push(newTemplate); + + removeTemplate = (template: Template) => { + this.templates.splice(this.templates.indexOf(template), 1); + template.cleanup(); + }; + + addFieldCondition = (fieldTitle: string, condition: Conditional) => { + if (this.conditionalFieldLogic[fieldTitle] === undefined) { + this.conditionalFieldLogic[fieldTitle] = [condition]; + } else { + this.conditionalFieldLogic[fieldTitle].push(condition); + } + } + + removeFieldCondition = (fieldTitle: string, condition: Conditional) => { + if (this.conditionalFieldLogic[fieldTitle]) { + this.conditionalFieldLogic[fieldTitle] = this.conditionalFieldLogic[fieldTitle].filter(cond => cond !== condition); + } + } + + addDataField = (title: string) => { + this.templates.forEach(template => template.addDataField(title)); + } + + removeDataField = (title: string) => { + this.templates.forEach(template => template.removeDataField(title)); + } + + createDocsFromTemplate = action((dv: DataVizBox, template: Template, cols: Col[], debug: boolean = false) => { + const csvFields = Array.from(Object.keys(dv.records[0])); + + const processContent = async (content: { [title: string]: string }) => { + const templateCopy = template.clone(); + + csvFields + .filter(title => title) + .forEach(title => { + const field = templateCopy.getFieldByTitle(title); + field && field.setContent(content[title], field.viewType); + }); + + const gptFunc = (type: TemplateFieldType) => (type === TemplateFieldType.VISUAL ? TemplateMenuAIUtils.renderGPTImageCall : TemplateMenuAIUtils.renderGPTTextCall); + const applyGPTContent = async () => { + const promises = cols + .filter(field => field.AIGenerated) + .map(field => { + const templateField: TemplateField = templateCopy.getFieldByTitle(field.title) as TemplateField; + if (templateField !== undefined) { + return gptFunc(field.type)(templateCopy, field, templateField.getID); + } + return null; + }) + .filter(p => p !== null); + + await Promise.all(promises); + }; + + await applyGPTContent(); + + templateCopy.applyConditionalLogic(this.conditionalFieldLogic); + + return templateCopy.getRenderedDoc(); + }; + + const rowContents = debug + ? [{}, {}, {}, {}] + : NumListCast(dv.layoutDoc.dataViz_selectedRows).map(row => + csvFields.reduce( + (values, col) => { + values[col] = dv.records[row][col]; + return values; + }, + {} as { [title: string]: string } + ) + ); + + return Promise.all(rowContents.map(processContent)).then( + action(renderedDocs => { + return renderedDocs; + }) + ); + }); +} diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateMenuAIUtils.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateMenuAIUtils.ts new file mode 100644 index 000000000..9bc2bfce2 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateMenuAIUtils.ts @@ -0,0 +1,130 @@ +import { action } from "mobx"; +import { Upload } from "openai/resources"; +import { ClientUtils } from "../../../../../../ClientUtils"; +import { Networking } from "../../../../../Network"; +import { gptImageCall, gptAPICall, GPTCallType } from "../../../../../apis/gpt/GPT"; +import { Col } from "../DocCreatorMenu"; +import { TemplateFieldSize, TemplateFieldType } from "../TemplateBackend"; +import { TemplateField, ViewType } from "../TemplateFieldTypes/TemplateField"; +import { Template } from "../Template"; +import { Doc } from "../../../../../../fields/Doc"; +import { DrawingFillHandler } from "../../../../smartdraw/DrawingFillHandler"; +import { CollectionFreeFormView } from "../../../../collections/collectionFreeForm"; + +export class TemplateMenuAIUtils { + + public static 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); + } + }; + + public static renderGPTImageCall = async (template: Template, col: Col, fieldNumber: number): Promise<boolean> => { + const generateAndLoadImage = async (id: number, prompt: string) => { + const url = await this.generateGPTImage(prompt); + var field: TemplateField = template.getFieldByID(id); + + field.setContent(url ?? '', ViewType.IMG); + field = template.getFieldByID(id); + field.setTitle(col.title); + }; + + const fieldContent: string = template.compiledContent; + + try { + const sysPrompt = + `#${Math.random() * 100}: 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(fieldNumber, prompt); + } catch (e) { + console.log(e); + } + return true; + }; + + public static renderGPTTextCall = async (template: Template, col: Col, fieldNum: number | undefined): 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(`${Math.random() * 100000}: ${prompt}`, GPTCallType.FILL); + + if (res) { + const assignments: { [title: string]: { number: string; content: string } } = JSON.parse(res); + Object.entries(assignments).forEach(([, /* title */ info]) => { + const field: TemplateField = template.getFieldByID(Number(info.number)); + + field.setContent(info.content ?? '', ViewType.TEXT); + field.setTitle(col.title); + }); + } + } catch (err) { + console.log(err); + } + + return true; + }; + + /** + * 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 + */ + public static applyGPTContentToTemplate = async (template: Template, assignments: { [field: string]: Col }): Promise<Template | undefined> => { + const GPTTextCalls = Object.entries(assignments).filter(([, col]) => col.type === TemplateFieldType.TEXT && col.AIGenerated); + const GPTIMGCalls = Object.entries(assignments).filter(([, col]) => col.type === TemplateFieldType.VISUAL && col.AIGenerated); + + if (GPTTextCalls.length) { + const promises = GPTTextCalls.map(([id, col]) => { + return TemplateMenuAIUtils.renderGPTTextCall(template, col, Number(id)); + }); + + await Promise.all(promises); + } + + if (GPTIMGCalls.length) { + const promises = GPTIMGCalls.map(async ([id, col]) => { + return TemplateMenuAIUtils.renderGPTImageCall(template, col, Number(id)); + }); + + await Promise.all(promises); + } + + return template; + }; + +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss index 57f4a1e94..463e69c67 100644 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss @@ -27,54 +27,36 @@ background: whitesmoke; background-color: rgb(50, 50, 50); border-radius: 5px; - border: 1px solid rgb(180, 180, 180); padding: 0px; font-size: 13px; //box-shadow: 3px 3px rgb(29, 29, 31); &:hover { box-shadow: none; - } - - &.right{ - margin-left: 0px; - font-size: 12px; + background-color: rgb(60, 60, 65); } - &.close-menu { - font-size: 12px; - width: 18px; - height: 18px; - font-size: 12px; - margin-left: auto; - margin-right: 5px; - margin-bottom: 3px; + &.no-margin { + margin: 0px; } - &.options { - margin-left: 0px; + &.border { + border: 1px solid rgb(180, 180, 180); } - &:hover { - background-color: rgb(60, 60, 65); + &.float-right { + float: right; + margin-left: auto; } - &.top-bar { - border-bottom: 25px solid #555; - border-left: 12px solid transparent; - border-right: 12px solid transparent; - // border-top-left-radius: 5px; - // border-top-right-radius: 5px; - border-radius: 0px; - height: 0; - width: 50px; + &.absolute-right { + position: absolute; + right: 0px; } - - &.preview-toggle { - margin: 0px; - border-top-left-radius: 0px; - border-bottom-left-radius: 0px; - border-left: 0px; + + &.right{ + margin-left: 0px; + font-size: 12px; } } @@ -230,6 +212,14 @@ &.full { width: 100%; } + + &.no-margin-bottom { + margin-bottom: 0px; + } + + &.no-margin-top { + margin-top: 0px; + } } //------------------------------------------------------------------------------------------------------------------------------------------ @@ -277,18 +267,6 @@ scrollbar-width: none; } -.docCreatorMenu-preview-container { - display: grid; - grid-template-columns: repeat(2, 140px); - grid-template-rows: 140px; - grid-auto-rows: 141px; - overflow-y: scroll; - margin: 0px; - margin-top: 0px; - width: 100%; - height: 100%; -} - .docCreatorMenu-expanded-template-preview { display: flex; flex-direction: column; @@ -297,6 +275,7 @@ position: relative; width: 100%; height: 100%; + flex-grow: 1; .top-panel{ width: 100%; @@ -307,7 +286,7 @@ display: flex; flex-direction: column; justify-content: flex-start; - height: 100%; + height: fit-content; position: absolute; right: 0px; top: 0px; @@ -322,15 +301,12 @@ display: flex; justify-content: center; align-items: center; - width: 113px; - height: 113px; - margin-top: 10px; - margin-left: 10px; + height: 100%; + aspect-ratio: 1; color: none; border: 1px solid rgb(163, 163, 163); border-radius: 5px; box-shadow: 5px 5px rgb(29, 29, 31); - flex: 0 0 auto; &:hover{ background-color: rgb(72, 72, 73); @@ -382,16 +358,15 @@ .docCreatorMenu-preview-image{ background-color: transparent; - height: 100px; - width: 100px; + height: 100%; display: block; object-fit: contain; border-radius: 5px; - &.expanded { - height: 100%; - width: 100%; - } +} + +.docCreatorMenu-variations-tab { + flex-grow: .5; } .docCreatorMenu-section { @@ -399,12 +374,12 @@ flex-direction: column; align-items: center; position: relative; + flex-grow: 1; + height: 100%; + width: 100%; margin: 0px; margin-top: 0px; margin-bottom: 0px; - width: 100%; - height: 200; - flex: 0 0 auto; } .docCreatorMenu-GPT-options-container { @@ -412,28 +387,29 @@ flex-direction: row; justify-content: center; align-items: center; - position: relative; - width: auto; + position: absolute; + left: 50%; + bottom: 0px; margin: 0px; + margin-bottom: 10px; margin-top: 5px; padding: 0px; } .docCreatorMenu-templates-preview-window { - display: flex; - flex-direction: row; - //justify-content: center; - align-items: center; - overflow-y: scroll; - position: relative; - color: black; - height: 125px; + display: grid; + justify-content: space-evenly; + row-gap: 2rem; + grid-template-columns: repeat(auto-fill, minmax(150px, 30%)); + margin: 5px; width: calc(100% - 10px); - -ms-overflow-style: none; - scrollbar-width: none; + height: 100%; + padding-bottom: 40px; - .loading-spinner { - justify-self: center; + &.scrolling { + overflow-y: scroll; + max-height: 300px; + padding-bottom: 0px; } } @@ -447,20 +423,14 @@ position: relative; display: flex; flex-direction: row; + color: whitesmoke; width: 100%; } -.section-reveal-options { - margin-top: 0px; - margin-bottom: 0px; - margin-right: 0px; - margin-left: auto; - border: 0px; - background: none; - - &.float-right { - float: right; - } +.docCreatorMenu-templates-displays { + display: flex; + flex-direction: column; + height: 100%; } .docCreatorMenu-section-title { @@ -690,6 +660,19 @@ } } +.loading-spinner { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; + z-index: 200; + font-size: 20px; + font-weight: bold; + color: #17175e; +} + .docCreatorMenu-layout-preview-window-wrapper { flex: 0 0 auto; display: flex; @@ -776,6 +759,8 @@ } } + + //------------------------------------------------------------------------------------------------------------------------------------------ // DocCreatorMenu dashboard CSS //-------------------------------------------------------------------------------------------------------------------------------------------- @@ -797,6 +782,7 @@ scrollbar-width: none; .panels-container { + display: flex; height: 100%; width: 100%; flex-direction: column; @@ -810,114 +796,12 @@ background-color: rgb(50, 50, 50); } -// .field-panel { -// position: relative; -// display: flex; -// // align-items: flex-start; -// flex-direction: column; -// gap: 5px; -// padding: 5px; -// height: 100px; -// //width: 100%; -// border: 1px solid rgb(180, 180, 180); -// margin: 5px; -// margin-top: 0px; -// border-radius: 3px; -// flex: 0 0 auto; - -// .properties-wrapper { -// display: flex; -// flex-direction: row; -// align-items: flex-start; -// gap: 5px; - -// .field-property-container { -// background-color: rgb(40, 40, 40); -// border: 1px solid rgb(100, 100, 100); -// border-radius: 3px; -// width: 30%; -// height: 25px; -// padding-left: 3px; -// align-items: center; -// color: whitesmoke; -// } - -// .field-type-selection-container { -// display: flex; -// flex-direction: row; -// align-items: center; -// background-color: rgb(40, 40, 40); -// border: 1px solid rgb(100, 100, 100); -// border-radius: 3px; -// width: 31%; -// height: 25px; -// padding-left: 3px; -// color: whitesmoke; - -// .placeholder { -// color: gray; -// } - -// &:hover .placeholder { -// display: none; -// } - -// .bubbles { -// display: none; -// } - -// .text { -// margin-top: 5px; -// margin-bottom: 5px; -// } - -// &:hover .bubbles { -// display: flex; -// flex-direction: row; -// align-items: flex-start; -// } - -// &:hover .type-display { -// display: none; -// } - -// .bubble { -// margin: 5px; -// } - -// &:hover .bubble { -// margin-top: 7px; -// } -// } -// } - -// .field-description-container { -// background-color: rgb(40, 40, 40); -// border: 1px solid rgb(100, 100, 100); -// border-radius: 3px; -// width: 100%; -// height: 100%; -// resize: none; - -// ::-webkit-scrollbar-track { -// background: none; -// } -// } - -// .top-right { -// position: absolute; -// top: 0px; -// right: 0px; -// } -// } -// } - .field-panel { display: flex; flex-direction: column; align-items: center; justify-content: flex-start; - height: 285px; + height: fit-content; width: calc(100% - 10px); border: 1px solid rgb(180, 180, 180); margin: 5px; @@ -938,12 +822,19 @@ border-top-right-radius: 5px; border-top-left-radius: 5px; width: 100%; - height: 20px; + height: fit-content; background-color: rgb(50, 50, 50); color: rgb(168, 167, 167); + font-size: medium; .field-title { color: whitesmoke; + font-size: large; + } + + &:hover { + background-color: rgb(72, 72, 72); + cursor: pointer; } } @@ -1038,14 +929,14 @@ .desc-box { width: 88%; - height: 50px; + height: fit-content; border: 1px solid rgb(180, 180, 180); border-radius: 5px; background-color: rgb(50, 50, 50); box-shadow: 5px 5px rgb(29, 29, 31); .content { - height: calc(100% - 20px); + height: fit-content; width: 100%; background-color: rgb(50, 50, 50); border-bottom-right-radius: 5px; @@ -1057,4 +948,243 @@ } + .conditionals-section { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + width: 100%; + + .conditionals-title { + display: flex; + flex-direction: row; + width: 100%; + justify-content: center; + align-items: center; + margin: 5px; + margin-bottom: 20px; + font-size: large; + } + } + + .form-row { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: flex-start; + color: whitesmoke; + width: 100%; + height: fit-content; + margin-bottom: 15px; + flex-wrap: wrap; + gap: 5px; + + .form-row-plain-text { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: fit-content; + padding-top: 2px; + padding-bottom: 2px; + } + + .operator-options-dropdown { + display: flex; + flex-direction: column; + height: fit-content; + + .operator-dropdown-option { + display: none; + } + + .operator-dropdown-current { + border-radius: 5px; + background-color: rgb(50, 50, 50); + border: 1px solid rgb(180, 180, 180); + text-align: center; + padding: 2.25px; + padding-left: 4px; + padding-right: 4px; + } + + &:hover .operator-dropdown-current { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } + + &:hover .operator-dropdown-option { + display: flex; + height: fit-content; + align-items: center; + border: 1px solid rgb(180, 180, 180); + background-color: rgb(50, 50, 50); + padding: 2.25px; + padding-left: 8px; + padding-right: 8px; + text-align: center; + + &:hover { + background-color: rgb(70, 70, 70); + cursor: pointer; + } + } + } + + .form-row-textarea { + height: 24px; + width: 110px; + border-radius: 5px; + background-color: rgb(50, 50, 50); + border: 1px solid rgb(180, 180, 180); + resize: none; + overflow-y: scroll; + white-space: nowrap; + } + + } + + .form { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + width: 80%; + + .form-action-button { + display: flex; + justify-content: center; + align-items: center; + margin: 3px; + cursor: pointer; + + } + } + +} + +//------------------------------------------------------------------------------------------------------------------------------------------ +// EditingWindow CSS +//-------------------------------------------------------------------------------------------------------------------------------------------- + +.docCreatorMenu-editing-firefly-section { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + padding: 5px; } + +.docCreatorMenu-firefly-options { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + height: fit-content; + width: 100%; +} + +.docCreatorMenu-variation-prompt-row { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 15px; + height: fit-content; + width: 100%; +} + +.docCreatorMenu-variation-prompt-input-textbox { + height: 40px; + width: 80%; + color: white; + margin-top: 1%; + margin-bottom: 1%; + margin-left: 5%; + background-color: rgb(50, 50, 50); + border-radius: 5px; + overflow: hidden; + resize: none; +} + +.options‑menu { + display: flex; + align-items: center; + justify-content: center; + gap: 2rem; + padding: 0.5rem 1rem; + background: rgb(50, 50, 50); + color: whitesmoke; + font-family: system-ui, sans-serif; + font-size: 0.9rem; + flex-wrap: wrap; + } + + .menu‑item { + display: flex; + align-items: center; + gap: 0.5rem; + white-space: nowrap; + } + + .menu‑item input[type="range"] { + width: 7rem; + accent-color: whitesmoke; + } + + .value { + min-width: 2ch; + text-align: right; + } + + .switch { + gap: 0.75rem; + margin-bottom: 0px; + } + + .switch .slider { + position: relative; + width: 2.2rem; + height: 1.1rem; + background: whitesmoke; + border-radius: 1rem; + cursor: pointer; + transition: background 0.2s; + } + + .switch .slider::before { + content: ''; + position: absolute; + top: 0.1rem; + left: 0.1rem; + width: 0.9rem; + height: 0.9rem; + background: rgb(50, 50, 50); + border-radius: 50%; + transition: transform 0.2s; + } + + .switch input { + display: none; + } + + .switch input:checked + .slider { + background: #78c2f1; + } + + .switch input:checked + .slider::before { + transform: translateX(1.1rem); + } + +.firefly-option-label { + padding: .2em .6em .3em; + font-size: 100%; + color: whitesmoke; + text-align: center; + margin-bottom: 0px; + font-weight: 500; +} + + diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx index 64416c26d..9a84e69a9 100644 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx @@ -1,5 +1,6 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Colors } from '@dash/components'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { IDisposer } from 'mobx-utils'; @@ -11,26 +12,35 @@ import { Doc, NumListCast, StrListCast, returnEmptyDoclist } from '../../../../. import { Id } from '../../../../../fields/FieldSymbols'; import { ImageCast, StrCast } from '../../../../../fields/Types'; import { ImageField } from '../../../../../fields/URLField'; +import { Upload } from '../../../../../server/SharedMediaTypes'; 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 { Transform } from '../../../../util/Transform'; import { UndoManager, undoable } from '../../../../util/UndoManager'; import { ObservableReactComponent } from '../../../ObservableReactComponent'; +import { DefaultStyleProvider } from '../../../StyleProvider'; 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 { TemplateField, ViewType } from './TemplateFieldTypes/TemplateField'; import { Template } from './Template'; -import { Field, FieldContentType } from './FieldTypes/Field'; -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { Upload } from '../../../../../server/SharedMediaTypes'; +import { TemplateFieldSize, TemplateFieldType, TemplateLayouts } from './TemplateBackend'; +import { Conditional, TemplateManager } from './Backend/TemplateManager'; +import { DrawingFillHandler } from '../../../smartdraw/DrawingFillHandler'; +import { CgPathIntersect } from 'react-icons/cg'; +import { StaticContentField } from './TemplateFieldTypes/StaticContentField'; +import { TemplateMenuAIUtils } from './Backend/TemplateMenuAIUtils' +import { TemplatePreviewGrid } from './Menu/TemplatePreviewGrid'; +import { FireflyStructureOptions, TemplateEditingWindow } from './Menu/TemplateEditingWindow'; +import { DocCreatorMenuButton } from './Menu/DocCreatorMenuButton'; +import { ConditionalsTextArea } from './Menu/ConditionalsTextarea'; +import { TemplatesRenderPreviewWindow } from './Menu/TemplateRenderPreviewWindow'; +import { TemplateMenuFieldOptions } from './Menu/TemplateMenuFieldOptions'; export enum LayoutType { FREEFORM = 'Freeform', @@ -61,6 +71,7 @@ export type Col = { title: string; type: TemplateFieldType; defaultContent?: string; + AIGenerated?: boolean; }; interface DocCreateMenuProps { @@ -72,33 +83,20 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> // eslint-disable-next-line no-use-before-define static Instance: DocCreatorMenu; - private _disposers: { [name: string]: IDisposer } = {}; - + DEBUG_MODE: boolean = false; 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 _docsRendering: boolean = false; // dictates loading symbol - @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 _userTemplates: Template[] = []; @observable _selectedTemplate: Template | undefined = undefined; @observable _currEditingTemplate: Template | undefined = undefined; + @observable _editedTemplateTrail: Template[] = []; @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; @@ -110,9 +108,8 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> @observable _startPos?: { x: number; y: number }; @observable _shouldDisplay: boolean = false; - @observable _menuContent: 'templates' | 'options' | 'saved' | 'dashboard' = 'templates'; + @observable _menuContent: 'templates' | 'renderPreview' | 'saved' | 'dashboard' | 'templateEditing' = '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 }; @@ -122,7 +119,8 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> @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; + + @observable _variations: Template[] = []; constructor(props: DocCreateMenuProps) { super(props); @@ -134,56 +132,13 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> @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 + this._suggestedTemplates = templates; //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); } @@ -210,17 +165,13 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> .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) => { + setUpButtonClick = (e: React.PointerEvent, func: (...args: any) => void) => { setupMoveUpEvents( this, e, @@ -269,7 +220,6 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> } componentWillUnmount() { - Object.values(this._disposers).forEach(disposer => disposer?.()); document.removeEventListener('pointerdown', this.onPointerDown, true); document.removeEventListener('pointerup', this.onPointerUp); } @@ -319,10 +269,10 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> 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)})}); + 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; }; @@ -364,15 +314,8 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> 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; - } + + async createDocsForPreview(): Promise<Doc[]> { return this._dataViz && this._selectedTemplate ? ((await this.templateManager.createDocsFromTemplate(this._dataViz, this._selectedTemplate, this.fieldsInfos, this.DEBUG_MODE)).filter(doc => doc).map(doc => doc!) ?? []) as unknown as Doc[] : []; } @action updateSelectedTemplate = async (template: Template) => { if (this._selectedTemplate === template) { @@ -380,31 +323,15 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> 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); - }; + // testTemplate = async () => { + // this._suggestedTemplates = this.templateManager.templates; //prettier-ignore + // }; @action addField = () => { - const newFields: Col[] = this._userCreatedFields.concat([{ title: '', type: TemplateFieldType.UNSET, desc: '', sizes: [] }]); + const newFields: Col[] = this._userCreatedFields.concat([{ title: '', type: TemplateFieldType.UNSET, desc: '', sizes: [], AIGenerated: true }]); this._userCreatedFields = newFields; }; @@ -439,11 +366,18 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> }; @action setColType = (column: Col, type: TemplateFieldType) => { + if (type === TemplateFieldType.DATA) { + this.templateManager.addDataField(column.title); + } else if (column.type === TemplateFieldType.DATA) { + this.templateManager.removeDataField(column.title); + } + if (this.selectedFields.includes(column.title)) { this._dataViz?.setColumnType(column.title, type); } else { column.type = type; } + this.forceUpdate(); }; @@ -469,53 +403,10 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> 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 += `---------- NEW TEMPLATE TO INCLUDE: The title is: ${template.title}. Its fields are: `; descriptions += template.descriptionSummary; }); @@ -540,10 +431,7 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> const inputText = fieldDescriptions.concat(colDescriptions); - ++this._callCount; - const origCount = this._callCount; - - const prompt: string = `(${origCount}) ${inputText}`; + const prompt: string = `(${Math.random() * 100000}) ${inputText}`; this._GPTLoading = true; @@ -555,15 +443,15 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> const brokenDownAssignments: [Template, { [fieldID: number]: Col }][] = []; Object.entries(assignments).forEach(([tempTitle, assignment]) => { - const template = templates.filter(t => t.mainField.getTitle() === tempTitle)[0]; + const template = templates.filter(temp => temp.title === 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); + if (!col.AIGenerated) { + var field = template.getFieldByID(Number(fieldID)); + field.setContent(col.defaultContent ?? '', col.type === TemplateFieldType.VISUAL ? ViewType.IMG : ViewType.TEXT); + field = template.getFieldByID(Number(fieldID)); field.setTitle(col.title); } else { a[Number(fieldID)] = this.getColByTitle(colTitle); @@ -585,776 +473,109 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> }; 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); + const templates: Template[] = []; - setTimeout(() => { - this.setSuggestedTemplates(templates); - this._GPTLoading = false; - }); - }; + if (this.DEBUG_MODE) { + templates.push(...this.templateManager.templates); + } else { + this._dataViz?.updateColDefaults(); - 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)); + const contentFields = this.fieldsInfos.filter(field => field.type !== TemplateFieldType.DATA); - field.setContent(url ?? '', FieldContentType.IMAGE); - field.setTitle(column.title); - }; + templates.push(...this.templateManager.getValidTemplates(contentFields)); - const fieldContent: string = template.compiledContent; + const assignments = await this.assignColsToFields(templates, contentFields); - 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); + const renderedTemplatePromises = assignments.map(([template, assgns]) => TemplateMenuAIUtils.applyGPTContentToTemplate(template, assgns)); - await generateAndLoadImage(String(fieldNumber), col, prompt); - } catch (e) { - console.log(e); + await Promise.all(renderedTemplatePromises); } - 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; + setTimeout( + action(() => { + this.setSuggestedTemplates(templates); + this._GPTLoading = false; + }) + ); }; - addRenderedCollectionToMainview = () => { - const collection = this._renderedDocCollection; - if (!collection) return; + generateVariations = async (onDoc: Doc, prompt: string, options: FireflyStructureOptions): Promise<string[]> => { + const { numVariations, temperature, useStyleRef } = options; + this.variations = []; 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; - } - }; + const clone: Doc = (await Doc.MakeClone(onDoc)).clone; + mainCollection.addDocument(clone); + clone.x = 10000; + clone.y = 10000; - 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> - ); + await DrawingFillHandler.drawingToImage(clone, 100 - temperature, prompt, useStyleRef ? clone : undefined, this, numVariations) - 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> - ); + return this.variations; } - get templatesPreviewContents() { - const GPTOptions = <div></div>; + variations: string[] = [] - 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 addVariation = (url: string) => { + this.variations.push(url); } - @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; + addRenderedCollectionToMainview = (collection: Doc) => { + if (collection) { + 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(); } }; - @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, - }; - } + @action editLastTemplate = () => { if (this._editedTemplateTrail.length) this._currEditingTemplate = this._editedTemplateTrail.pop()} - /** - * 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; - } - }; + @action setExpandedView = (template: Template | undefined) => { - const collection: Doc = collectionFactory()(this._fullyRenderedDocs, { - isDefaultTemplateDoc: true, - _height: verticalSpan, - _width: horizontalSpan, - title: 'title', - backgroundColor: 'gray', - }); + if (template) { + this._menuContent = 'templateEditing'; + this._currEditingTemplate && this._editedTemplateTrail.push(this._currEditingTemplate); + } else { + this._menuContent = 'templates'; + } - this.applyLayout(collection, this._fullyRenderedDocs); + this._currEditingTemplate = template; - this._renderedDocCollection = collection; + //Docs.Create.FreeformDocument([doc], { _height: NumListCast(doc._height)[0], _width: NumListCast(doc._width)[0], title: ''}); }; - 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} + @computed + get templatesView() { return ( + <div className='docCreatorMenu-templates-view'> + <div className="docCreatorMenu-templates-displays"> + <TemplatePreviewGrid + title={'Suggested Templates'} + menu={this} + loading={this._GPTLoading} + optionsButtonOpts={this.optionsButtonOpts} + templates={this._suggestedTemplates} /> - </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 className="docCreatorMenu-GPT-options"> + <div className="docCreatorMenu-GPT-options-container"> + <DocCreatorMenuButton icon={'arrows-rotate'} styles={'border'} function={this.generatePresetTemplates}/> </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> - ); - } + </div> + )}; + + private optionsButtonOpts: [IconProp, () => any] = ['gear', () => (this._menuContent = 'dashboard')]; get renderSelectedViewType() { switch (this._menuContent) { - case 'templates': - return this.templatesPreviewContents; - case 'options': - return this.optionsMenuContents; - case 'dashboard': - return this.dashboardContents; - default: - return undefined; - } + case 'templates': return this.templatesView; + case 'templateEditing': return <TemplateEditingWindow template={this._currEditingTemplate as Template} menu={this} />; + case 'renderPreview': return <TemplatesRenderPreviewWindow menu={this}/>; + case 'dashboard': return <TemplateMenuFieldOptions menu={this} templateManager={this.templateManager}/>; + } // prettier-ignore + return undefined; } get resizePanes() { @@ -1375,34 +596,17 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> } 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> + const topButton = (icon: string, opt: string, func: () => void, tag: string) => ( + <div className={`top-button-container ${tag} ${opt === this._menuContent ? 'selected' : ''}`}> + <div className="top-button-content" onPointerDown={e => this.setUpButtonClick(e, action(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)); - }; + const onPreviewSelected = () => (this._menuContent = 'templates'); + const onSavedSelected = () => (this._menuContent = 'dashboard'); + const onOptionsSelected = () => (this._menuContent = 'renderPreview'); return ( <div className="docCreatorMenu"> @@ -1435,9 +639,7 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> return true; }, emptyFunction, - undoable(clickEv => { - clickEv.stopPropagation(); - }, 'drag menu') + undoable(clickEv => clickEv.stopPropagation(), 'drag menu') ) }> <div className="docCreatorMenu-top-buttons-container"> @@ -1445,9 +647,7 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> {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> + <DocCreatorMenuButton icon={'minus'} styles={'float-right'} function={this.closeMenu}/> </div> {this.renderSelectedViewType} </div> @@ -1455,4 +655,4 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> </div> ); } -} +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx deleted file mode 100644 index c5254c17d..000000000 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx +++ /dev/null @@ -1,117 +0,0 @@ -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 deleted file mode 100644 index ea9b566b3..000000000 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/Field.tsx +++ /dev/null @@ -1,66 +0,0 @@ -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 deleted file mode 100644 index 3886774d2..000000000 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/FieldUtils.tsx +++ /dev/null @@ -1,79 +0,0 @@ -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 deleted file mode 100644 index 47b43f051..000000000 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/StaticField.tsx +++ /dev/null @@ -1,147 +0,0 @@ -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/Menu/ConditionalsTextarea.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/ConditionalsTextarea.tsx new file mode 100644 index 000000000..2ca0bde3f --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/ConditionalsTextarea.tsx @@ -0,0 +1,65 @@ +import { observer } from "mobx-react"; +import { ObservableReactComponent } from "../../../../ObservableReactComponent"; +import { Conditional } from "../Backend/TemplateManager"; +import { action, makeObservable, observable, runInAction } from "mobx"; +import React from "react"; + +interface ConditionalsTextAreaProps { + conditional: Conditional; + property: keyof Conditional; +} + +@observer +export class ConditionalsTextArea extends ObservableReactComponent<ConditionalsTextAreaProps> { + + private mirrorRef: HTMLSpanElement | null = null; + + @observable private inputWidth: string = '60px'; + + constructor(props: ConditionalsTextAreaProps) { + super(props); + makeObservable(this); + } + + setMirrorRef: React.LegacyRef<HTMLSpanElement> = (node) => { this.mirrorRef = node } + + @action updateInputWidth() { + const mirror = this.mirrorRef; + if (mirror) { + const width = mirror.offsetWidth; + if ( width + 8 > 60) this.inputWidth = `${width + 8}px`; + } + } + + render() { + return ( + <div style={{ display: 'inline-block', position: 'relative' }}> + <span + ref={this.setMirrorRef} + style={{ + position: 'absolute', + visibility: 'hidden', + whiteSpace: 'pre', + font: 'inherit', + padding: 0, + }} + > + {this._props.conditional[this._props.property] || ' '} + </span> + <input + className="form-row-input" + value={this.props.conditional[this.props.property] ?? ''} + onChange={e => { + runInAction(() => { + this.props.conditional[this.props.property] = e.target.value as any; + }); + this.updateInputWidth(); + }} + style={{ width: this.inputWidth }} + placeholder={this.props.property} + /> + </div> + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/DocCreatorMenuButton.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/DocCreatorMenuButton.tsx new file mode 100644 index 000000000..1d8139d40 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/DocCreatorMenuButton.tsx @@ -0,0 +1,41 @@ +import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { ObservableReactComponent } from "../../../../ObservableReactComponent"; +import React from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { setupMoveUpEvents, returnFalse } from "../../../../../../ClientUtils"; +import { emptyFunction } from "../../../../../../Utils"; +import { undoable } from "../../../../../util/UndoManager"; +import { observer } from "mobx-react"; + +interface DocCreatorMenuButtonProps { + icon: IconProp; + function: () => any; + styles?: string; +} + +@observer +export class DocCreatorMenuButton extends ObservableReactComponent<DocCreatorMenuButtonProps> { + + setupButtonClick = (e: React.PointerEvent, func: (...args: any) => void) => { + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + undoable(clickEv => { + clickEv.stopPropagation(); + clickEv.preventDefault(); + func(); + }, 'create docs') + ); + }; + + render() { + + return ( + <button className={`docCreatorMenu-menu-button ${this._props.styles}`} onPointerDown={e => this.setupButtonClick(e, async () => this._props.function())}> + <FontAwesomeIcon icon={this._props.icon} /> + </button> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateEditingWindow.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateEditingWindow.tsx new file mode 100644 index 000000000..3eaed79b6 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateEditingWindow.tsx @@ -0,0 +1,242 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, makeAutoObservable, makeObservable, observable, reaction, runInAction } from "mobx"; +import React from "react"; +import { returnFalse, returnEmptyFilter, returnTrue } from "../../../../../../ClientUtils"; +import { emptyFunction } from "../../../../../../Utils"; +import { Doc, returnEmptyDoclist } from "../../../../../../fields/Doc"; +import { DefaultStyleProvider } from "../../../../StyleProvider"; +import { DocumentView, DocumentViewInternal } from "../../../DocumentView"; +import { DocCreatorMenu } from "../DocCreatorMenu"; +import { TemplatePreviewGrid } from "./TemplatePreviewGrid"; +import { observer } from "mobx-react"; +import { Transform } from "../../../../../util/Transform"; +import { Template } from "../Template"; +import { TemplateMenuAIUtils } from "../Backend/TemplateMenuAIUtils"; +import { ObservableReactComponent } from "../../../../ObservableReactComponent"; +import { IDisposer } from "mobx-utils"; +import { ImageField } from "../../../../../../fields/URLField"; +import { DocCreatorMenuButton } from "./DocCreatorMenuButton"; +import { TbHistory } from "react-icons/tb"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { docStyle } from "pdfjs-dist/types/web/ui_utils"; + +export type FireflyStructureOptions = { + numVariations: number; + temperature: number; + useStyleRef: boolean; +} + +interface FireflyVariationsTabProps { + menu: DocCreatorMenu; + template: Template; +} + +@observer +export class FireflyVariationsTab extends ObservableReactComponent<FireflyVariationsTabProps> { + + private prompt: string = 'Use this template to generate an empty baseball card template.'; + + @observable private promptInput: HTMLTextAreaElement | null = null; + + @observable _loading: boolean = false; + @observable _variationsTabOpen: boolean = false; + @observable _variationURLs: string[] = []; + + @observable private fireflyOptions: FireflyStructureOptions = {numVariations: 3, temperature: 0, useStyleRef: false}; + + constructor(props: FireflyVariationsTabProps) { + super(props); + makeObservable(this); + } + + generateVariations = async () => { + this._props.menu._variations = []; + this._loading = true; + const cloneTemplate = this._props.template.clone(false); + cloneTemplate.setMatteBackground(); + const doc: Doc = cloneTemplate.getRenderedDoc()!; + this._variationURLs = await this._props.menu.generateVariations(doc, this.prompt, this.fireflyOptions); + this._variationURLs.forEach(url => { + const newTemplate: Template = this._props.template.clone(true); + this._props.menu._variations.push(newTemplate); + }); + setTimeout(() => { + this._variationURLs.forEach((url, i) => { + this._props.menu._variations[i].setImageAsBackground(url, true); + }); + this._loading = false; + }); + } + + setPromptInputRef: React.LegacyRef<HTMLTextAreaElement> = (node) => { + this.promptInput = node; + } + + private optionsButtonOpts: [IconProp, () => any] = ['gear', () => {}]; + private previewBoxRightButtonOpts: [IconProp, () => any] = ['gear', () => this.forceUpdate()]; + + render() { + return ( + <div className='docCreatorMenu-editing-firefly-section'> + <div className="docCreatorMenu-option-divider full no-margin-bottom"/> + <TemplatePreviewGrid + menu={this._props.menu} + title={'Generate Variations'} + loading={this._loading} + styles={'scrolling'} + templates={this._props.menu._variations} + optionsButtonOpts={this.optionsButtonOpts} + previewBoxRightButtonOpts={this.previewBoxRightButtonOpts} + /> + <div className="docCreatorMenu-firefly-options"> + <div className="docCreatorMenu-variation-prompt-row"> + <textarea + className="docCreatorMenu-variation-prompt-input-textbox" + ref={this.setPromptInputRef} + onChange={e => this.prompt = e.target.value} + onInput={() => { + if (this.promptInput !== null) { + this.promptInput.style.height = 'auto'; + this.promptInput.style.height = this.promptInput.scrollHeight + 'px'; + } + }} + defaultValue={''} + placeholder={'Enter a custom prompt here (optional)'} + /> + <DocCreatorMenuButton icon={'arrows-rotate'} styles={'border'} function={this.generateVariations}/> + </div> + <nav className="options‑menu"> + <label className="menu‑item switch"> + <input type="checkbox" checked={this.fireflyOptions.useStyleRef} + onChange={(e) => runInAction(() => this.fireflyOptions.useStyleRef = e.target.checked)} + /> + <span className="slider round"></span> + <span className="firefly-option-label">Use template as style guide</span> + </label> + <div className="menu‑item"> + <span className="firefly-option-label">Variations</span> + <input type="range" id="variations" + min="1" + max="5" + value={this.fireflyOptions.numVariations} + onChange={(e) => runInAction(() => this.fireflyOptions.numVariations = Number(e.target.value))} + /> + <span className="value" id="varVal">{this.fireflyOptions.numVariations}</span> + </div> + <div className="menu‑item"> + <span className="firefly-option-label">Temperature</span> + <input type="range" id="temperature" + min="1" + max="100" + value={this.fireflyOptions.temperature} + onChange={(e) => runInAction(() => this.fireflyOptions.temperature = Number(e.target.value))} + /> + <span className="value" id="tempVal">{this.fireflyOptions.temperature}</span> + </div> + </nav> + </div> + </div> + ) + } +} + +interface TemplateEditingWindowProps { + menu: DocCreatorMenu; + template: Template; +} + +@observer +export class TemplateEditingWindow extends ObservableReactComponent<TemplateEditingWindowProps> { + + private disposers: { [name: string]: IDisposer } = {}; + + @observable private previewWindow: HTMLDivElement | null = null; + + @observable _variationsTabOpen: boolean = false; + + constructor(props: TemplateEditingWindowProps) { + super(props); + makeObservable(this); + } + + componentDidMount(): void { + this.disposers.windowDimensions = reaction(() => + this._props.menu._resizing, + () => { this.forceUpdate() }, + { fireImmediately: true } + ); + } + + componentWillUnmount() { + Object.values(this.disposers).forEach(disposer => disposer?.()); + } + + setContainerRef: React.LegacyRef<HTMLDivElement> = (node) => { + this.previewWindow = node; + } + + @action setVariationTab = (open: boolean) => { + this._variationsTabOpen = open; + if (this.previewWindow && open) { + this.previewWindow.style.height = String(Number(this.previewWindow.clientHeight) * .6); + } else if (this.previewWindow && !open) { + this.previewWindow.style.height = String(Number(this.previewWindow.clientHeight) * 5/3); + } + } + + get renderedDocPreview(){ + const doc: Doc = this._props.template.getRenderedDoc() as unknown as Doc; + + return ( + <div className="docCreatorMenu-expanded-template-preview" ref={this.setContainerRef}> + {this.previewWindow ? <DocumentView + Document={doc} + isContentActive={emptyFunction} + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + PanelWidth={() => this.previewWindow?.clientWidth ?? 500} + PanelHeight={() => this.previewWindow?.clientHeight ?? 500} + ScreenToLocalTransform={() => new Transform(-this._props.menu._pageX - 5, -this._props.menu._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} + /> : null} + </div> + ) + } + + render() { + return ( + <div className='docCreatorMenu-templates-view'> + <div className="docCreatorMenu-expanded-template-preview"> + <div className="top-panel"/> + {this.renderedDocPreview} + {this._variationsTabOpen ? <FireflyVariationsTab + menu={this._props.menu} + template={this._props.template} + /> + : null} + <div className="right-buttons-panel"> + <DocCreatorMenuButton icon={'minimize'} function={() => { + // if (this._props.template === this._props.menu._selectedTemplate) { + // this._props.menu.updateRenderedPreviewCollection(this._props.template); + // } + this._props.menu.setExpandedView(undefined); + }}/> + <DocCreatorMenuButton icon={'lightbulb'} function={() => this.setVariationTab(!this._variationsTabOpen)}/> + <DocCreatorMenuButton icon={'arrow-rotate-backward'} function={() => { this._props.menu.editLastTemplate(); this.forceUpdate(); }}/> + </div> + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateMenuFieldOptions.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateMenuFieldOptions.tsx new file mode 100644 index 000000000..beda45ac3 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateMenuFieldOptions.tsx @@ -0,0 +1,193 @@ +import { makeObservable, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import { ObservableReactComponent } from "../../../../ObservableReactComponent"; +import { Col, DocCreatorMenu } from "../DocCreatorMenu"; +import React from "react"; +import { Conditional, TemplateManager } from "../Backend/TemplateManager"; +import { TemplateFieldType, TemplateFieldSize } from "../TemplateBackend"; +import { DocCreatorMenuButton } from "./DocCreatorMenuButton"; + +interface TemplateMenuFieldOptionsProps { + menu: DocCreatorMenu; + templateManager: TemplateManager; +} + +@observer +export class TemplateMenuFieldOptions extends ObservableReactComponent<TemplateMenuFieldOptionsProps> { + + @observable _collapsedCols: String[] = []; //any columns whose options panels are hidden + + constructor(props: TemplateMenuFieldOptionsProps) { + super(props); + makeObservable(this); + } + + @observable private _newCondCache: Record<string, Conditional> = {}; + + getParams = (title: string, parameters?: Conditional): Conditional => { + if (parameters) return parameters; + + if (!this._newCondCache[title]) { + this._newCondCache[title] = observable<Conditional>({ + field: title, + operator: '=', + condition: '', + target: 'Own', + attribute: '', + value: '' + }); + } + return this._newCondCache[title]; + }; + + conditionForm = (title: string, parameters?: Conditional, empty: boolean = false) => { + + const contentFieldTitles = this._props.menu.fieldsInfos.filter(field => field.type !== TemplateFieldType.DATA).map(field => field.title).concat('Template'); + var params: Conditional = this.getParams(title, parameters); + + return ( + <div className='form'> + <div className='form-row'> + <div className='form-row-plain-text'>If</div> + <div className='form-row-plain-text'>{title}</div> + <div className="operator-options-dropdown"> + <span className="operator-dropdown-current">{params.operator ?? '='}</span> + <div className='operator-dropdown-option' onPointerDown={() => {params.operator = '='}}>{'='}</div> + </div> + <input + className="form-row-textarea" + onChange={e => runInAction(() => params.condition = e.target.value)} + placeholder='value' + value={params.condition} + /> + <div className='form-row-plain-text'>then</div> + <div className="operator-options-dropdown"> + <span className="operator-dropdown-current">{params.target ?? 'Own'}</span> + {contentFieldTitles.map(fieldTitle => + <div className='operator-dropdown-option' onPointerDown={() => {params.target = fieldTitle}}>{fieldTitle === title ? 'Own' : fieldTitle}</div> + )} + </div> + <input + className="form-row-textarea" + onChange={e => runInAction(() => params.attribute = e.target.value)} + placeholder='attribute' + value={params.attribute} + /> + <div className='form-row-plain-text'>{'becomes'}</div> + <input + className="form-row-textarea" + onChange={e => runInAction(() => params.value = e.target.value)} + placeholder='value' + value={params.value} + /> + </div> + {empty ? + <DocCreatorMenuButton icon={'plus'} styles={'float-right border'} function={() => { + this._newCondCache[title] = observable<Conditional>({ + field: title, + operator: '=', + condition: '', + target: 'Own', + attribute: '', + value: '' + }); + this._props.templateManager.addFieldCondition(title, params); + }}/> + : + <DocCreatorMenuButton icon={'minus'} styles={'float-right border'} function={() => this._props.templateManager.removeFieldCondition(title, params)}/> + } + </div> + ) + } + + fieldPanel = (field: Col, id: number) => ( + <div className="field-panel" key={id}> + <div className="top-bar" onPointerDown={e => this._props.menu.setUpButtonClick(e, runInAction(() => () => { + if (this._collapsedCols.includes(field.title)) { + this._collapsedCols = this._collapsedCols.filter(col => col !== field.title); + } else { + this._collapsedCols.push(field.title); + } + }))}> + <span className="field-title">{`${field.title} Field`}</span> + <DocCreatorMenuButton icon={'minus'} styles={'no-margin absolute-right'} function={() => this._props.menu.removeField(field)}/> + </div> + { this._collapsedCols.includes(field.title) ? null : + <> + <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._props.menu.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' + : field.type === TemplateFieldType.DATA ? 'Data Field' + : '' + }</span> + <div className="bubbles"> + <input className="bubble" type="radio" name="type" onClick={() => this._props.menu.setColType(field, TemplateFieldType.TEXT)} /> + <div className="text">Text</div> + <input className="bubble" type="radio" name="type" onClick={() => this._props.menu.setColType(field, TemplateFieldType.VISUAL)} /> + <div className="text">File</div> + <input className="bubble" type="radio" name="type" onClick={() => this._props.menu.setColType(field, TemplateFieldType.DATA)} /> + <div className="text">Data</div> + </div> + </div> + </div> + </div> + { field.type === TemplateFieldType.DATA ? null : + (<> + <div className="sizes-box"> + <div className="top-bar"> Valid Sizes </div> + <div className="content"> + <div className="bubbles"> + {Object.values(TemplateFieldSize).map(size => ( + <div key={field + size}> + <input className="bubble" type="checkbox" name="type" checked={field.sizes.includes(size)} onChange={e => this._props.menu.modifyColSizes(field, size, e.target.checked)} /> + <div className="text">{size}</div> + </div> + ))} + </div> + </div> + </div> + <div className="desc-box"> + <div className="top-bar"> Prompt </div> + <textarea + className="content" + onChange={e => this._props.menu.setColDesc(field, e.target.value)} + defaultValue={field.desc === this._props.menu._dataViz?.GPTSummary?.get(field.title)?.desc ? '' : field.desc} + placeholder={this._props.menu._dataViz?.GPTSummary?.get(field.title)?.desc ?? 'Add a description/prompt to help with template generation.'} + /> + </div> + </>) + } + <div className="conditionals-section"> + <span className="conditionals-title">Conditional Logic</span> + {this.conditionForm(field.title, undefined, true)} + {this._props.templateManager.conditionalFieldLogic[field.title]?.map(condition => this.conditionForm(condition.field, condition))} + </div> + </> + } + </div> + ); + + + + render() { + return ( + <div className="docCreatorMenu-dashboard-view"> + <div className="topbar"> + <DocCreatorMenuButton icon={'plus'} function={this._props.menu.addField}/> + <DocCreatorMenuButton icon={'arrow-left'} styles={'float-right'} function={() => runInAction(() => (this._props.menu._menuContent = 'templates'))}/> + </div> + <div className="panels-container">{this._props.menu.fieldsInfos.map((field, i) => this.fieldPanel(field, i))}</div> + </div> + ); + } + + +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewBox.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewBox.tsx new file mode 100644 index 000000000..de2f9e455 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewBox.tsx @@ -0,0 +1,97 @@ +import { Colors } from "@dash/components/src"; +import { FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import { Template } from "../Template"; +import { makeObservable, observable, reaction, runInAction } from "mobx"; +import React from "react"; +import { ObservableReactComponent } from "../../../../ObservableReactComponent"; +import { DocCreatorMenu } from "../DocCreatorMenu"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { DocumentView } from "../../../DocumentView"; +import { emptyFunction } from "../../../../../../Utils"; +import { returnEmptyFilter, returnFalse } from "../../../../../../ClientUtils"; +import { Transform } from "../../../../../util/Transform"; +import { DefaultStyleProvider } from "../../../../StyleProvider"; +import { Doc, returnEmptyDoclist } from "../../../../../../fields/Doc"; +import { IDisposer } from "mobx-utils"; +import { ImageField } from "../../../../../../fields/URLField"; +import { ImageCast } from "../../../../../../fields/Types"; +import { observer } from "mobx-react"; + +export interface TemplatePreviewBoxProps { + template: Template; + menu: DocCreatorMenu; + leftButtonOpts?: [icon: IconProp, func: (...args: any) => void] + rightButtonOpts?: [icon: IconProp, func: (...args: any) => void] +} + +@observer +export class TemplatePreviewBox extends ObservableReactComponent<TemplatePreviewBoxProps> { + + @observable private previewWindow: HTMLDivElement | null = null; + + setContainerRef: React.LegacyRef<HTMLDivElement> = (node) => { + this.previewWindow = node; + } + + constructor(props: TemplatePreviewBoxProps) { + super(props); + makeObservable(this); + } + + get doc() { + return this._props.template.getRenderedDoc() as Doc; + } + + render() { + const template = this._props.template; + + return ( + <div + key={template.title} + className="docCreatorMenu-preview-window" + ref={this.setContainerRef} + style={{ + border: this._props.menu._selectedTemplate === template ? `solid 3px ${Colors.MEDIUM_BLUE}` : '', + boxShadow: this._props.menu._selectedTemplate === template ? `0 0 15px rgba(68, 118, 247, .8)` : '', + }} + onPointerDown={e => this._props.menu.setUpButtonClick(e, () => this._props.menu.updateSelectedTemplate(template))} + > + { this._props.leftButtonOpts ? + <button + className="option-button left" + onPointerDown={e => + this._props.menu.setUpButtonClick(e, () => this._props.leftButtonOpts) + }> + <FontAwesomeIcon icon={this._props.leftButtonOpts![0]} color="white" /> + </button> : null + } + { this._props.rightButtonOpts ? + <button className="option-button right" onPointerDown={e => this._props.menu.setUpButtonClick(e, () => this._props.rightButtonOpts)}> + <FontAwesomeIcon icon={this._props.rightButtonOpts![0]} color="white" /> + </button> : null } + <DocumentView + Document={this.doc} + isContentActive={emptyFunction} // !!! should be return false + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + PanelWidth={() => this.previewWindow?.clientWidth ?? this._props.menu._menuDimensions.height * .3} + PanelHeight={() => this.previewWindow?.clientHeight ?? this._props.menu._menuDimensions.height * .3} + ScreenToLocalTransform={() => new Transform(-this._props.menu._pageX - 5, -this._props.menu._pageY - 35, 1)} + renderDepth={1} + whenChildContentsActiveChanged={emptyFunction} + focus={emptyFunction} + styleProvider={DefaultStyleProvider} + addDocTab={this._props.menu._props.addDocTab} + pinToPres={() => undefined} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + fitContentsToBox={returnFalse} + fitWidth={returnFalse} + hideDecorations={true} + /> + </div> + ) + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewGrid.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewGrid.tsx new file mode 100644 index 000000000..d53853c52 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewGrid.tsx @@ -0,0 +1,61 @@ +import { Colors } from "@dash/components/src"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, makeObservable, observable, runInAction } from "mobx"; +import React from "react"; +import ReactLoading from "react-loading"; +import { Doc } from "../../../../../../fields/Doc"; +import { StrCast } from "../../../../../../fields/Types"; +import { ObservableReactComponent } from "../../../../ObservableReactComponent"; +import { Template } from "../Template"; +import { observer } from "mobx-react"; +import { DocCreatorMenu } from "../DocCreatorMenu"; +import { TemplatePreviewBox } from "./TemplatePreviewBox"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { DocCreatorMenuButton } from "./DocCreatorMenuButton"; + +export interface SuggestedTemplatesProps { + menu: DocCreatorMenu; + loading?: boolean; + templates: Template[]; + title: string; + styles?: string; + optionsButtonOpts?: [IconProp, (...args: any) => any]; + previewBoxLeftButtonOpts?: [IconProp, (...args: any) => any]; + previewBoxRightButtonOpts?: [IconProp, (...args: any) => any]; +} + +@observer +export class TemplatePreviewGrid extends ObservableReactComponent<SuggestedTemplatesProps> { + + constructor(props: SuggestedTemplatesProps) { + super(props); + makeObservable(this); + } + + render() { + return ( + <div className="docCreatorMenu-section"> + <div className="docCreatorMenu-section-topbar"> + <div className="docCreatorMenu-section-title">{this.props.title}</div> + {this._props.optionsButtonOpts ? + <DocCreatorMenuButton icon={this._props.optionsButtonOpts[0] as IconProp} styles={'float-right'} function={() => runInAction(this._props.optionsButtonOpts![1])}/> + : null} + </div> + <div className={"docCreatorMenu-templates-preview-window " + this._props.styles}> + {this._props.loading ? + (<div className="loading-spinner"> + <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} /> + </div>) + : this.props.templates.map(template => ( + <TemplatePreviewBox + template={template} + menu={this.props.menu} + leftButtonOpts={["magnifying-glass", (template: Template) => { this.props.menu.setExpandedView(template); this.forceUpdate(); }]} + rightButtonOpts={this._props.previewBoxRightButtonOpts} + /> + ))} + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateRenderPreviewWindow.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateRenderPreviewWindow.tsx new file mode 100644 index 000000000..219152549 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateRenderPreviewWindow.tsx @@ -0,0 +1,346 @@ +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import { ObservableReactComponent } from "../../../../ObservableReactComponent"; +import { DataVizTemplateLayout, DocCreatorMenu, LayoutType } from "../DocCreatorMenu"; +import React from "react"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { setupMoveUpEvents, returnFalse, returnEmptyFilter } from "../../../../../../ClientUtils"; +import { emptyFunction } from "../../../../../../Utils"; +import { undoable } from "../../../../../util/UndoManager"; +import ReactLoading from "react-loading"; +import { Doc, NumListCast, returnEmptyDoclist } from "../../../../../../fields/Doc"; +import { StrCast } from "../../../../../../fields/Types"; +import { DefaultStyleProvider } from "../../../../StyleProvider"; +import { DocumentView } from "../../../DocumentView"; +import { Transform } from "../../../../../util/Transform"; +import { Docs, DocumentOptions } from "../../../../../documents/Documents"; +import { Template } from "../Template"; + +interface TemplatesRenderPreviewWindowProps { + menu: DocCreatorMenu; +} + +@observer +export class TemplatesRenderPreviewWindow extends ObservableReactComponent<TemplatesRenderPreviewWindowProps> { + + @observable private _layout: { type: LayoutType; yMargin: number; xMargin: number; columns?: number; repeat: number } = { type: LayoutType.FREEFORM, yMargin: 10, xMargin: 10, columns: 0, repeat: 0 }; + + @observable private renderedDocs: Doc[] = []; + @observable private renderedDocCollection: Doc | undefined = undefined; + + @observable private loading: boolean = false; + + constructor(props: TemplatesRenderPreviewWindowProps) { + super(props); + makeObservable(this); + this.updateRenderedPreviewCollection(); + } + + @computed get canMakeDocs() { + return this._props.menu._selectedTemplate !== undefined && this._layout !== undefined; + } + + @computed get docsToRender() { + if (this._props.menu.DEBUG_MODE) { + return [1, 2, 3, 4]; + } else { + return NumListCast(this._props.menu._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; + } + } + + @action updateRenderedPreviewCollection = async () => { + this.loading = true; + this.renderedDocs = await this._props.menu.createDocsForPreview(); + this.updateRenderedDocCollection(); + }; + + /** + * 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.renderedDocs) return; + + 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; + } // prettier-ignore + }; + + const collection = collectionFactory()(this.renderedDocs, { + isDefaultTemplateDoc: true, + title: 'title', + backgroundColor: 'gray', + x: 200, + y: 200, + _width: 4000, + _height: 4000, + }); + + this.applyLayout(collection, this.renderedDocs); + + this.renderedDocCollection = collection; + + this.loading = false; + + this.forceUpdate(); + }; + + @action updateMargin = (input: string, xOrY: 'x' | 'y') => { + this._layout[`${xOrY}Margin`] = Number(input); + setTimeout(() => { + if (!this.renderedDocCollection || !this.renderedDocs) return; + this.applyLayout(this.renderedDocCollection, this.renderedDocs); + }); + }; + + @action updateColumns = (input: string) => { + this._layout.columns = Number(input); + this.updateRenderedDocCollection(); + }; + + applyLayout = (collection: Doc, docs: Doc[]) => { + const { horizontalSpan, verticalSpan } = this.previewInfo; + collection._height = verticalSpan; + collection._width = horizontalSpan; + + const columns: number = this._layout.columns ?? this.columnsCount; + const xGap: number = this._layout.xMargin; + const yGap: number = this._layout.yMargin; + 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.renderedDocs[0]._height); + const docWidth: number = Number(this.renderedDocs[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, + }; + } + + 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', (input: string) => this.updateMargin(input, 'y'), this._layout.xMargin, '2')} + {optionInput('arrows-left-right', (input: string) => this.updateMargin(input, 'x'), this._layout.xMargin, '3')} + {optionInput('table-columns', this.updateColumns, this._layout.columns, '4', true)} + </div> + ); + default: + break; + } + } + + layoutPreviewContents = action(() => { + return this.loading ? ( + <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._props.menu._menuDimensions.width - 80} + PanelHeight={() => this._props.menu._menuDimensions.height - 105} + ScreenToLocalTransform={() => new Transform(-this._props.menu._pageX - 5, -this._props.menu._pageY - 35, 1)} + renderDepth={5} + whenChildContentsActiveChanged={emptyFunction} + focus={emptyFunction} + styleProvider={DefaultStyleProvider} + addDocTab={this._props.menu._props.addDocTab} + pinToPres={() => undefined} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + fitContentsToBox={returnFalse} + fitWidth={returnFalse} + hideDecorations={true} + /> + </div> + ); + }); + + 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> + ); + }; + + layoutOption = (option: LayoutType, optStyle?: object, specialFunc?: () => void) => { + return ( + <div + className="docCreatorMenu-dropdown-option" + style={optStyle} + onPointerDown={e => + this._props.menu.setUpButtonClick(e, () => { + specialFunc?.(); + runInAction(() => { + this._layout.type = option; + this.updateRenderedDocCollection(); + }); + }) + }> + {option} + </div> + ); + }; + + get optionsMenuContents() { + + 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"> + {this.layoutOption(LayoutType.FREEFORM, undefined, () => { + if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length)); + })} + {this.layoutOption(LayoutType.CAROUSEL)} + {this.layoutOption(LayoutType.CAROUSEL3D)} + {this.layoutOption(LayoutType.MASONRY)} + </div> + </div> + </div> + {this._layout.type ? this.layoutConfigOptions : null} + {this.layoutPreviewContents()} + {this.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(); + //previous implementation deprecated; return later to add or scrap + return; + }, 'save layout') + ) + }> + <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(); + this.renderedDocCollection && this._props.menu.addRenderedCollectionToMainview(this.renderedDocCollection); + }, 'make docs') + ) + }> + <FontAwesomeIcon icon="plus" /> + </button> + </div> + </div> + ); + } + + render() { return this.optionsMenuContents } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.ts new file mode 100644 index 000000000..fd87ae973 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.ts @@ -0,0 +1,220 @@ +import { makeAutoObservable } from 'mobx'; +import { Col } from './DocCreatorMenu'; +import { TemplateFieldType, TemplateLayouts } from './TemplateBackend'; +import { DynamicField } from './TemplateFieldTypes/DynamicField'; +import { FieldSettings, TemplateField, ViewType } from './TemplateFieldTypes/TemplateField'; +import { Conditional } from './Backend/TemplateManager'; +import { ImageField } from '../../../../../fields/URLField'; +import { Doc } from '../../../../../fields/Doc'; +import { TemplateDataField } from './TemplateFieldTypes/DataField'; + +export class Template { + _mainField: DynamicField; + + private dataFields: TemplateDataField[] = []; + + /** + * A Template can be created from a description of its fields (FieldSettings) or from a DynamicField + * @param definition definition of template as settings or DynamicField + */ + constructor(definition: FieldSettings | DynamicField) { + makeAutoObservable(this); + this._mainField = definition instanceof DynamicField ? definition : this.setupMainField(definition); + } + + get childFields(): TemplateField[] { + return this._mainField?.getSubfields ?? []; + } + get allFields(): TemplateField[] { + return this._mainField?.getAllSubfields ?? []; + } + get contentFields(): TemplateField[] { + return this.allFields.filter(field => field.isContentField); + } + get doc() { + return this._mainField?.renderedDoc; + } + get title() { + return this._mainField?.getTitle(); + } + + 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; + } + + cleanup = () => { + //dispose each subfields disposers, etc. + }; + + clone = (withContent: boolean = false) => { + const clone = new Template(this._mainField?.makeClone(undefined, withContent) ?? TemplateLayouts.BasicSettings); + this.dataFields.forEach(field => clone.addDataField(field.title)); + return clone; + }; + + getRenderedDoc = () => this.doc; + + getFieldByID = (id: number): TemplateField => this.allFields.filter(field => field.getID === id)[0]; + + getFieldByTitle = (title: string) => [...this.allFields, ...this.dataFields].filter(field => field.getTitle() === title)[0]; + + setupMainField = (templateInfo: FieldSettings) => TemplateField.CreateField(templateInfo, 1, undefined) as DynamicField; + + printFieldInfo = () => { + this.allFields.forEach(field => { + const doc = field.renderedDoc; + }); + }; + + assignColToField = (fieldID: number, col: Col) => { + const field = this.getFieldByID(fieldID); + field.setContent(col.defaultContent ?? '', col.type === TemplateFieldType.VISUAL ? ViewType.IMG : ViewType.TEXT); + field.setTitle(col.title); + } + + addDataField = (title: string, content?: string) => { + this.dataFields.push(new TemplateDataField(title, content)); + } + + removeDataField = (title: string) => { + this.dataFields = this.dataFields.filter(field => !(field.title === title)); + } + + isValidTemplate = (cols: Col[]) => { + const maxMatches = this.maxMatches(this.getMatches(cols)); + return maxMatches === this.contentFields.length && this.title !== 'template_framework'; + }; + + applyConditionalLogicToField = (field: TemplateField | TemplateDataField, logic: Record<string, Conditional[]>) => { + if (field instanceof DynamicField) return; + const fieldStatements: Conditional[] = logic[field.getTitle()]; + const content = field.getContent() + fieldStatements && fieldStatements.forEach(statement => { + console.log(statement); + if (content === statement.condition) { + if (statement.target === 'Template') { + this._mainField.renderedDoc![statement.attribute] = statement.value; + Object.assign(this._mainField.settings.opts, {[statement.attribute]: statement.value}); + } else { + const targetField: TemplateField = this.getFieldByTitle(statement.target) as TemplateField; + if (targetField) { + targetField.renderedDoc![statement.attribute] = statement.value; + Object.assign(targetField.settings.opts, {[statement.attribute]: statement.value}); + } + } + } + }) + } + + applyConditionalLogic = (logic: Record<string, Conditional[]>) => { + const fields: (TemplateField | TemplateDataField)[] = [...this.allFields, ...this.dataFields]; + fields.forEach(field => this.applyConditionalLogicToField(field, logic)); + } + + setImageAsBackground(url: string, makeTransparent: boolean = false) { + const fieldSettings: FieldSettings = { + tl: [-1, -1], + br: [1, 1], + opts: {}, + viewType: ViewType.IMG, + } + + const field: TemplateField = TemplateField.CreateField(fieldSettings, Math.random() * 100 + 100, this._mainField); + field.setContent(url); + + if (makeTransparent) { + this.allFields.forEach(field => { + field.updateDocSetting('backgroundColor', 'transparent'); + field.updateDocSetting('borderWidth', '0'); + }); + } + + this._mainField.makeBackgroundField(field); + } + + /** + * This function is just a hack for now to get around weird document icon stuff (specifically it misses the background) + */ + setMatteBackground(makeTransparent: boolean = false) { + if (this._mainField.hasBackground) { + return; + } + + const fieldSettings: FieldSettings = { + tl: [-1, -1], + br: [1, 1], + opts: {backgroundColor: String(this._mainField.renderedDoc!.backgroundColor)}, + viewType: ViewType.TEXT, + } + + const field: TemplateField = TemplateField.CreateField(fieldSettings, Math.random() * 100 + 100, this._mainField); + + if (makeTransparent) { + this.allFields.forEach(field => { + field.updateDocSetting('backgroundColor', 'transparent'); + field.updateDocSetting('borderWidth', '0'); + }); + } + + this._mainField.makeBackgroundField(field); + } + + 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]) { + 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; + }; +} diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx deleted file mode 100644 index 0a5097d4a..000000000 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx +++ /dev/null @@ -1,139 +0,0 @@ -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.ts index d3282eda3..26fd3a8fc 100644 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.tsx +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.ts @@ -1,10 +1,10 @@ -import { FieldSettings, ViewType } from "./FieldTypes/Field"; -import { } from "./FieldTypes/StaticField"; +import { FieldSettings, ViewType } from './TemplateFieldTypes/TemplateField'; export enum TemplateFieldType { TEXT = 'text', VISUAL = 'visual', UNSET = 'unset', + DATA = 'data', } export enum TemplateFieldSize { @@ -20,6 +20,14 @@ export class TemplateLayouts { return Object.values(TemplateLayouts); } + public static BasicSettings: FieldSettings = { + title: 'template_framework', + tl: [0, 0], + br: [400, 700], + viewType: ViewType.FREEFORM, + opts: {}, + }; + public static FourField001: FieldSettings = { title: 'fourfield001', tl: [0, 0], @@ -27,7 +35,7 @@ export class TemplateLayouts { viewType: ViewType.FREEFORM, opts: { backgroundColor: '#C0B887', - cornerRounding: .05, + _layout_borderRounding: '.05', //borderColor: '#6B461F', //borderWidth: '12', }, @@ -41,9 +49,9 @@ export class TemplateLayouts { description: 'A title field for very short text that contextualizes the content.', opts: { backgroundColor: 'transparent', - color: '#F1F0E9', - contentXCentering: 'h-center', - fontBold: true, + text_fontColor: '#F1F0E9', + hCentering: 'h-center', + contentBold: true, }, }, { @@ -54,9 +62,9 @@ export class TemplateLayouts { sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], description: 'The main focus of the template; could be an image, long text, etc.', opts: { - cornerRounding: .05, + _layout_borderRounding: '.05', borderColor: '#8F5B25', - borderWidth: '6', + borderWidth: 6, backgroundColor: '#CECAB9', }, }, @@ -69,8 +77,8 @@ export class TemplateLayouts { description: 'A caption for field #2, very short text.', opts: { backgroundColor: 'transparent', - contentXCentering: 'h-center', - color: '#F1F0E9', + hCentering: 'h-center', + text_fontColor: '#F1F0E9', }, }, { @@ -81,9 +89,9 @@ export class TemplateLayouts { sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], description: 'A medium-sized field for medium/long text.', opts: { - cornerRounding: .05, + _layout_borderRounding: '.05', borderColor: '#8F5B25', - borderWidth: '6', + borderWidth: 6, backgroundColor: '#CECAB9', }, }, @@ -93,7 +101,7 @@ export class TemplateLayouts { public static FourField002: FieldSettings = { title: 'fourfield002', viewType: ViewType.FREEFORM, - tl: [0,0], + tl: [0, 0], br: [425, 778], opts: { backgroundColor: '#242425', @@ -107,10 +115,10 @@ export class TemplateLayouts { 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', + borderWidth: 8, borderColor: '#F8E71C', backgroundColor: '#242425', - color: 'white', + text_fontColor: 'white', }, }, { @@ -122,9 +130,9 @@ export class TemplateLayouts { description: 'A tiny field for just a word or two of plain text.', opts: { backgroundColor: 'transparent', - color: 'white', - contentXCentering: 'h-center', - fontTransform: 'uppercase', + text_fontColor: 'white', + hCentering: 'h-center', + text_transform: 'uppercase', }, }, { @@ -136,9 +144,9 @@ export class TemplateLayouts { description: 'A tiny field for just a word or two of plain text.', opts: { backgroundColor: 'transparent', - color: 'white', - contentXCentering: 'h-center', - fontTransform: 'uppercase', + text_fontColor: 'white', + hCentering: 'h-center', + text_transform: 'uppercase', }, }, { @@ -149,9 +157,9 @@ export class TemplateLayouts { 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', + borderWidth: 8, borderColor: '#F8E71C', - color: 'white', + text_fontColor: 'white', backgroundColor: '#242425', }, }, @@ -161,7 +169,7 @@ export class TemplateLayouts { br: [-0.525, 0.075], opts: { backgroundColor: '#F8E71C', - rotation: 45, + _rotation: 45, }, }, { @@ -170,7 +178,7 @@ export class TemplateLayouts { br: [-0.2175, 0.0245], opts: { backgroundColor: '#F8E71C', - rotation: 45, + _rotation: 45, }, }, { @@ -179,7 +187,7 @@ export class TemplateLayouts { br: [0.045, 0.0245], opts: { backgroundColor: '#F8E71C', - rotation: 45, + _rotation: 45, }, }, { @@ -188,7 +196,7 @@ export class TemplateLayouts { br: [0.3075, 0.0245], opts: { backgroundColor: '#F8E71C', - rotation: 45, + _rotation: 45, }, }, { @@ -197,7 +205,7 @@ export class TemplateLayouts { br: [0.8, 0.075], opts: { backgroundColor: '#F8E71C', - rotation: 45, + _rotation: 45, }, }, ], @@ -266,8 +274,8 @@ export class TemplateLayouts { public static FourField004: FieldSettings = { title: 'fourfield04', viewType: ViewType.FREEFORM, - tl: [0,0], - br: [414,583], + tl: [0, 0], + br: [414, 583], opts: { backgroundColor: '#6CCAF0', //borderColor: '#1088C3', @@ -283,9 +291,9 @@ export class TemplateLayouts { description: 'A tiny field for just a word or two of plain text.', opts: { backgroundColor: '#E2B4F5', - borderWidth: '9', + borderWidth: 9, borderColor: '#9222F1', - contentXCentering: 'h-center', + hCentering: 'h-center', }, }, { @@ -297,9 +305,9 @@ export class TemplateLayouts { description: 'A tiny field for just a word or two of plain text.', opts: { backgroundColor: '#F5B4DD', - borderWidth: '9', + borderWidth: 9, borderColor: '#E260F3', - contentXCentering: 'h-center', + hCentering: 'h-center', }, }, { @@ -310,7 +318,7 @@ export class TemplateLayouts { 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', + borderWidth: 16, borderColor: '#A2BD77', }, }, @@ -322,7 +330,7 @@ export class TemplateLayouts { sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], description: 'A medium to large field for text that describes the visual content above', opts: { - borderWidth: '9', + borderWidth: 9, borderColor: '#F0D601', backgroundColor: '#F3F57D', }, @@ -334,7 +342,7 @@ export class TemplateLayouts { opts: { backgroundColor: 'transparent', borderColor: '#007C0C', - borderWidth: '10', + borderWidth: 10, }, }, ], @@ -343,218 +351,229 @@ export class TemplateLayouts { public static FourField005: FieldSettings = { title: 'fourfield05', viewType: ViewType.FREEFORM, - tl: [0,0], - br: [400,550], + tl: [0, 0], + br: [400, 514], opts: { backgroundColor: '#95A575', }, subfields: [ { viewType: ViewType.STATIC, - tl: [-0.9, -.925], - br: [-.075, -.775], + tl: [-0.9, -0.925], + br: [-0.075, -0.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", + borderWidth: 8, + hCentering: 'h-center', backgroundColor: '#B8DC90', }, }, { viewType: ViewType.STATIC, - tl: [.075, -.925], - br: [.9, -.775], + tl: [0.075, -0.925], + br: [0.9, -0.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", + borderWidth: 8, + hCentering: 'h-center', backgroundColor: '#B8DC90', }, }, { viewType: ViewType.DEC, - tl: [-.82, -.4], - br: [-.5, -.2], + tl: [-0.82, -0.4], + br: [-0.5, -0.2], opts: { backgroundColor: '#94B058', borderColor: '#3B4A2C', - borderWidth: '8', + borderWidth: 8, }, }, { viewType: ViewType.STATIC, - tl: [-0.66, -.65], - br: [0.66, .25], + tl: [-0.66, -0.65], + br: [0.66, 0.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', + borderWidth: 8, backgroundColor: '#B8DC90', }, }, { viewType: ViewType.STATIC, - tl: [-.875, .425], - br: [0.875, .925], + tl: [-0.875, 0.425], + br: [0.875, 0.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", + borderWidth: 8, + hCentering: 'h-center', backgroundColor: '#B8DC90', }, }, { viewType: ViewType.DEC, - tl: [-1.1, -.62], - br: [-.9, -.5], + tl: [-1.1, -0.62], + br: [-0.9, -0.5], opts: { backgroundColor: '#7A9D31', borderColor: '#3B4A2C', - borderWidth: '8', + borderWidth: 8, }, }, { viewType: ViewType.DEC, tl: [-1.1, 0], - br: [-.9, .15], + br: [-0.9, 0.15], opts: { backgroundColor: '#94B058', borderColor: '#3B4A2C', - borderWidth: '8', + borderWidth: 8, }, }, { viewType: ViewType.DEC, - tl: [-.93, -.265], - br: [-.715, -.125], + tl: [-0.93, -0.265], + br: [-0.715, -0.125], opts: { backgroundColor: '#728745', borderColor: '#3B4A2C', - borderWidth: '8', + borderWidth: 8, }, }, { viewType: ViewType.DEC, - tl: [.7, -.45], - br: [.85, -.3], + tl: [0.7, -0.45], + br: [0.85, -0.3], opts: { backgroundColor: '#7A9D31', borderColor: '#3B4A2C', - borderWidth: '8', + borderWidth: 8, }, }, { viewType: ViewType.DEC, - tl: [.8, .03], - br: [1.2, .33], + tl: [0.8, 0.03], + br: [1.2, 0.33], opts: { backgroundColor: '#728745', borderColor: '#3B4A2C', - borderWidth: '8', + borderWidth: 8, }, }, { viewType: ViewType.DEC, - tl: [.875, -.13], - br: [1.2, .12], + tl: [0.875, -0.13], + br: [1.2, 0.12], opts: { backgroundColor: '#94B058', borderColor: '#3B4A2C', - borderWidth: '8', + borderWidth: 8, }, }, - ] - } + ], + }; public static FourFieldCarousel: FieldSettings = { title: 'title_fourfieldcarousel', viewType: ViewType.FREEFORM, - tl:[0,0], - br:[500, 600], + tl: [0, 0], + br: [500, 600], opts: { - backgroundColor: '#DDD3A9', + backgroundColor: '#D7CBAB', }, subfields: [ { viewType: ViewType.STATIC, - tl: [-0.8, -.9], - br: [0.8, -.5], + tl: [-0.8, -0.9], + br: [0.8, -0.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", + hCentering: 'h-center', backgroundColor: 'transparent', + text_transform: 'uppercase', }, }, { viewType: ViewType.CAROUSEL3D, - tl: [-0.9, -.3], - br: [0.9, .9], + tl: [-0.9, -0.5], + br: [0.9, 0.25], opts: { - borderColor: 'yellow', - borderWidth: '8', - backgroundColor: 'transparent', + borderColor: '#847F69', + borderWidth: 8, + backgroundColor: '#C8BA94', }, subfields: [ { viewType: ViewType.STATIC, - tl: [-.3, -.6], - br: [.3, .6], + tl: [-0.4, -0.6], + br: [0.4, 0.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', + //borderColor: 'yellow', + //borderWidth: '8', }, }, { viewType: ViewType.STATIC, - tl: [-.3, -.6], - br: [.3, .6], + tl: [-0.4, -0.6], + br: [0.4, 0.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', + //borderColor: 'black', + //borderWidth: '8', }, }, { viewType: ViewType.STATIC, - tl: [-.3, -.6], - br: [.3, .6], + tl: [-0.4, -0.6], + br: [0.4, 0.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', + //borderColor: 'yellow', + //borderWidth: '8', }, }, - ] + ], }, - ] - } + { + viewType: ViewType.STATIC, + tl: [-0.9, 0.35], + br: [0.9, 0.9], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], + description: 'A medium text field for a description of the content in the carousel.', + opts: { + hCentering: 'h-center', + backgroundColor: 'transparent', + }, + }, + ], + }; public static ThreeField001: FieldSettings = { title: 'threefield001', viewType: ViewType.FREEFORM, - tl: [0,0], + tl: [0, 0], br: [575, 770], opts: { backgroundColor: '#DDD3A9', @@ -567,23 +586,23 @@ export class TemplateLayouts { description: 'A medium to large field for visual content that is the central focus.', opts: { borderColor: 'yellow', - borderWidth: '8', + borderWidth: 8, backgroundColor: '#DDD3A9', - rotation: 45, + _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: [-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, @@ -594,7 +613,7 @@ export class TemplateLayouts { description: 'A very small text field for one to a few words. A good caption for the image.', opts: { backgroundColor: 'transparent', - contentXCentering: 'h-center', + hCentering: 'h-center', }, }, { @@ -606,7 +625,7 @@ export class TemplateLayouts { description: 'A medium to large text field for a thorough description of the image. ', opts: { backgroundColor: 'transparent', - color: 'white', + text_fontColor: 'white', }, }, { @@ -615,18 +634,18 @@ export class TemplateLayouts { br: [1.8, -0.66], opts: { backgroundColor: '#CEB155', - rotation: 45, + _rotation: 45, }, subfields: [ { viewType: ViewType.DEC, - tl: [-1, -.7], - br: [1, -.625], + tl: [-1, -0.7], + br: [1, -0.625], opts: { backgroundColor: 'yellow', }, }, - ] + ], }, { viewType: ViewType.FREEFORM, @@ -634,18 +653,18 @@ export class TemplateLayouts { br: [-0.2, -0.66], opts: { backgroundColor: '#CEB155', - rotation: 135, + _rotation: 135, }, subfields: [ { viewType: ViewType.DEC, - tl: [-1, -.7], - br: [1, -.625], + tl: [-1, -0.7], + br: [1, -0.625], opts: { backgroundColor: 'yellow', }, }, - ] + ], }, { viewType: ViewType.FREEFORM, @@ -653,18 +672,18 @@ export class TemplateLayouts { br: [1.66, 1.25], opts: { backgroundColor: '#CEB155', - rotation: 135, + _rotation: 135, }, subfields: [ { viewType: ViewType.DEC, - tl: [-1, -.7], - br: [1, -.625], + tl: [-1, -0.7], + br: [1, -0.625], opts: { backgroundColor: 'yellow', }, }, - ] + ], }, { viewType: ViewType.FREEFORM, @@ -672,18 +691,18 @@ export class TemplateLayouts { br: [-0.33, 1.25], opts: { backgroundColor: '#CEB155', - rotation: 45, + _rotation: 45, }, subfields: [ { viewType: ViewType.DEC, - tl: [-1, -.7], - br: [1, -.625], + tl: [-1, -0.7], + br: [1, -0.625], opts: { backgroundColor: 'yellow', }, }, - ] + ], }, ], }; @@ -691,7 +710,7 @@ export class TemplateLayouts { public static ThreeField002: FieldSettings = { title: 'threefield002', viewType: ViewType.FREEFORM, - tl: [0,0], + tl: [0, 0], br: [477, 662], opts: { backgroundColor: '#9E9C95', @@ -705,7 +724,7 @@ export class TemplateLayouts { sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], description: 'A medium to large visual field for the main content of the template', opts: { - borderWidth: '15', + borderWidth: 15, borderColor: '#E0E0DA', }, }, @@ -718,10 +737,10 @@ export class TemplateLayouts { 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', + text_fontColor: '#AF0D0D', + text_transform: 'uppercase', + contentBold: true, + hCentering: 'h-left', }, }, { @@ -733,8 +752,8 @@ export class TemplateLayouts { 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', + text_fontColor: 'black', + hCentering: 'h-right', }, }, { @@ -747,6 +766,4 @@ export class TemplateLayouts { }, ], }; -} - - +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DataField.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DataField.ts new file mode 100644 index 000000000..aaa475bed --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DataField.ts @@ -0,0 +1,21 @@ + +import { Template } from "../Template"; +import { TemplateField, ViewType } from "./TemplateField"; + +export class TemplateDataField { + + viewType: ViewType = ViewType.NONE; + + title: string = ''; + content: string | undefined; + + constructor(title: string, content?: string) { + this.title = title; + this.content = content; + } + + setContent(content: string, viewType?: ViewType) { this.content = content } + getContent() { return this.content } + + getTitle() { return this.title } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DecorationField.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DecorationField.ts new file mode 100644 index 000000000..98a9dc7a6 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DecorationField.ts @@ -0,0 +1,3 @@ +import { DynamicField } from './DynamicField'; + +export class DecorationField extends DynamicField {} diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DynamicField.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DynamicField.ts new file mode 100644 index 000000000..1576dd240 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DynamicField.ts @@ -0,0 +1,136 @@ +import { reaction } from 'mobx'; +import { IDisposer } from 'mobx-utils'; +import { Doc, DocListCast } from '../../../../../../fields/Doc'; +import { DocData } from '../../../../../../fields/DocSymbols'; +import { List } from '../../../../../../fields/List'; +import { NumCast } from '../../../../../../fields/Types'; +import { Docs } from '../../../../../documents/Documents'; +import { DocumentType } from '../../../../../documents/DocumentTypes'; +import { FieldSettings, TemplateField, ViewType } from './TemplateField'; + +export class DynamicField extends TemplateField { + protected _disposers: { [name: string]: IDisposer } = {}; + protected _subfields: TemplateField[] = []; + protected backgroundField: TemplateField | undefined; + + get getSubfields() { + return this._subfields; + } + get getAllSubfields(): TemplateField[] { + return this.getSubfields.flatMap(field => [field, ...((field as DynamicField).getAllSubfields ?? [])]); + } + + get hasBackground() { + return this.backgroundField !== undefined; + } + + handleFieldUpdate = (newDocsList: Doc[]) => { + const currRenderedDocs = new Set(this.getSubfields.filter(field => field.renderedDoc).map(field => field.renderedDoc!)); + newDocsList.forEach(doc => !currRenderedDocs.has(doc) && this.addFieldFromDoc(doc)); + currRenderedDocs.forEach(doc => { + if (!newDocsList.includes(doc)) { + this._subfields.forEach(field => field.renderedDoc === doc && this.removeField(field)); + } + }); + }; + + addFieldFromDoc = (doc: Doc) => { + const par = this._renderDoc; + const settings: FieldSettings = { + tl: [Number(doc._x) / NumCast(par?._width, 1), Number(doc?._y) / NumCast(par?._height, 1)], + br: [(Number(doc._x) + Number(doc._width)) / NumCast(par?._width, 1), (Number(doc._y) + Number(doc._height)) / NumCast(par?._height, 1)], + viewType: doc.type === DocumentType.COL ? ViewType.FREEFORM : ViewType.STATIC, + opts: {}, + }; + + this._subfields.push(TemplateField.CreateField(settings, this._subfields.length, this)); + }; + + addField = (field: TemplateField, layer: number = 0) => { + if (!this._subfields.includes(field)) { + console.log('success') + console.log('subs: ', this._subfields) + this._subfields.splice(layer, 0, field); + console.log('subffelds: ', this._subfields) + } + }; + + dispose = () => Object.values(this._disposers).forEach(disposer => disposer?.()); + + removeField = (field: TemplateField) => { + // field.renderedDoc && this._renderDoc && Doc.RemoveDocFromList(this._renderDoc, undefined, field.renderedDoc); + this._subfields.splice(this._subfields.indexOf(field), 1); + (field as DynamicField).dispose?.(); + }; + + // implement Field's abstract method for replacing a subfield with a new one + exchangeFields(newField: TemplateField, oldField: TemplateField) { + this._subfields.splice(this._subfields.indexOf(oldField), 1, newField); + this.refreshRenderedDoc(); + } + + get isContentField(): boolean { + return false; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + setContent(content: string, type: ViewType) {} + + getContent = () => ''; + + addChildToDocument = (doc: Doc) => this._renderDoc && Doc.SetContainer(doc, this._renderDoc); + + makeBackgroundField = (field: TemplateField) => { + if (this.backgroundField && this.backgroundField !== field) { + this.removeField(this.backgroundField); + this.backgroundField = undefined; + } + if (field && field !== this.backgroundField) { + this.addField(field); + this.backgroundField = field; + } + this.refreshRenderedDoc(); + } + + matches = (): Array<number> => []; + + makeClone(parent?: DynamicField, withContent: boolean = false) { + const dynClone = super.makeClone(parent) as DynamicField; + dynClone._subfields = this.getSubfields.map(field => { + if (field === this.backgroundField) { + console.log('background found') + const backgroundField: TemplateField = field.makeClone(dynClone, true); + dynClone.makeBackgroundField(backgroundField); + return backgroundField; + } else { + return field.makeClone(dynClone, withContent) + } + }); + if (dynClone._renderDoc) { + dynClone._renderDoc[DocData].data = new List<Doc>(dynClone.getSubfields.filter(sub => sub.renderedDoc).map(sub => sub.renderedDoc!)); + } + return dynClone; + } + + initRenderDoc = (settings: FieldSettings) => { + this._disposers.fieldList = reaction(() => DocListCast(this._renderDoc?.[Doc.LayoutFieldKey(this._renderDoc)]), this.handleFieldUpdate); + this._subfields = settings.subfields?.map((fieldSettings, index) => {return TemplateField.CreateField(fieldSettings, index, this)}) || []; + const renderedSubfields = this._subfields.filter(field => field.renderedDoc).map(field => field.renderedDoc!); + settings.opts.title = settings.title; + this._renderDoc = (() => { switch (settings.viewType) { + case ViewType.CAROUSEL3D: return Docs.Create.Carousel3DDocument(renderedSubfields, settings.opts); + case ViewType.FREEFORM: + default: return Docs.Create.FreeformDocument(renderedSubfields, settings.opts); + }})(); // prettier-ignore + return this; + }; + + refreshRenderedDoc = () => { + const renderedSubfields = this._subfields.filter(field => field.renderedDoc).map(field => field.renderedDoc!); + this._renderDoc = (() => { switch (this.settings.viewType) { + case ViewType.CAROUSEL3D: return Docs.Create.Carousel3DDocument(renderedSubfields, this.settings.opts); + case ViewType.FREEFORM: + default: return Docs.Create.FreeformDocument(renderedSubfields, this.settings.opts); + }})(); + } +} diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/StaticContentField.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/StaticContentField.ts new file mode 100644 index 000000000..2a8e4f09b --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/StaticContentField.ts @@ -0,0 +1,63 @@ +import { FontSize } from '@dash/components'; +import { FieldResult } from '../../../../../../fields/Doc'; +import { DocData } from '../../../../../../fields/DocSymbols'; +import { RichTextField } from '../../../../../../fields/RichTextField'; +import { ImageField } from '../../../../../../fields/URLField'; +import { Docs } from '../../../../../documents/Documents'; +import { FieldSettings, TemplateField, ViewType } from './TemplateField'; +import { TemplateFieldUtils } from './TemplateFieldUtils'; + +export abstract class StaticContentField extends TemplateField { + protected _content: string = ''; + + getContent = () => this._content ?? 'unset'; + get isContentField(): boolean { + return true; + } + protected setDataContent(viewType: ViewType, fieldKey: string, data: FieldResult, content: string, type?: ViewType) { + super.setContent(content, type); + + if (type === viewType || type === undefined) { + this._content = content; + this._renderDoc && (this._renderDoc[DocData][fieldKey] = data); + } else { + this.changeFieldType(type).setContent(content, type); + } + } +} + +export class ImageTemplateField extends StaticContentField { + setContent(url: string, type?: ViewType) { + this.setDataContent(ViewType.IMG, 'data', new ImageField(url), url, type); + this._renderDoc!['backgroundColor'] = 'white'; + } + + initRenderDoc(settings: FieldSettings) { + settings.opts.title = settings.title ?? ''; + settings.opts._layout_fitWidth = false; + this._renderDoc = Docs.Create.ImageDocument(this._content, settings.opts); + return this; + } + + updateDocSetting(setting: string, newVal: string) { + if (this._renderDoc) this._renderDoc[setting] = newVal; + if (setting !== 'backgroundColor') { + const settings: {[s: string]: string } = {[setting]: newVal} + Object.assign(this.settings.opts, settings); + } + } +} + +export class TextTemplateField extends StaticContentField { + setContent(text: string, type?: ViewType) { + const fontSize: number = TemplateFieldUtils.calculateFontSize(this._dimensions?.width ?? 10, this._dimensions?.height ?? 10, text, true); + this.setDataContent(ViewType.TEXT, 'text', RichTextField.textToRtf(text, undefined, {fontSize: fontSize}), text, type); + } + + initRenderDoc(settings: FieldSettings) { + settings.opts.title = settings.title ?? ''; + settings.opts.text_fontSize = TemplateFieldUtils.calculateFontSize(this._dimensions?.width ?? 10, this._dimensions?.height ?? 10, '', true) + ''; + this._renderDoc = Docs.Create.TextDocument(this._content, settings.opts); + return this; + } +} diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField.ts new file mode 100644 index 000000000..a1107caf3 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField.ts @@ -0,0 +1,174 @@ +/* eslint-disable no-use-before-define */ +import { Doc } from '../../../../../../fields/Doc'; +import { DocumentOptions } from '../../../../../documents/Documents'; +import { Conditional } from '../Backend/TemplateManager'; +import { Col } from '../DocCreatorMenu'; +import { Template } from '../Template'; +import { TemplateFieldSize, TemplateFieldType } from '../TemplateBackend'; + +export abstract class TemplateField { + /** + * Creates and initializes a new TemplateField based on the settings and parameters + * + * implemented in FieldUtils and assigned in main (to avoid import cycles) + * + * @param settings - specification of the field type and other parameters + * @param index - + * @param parent - TemplateField that contains the new field + * @param sameId - + * @returns TemplateField + */ + static CreateField: (settings: FieldSettings, index: number, parent: TemplateField | undefined, sameId?: boolean) => TemplateField; + + protected _parent?: TemplateField; + protected _id: number; + protected _title: string = ''; + protected _settings: FieldSettings; + protected _renderDoc: Doc | undefined; + protected _dimensions: FieldDimensions | undefined; + + constructor(settings: FieldSettings, id: number = 1, parent?: TemplateField) { + this._id = id; + this._parent = parent; + this._settings = settings; + this._title = settings.title ?? ''; + this._dimensions = this.getLocalDimensions(this._settings, this._parent?.getDimensions); + this.applyBasicOpts(this._dimensions, settings); + return this; + } + + get renderedDoc() { + return this._renderDoc; + } + get getDimensions() { + return this._dimensions; + } + get getID() { + return this._id; + } + get getDescription(): string { + return this._settings?.description ?? ''; + } + get viewType(): ViewType | undefined { + return this._settings?.viewType; + } + + get settings(): FieldSettings { + return this._settings; + } + + abstract get isContentField(): boolean; + abstract initRenderDoc(settings: FieldSettings): TemplateField; + abstract getContent(): string; + + setContent(content: string, type?: ViewType) { + if (type) this._settings.viewType = type; + } + + setTitle = (title: string) => { + this._title = title; + this.settings.title = title; + if (this._renderDoc) this._renderDoc.title = title + }; + getTitle = () => this._title; + + updateDocSetting(setting: string, newVal: string) { + if (this._renderDoc) this._renderDoc[setting] = newVal; + const settings: {[s: string]: string } = {[setting]: newVal} + Object.assign(this.settings.opts, settings); + } + + makeClone(parent?: TemplateField, withContent: boolean = false) { + const settings: FieldSettings = structuredClone(this._settings); + const cloned = TemplateField.CreateField(settings, this._id, parent, true); // create a value for this.Document/subfields that we want to ignore + cloned.renderedDoc!.width = this.renderedDoc!.width; + cloned.renderedDoc!.height = this.renderedDoc!.height; + cloned.renderedDoc!.x = this.renderedDoc!.x; + cloned.renderedDoc!.y = this.renderedDoc!.y; + cloned.renderedDoc!.backgroundColor = this.renderedDoc!.backgroundColor; + cloned.setTitle(this._title); + cloned._dimensions = this._dimensions; + withContent && cloned.setContent(this.getContent()); + return cloned; + } + + exchangeFields(newField: TemplateField, oldField: TemplateField) { + throw new Error('Only DynamicField can exchange fields.' + newField._title + ' ' + oldField._title); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + changeFieldType = (newType: ViewType): TemplateField => { + this._settings.viewType = newType; + const newField = TemplateField.CreateField(this._settings, this._id, this._parent, true); + this._parent?.exchangeFields(newField, this); + return newField; + }; + + matches = (cols: Col[]): number[] => { + const colMatchesField = (col: Col) => (this._settings?.sizes?.some(size => col.sizes?.includes(size)) && this._settings.types?.includes(col.type)) ?? false; + + const matches: Array<number> = []; + + cols.forEach((col, v) => { + if (colMatchesField(col)) { + matches.push(v); + } + }); + + return matches; + }; + + private getLocalDimensions = (coords: { tl: [number, number]; br: [number, number] }, parentDimensions?: FieldDimensions): FieldDimensions => { + if (!parentDimensions) { + return { width: coords.br[0] - coords.tl[0], height: coords.br[1] - coords.tl[1], coord: { x: coords.tl[0], y: coords.tl[1] } }; + } + 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 + return { width: r-l, height: b-t, coord: { x: l, y: t } }; //prettier-ignore + }; + + private applyBasicOpts = (dimensions: FieldDimensions, settings: FieldSettings | undefined) => { + const opts: DocumentOptions = settings?.opts ?? {}; + opts.isDefaultTemplateDoc ??= true; + opts._layout_hideScroll ??= true; + opts.x ??= dimensions.coord.x; + opts.y ??= dimensions.coord.y; + opts._height ??= dimensions.height; + opts._width ??= dimensions.width; + opts._nativeWidth ??= dimensions.width; + opts._nativeHeight ??= dimensions.height; + opts._layout_nativeDimEditable ??= true; + opts.layout_boxShadow = 'none'; + }; +} + +export type FieldSettings = { + tl: [number, number]; + br: [number, number]; + opts: DocumentOptions; + types?: TemplateFieldType[]; + sizes?: TemplateFieldSize[]; + title?: string; + viewType: ViewType; + template?: Template; + subfields?: FieldSettings[]; + description?: string; +}; + +export enum ViewType { + CAROUSEL3D = 'carousel3d', + FREEFORM = 'freeform', + STATIC = 'static', + DEC = 'decoration', + IMG = 'image', + TEXT = 'text', + NONE = 'none' +} + +export type FieldDimensions = { + width: number; + height: number; + coord: { x: number; y: number }; +}; + diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateFieldUtils.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateFieldUtils.ts new file mode 100644 index 000000000..b0b531b57 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateFieldUtils.ts @@ -0,0 +1,71 @@ +import { DecorationField } from './DecorationField'; +import { DynamicField } from './DynamicField'; +import { ImageTemplateField, TextTemplateField } from './StaticContentField'; +import { FieldSettings, TemplateField, ViewType } from './TemplateField'; + +export class TemplateFieldUtils { + /** + * Creates and initializes a new TemplateField based on the settings and parameters + * + * implements Field.initField ... see main.tsx + * + * @param settings - specification of the field type and other parameters + * @param index - + * @param parent - optional TemplateField that contains the new field + * @param sameId - + * @returns TemplateField + */ + public static CreateField = (settings: FieldSettings, index: number, parent?: TemplateField, sameId: boolean = false): TemplateField => + ((...args) => { + switch (settings?.viewType) { + case ViewType.FREEFORM: + case ViewType.CAROUSEL3D: return new DynamicField(...args).initRenderDoc(settings); + case ViewType.IMG: return new ImageTemplateField(...args).initRenderDoc(settings); + case ViewType.TEXT: return new TextTemplateField(...args).initRenderDoc(settings); + case ViewType.DEC: return new DecorationField(...args).initRenderDoc(settings); + default: return new TextTemplateField(...args).initRenderDoc(settings); + } // prettier-ignore + })(settings, sameId ? index : parent ? Number(`${parent.getID}${index}`) : 1, parent); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + 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; + }; +} diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx deleted file mode 100644 index 50ae4d72a..000000000 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx +++ /dev/null @@ -1,22 +0,0 @@ -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 diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index ad2731109..21bef3426 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -100,7 +100,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { return this._props.docView?.()?.screenToViewTransform().Scale || 1; } @computed get rowHeight() { - return (this.viewScale * this._tableHeight) / this._tableDataIds.length; + return (this.viewScale * this._tableHeight) / (this._tableDataIds.length + 1); // add 1 for header row } @computed get startID() { return this.rowHeight ? Math.max(Math.floor(this._scrollTop / this.rowHeight) - 1, 0) : 0; @@ -400,8 +400,9 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { this._tableHeight = r?.getBoundingClientRect().height ?? 0; } })}> - <div style={{ height: this.startID * Number(DATA_VIZ_TABLE_ROW_HEIGHT) }} /> + {/* <div style={{ height: this.startID * Number(DATA_VIZ_TABLE_ROW_HEIGHT) }} /> */} <thead> + <tr style={{ height: this.startID * Number(DATA_VIZ_TABLE_ROW_HEIGHT) }}></tr> <tr> {this.columns.map(col => ( <th @@ -440,7 +441,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { <tbody> {this._tableDataIds .filter((rowId, i) => this.startID - 2 <= i && i <= this.endID + 2) - ?.map(rowId => ( + .map(rowId => ( <tr key={rowId} className={`tableBox-row ${this.columns[0]}`} @@ -470,8 +471,9 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { })} </tr> ))} + <tr style={{ height: (this._tableDataIds.length - this.endID) * Number(DATA_VIZ_TABLE_ROW_HEIGHT) }}></tr> </tbody> - <div style={{ height: (this._tableDataIds.length - this.endID) * Number(DATA_VIZ_TABLE_ROW_HEIGHT) }} /> + {/* <div style={{ height: (this._tableDataIds.length - this.endID) * Number(DATA_VIZ_TABLE_ROW_HEIGHT) }} /> */} </table> </div> </div> diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 5a6292fab..060ba353e 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -105,7 +105,7 @@ display: flex; height: 100%; img { - object-fit: contain; + // object-fit: contain; height: 100%; } diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx index f773957e7..23055fdc3 100644 --- a/src/client/views/smartdraw/DrawingFillHandler.tsx +++ b/src/client/views/smartdraw/DrawingFillHandler.tsx @@ -6,6 +6,7 @@ import { Upload } from '../../../server/SharedMediaTypes'; import { gptDescribeImage } from '../../apis/gpt/GPT'; import { Docs } from '../../documents/Documents'; import { Networking } from '../../Network'; +import { DocCreatorMenu } from '../nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu'; import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; import { OpenWhere } from '../nodes/OpenWhere'; import { AspectRatioLimits, FireflyDimensionsMap, FireflyImageDimensions, FireflyStylePresets } from './FireflyConstants'; |
