diff options
13 files changed, 307 insertions, 437 deletions
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..6d63078a8 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateManager.ts @@ -0,0 +1,99 @@ +import { action, makeAutoObservable } from 'mobx'; +import { Col } from '../DocCreatorMenu'; +import { FieldSettings } from '../TemplateFieldTypes/TemplateField'; +import { Template } from '../Template'; +import { NumListCast } from '../../../../../../fields/Doc'; +import { DataVizBox } from '../../DataVizBox'; +import { TemplateFieldType } from '../TemplateBackend'; +import { TemplateMenuGPTManager } from './TemplateMenuGPTManager'; + +export type Conditional = { + field: string; + operator: '=' | '>' | '<'; + condition: string; + target: 'self' | 'template'; + attribute: string; + value: string; +} + +export class TemplateManager { + + templates: Template[] = []; + + fieldConditions: Record<string, Conditional[]> = {}; + + constructor(templateSettings: FieldSettings[]) { + makeAutoObservable(this); + this.templates = this.initializeTemplates(templateSettings); + } + + initializeTemplates = (templateSettings: FieldSettings[]) => templateSettings.map(settings => { + return new Template(settings, this.fieldConditions)}); + + 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.fieldConditions[fieldTitle] === undefined) { + this.fieldConditions[fieldTitle] = [condition]; + } else { + this.fieldConditions[fieldTitle].push(condition); + } + } + + removeFieldCondition = (fieldTitle: string, condition: Conditional) => { + if (this.fieldConditions[fieldTitle]) { + this.fieldConditions[fieldTitle] = this.fieldConditions[fieldTitle].filter(cond => cond !== condition); + } + } + + createDocsFromTemplate = action((dv: DataVizBox, template: Template, csvColumns: Col[], GPTManager: TemplateMenuGPTManager, debug: boolean = false) => { + const fields = Array.from(Object.keys(dv.records[0])); + + const processContent = (content: { [title: string]: string }) => { + const templateCopy = template.cloneBase(); + + fields + .filter(title => title) + .forEach(title => { + const field = templateCopy.getFieldByTitle(title); + field && field.setContent(content[title], field.viewType); + }); + + const gptFunc = (type: TemplateFieldType) => (type === TemplateFieldType.VISUAL ? GPTManager.renderGPTImageCall : GPTManager.renderGPTTextCall); + const gptPromises = csvColumns + .filter(field => field.type !== TemplateFieldType.UNSET && field.AIGenerated) + .map(field => { + const templateField = templateCopy.getFieldByTitle(field.title); + if (templateField !== undefined) { + return gptFunc(field.type)(templateCopy, field, templateField.getID); + } + }); + + return Promise.all(gptPromises) + }; + + const rowContents = debug + ? [{}, {}, {}, {}] + : NumListCast(dv.layoutDoc.dataViz_selectedRows).map(row => + fields.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/TemplateMenuFireflyManager.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateMenuFireflyManager.ts new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateMenuFireflyManager.ts diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateMenuGPTManager.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateMenuGPTManager.ts new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateMenuGPTManager.ts diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx index de345a335..ed2e20843 100644 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx @@ -30,10 +30,12 @@ import './DocCreatorMenu.scss'; import { TemplateField, ViewType } from './TemplateFieldTypes/TemplateField'; import { Template } from './Template'; import { TemplateFieldSize, TemplateFieldType, TemplateLayouts } from './TemplateBackend'; -import { TemplateManager } from './TemplateManager'; +import { TemplateManager } from './Backend/TemplateManager'; import { DrawingFillHandler } from '../../../smartdraw/DrawingFillHandler'; import { CgPathIntersect } from 'react-icons/cg'; import { StaticContentField } from './TemplateFieldTypes/StaticContentField'; +import { SuggestedTemplatesWindow } from './Menu/SuggestedTemplatesWindow'; +import { TemplateMenuGPTManager } from './Backend/TemplateMenuGPTManager'; export enum LayoutType { FREEFORM = 'Freeform', @@ -64,6 +66,7 @@ export type Col = { title: string; type: TemplateFieldType; defaultContent?: string; + AIGenerated?: boolean; }; export type Conditional = { @@ -84,10 +87,11 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> // eslint-disable-next-line no-use-before-define static Instance: DocCreatorMenu; - private DEBUG_MODE: boolean = false; + private DEBUG_MODE: boolean = true; private _disposers: { [name: string]: IDisposer } = {}; private _ref: HTMLDivElement | null = null; private templateManager: TemplateManager; + private GPTManager: TemplateMenuGPTManager; @observable _fullyRenderedDocs: Doc[] = []; // collection of templates filled in with content @observable _renderedDocCollection: Doc | undefined = undefined; // fullyRenderedDocs in a parent collection @@ -148,6 +152,7 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> makeObservable(this); DocCreatorMenu.Instance = this; this.templateManager = new TemplateManager(TemplateLayouts.allTemplates); + this.GPTManager = new TemplateMenuGPTManager(); } setContainerRef: React.LegacyRef<HTMLDivElement> = (node) => { @@ -162,12 +167,6 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> this._suggestedTemplates = []; this._userCreatedFields = []; }; - @action addUserTemplate = (template: Template) => { - this._userTemplates.push(template); - }; - @action removeUserTemplate = (template: Template) => { - this._userTemplates.splice(this._userTemplates.indexOf(template), 1); - }; @action setSuggestedTemplates = (templates: Template[]) => { this._suggestedTemplates = templates; //prettier-ignore }; @@ -238,7 +237,7 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> return bounds; } - setUpButtonClick = (e: React.PointerEvent, func: () => void) => { + setUpButtonClick = (e: React.PointerEvent, func: (...args: any) => void) => { setupMoveUpEvents( this, e, @@ -518,6 +517,31 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> this.forceUpdate(); }; + generatePresetTemplates = async (debug: boolean) => { + const templates: Template[] = []; + + if (debug) { + templates.push(...this.templateManager.templates); + } else { + this._dataViz?.updateColDefaults(); + + templates.push(...this.templateManager.getValidTemplates(this.fieldsInfos)); + + const assignments = await this.GPTManager.assignColsToFields(templates, this.fieldsInfos); + + const renderedTemplatePromises = assignments.map(([template, assgns]) => this.GPTManager.applyGPTContentToTemplate(template, assgns)); + + await Promise.all(renderedTemplatePromises); + } + + setTimeout( + action(() => { + this.setSuggestedTemplates(templates); + this._GPTLoading = false; + }) + ); + }; + @action setVariationTab = (open: boolean) => { this._variationsTab = open; if (this._previewWindow && open) { @@ -554,292 +578,6 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> doc.y = 10000; } - 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.title}. Its fields are: `; - descriptions += template.descriptionSummary; - }); - - return descriptions; - }; - - compileColDescriptions = (cols: Col[]): string => { - let descriptions: string = ' ------------- COL DESCRIPTIONS START HERE:'; - cols.forEach(col => (descriptions += `{title: ${col.title}, sizes: ${String(col.sizes)}, type: ${col.type}, descreiption: ${col.desc}} `)); - - return descriptions; - }; - - getColByTitle = (title: string) => { - return this.fieldsInfos.filter(col => col.title === title)[0]; - }; - - @action - assignColsToFields = async (templates: Template[], cols: Col[]): Promise<[Template, { [field: number]: Col }][]> => { - const fieldDescriptions: string = this.compileFieldDescriptions(templates); - const colDescriptions: string = this.compileColDescriptions(cols); - - const inputText = fieldDescriptions.concat(colDescriptions); - - ++this._callCount; - const origCount = this._callCount; - - const prompt: string = `(${origCount}) ${inputText}`; - - this._GPTLoading = true; - - try { - const res = await gptAPICall(prompt, GPTCallType.TEMPLATE); - - if (res) { - const assignments: { [templateTitle: string]: { [fieldID: string]: string } } = JSON.parse(res); - const brokenDownAssignments: [Template, { [fieldID: number]: Col }][] = []; - - Object.entries(assignments).forEach(([tempTitle, assignment]) => { - const template = templates.filter(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 field - const field = template.getFieldByID(Number(fieldID)); - field.setContent(col.defaultContent ?? '', col.type === TemplateFieldType.VISUAL ? ViewType.IMG : ViewType.TEXT); - field.setTitle(col.title); - this._conditions.filter(c => c.field === field.getTitle()).forEach(conditional => { - if (field.getContent() === conditional.condition){ - if (conditional.target === 'self'){ - field.renderedDoc![conditional.attribute] = conditional.value; - (field.settings.opts as any)[conditional.attribute] = conditional.value; - } else if (conditional.target === 'template'){ - template._mainField!.renderedDoc![conditional.attribute] = conditional.value; - (template._mainField!.settings.opts as any)[conditional.attribute] = conditional.value; - } - } - }) - } else { - a[Number(fieldID)] = this.getColByTitle(colTitle); - } - return a; - }, - {} as { [field: number]: Col } - ); - brokenDownAssignments.push([template, toObj]); - }); - - return brokenDownAssignments; - } - } catch (err) { - console.error(err); - } - - return []; - }; - - generatePresetTemplates = async () => { - const templates: Template[] = []; - - if (this.DEBUG_MODE) { - templates.push(...this.templateManager.templates); - } else { - this._dataViz?.updateColDefaults(); - - templates.push(...this.templateManager.getValidTemplates(this.fieldsInfos)); - - const assignments = await this.assignColsToFields(templates, this.fieldsInfos); - - const renderedTemplatePromises = assignments.map(([template, assgns]) => this.applyGPTContentToTemplate(template, assgns)); - - await Promise.all(renderedTemplatePromises); - } - - setTimeout( - action(() => { - this.setSuggestedTemplates(templates); - this._GPTLoading = false; - }) - ); - }; - - renderGPTImageCall = async (template: Template, col: Col, fieldNumber: number | undefined): Promise<boolean> => { - const generateAndLoadImage = async (fieldNum: string, column: Col, prompt: string) => { - const url = await this.generateGPTImage(prompt); - const field: TemplateField = template.getFieldByID(Number(fieldNum)); - - field.setContent(url ?? '', ViewType.IMG); - field.setTitle(col.title); - }; - - const fieldContent: string = template.compiledContent; - - try { - const sysPrompt = - 'Your job is to create a prompt for an AI image generator to help it generate an image based on existing content in a template and a user prompt. Your prompt should focus heavily on visual elements to help the image generator; avoid unecessary info that might distract it. ONLY INCLUDE THE PROMPT, NO OTHER TEXT OR EXPLANATION. The existing content is as follows: ' + - fieldContent + - ' **** The user prompt is: ' + - col.desc; - - const prompt = await gptAPICall(sysPrompt, GPTCallType.COMPLETEPROMPT); - - await generateAndLoadImage(String(fieldNumber), col, prompt); - } catch (e) { - console.log(e); - } - return true; - }; - - renderGPTTextCall = async (template: Template, col: Col, fieldNum: number | 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(`${++this._callCount}: ${prompt}`, GPTCallType.FILL); - - // console.log('prompt: ', prompt, ' response: ', res); - - 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)); - // const column = this.getColByTitle(title); - - field.setContent(info.content ?? '', ViewType.TEXT); - field.setTitle(col.title); - }); - } - } catch (err) { - console.log(err); - } - - return true; - }; - - createDocsFromTemplate = action((dv: DataVizBox, template: Template) => { - this._docsRendering = true; - const fields = Array.from(Object.keys(dv.records[0])); - - const processContent = (content: { [title: string]: string }) => { - const templateCopy = template.cloneBase(); - - fields - .filter(title => title) - .forEach(title => { - const field = templateCopy.getFieldByTitle(title); - if (field !== undefined) { - field.setContent(content[title], field.viewType); - console.log('content set') - this._conditions.filter(c => c.field === title).forEach(conditional => { - console.log('in conditional') - if (content[title] === conditional.condition){ - if (conditional.target === 'self'){ - field.renderedDoc![conditional.attribute] = conditional.value; - (field.settings.opts as any)[conditional.attribute] = conditional.value; - } else if (conditional.target === 'template'){ - console.log('setting', conditional.attribute, 'to: ', conditional.value) - templateCopy._mainField!.renderedDoc![conditional.attribute] = conditional.value; - (templateCopy._mainField!.settings.opts as any)[conditional.attribute] = conditional.value; - } - } - }) - } - }); - - const gptFunc = (type: TemplateFieldType) => (type === TemplateFieldType.VISUAL ? this.renderGPTImageCall : this.renderGPTTextCall); - const gptPromises = this._userCreatedFields - .filter(field => field.type !== TemplateFieldType.UNSET) - .map(field => { - const templateField = templateCopy.getFieldByTitle(field.title); - if (templateField !== undefined) { - return gptFunc(field.type)(templateCopy, field, templateField.getID); - } - }); - - return Promise.all(gptPromises).then(() => (this._DOCCC = templateCopy._mainField?.renderedDoc)); - }; - - const rowContents = this.DEBUG_MODE - ? [{}, {}, {}, {}] - : NumListCast(dv.layoutDoc.dataViz_selectedRows).map(row => - fields.reduce( - (values, col) => { - values[col] = dv.records[row][col]; - return values; - }, - {} as { [title: string]: string } - ) - ); - return Promise.all(rowContents.map(processContent)).then( - action(renderedDocs => { - this._docsRendering = false; // removes loading indicator - return renderedDocs; - }) - ); - }); - addRenderedCollectionToMainview = () => { const collection = this._renderedDocCollection; if (collection) { @@ -978,110 +716,6 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> /> ); - get templatesPreviewContents() { - const GPTOptions = <div></div>; - - //<img className='docCreatorMenu-preview-image expanded' src={this._expandedPreview.icon!.url.href.replace(".png", "_o.png")} /> - - return ( - <div className={`docCreatorMenu-templates-view`}> - {this._expandedPreview ? ( - this.editingWindow - ) : ( - <div className="docCreatorMenu-templates-displays"> - <div className="docCreatorMenu-section"> - <div className="docCreatorMenu-section-topbar"> - <div className="docCreatorMenu-section-title">Suggested Templates</div> - <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._menuContent = 'dashboard')))}> - <FontAwesomeIcon icon="gear" /> - </button> - </div> - <div className="docCreatorMenu-templates-preview-window" style={{ justifyContent: this._GPTLoading || this._menuDimensions.width > 400 ? 'center' : '' }}> - {this._GPTLoading ? ( - <div className="loading-spinner"> - <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} /> - </div> - ) : ( - this._suggestedTemplates.map(template => ( - <div - key={template.title} - className="docCreatorMenu-preview-window" - 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); - this.forceUpdate(); - }) - }> - <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> - {this.docPreview(template.getRenderedDoc())} - </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 => ( - <div - key={template.toString()} - className="docCreatorMenu-preview-window" - 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> - {this.docPreview(template.getRenderedDoc())} - </div> - ))} - </div> - </div> - </div> - )} - </div> - ); - } - @action updateXMargin = (input: string) => { this._layout.xMargin = Number(input); setTimeout(() => { @@ -1503,7 +1137,7 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> get renderSelectedViewType() { switch (this._menuContent) { - case 'templates': return this.templatesPreviewContents; + case 'templates': return <SuggestedTemplatesWindow menu={this} setupButtonClick={this.setUpButtonClick}/>; case 'options': return this.optionsMenuContents; case 'dashboard': return this.dashboardContents; } // prettier-ignore diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/ExpandedTemplatePreview.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/ExpandedTemplatePreview.tsx new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/ExpandedTemplatePreview.tsx diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/FieldOptionsScreen.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/FieldOptionsScreen.tsx new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/FieldOptionsScreen.tsx diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/SuggestedTemplatesWindow.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/SuggestedTemplatesWindow.tsx new file mode 100644 index 000000000..517a998d9 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/SuggestedTemplatesWindow.tsx @@ -0,0 +1,81 @@ +import { Colors } from "@dash/components/src"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, 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"; + +export interface SuggestedTemplatesProps { + menu: DocCreatorMenu; + setupButtonClick: (e: React.PointerEvent, func: () => void) => void; +} + +@observer +export class SuggestedTemplatesWindow extends ObservableReactComponent<SuggestedTemplatesProps> { + + @observable _GPTLoading: boolean = false; + + @observable _suggestedTemplates: Template[] = []; + @observable _userTemplates: Template[] = []; + + @action addUserTemplate = (template: Template) => { this._userTemplates.push(template) }; + @action removeUserTemplate = (template: Template) => { this._userTemplates.splice(this._userTemplates.indexOf(template), 1) }; + + render() { + return ( + <div className='docCreatorMenu-templates-view'> + <div className="docCreatorMenu-templates-displays"> + <div className="docCreatorMenu-section"> + <div className="docCreatorMenu-section-topbar"> + <div className="docCreatorMenu-section-title">Suggested Templates</div> + <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.props.setupButtonClick(e, () => runInAction(() => (this.props.menu._menuContent = 'dashboard')))}> + <FontAwesomeIcon icon="gear" /> + </button> + </div> + <div className="docCreatorMenu-templates-preview-window" style={{ justifyContent: this.props.menu._menuDimensions.width > 400 ? 'center' : '' }}> + {this._suggestedTemplates.map(template => ( + <TemplatePreviewBox + template={template} + menu={this.props.menu} + leftButtonOpts={["magnifying-glass", (template: Template) => { this.props.menu.setExpandedView(template); this.forceUpdate(); }]} + rightButtonOpts={["plus", (template: Template) => this.addUserTemplate(template)]} + /> + ))} + </div> + <div className="docCreatorMenu-GPT-options"> + <div className="docCreatorMenu-GPT-options-container"> + <button className="docCreatorMenu-menu-button" onPointerDown={e => this.props.setupButtonClick(e, () => this.props.menu.generatePresetTemplates())}> + <FontAwesomeIcon icon="arrows-rotate" /> + </button> + </div> + </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"> + <FontAwesomeIcon icon="gear" /> + </button> + </div> + <div className="docCreatorMenu-templates-preview-window" style={{ justifyContent: this.props.menu._menuDimensions.width > 400 ? 'center' : '' }}> + {this._userTemplates.map(template => ( + <TemplatePreviewBox + template={template} + menu={this.props.menu} + + /> + ))} + </div> + </div> + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateEditingScreen.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateEditingScreen.tsx new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateEditingScreen.tsx 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..e40192fa8 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewBox.tsx @@ -0,0 +1,48 @@ +import { Colors } from "@dash/components/src"; +import { FontAwesomeIcon, FontAwesomeIconProps } from "@fortawesome/react-fontawesome"; +import { Template } from "../Template"; +import { runInAction } from "mobx"; +import React from "react"; +import { ObservableReactComponent } from "../../../../ObservableReactComponent"; +import { DocCreatorMenu } from "../DocCreatorMenu"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; + +export interface TemplatePreviewBoxProps { + template: Template; + menu: DocCreatorMenu; + leftButtonOpts?: [icon: IconProp, func: (template: Template) => void] + rightButtonOpts?: [icon: IconProp, func: (template: Template) => void] +} + +export class TemplatePreviewBox extends ObservableReactComponent<TemplatePreviewBoxProps> { + + render() { + const template = this.props.template; + + return ( + <div + key={template.title} + className="docCreatorMenu-preview-window" + 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, () => runInAction(() => this.props.menu._selectedTemplate = 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 } + {this.props.menu.docPreview(template.getRenderedDoc())} + </div> + ) + } +}
\ 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 index 2fe146b1c..1a4384bc1 100644 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.ts +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.ts @@ -1,19 +1,23 @@ import { makeAutoObservable } from 'mobx'; import { Col } from './DocCreatorMenu'; -import { TemplateLayouts } from './TemplateBackend'; +import { TemplateFieldType, TemplateLayouts } from './TemplateBackend'; import { DynamicField } from './TemplateFieldTypes/DynamicField'; -import { FieldSettings, TemplateField } from './TemplateFieldTypes/TemplateField'; +import { FieldSettings, TemplateField, ViewType } from './TemplateFieldTypes/TemplateField'; +import { Conditional } from './Backend/TemplateManager'; export class Template { - _mainField: DynamicField | undefined; + _mainField: DynamicField; + + private conditionalLogic: Record<string, Conditional[]>; /** * 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) { + constructor(definition: FieldSettings | DynamicField, conditionalLogic: Record<string, Conditional[]>) { makeAutoObservable(this); this._mainField = definition instanceof DynamicField ? definition : this.setupMainField(definition); + this.conditionalLogic = conditionalLogic; } get childFields(): TemplateField[] { @@ -52,7 +56,7 @@ export class Template { //dispose each subfields disposers, etc. }; - cloneBase = () => new Template(this._mainField?.makeClone(undefined) ?? TemplateLayouts.BasicSettings); + cloneBase = () => new Template(this._mainField?.makeClone(undefined) ?? TemplateLayouts.BasicSettings, this.conditionalLogic); getRenderedDoc = () => this.doc; @@ -68,11 +72,37 @@ export class Template { }); }; + 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); + } + isValidTemplate = (cols: Col[]) => { const maxMatches = this.maxMatches(this.getMatches(cols)); return maxMatches === this.contentFields.length; }; + applyConditionalLogicToField = (field: TemplateField) => { + if (field instanceof DynamicField) return; + const logic: Conditional[] = this.conditionalLogic[field.getTitle()]; + const content = field.getContent() + logic && logic.forEach(statement => { + if (content === statement.condition) { + if (statement.target === 'template') { + this._mainField.renderedDoc![statement.attribute] = statement.value; + } else { + field.renderedDoc![statement.attribute] = statement.value; + } + } + }) + } + + applyConditionalLogic = () => { + const fields: TemplateField[] = [this._mainField, ...this.allFields]; + fields.forEach(this.applyConditionalLogicToField); + } + getMatches = (cols: Col[]): number[][] => { const numFields = this.contentFields.length; diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DynamicField.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DynamicField.ts index 56db33ed5..377f5acb2 100644 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DynamicField.ts +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DynamicField.ts @@ -19,8 +19,6 @@ export class DynamicField extends TemplateField { return this.getSubfields.flatMap(field => [field, ...((field as DynamicField).getAllSubfields ?? [])]); } - setSubFields = (fields: TemplateField[]) => (this._subfields = fields); - 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)); @@ -89,10 +87,9 @@ export class DynamicField extends TemplateField { initRenderDoc = (settings: FieldSettings) => { this._disposers.fieldList = reaction(() => DocListCast(this._renderDoc?.[Doc.LayoutFieldKey(this._renderDoc)]), this.handleFieldUpdate); - this._subfields = settings.subfields?.map((fieldSettings, index) => TemplateField.CreateField(fieldSettings, index, this)) || []; + this._subfields = settings.subfields?.map((fieldSettings, index) => {fieldSettings.template = this.settings.template; return TemplateField.CreateField(fieldSettings, index, this)}) || []; const renderedSubfields = this._subfields.filter(field => field.renderedDoc).map(field => field.renderedDoc!); settings.opts.title = settings.title; - console.log('initializing dynamicfield with color: ', settings.opts.backgroundColor) this._renderDoc = (() => { switch (settings.viewType) { case ViewType.CAROUSEL3D: return Docs.Create.Carousel3DDocument(renderedSubfields, settings.opts); case ViewType.FREEFORM: diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField.ts index ecb4f6f24..0ad1accc1 100644 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField.ts +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField.ts @@ -1,7 +1,9 @@ /* 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 { @@ -16,7 +18,7 @@ export abstract class TemplateField { * @param sameId - * @returns TemplateField */ - static CreateField: (settings: FieldSettings, index: number, parent: TemplateField | undefined, sameId?: boolean) => TemplateField; + static CreateField: (settings: FieldSettings, index: number, parent: TemplateField | undefined, template: Template, sameId?: boolean) => TemplateField; protected _parent?: TemplateField; protected _id: number; @@ -24,13 +26,15 @@ export abstract class TemplateField { protected _settings: FieldSettings; protected _renderDoc: Doc | undefined; protected _dimensions: FieldDimensions | undefined; + public template: Template; - constructor(settings: FieldSettings, id: number = 1, parent?: TemplateField) { + constructor(settings: FieldSettings, id: number = 1, template: Template, 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.template = template; this.applyBasicOpts(this._dimensions, settings); return this; } @@ -71,7 +75,7 @@ export abstract class TemplateField { makeClone(parent?: TemplateField) { 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 + const cloned = TemplateField.CreateField(settings, this._id, parent, this.template, true); // create a value for this.Document/subfields that we want to ignore this._renderDoc && Doc.MakeClone(this._renderDoc).then(({ clone }) => (cloned._renderDoc = clone)); cloned._title = this._title; cloned._dimensions = this._dimensions; @@ -85,7 +89,7 @@ export abstract class TemplateField { changeFieldType = (newType: ViewType): TemplateField => { const newSettings = this._settings; newSettings.viewType = newType; - const newField = TemplateField.CreateField(newSettings, this._id, this._parent, true); + const newField = TemplateField.CreateField(newSettings, this._id, this._parent, this.template, true); this._parent?.exchangeFields(newField, this); return newField; }; @@ -139,6 +143,7 @@ export type FieldSettings = { types?: TemplateFieldType[]; sizes?: TemplateFieldSize[]; description?: string; + template?: Template; }; export enum ViewType { diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.ts deleted file mode 100644 index 0978444e3..000000000 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { makeAutoObservable } from 'mobx'; -import { Col } from './DocCreatorMenu'; -import { FieldSettings } from './TemplateFieldTypes/TemplateField'; -import { Template } from './Template'; - -export class TemplateManager { - templates: Template[] = []; - - constructor(templateSettings: FieldSettings[]) { - makeAutoObservable(this); - this.templates = this.initializeTemplates(templateSettings); - } - - initializeTemplates = (templateSettings: FieldSettings[]) => templateSettings.map(settings => 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(); - }; -} |