diff options
Diffstat (limited to 'src/client/views/nodes')
58 files changed, 3471 insertions, 3118 deletions
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index 4337401e3..933a383ea 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables.module.scss'; +@use '../global/globalCssVariables.module.scss' as global; .audiobox-container { width: 100%; @@ -19,30 +19,30 @@ .audiobox-dictation { width: 40px; - background: $medium-gray; - color: $dark-gray; + background: global.$medium-gray; + color: global.$dark-gray; display: flex; justify-content: center; align-items: center; &:hover { - color: $black; + color: global.$black; } } .audiobox-start-record { - color: $white; - background: $dark-gray; + color: global.$white; + background: global.$dark-gray; display: flex; align-items: center; justify-content: center; - font-size: $body-text; + font-size: global.$body-text; width: 100%; height: 100%; gap: 5px; &:hover { - background: $black; + background: global.$black; } } @@ -54,11 +54,11 @@ gap: 5px; width: 100%; height: 100%; - background: $dark-gray; + background: global.$dark-gray; color: white; .record-timecode { - font-size: $large-header; + font-size: global.$large-header; } .record-button { @@ -66,7 +66,7 @@ width: 30px; height: 30px; border-radius: 50%; - background: $dark-gray; + background: global.$dark-gray; display: flex; align-items: center; justify-content: center; @@ -76,7 +76,7 @@ } &:hover { - background: $black; + background: global.$black; } } } @@ -87,10 +87,10 @@ display: flex; flex-direction: column; align-items: center; - background: $dark-gray; + background: global.$dark-gray; width: 100%; height: 100%; - color: $white; + color: global.$white; .audiobox-button { margin: 2.5px; @@ -98,7 +98,7 @@ width: 25px; height: 25px; border-radius: 50%; - background: $dark-gray; + background: global.$dark-gray; display: flex; align-items: center; justify-content: center; @@ -108,7 +108,7 @@ } &:hover { - background: $black; + background: global.$black; } } @@ -132,7 +132,7 @@ height: 6px; cursor: pointer; box-shadow: 0; - background: $light-gray; + background: global.$light-gray; border-radius: 3px; } @@ -142,7 +142,7 @@ height: 10px; width: 10px; border-radius: 10px; - background: $medium-blue; + background: global.$medium-blue; cursor: pointer; -webkit-appearance: none; margin-top: -2px; @@ -180,12 +180,12 @@ .audiobox-playback { width: 100%; height: 100%; - background: $white; + background: global.$white; .audiobox-timeline { height: calc(100% - 50px); width: 100%; - background: $white; + background: global.$white; position: absolute; } @@ -203,7 +203,7 @@ width: 100%; height: 20px; padding: 3px; - font-size: $small-text; + font-size: global.$small-text; .bottom-controls-middle { display: flex; diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index beea6ab3c..ce1e9280a 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -69,7 +69,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF { key: 'freeform_panX' }, { key: 'freeform_panY' }, ]; // fields that are configured to be animatable using animation frames - public static animStringFields = ['backgroundColor', 'color', 'fillColor']; // fields that are configured to be animatable using animation frames + public static animStringFields = ['backgroundColor', 'borderColor', 'color', 'fillColor']; // fields that are configured to be animatable using animation frames public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames public static from(dv?: DocumentView): CollectionFreeFormDocumentView | undefined { return dv?._props.reactParent instanceof CollectionFreeFormDocumentView ? dv._props.reactParent : undefined; @@ -180,7 +180,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF const timecode = Math.round(time); Object.keys(vals).forEach(val => { const findexed = Cast(d[`${val}_indexed`], listSpec('number'), []).slice(); - findexed[timecode] = vals[val] || 0; + findexed[timecode] = vals[val] as unknown as number; d[`${val}_indexed`] = new List<number>(findexed); }); } diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index cb0831d3c..5315612e1 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -291,7 +291,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() this.askGPTPhonemes(this._inputValue); this._renderSide = this.backKey; this._outputValue = ''; - } else if (this._inputValue) this.askGPT(GPTCallType.QUIZ); + } else if (this._inputValue) this.askGPT(GPTCallType.QUIZDOC); }; onPointerMove = ({ movementX }: PointerEvent) => { @@ -511,7 +511,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() */ askGPT = async (callType: GPTCallType) => { const questionText = this.frontText; - const queryText = questionText + (callType == GPTCallType.QUIZ ? ' UserAnswer: ' + this._inputValue + '. ' + ' Rubric: ' + this.backText : ''); + const queryText = questionText + (callType == GPTCallType.QUIZDOC ? ' UserAnswer: ' + this._inputValue + '. ' + ' Rubric: ' + this.backText : ''); this.loading = true; const res = !this.frontText @@ -522,7 +522,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() case GPTCallType.CHATCARD: DocCast(this.dataDoc[this.backKey])[DocData].text = resp; break; - case GPTCallType.QUIZ: + case GPTCallType.QUIZDOC: this._renderSide = this.backKey; this._outputValue = resp.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric'); break; diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index b874d077b..d5e37b3b5 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -32,11 +32,12 @@ import { DocumentView } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; import { FocusViewOptions } from '../FocusViewOptions'; import './DataVizBox.scss'; -import { Col, DataVizTemplateInfo, DocCreatorMenu, LayoutType, TemplateFieldSize, TemplateFieldType } from './DocCreatorMenu'; +import { Col, DataVizTemplateInfo, DocCreatorMenu, LayoutType} from './DocCreatorMenu/DocCreatorMenu'; import { Histogram } from './components/Histogram'; import { LineChart } from './components/LineChart'; import { PieChart } from './components/PieChart'; import { TableBox } from './components/TableBox'; +import { TemplateFieldSize, TemplateFieldType } from './DocCreatorMenu/TemplateBackend'; export enum DataVizView { TABLE = 'table', @@ -171,7 +172,6 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const colInfo = this.colsInfo.get(colTitle); if (colInfo) { colInfo.title = newTitle; - console.log(colInfo.title); } else { this.colsInfo.set(colTitle, { title: newTitle, desc: '', type: TemplateFieldType.UNSET, sizes: [] }); } @@ -489,7 +489,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } // Changing which document to add the annotation to (the currently selected PDF) - GPTPopup.Instance.setSidebarId('data_sidebar'); + GPTPopup.Instance.setSidebarFieldKey('data_sidebar'); GPTPopup.Instance.addDoc = this.sidebarAddDocument; }; @@ -510,7 +510,6 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { openDocCreatorMenu = (x: number, y: number) => { DocCreatorMenu.Instance.toggleDisplay(x, y); DocCreatorMenu.Instance.setDataViz(this); - DocCreatorMenu.Instance.setTemplateDocs(this.getPossibleTemplates()); }; specificContextMenu = (e: React.MouseEvent) => { @@ -523,7 +522,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; askGPT = action(async () => { - GPTPopup.Instance.setSidebarId('data_sidebar'); + GPTPopup.Instance.setSidebarFieldKey('data_sidebar'); GPTPopup.Instance.addDoc = this.sidebarAddDocument; GPTPopup.Instance.createFilteredDoc = this.createFilteredDoc; GPTPopup.Instance.setDataJson(''); @@ -622,107 +621,6 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } }; - getPossibleTemplates = (): Doc[] => { - const linkedDocs: Doc[] = LinkManager.Instance.getAllRelatedLinks(this.Document).map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document))); - const linkedCollections: Doc[] = linkedDocs.filter(doc => doc.type === 'config').map(doc => DocCast(doc.annotationOn)); - const isColumnTitle = (title: string): boolean => { - const colTitles: string[] = Object.keys(this.records[0]); - for (let i = 0; i < colTitles.length; ++i) { - if (colTitles[i] === title) { - return true; - } - } - return false; - }; - const isValidTemplate = (collection: Doc) => { - const childDocs = DocListCast(collection[Doc.LayoutFieldKey(collection)]); - for (let i = 0; i < childDocs.length; ++i) { - if (isColumnTitle(String(childDocs[i].title))) return true; - } - return false; - }; - return linkedCollections.filter(col => isValidTemplate(col)); - }; - - ApplyTemplateTo = (templateDoc: Doc, target: Doc, targetKey: string, titleTarget: string | undefined) => { - if (!Doc.AreProtosEqual(target[targetKey] as Doc, templateDoc)) { - if (target.resolvedDataDoc) { - target[targetKey] = new PrefetchProxy(templateDoc); - } else { - titleTarget && (Doc.GetProto(target).title = titleTarget); - const setDoc = [AclAdmin, AclEdit, AclAugment].includes(GetEffectiveAcl(Doc.GetProto(target))) ? Doc.GetProto(target) : target; - setDoc[targetKey] = new PrefetchProxy(templateDoc); - } - } - return target; - }; - - applyLayout = (templateInfo: DataVizTemplateInfo, docs: Doc[]) => { - if (templateInfo.layout.type === LayoutType.Stacked) return; - const columns: number = templateInfo.columns; - const xGap: number = templateInfo.layout.xMargin; - const yGap: number = templateInfo.layout.yMargin; - // const repeat: number = templateInfo.layout.repeat; - const startX: number = templateInfo.referencePos.x; - const startY: number = templateInfo.referencePos.y; - const templWidth = Number(templateInfo.doc._width); - const templHeight = Number(templateInfo.doc._height); - - let i: number = 0; - let docsChanged: number = 0; - let curX: number = startX; - let curY: number = startY; - - while (docsChanged < docs.length) { - while (i < columns && docsChanged < docs.length) { - docs[docsChanged].x = curX; - docs[docsChanged].y = curY; - curX += templWidth + xGap; - ++docsChanged; - ++i; - } - - i = 0; - curX = startX; - curY += templHeight + yGap; - } - }; - - // @action addSavedLayout = (layout: DataVizTemplateLayout) => { - // const saved = Cast(this.layoutDoc.dataViz_savedTemplates, listSpec('RefField')); - - // } - - @action - createDocsFromTemplate = (templateInfo: DataVizTemplateInfo) => { - if (!templateInfo.doc) return; - const mainCollection = this.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView; - const fields: string[] = Array.from(Object.keys(this.records[0])); - const selectedRows = NumListCast(this.layoutDoc.dataViz_selectedRows); - const docs: Doc[] = selectedRows.map(row => { - const values: string[] = []; - fields.forEach(col => values.push(this.records[row][col])); - - const proto = new Doc(); - proto.author = ClientUtils.CurrentUserEmail(); - values.forEach((val, i) => { - proto[fields[i]] = val as FieldType; - }); - - const target = Doc.MakeDelegate(proto); - const targetKey = StrCast(templateInfo.doc!.layout_fieldKey, 'layout'); - const applied = this.ApplyTemplateTo(templateInfo.doc!, target, targetKey, templateInfo.doc!.title + `${row}`); - target.layout_fieldKey = targetKey; - - //this.applyImagesTo(target, fields); - return applied; - }); - - docs.forEach(doc => mainCollection.addDocument(doc)); - - this.applyLayout(templateInfo, docs); - }; - /** * creates a new dataviz document filter from this one * it appears to the right of this document, with the diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx deleted file mode 100644 index 7fc906e59..000000000 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx +++ /dev/null @@ -1,2367 +0,0 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Colors } from '@dash/components'; -import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; -import { observer } from 'mobx-react'; -import { IDisposer } from 'mobx-utils'; -import * as React from 'react'; -import ReactLoading from 'react-loading'; -import { ClientUtils, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; -import { emptyFunction } from '../../../../Utils'; -import { Doc, NumListCast, StrListCast, returnEmptyDoclist } from '../../../../fields/Doc'; -import { Id } from '../../../../fields/FieldSymbols'; -import { Cast, DocCast, ImageCast, StrCast } from '../../../../fields/Types'; -import { ImageField } from '../../../../fields/URLField'; -import { Networking } from '../../../Network'; -import { GPTCallType, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT'; -import { Docs } from '../../../documents/Documents'; -import { DragManager } from '../../../util/DragManager'; -import { MakeTemplate } from '../../../util/DropConverter'; -import { SnappingManager } from '../../../util/SnappingManager'; -import { UndoManager, undoable } from '../../../util/UndoManager'; -import { LightboxView } from '../../LightboxView'; -import { ObservableReactComponent } from '../../ObservableReactComponent'; -import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView'; -import { DocumentView, DocumentViewInternal } from '../DocumentView'; -import { FieldViewProps } from '../FieldView'; -import { OpenWhere } from '../OpenWhere'; -import { DataVizBox } from './DataVizBox'; -import './DocCreatorMenu.scss'; -import { DefaultStyleProvider, returnEmptyDocViewList } from '../../StyleProvider'; -import { Transform } from '../../../util/Transform'; -import { IconProp } from '@fortawesome/fontawesome-svg-core'; - -export enum LayoutType { - Stacked = 'stacked', - Grid = 'grid', - Row = 'row', - Column = 'column', - Custom = 'custom', -} - -@observer -export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> { - static Instance: DocCreatorMenu; - - private _disposers: { [name: string]: IDisposer } = {}; - - private _ref: HTMLDivElement | null = null; - - @observable _templateDocs: Doc[] = []; - @observable _selectedTemplate: Doc | undefined = undefined; - @observable _columns: Col[] = []; - @observable _selectedCols: { title: string; type: string; desc: string }[] | undefined = []; - - @observable _layout: { type: LayoutType; yMargin: number; xMargin: number; columns?: number; repeat: number } = { type: LayoutType.Grid, yMargin: 0, xMargin: 0, repeat: 0 }; - @observable _layoutPreview: boolean = true; - @observable _layoutPreviewScale: number = 1; - @observable _savedLayouts: DataVizTemplateLayout[] = []; - @observable _expandedPreview: { icon: ImageField; doc: Doc } | undefined = undefined; - - @observable _suggestedTemplates: Doc[] = []; - @observable _GPTOpt: boolean = false; - @observable _userPrompt: string = ''; - @observable _callCount: number = 0; - @observable _GPTLoading: boolean = false; - - @observable _pageX: number = 0; - @observable _pageY: number = 0; - @observable _indicatorX: number | undefined = undefined; - @observable _indicatorY: number | undefined = undefined; - - @observable _hoveredLayoutPreview: number | undefined = undefined; - @observable _mouseX: number = -1; - @observable _mouseY: number = -1; - @observable _startPos?: { x: number; y: number }; - @observable _shouldDisplay: boolean = false; - - @observable _menuContent: 'templates' | 'options' | 'saved' | 'dashboard' = 'templates'; - @observable _dragging: boolean = false; - @observable _draggingIndicator: boolean = false; - @observable _dataViz?: DataVizBox; - @observable _interactionLock: any; - @observable _snapPt: any; - @observable _resizeHdlId: string = ''; - @observable _resizing: boolean = false; - @observable _offset: { x: number; y: number } = { x: 0, y: 0 }; - @observable _resizeUndo: UndoManager.Batch | undefined = undefined; - @observable _initDimensions: { width: number; height: number; x?: number; y?: number } = { width: 300, height: 400, x: undefined, y: undefined }; - @observable _menuDimensions: { width: number; height: number } = { width: 400, height: 400 }; - @observable _editing: boolean = false; - - constructor(props: any) { - super(props); - makeObservable(this); - DocCreatorMenu.Instance = this; - //setTimeout(() => this.generateTemplates('')); - } - - @action setDataViz = (dataViz: DataVizBox) => { - this._dataViz = dataViz; - }; - @action setTemplateDocs = (docs: Doc[]) => { - this._templateDocs = docs.map(doc => (doc.annotationOn ? DocCast(doc.annotationOn) : doc)); - }; - @action setGSuggestedTemplates = (docs: Doc[]) => { - this._suggestedTemplates = docs; - }; - - @computed get docsToRender() { - return this._selectedTemplate ? NumListCast(this._dataViz?.layoutDoc.dataViz_selectedRows) : []; - } - - @computed get rowsCount() { - switch (this._layout.type) { - case LayoutType.Row: - case LayoutType.Stacked: - return 1; - case LayoutType.Column: - return this.docsToRender.length; - case LayoutType.Grid: - return Math.ceil(this.docsToRender.length / (this._layout.columns ?? 1)) ?? 0; - default: - return 0; - } - } - - @computed get columnsCount() { - switch (this._layout.type) { - case LayoutType.Row: - return this.docsToRender.length; - case LayoutType.Column: - case LayoutType.Stacked: - return 1; - case LayoutType.Grid: - return this._layout.columns ?? 0; - default: - return 0; - } - } - - @computed get selectedFields() { - return StrListCast(this._dataViz?.layoutDoc._dataViz_axes); - } - - @computed get fieldsInfos(): Col[] { - const colInfo = this._dataViz?.colsInfo; - return this.selectedFields - .map(field => { - const fieldInfo = colInfo?.get(field); - - const col: Col = { - title: field, - type: fieldInfo?.type ?? TemplateFieldType.UNSET, - desc: fieldInfo?.desc ?? '', - sizes: fieldInfo?.sizes ?? [TemplateFieldSize.MEDIUM], - }; - - if (fieldInfo?.defaultContent !== undefined) { - col.defaultContent = fieldInfo.defaultContent; - } - - return col; - }) - .concat(this._columns); - } - - @computed get canMakeDocs() { - return this._selectedTemplate !== undefined && this._layout !== undefined; - } - - get bounds(): { t: number; b: number; l: number; r: number } { - const rect = this._ref?.getBoundingClientRect(); - const bounds = { t: rect?.top ?? 0, b: rect?.bottom ?? 0, l: rect?.left ?? 0, r: rect?.right ?? 0 }; - return bounds; - } - - setUpButtonClick = (e: any, func: Function) => { - setupMoveUpEvents( - this, - e, - returnFalse, - emptyFunction, - undoable(clickEv => { - clickEv.stopPropagation(); - clickEv.preventDefault(); - func(); - }, 'create docs') - ); - }; - - @action - onPointerDown = (e: PointerEvent) => { - this._mouseX = e.clientX; - this._mouseY = e.clientY; - }; - - @action - onPointerUp = (e: PointerEvent) => { - if (this._resizing) { - this._initDimensions.width = this._menuDimensions.width; - this._initDimensions.height = this._menuDimensions.height; - this._initDimensions.x = this._pageX; - this._initDimensions.y = this._pageY; - document.removeEventListener('pointermove', this.onResize); - SnappingManager.SetIsResizing(undefined); - this._resizing = false; - } - if (this._dragging) { - document.removeEventListener('pointermove', this.onDrag); - this._dragging = false; - } - if (e.button !== 2 && !e.ctrlKey) return; - const curX = e.clientX; - const curY = e.clientY; - if (Math.abs(this._mouseX - curX) > 1 || Math.abs(this._mouseY - curY) > 1) { - this._shouldDisplay = false; - } - }; - - componentDidMount() { - document.addEventListener('pointerdown', this.onPointerDown, true); - document.addEventListener('pointerup', this.onPointerUp); - this._disposers.templates = reaction( - () => this._templateDocs.slice(), - docs => this.updateIcons(docs) - ); - this._disposers.gpt = reaction( - () => this._suggestedTemplates.slice(), - docs => this.updateIcons(docs) - ); - //this._disposers.columns = reaction(() => this._dataViz?.layoutDoc._dataViz_axes, () => {this.generateTemplates('')}) - this._disposers.lightbox = reaction( - () => LightboxView.LightboxDoc(), - doc => { - // NOTE: bcz; commented this out because the doc creator would appear everytime I close out of the lightbox - // doc ? this._shouldDisplay && this.closeMenu() : !this._shouldDisplay && this.openMenu(); - } - ); - //this._disposers.fields = reaction(() => this._dataViz?.axes, cols => this._selectedCols = cols?.map(col => { return {title: col, type: '', desc: ''}})) - } - - componentWillUnmount() { - Object.values(this._disposers).forEach(disposer => disposer?.()); - document.removeEventListener('pointerdown', this.onPointerDown, true); - document.removeEventListener('pointerup', this.onPointerUp); - } - - updateIcons = (docs: Doc[]) => { - console.log('called'); - docs.map(this.getIcon); - }; - - @action - updateSelectedCols = (cols: string[]) => { - this._selectedCols; - }; - - @action - toggleDisplay = (x: number, y: number) => { - if (this._shouldDisplay) { - this._shouldDisplay = false; - } else { - this._pageX = x; - this._pageY = y; - this._shouldDisplay = true; - } - }; - - @action - closeMenu = () => { - this._shouldDisplay = false; - }; - - @action - openMenu = () => { - const allTemplates = this._templateDocs.concat(this._suggestedTemplates); - this._shouldDisplay = true; - this.updateIcons(allTemplates); - }; - - @action - onResizePointerDown = (e: React.PointerEvent): void => { - this._resizing = true; - document.addEventListener('pointermove', this.onResize); - SnappingManager.SetIsResizing(DocumentView.Selected().lastElement()?.Document[Id]); // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them - e.stopPropagation(); - const id = (this._resizeHdlId = e.currentTarget.className); - const pad = id.includes('Left') || id.includes('Right') ? Number(getComputedStyle(e.target as any).width.replace('px', '')) / 2 : 0; - const bounds = e.currentTarget.getBoundingClientRect(); - this._offset = { - x: id.toLowerCase().includes('left') ? bounds.right - e.clientX - pad : bounds.left - e.clientX + pad, // - y: id.toLowerCase().includes('top') ? bounds.bottom - e.clientY - pad : bounds.top - e.clientY + pad, - }; - this._resizeUndo = UndoManager.StartBatch('drag resizing'); - this._snapPt = { x: e.pageX, y: e.pageY }; - }; - - @action - onResize = (e: any): boolean => { - const dragHdl = this._resizeHdlId.split(' ')[1]; - const thisPt = DragManager.snapDrag(e, -this._offset.x, -this._offset.y, this._offset.x, this._offset.y); - - const { scale, refPt, transl } = this.getResizeVals(thisPt, dragHdl); - !this._interactionLock && runInAction(async () => { // resize selected docs if we're not in the middle of a resize (ie, throttle input events to frame rate) - this._interactionLock = true; - const scaleAspect = {x: scale.x, y: scale.y}; - this.resizeView(refPt, scaleAspect, transl); // prettier-ignore - await new Promise<any>(res => { setTimeout(() => { res(this._interactionLock = undefined)})}); - }); // prettier-ignore - return true; - }; - - @action - onDrag = (e: any): boolean => { - this._pageX = e.pageX - (this._startPos?.x ?? 0); - this._pageY = e.pageY - (this._startPos?.y ?? 0); - this._initDimensions.x = this._pageX; - this._initDimensions.y = this._pageY; - return true; - }; - - getResizeVals = (thisPt: { x: number; y: number }, dragHdl: string) => { - const [w, h] = [this._initDimensions.width, this._initDimensions.height]; - const [moveX, moveY] = [thisPt.x - this._snapPt.x, thisPt.y - this._snapPt.y]; - let vals: { scale: { x: number; y: number }; refPt: [number, number]; transl: { x: number; y: number } }; - switch (dragHdl) { - case 'topLeft': vals = { scale: { x: 1 - moveX / w, y: 1 -moveY / h }, refPt: [this.bounds.r, this.bounds.b], transl: {x: moveX, y: moveY } }; break; - case 'topRight': vals = { scale: { x: 1 + moveX / w, y: 1 -moveY / h }, refPt: [this.bounds.l, this.bounds.b], transl: {x: 0, y: moveY } }; break; - case 'top': vals = { scale: { x: 1, y: 1 -moveY / h }, refPt: [this.bounds.l, this.bounds.b], transl: {x: 0, y: moveY } }; break; - case 'left': vals = { scale: { x: 1 - moveX / w, y: 1 }, refPt: [this.bounds.r, this.bounds.t], transl: {x: moveX, y: 0 } }; break; - case 'bottomLeft': vals = { scale: { x: 1 - moveX / w, y: 1 + moveY / h }, refPt: [this.bounds.r, this.bounds.t], transl: {x: moveX, y: 0 } }; break; - case 'right': vals = { scale: { x: 1 + moveX / w, y: 1 }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break; - case 'bottomRight':vals = { scale: { x: 1 + moveX / w, y: 1 + moveY / h }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break; - case 'bottom': vals = { scale: { x: 1, y: 1 + moveY / h }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break; - default: vals = { scale: { x: 1, y: 1 }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break; - } // prettier-ignore - return vals; - }; - - resizeView = (refPt: number[], scale: { x: number; y: number }, translation: { x: number; y: number }) => { - const refCent = [refPt[0], refPt[1]]; // fixed reference point for resize (ie, a point that doesn't move) - if (this._initDimensions.x === undefined) this._initDimensions.x = this._pageX; - if (this._initDimensions.y === undefined) this._initDimensions.y = this._pageY; - const { height, width, x, y } = this._initDimensions; - - this._menuDimensions.width = Math.max(300, scale.x * width); - this._menuDimensions.height = Math.max(200, scale.y * height); - this._pageX = x + translation.x; - this._pageY = y + translation.y; - }; - - async getIcon(doc: Doc) { - const docView = DocumentView.getDocumentView(doc); - if (docView) { - docView.ComponentView?.updateIcon?.(); - return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 500)); - } - return undefined; - } - - @action updateSelectedTemplate = (template: Doc) => { - if (this._selectedTemplate === template) { - this._selectedTemplate = undefined; - return; - } else { - this._selectedTemplate = template; - MakeTemplate(template); - } - }; - - @action updateSelectedSavedLayout = (layout: DataVizTemplateLayout) => { - this._layout.xMargin = layout.layout.xMargin; - this._layout.yMargin = layout.layout.yMargin; - this._layout.type = layout.layout.type; - this._layout.columns = layout.columns; - }; - - isSelectedLayout = (layout: DataVizTemplateLayout) => { - return this._layout.xMargin === layout.layout.xMargin && this._layout.yMargin === layout.layout.yMargin && this._layout.type === layout.layout.type && this._layout.columns === layout.columns; - }; - - @action - generateTemplates = async (inputText: string) => { - ++this._callCount; - const origCount = this._callCount; - - let prompt: string = `(#${origCount}) Please generate for the fields:`; - this.selectedFields?.forEach(field => (prompt += ` ${field},`)); - prompt += ` (-----NOT A FIELD-----) Additional prompt: ${inputText}`; - - this._GPTLoading = true; - - try { - const res = await gptAPICall(prompt, GPTCallType.TEMPLATE); - - if (res && this._callCount === origCount) { - this._suggestedTemplates = []; - const templates: { template_type: string; fieldVals: { title: string; tlx: string; tly: string; brx: string; bry: string }[] }[] = JSON.parse(res); - this.createGeneratedTemplates(templates, 500, 500); - } - } catch (err) { - console.error(err); - } - }; - - @action - createGeneratedTemplates = (layouts: { template_type: string; fieldVals: { title: string; tlx: string; tly: string; brx: string; bry: string }[] }[], tempWidth: number, tempHeight: number) => { - const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView; - const GPTTemplates: Doc[] = []; - - layouts.forEach(layout => { - const fields: Doc[] = layout.fieldVals.map(field => { - const left: number = (Number(field.tlx) * tempWidth) / 2; - const top: number = Number(field.tly) * tempHeight / 2; //prettier-ignore - const right: number = (Number(field.brx) * tempWidth) / 2; - const bottom: number = Number(field.bry) * tempHeight / 2; //prettier-ignore - const height = bottom - top; - const width = right - left; - const doc = !field.title.includes('$$') - ? Docs.Create.TextDocument('', { _height: height, _width: width, title: field.title, x: left, y: top, text_fontSize: `${height / 2}` }) - : Docs.Create.ImageDocument('', { _height: height, _width: width, title: field.title.replace(/\$\$/g, ''), x: left, y: top }); - return doc; - }); - - const template = Docs.Create.FreeformDocument(fields, { _height: tempHeight, _width: tempWidth, title: layout.template_type, x: 400000, y: 400000 }); - - mainCollection.addDocument(template); - - GPTTemplates.push(template); - }); - - setTimeout(() => { - this.setGSuggestedTemplates(GPTTemplates); /*GPTTemplates.forEach(template => mainCollection.removeDocument(template))*/ - }, 100); - - this.forceUpdate(); - }; - - editTemplate = (doc: Doc) => { - //this.closeMenu(); - DocumentViewInternal.addDocTabFunc(doc, OpenWhere.addRight); - DocumentView.DeselectAll(); - Doc.UnBrushDoc(doc); - }; - - removeTemplate = (doc: Doc) => { - this._templateDocs.splice(this._templateDocs.indexOf(doc), 1); - }; - - testTemplate = async () => { - this.updateIcons(this._suggestedTemplates.slice()); - this.forceUpdate(); - - // try { - // const res = await gptImageCall('Image of panda eating a cookie'); - - // if (res) { - // const result = await Networking.PostToServer('/uploadRemoteImage', { sources: res }); - - // console.log(result); - // } - // } catch (e) { - // console.log(e); - // } - }; - - @action addField = () => { - const newFields: Col[] = this._columns.concat([{ title: '', type: TemplateFieldType.UNSET, desc: '', sizes: [] }]); - this._columns = newFields; - }; - - @action removeField = (field: { title: string; type: string; desc: string }) => { - if (this._dataViz?.axes.includes(field.title)) { - this._dataViz.selectAxes(this._dataViz.axes.filter(col => col !== field.title)); - } else { - const toRemove = this._columns.filter(f => f === field); - if (!toRemove) return; - - if (toRemove.length > 1) { - while (toRemove.length > 1) { - toRemove.pop(); - } - } - - if (this._columns.length === 1) { - this._columns = []; - } else { - this._columns.splice(this._columns.indexOf(toRemove[0]), 1); - } - } - }; - - @action setColTitle = (column: Col, title: string) => { - if (this.selectedFields.includes(column.title)) { - this._dataViz?.setColumnTitle(column.title, title); - } else { - column.title = title; - } - this.forceUpdate(); - }; - - @action setColType = (column: Col, type: TemplateFieldType) => { - if (this.selectedFields.includes(column.title)) { - this._dataViz?.setColumnType(column.title, type); - } else { - column.type = type; - } - this.forceUpdate(); - }; - - modifyColSizes = (column: Col, size: TemplateFieldSize, valid: boolean) => { - if (this.selectedFields.includes(column.title)) { - this._dataViz?.modifyColumnSizes(column.title, size, valid); - } else { - if (!valid && column.sizes.includes(size)) { - column.sizes.splice(column.sizes.indexOf(size), 1); - } else if (valid && !column.sizes.includes(size)) { - column.sizes.push(size); - } - } - this.forceUpdate(); - }; - - setColDesc = (column: Col, desc: string) => { - if (this.selectedFields.includes(column.title)) { - this._dataViz?.setColumnDesc(column.title, desc); - } else { - column.desc = desc; - } - this.forceUpdate(); - }; - - generateGPTImage = async (prompt: string): Promise<string | undefined> => { - console.log(prompt); - - try { - const res = await gptImageCall(prompt); - - if (res) { - const result = await Networking.PostToServer('/uploadRemoteImage', { sources: res }); - const source = ClientUtils.prepend(result[0].accessPaths.agnostic.client); - return source; - } - } catch (e) { - console.log(e); - } - }; - - matchesForTemplate = (template: TemplateDocInfos, cols: Col[]): number[][] => { - const colMatchesField = (col: Col, field: Field) => { - return field.sizes?.some(size => col.sizes?.includes(size)) && field.types?.includes(col.type); - }; - - const matches: number[][] = Array(template.fields.length) - .fill([]) - .map(() => []); - - template.fields.forEach((field, i) => { - cols.forEach((col, v) => { - if (colMatchesField(col, field)) { - matches[i].push(v); - } - }); - }); - - return matches; - }; - - maxMatches = (fieldsCt: number, matches: number[][]) => { - const used: boolean[] = Array(fieldsCt).fill(false); - const mt: number[] = Array(fieldsCt).fill(-1); - - const augmentingPath = (v: number): boolean => { - if (used[v]) return false; - used[v] = true; - for (const to of matches[v]) { - if (mt[to] === -1 || augmentingPath(mt[to])) { - mt[to] = v; - return true; - } - } - return false; - }; - - for (let v = 0; v < fieldsCt; ++v) { - used.fill(false); - augmentingPath(v); - } - - let count: number = 0; - - for (let i = 0; i < fieldsCt; ++i) { - if (mt[i] !== -1) ++count; - } - - return count; - }; - - findValidTemplates = (cols: Col[], templates: TemplateDocInfos[]) => { - let validTemplates: any[] = []; - templates.forEach(template => { - const numFields = template.fields.length; - if (!(numFields === cols.length)) return; - const matches = this.matchesForTemplate(template, cols); - if (this.maxMatches(numFields, matches) === numFields) { - validTemplates.push(template.title); - } - }); - - validTemplates = validTemplates.map(title => TemplateLayouts.getTemplateByTitle(title)); - - return validTemplates; - }; - - // createColumnField = (template: TemplateDocInfos, field: Field, column: Col): Doc => { - - // if (field.subfields) { - // const doc = FieldFuncs.FreeformField({ - // tl: field.tl, - // br: field.br }, - // template.height, - // template.width, - // column.title, - // '', - // field.opts - // ); - - // field.subfields[1].forEach(f => { - // const fDoc = () - // }) - - // } - - // return new Doc; - // } - - /** - * Populates a preset template framework with content from a datavizbox or any AI-generated content. - * @param template the preloaded template framework being filled in - * @param assignments a list of template field numbers (from top to bottom) and their assigned columns from the linked dataviz - * @returns a doc containing the fully rendered template - */ - fillPresetTemplate = async (template: TemplateDocInfos, assignments: { [field: string]: Col }): Promise<Doc> => { - const wordLimit = (size: TemplateFieldSize) => { - switch (size) { - case TemplateFieldSize.TINY: - return 2; - case TemplateFieldSize.SMALL: - return 5; - case TemplateFieldSize.MEDIUM: - return 20; - case TemplateFieldSize.LARGE: - return 50; - case TemplateFieldSize.HUGE: - return 100; - default: - return 10; - } - }; - - const renderTextCalls = async (): Promise<Doc[]> => { - const rendered: Doc[] = []; - - if (GPTTextCalls.length) { - try { - const prompt = fieldContent + GPTTextAssignment; - - const res = await gptAPICall(prompt, GPTCallType.FILL); - - if (res) { - const assignments: { [title: string]: { number: string; content: string } } = JSON.parse(res); - //console.log('assignments', GPTAssignments, 'assignment string', GPTAssignmentString, 'field content', fieldContent, 'response', res, 'assignments', assignments); - Object.entries(assignments).forEach(([title, info]) => { - const field: Field = template.fields[Number(info.number)]; - const col = this.getColByTitle(title); - - const doc = FieldUtils.TextField( - { - tl: field.tl, - br: field.br, - }, - template.height, - template.width, - col.title, - info.content ?? '', - field.opts - ); - - rendered.push(doc); - }); - } - } catch (err) { - console.log(err); - } - } - - return rendered; - }; - - const createGeneratedImage = async (fieldNum: string, col: Col, prompt: string) => { - const url = await this.generateGPTImage(prompt); - const field: Field = template.fields[Number(fieldNum)]; - const doc = FieldUtils.ImageField( - { - tl: field.tl, - br: field.br, - }, - template.height, - template.width, - col.title, - url ?? '', - field.opts - ); - - return doc; - }; - - const renderImageCalls = async (): Promise<Doc[]> => { - const rendered: Doc[] = []; - const calls = GPTIMGCalls; - - if (calls.length) { - try { - const renderedImages: Doc[] = await Promise.all( - calls.map(async ([fieldNum, col]) => { - const sysPrompt = - 'Your job is to create a prompt for an AI image generator to help it generate an image based on existing content in a template and a user prompt. Your prompt should focus heavily on visual elements to help the image generator; avoid unecessary info that might distract it. ONLY INCLUDE THE PROMPT, NO OTHER TEXT OR EXPLANATION. The existing content is as follows: ' + - fieldContent + - ' **** The user prompt is: ' + - col.desc; - - const prompt = await gptAPICall(sysPrompt, GPTCallType.COMPLETEPROMPT); - console.log(sysPrompt, prompt); - - return createGeneratedImage(fieldNum, col, prompt); - }) - ); - - const renderedTemplates: Doc[] = await Promise.all(renderedImages); - renderedTemplates.forEach(doc => rendered.push(doc)); - } catch (e) { - console.log(e); - } - } - - return rendered; - }; - - const fields: Doc[] = []; - - const GPTAssignments = Object.entries(assignments).filter(([f, col]) => this._columns.includes(col)); - const nonGPTAssignments: [string, Col][] = Object.entries(assignments).filter(a => !GPTAssignments.includes(a)); - const GPTTextCalls = GPTAssignments.filter(([str, col]) => col.type === TemplateFieldType.TEXT); - const GPTIMGCalls = GPTAssignments.filter(([str, col]) => col.type === TemplateFieldType.VISUAL); - - const stringifyGPTInfo = (calls: [string, Col][]): string => { - let string: string = '*** COLUMN INFO:'; - calls.forEach(([fieldNum, col]) => { - string += `--- title: ${col.title}, prompt: ${col.desc}, word limit: ${wordLimit(col.sizes[0])} words, assigned field: ${fieldNum} ---`; - }); - return (string += ' ***'); - }; - - const GPTTextAssignment = stringifyGPTInfo(GPTTextCalls); - - let fieldContent: string = ''; - - Object.entries(nonGPTAssignments).forEach(([f, strCol]) => { - const field: Field = template.fields[Number(f)]; - const col = strCol[1]; - - const doc = (col.type === TemplateFieldType.VISUAL ? FieldUtils.ImageField : FieldUtils.TextField)( - { - tl: field.tl, - br: field.br, - }, - template.height, - template.width, - col.title, - col.defaultContent ?? '', - field.opts - ); - - fieldContent += `--- Field #${f} (title: ${col.title}): ${col.defaultContent ?? ''} ---`; - - fields.push(doc); - }); - - template.decorations.forEach(dec => { - const doc = FieldUtils.FreeformField( - { - tl: dec.tl, - br: dec.br, - }, - template.height, - template.width, - '', - '', - dec.opts - ); - - fields.push(doc); - }); - - const createMainDoc = (): Doc => { - const main = Docs.Create.FreeformDocument(fields, { - _height: template.height, - _width: template.width, - title: template.title, - backgroundColor: template.opts.backgroundColor, - _layout_borderRounding: `${template.opts.cornerRounding}px` ?? '0px', - borderWidth: template.opts.borderWidth, - borderColor: template.opts.borderColor, - x: 40000, - y: 40000, - }); - - const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView; - mainCollection.addDocument(main); - - return main; - }; - - const textCalls = await renderTextCalls(); - const imageCalls = await renderImageCalls(); - - textCalls.forEach(doc => { - fields.push(doc); - }); - imageCalls.forEach(doc => { - fields.push(doc); - }); - - return createMainDoc(); - }; - - compileFieldDescriptions = (templates: TemplateDocInfos[]): string => { - let descriptions: string = ''; - templates.forEach(template => { - descriptions += `---------- NEW TEMPLATE TO INCLUDE: Description of template ${template.title}'s fields: `; - template.fields.forEach((field, index) => { - descriptions += `{Field #${index}: ${field.description}} `; - }); - }); - - return descriptions; - }; - - compileColDescriptions = (cols: Col[]): string => { - let descriptions: string = ' ------------- COL DESCRIPTIONS START HERE:'; - cols.forEach(col => (descriptions += `{title: ${col.title}, sizes: ${String(col.sizes)}, type: ${col.type}, descreiption: ${col.desc}} `)); - - return descriptions; - }; - - getColByTitle = (title: string) => { - return this.fieldsInfos.filter(col => col.title === title)[0]; - }; - - @action - assignColsToFields = async (templates: TemplateDocInfos[], cols: Col[]): Promise<[TemplateDocInfos, { [field: number]: Col }][]> => { - const fieldDescriptions: string = this.compileFieldDescriptions(templates); - const colDescriptions: string = this.compileColDescriptions(cols); - - const inputText = fieldDescriptions.concat(colDescriptions); - - ++this._callCount; - const origCount = this._callCount; - - let prompt: string = `(${origCount}) ${inputText}`; - - this._GPTLoading = true; - - try { - const res = await gptAPICall(prompt, GPTCallType.TEMPLATE); - - if (res && this._callCount === origCount) { - const assignments: { [templateTitle: string]: { [field: string]: string } } = JSON.parse(res); - const brokenDownAssignments: [TemplateDocInfos, { [field: number]: Col }][] = []; - - Object.entries(assignments).forEach(([tempTitle, assignment]) => { - const template = TemplateLayouts.getTemplateByTitle(tempTitle); - if (!template) return; - const toObj = Object.entries(assignment).reduce( - (a, [fieldNum, colTitle]) => { - a[Number(fieldNum)] = this.getColByTitle(colTitle); - return a; - }, - {} as { [field: number]: Col } - ); - brokenDownAssignments.push([template, toObj]); - }); - return brokenDownAssignments; - } - } catch (err) { - console.error(err); - } - - return []; - }; - - generatePresetTemplates = async () => { - this._dataViz?.updateColDefaults(); - - const cols = this.fieldsInfos; - const templates = this.findValidTemplates(cols, TemplateLayouts.allTemplates); - - const assignments: [TemplateDocInfos, { [field: number]: Col }][] = await this.assignColsToFields(templates, cols); - - const renderedTemplatePromises: Promise<Doc>[] = assignments.map(([template, assignments]) => this.fillPresetTemplate(template, assignments)); - - const renderedTemplates: Doc[] = await Promise.all(renderedTemplatePromises); - - setTimeout(() => { - this.setGSuggestedTemplates(renderedTemplates); - this._GPTLoading = false; - }); - }; - - @action setExpandedView = (info: { icon: ImageField; doc: Doc } | undefined) => { - if (info) { - const doc = info.doc; - const wrapper: Doc = Docs.Create.FreeformDocument([info.doc], { _height: NumListCast(doc._height)[0], _width: NumListCast(doc._width)[0], title: '' }); - const newInfo = { icon: new ImageField(''), doc: wrapper }; - this._expandedPreview = newInfo; - } else { - this._expandedPreview = info; - } - }; - - get editingWindow() { - const doc = this._expandedPreview?.doc ?? new Doc(); - const rendered = ( - <div className="docCreatorMenu-expanded-template-preview"> - <CollectionFreeFormView - Document={this._expandedPreview!.doc} - docViewPath={returnEmptyDocViewList} - childLayoutTemplate={() => Cast(doc.childLayoutTemplate, Doc, null)} - isContentActive={emptyFunction} - isAnyChildContentActive={() => true} - select={emptyFunction} - isSelected={returnFalse} - fieldKey={Doc.LayoutFieldKey(doc)} - addDocument={returnFalse} - moveDocument={returnFalse} - removeDocument={returnFalse} - PanelWidth={() => this._menuDimensions.width - 10} - PanelHeight={() => this._menuDimensions.height - 60} - ScreenToLocalTransform={() => new Transform(-this._pageX, -this._pageY, 1)} - renderDepth={5} - whenChildContentsActiveChanged={emptyFunction} - focus={emptyFunction} - styleProvider={DefaultStyleProvider} - addDocTab={this._props.addDocTab} - // eslint-disable-next-line no-use-before-define - pinToPres={() => undefined} - childFilters={returnEmptyFilter} - childFiltersByRanges={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - fitContentsToBox={returnTrue} - xPadding={0} - yPadding={0} - /> - </div> - ); - - return ( - <div className="docCreatorMenu-expanded-template-preview"> - <div className="top-panel" /> - {rendered} - <div className="right-buttons-panel"> - <button - className="docCreatorMenu-menu-button section-reveal-options top-right" - onPointerDown={e => - this.setUpButtonClick(e, () => { - this._expandedPreview && this.updateIcons(this._suggestedTemplates.slice()); - this.setExpandedView(undefined); - }) - }> - <FontAwesomeIcon icon="minimize" /> - </button> - <button className="docCreatorMenu-menu-button section-reveal-options top-right-lower" onPointerDown={e => this.setUpButtonClick(e, () => this._expandedPreview && this._templateDocs.push(this._expandedPreview.doc))}> - <FontAwesomeIcon icon="plus" color="white" /> - </button> - </div> - </div> - ); - } - - get templatesPreviewContents() { - const renderedTemplates: Doc[] = []; - - const GPTOptions = <div></div>; - - //<img className='docCreatorMenu-preview-image expanded' src={this._expandedPreview.icon!.url.href.replace(".png", "_o.png")} /> - - return ( - <div className={`docCreatorMenu-templates-view`}> - {this._expandedPreview ? ( - this.editingWindow - ) : ( - <div> - <div className="docCreatorMenu-section" style={{ height: this._GPTOpt ? 200 : 200 }}> - <div className="docCreatorMenu-section-topbar"> - <div className="docCreatorMenu-section-title">Suggested Templates</div> - <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._menuContent = 'dashboard')))}> - <FontAwesomeIcon icon="gear" /> - </button> - </div> - <div className="docCreatorMenu-templates-preview-window" style={{ justifyContent: this._GPTLoading || this._menuDimensions.width > 400 ? 'center' : '' }}> - {this._GPTLoading ? ( - <div className="loading-spinner"> - <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} /> - </div> - ) : ( - this._suggestedTemplates - ?.map(doc => ({ icon: ImageCast(doc.icon), doc })) - .filter(info => info.icon && info.doc) - .map(info => ( - <div - className="docCreatorMenu-preview-window" - style={{ - border: this._selectedTemplate === info.doc ? `solid 3px ${Colors.MEDIUM_BLUE}` : '', - boxShadow: this._selectedTemplate === info.doc ? `0 0 15px rgba(68, 118, 247, .8)` : '', - }} - onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(info.doc)))}> - <button - className="option-button left" - onPointerDown={e => - this.setUpButtonClick(e, () => { - this.setExpandedView(info); - }) - }> - <FontAwesomeIcon icon="magnifying-glass" color="white" /> - </button> - <button className="option-button right" onPointerDown={e => this.setUpButtonClick(e, () => this._templateDocs.push(info.doc))}> - <FontAwesomeIcon icon="plus" color="white" /> - </button> - <img className="docCreatorMenu-preview-image" src={info.icon!.url.href.replace('.png', '_o.png')} /> - </div> - )) - )} - </div> - <div className="docCreatorMenu-GPT-options"> - <div className="docCreatorMenu-GPT-options-container"> - <button className="docCreatorMenu-menu-button" onPointerDown={e => this.setUpButtonClick(e, () => this.generatePresetTemplates())}> - <FontAwesomeIcon icon="arrows-rotate" /> - </button> - </div> - {this._GPTOpt ? GPTOptions : null} - </div> - </div> - <hr className="docCreatorMenu-option-divider full no-margin" /> - <div className="docCreatorMenu-section"> - <div className="docCreatorMenu-section-topbar"> - <div className="docCreatorMenu-section-title">Your Templates</div> - <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, () => (this._GPTOpt = !this._GPTOpt))}> - <FontAwesomeIcon icon="gear" /> - </button> - </div> - <div className="docCreatorMenu-templates-preview-window" style={{ justifyContent: this._menuDimensions.width > 400 ? 'center' : '' }}> - <div className="docCreatorMenu-preview-window empty" onPointerDown={e => this.testTemplate()}> - <FontAwesomeIcon icon="plus" color="rgb(160, 160, 160)" /> - </div> - {this._templateDocs - .map(doc => ({ icon: ImageCast(doc.icon), doc })) - .filter(info => info.icon && info.doc) - .map(info => { - if (renderedTemplates.includes(info.doc)) return undefined; - renderedTemplates.push(info.doc); - return ( - <div - className="docCreatorMenu-preview-window" - style={{ - border: this._selectedTemplate === info.doc ? `solid 3px ${Colors.MEDIUM_BLUE}` : '', - boxShadow: this._selectedTemplate === info.doc ? `0 0 15px rgba(68, 118, 247, .8)` : '', - }} - onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(info.doc)))}> - <button - className="option-button left" - onPointerDown={e => - this.setUpButtonClick(e, () => { - this.editTemplate(info.doc); - }) - }> - <FontAwesomeIcon icon="pencil" color="black" /> - </button> - <button - className="option-button right" - onPointerDown={e => - this.setUpButtonClick(e, () => { - this.removeTemplate(info.doc); - }) - }> - <FontAwesomeIcon icon="trash" color="black" /> - </button> - <img className="docCreatorMenu-preview-image" src={info.icon!.url.href.replace('.png', '_o.png')} /> - </div> - ); - })} - </div> - </div> - </div> - )} - </div> - ); - } - - get savedLayoutsPreviewContents() { - return ( - <div className="docCreatorMenu-preview-container"> - {this._savedLayouts.map((layout, index) => ( - <div - className="docCreatorMenu-preview-window" - style={{ - border: this.isSelectedLayout(layout) ? `solid 3px ${Colors.MEDIUM_BLUE}` : '', - boxShadow: this.isSelectedLayout(layout) ? `0 0 15px rgba(68, 118, 247, .8)` : '', - }} - onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedSavedLayout(layout)))}> - {this.layoutPreviewContents(87, layout, true, index)} - </div> - ))} - </div> - ); - } - - @action updateXMargin = (input: string) => { - this._layout.xMargin = Number(input); - }; - @action updateYMargin = (input: string) => { - this._layout.yMargin = Number(input); - }; - @action updateColumns = (input: string) => { - this._layout.columns = Number(input); - }; - - get layoutConfigOptions() { - const optionInput = (icon: string, func: Function, def?: number, key?: string, noMargin?: boolean) => { - return ( - <div className="docCreatorMenu-option-container small no-margin" key={key} style={{ marginTop: noMargin ? '0px' : '' }}> - <div className="docCreatorMenu-option-title config layout-config"> - <FontAwesomeIcon icon={icon as any} /> - </div> - <input defaultValue={def} onInput={e => func(e.currentTarget.value)} className="docCreatorMenu-input config layout-config" /> - </div> - ); - }; - - switch (this._layout.type) { - case LayoutType.Row: - return <div className="docCreatorMenu-configuration-bar">{optionInput('arrows-left-right', this.updateXMargin, this._layout.xMargin, '0')}</div>; - case LayoutType.Column: - return <div className="docCreatorMenu-configuration-bar">{optionInput('arrows-up-down', this.updateYMargin, this._layout.yMargin, '1')}</div>; - case LayoutType.Grid: - return ( - <div className="docCreatorMenu-configuration-bar"> - {optionInput('arrows-up-down', this.updateYMargin, this._layout.xMargin, '2')} - {optionInput('arrows-left-right', this.updateXMargin, this._layout.xMargin, '3')} - {optionInput('table-columns', this.updateColumns, this._layout.columns, '4', true)} - </div> - ); - case LayoutType.Stacked: - return null; - default: - break; - } - } - - // doc = () => { - // return Docs.Create.FreeformDocument([], { _height: 200, _width: 200, title: 'title'}); - // } - - screenToLocalTransform = () => this._props.ScreenToLocalTransform(); - - layoutPreviewContents = (outerSpan: number, altLayout?: DataVizTemplateLayout, small: boolean = false, id?: number) => { - const doc: Doc | undefined = altLayout ? altLayout.template : this._selectedTemplate; - if (!doc) return; - - const layout = altLayout ? altLayout.layout : this._layout; - - const docWidth: number = Number(doc._width); - const docHeight: number = Number(doc._height); - const horizontalSpan: number = (docWidth + layout.xMargin) * (altLayout ? altLayout.columns : this.columnsCount) - layout.xMargin; - const verticalSpan: number = (docHeight + layout.yMargin) * (altLayout ? altLayout.rows : this.rowsCount) - layout.yMargin; - const largerSpan: number = horizontalSpan > verticalSpan ? horizontalSpan : verticalSpan; - const scaledDown = (input: number) => { - return input / ((largerSpan / outerSpan) * this._layoutPreviewScale); - }; - const fontSize = Math.min(scaledDown(docWidth / 3), scaledDown(docHeight / 3)); - - return ( - // <div className='divvv' style={{width: 100, height: 100, border: `1px solid white`}}> - // <CollectionFreeFormView - // // eslint-disable-next-line react/jsx-props-no-spreading - // {...this._props} - // Document={new Doc()} - // isContentActive={returnFalse} - // setContentViewBox={emptyFunction} - // NativeWidth={() => 100} - // NativeHeight={() => 100} - // pointerEvents={SnappingManager.IsDragging ? returnAll : returnNone} - // isAnnotationOverlay - // isAnnotationOverlayScrollable - // childDocumentsActive={returnFalse} - // fieldKey={this._props.fieldKey + '_annotations'} - // dropAction={dropActionType.move} - // select={emptyFunction} - // addDocument={returnFalse} - // removeDocument={returnFalse} - // moveDocument={returnFalse} - // renderDepth={this._props.renderDepth + 1}> - // {null} - // </CollectionFreeFormView> - // </div> - <div className="docCreatorMenu-layout-preview-window-wrapper" id={String(id) ?? undefined}> - <div className="docCreatorMenu-zoom-button-container"> - <button className="docCreatorMenu-zoom-button" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._layoutPreviewScale *= 1.25)))}> - <FontAwesomeIcon icon={'minus'} /> - </button> - <button className="docCreatorMenu-zoom-button zoom-in" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._layoutPreviewScale *= 0.75)))}> - <FontAwesomeIcon icon={'plus'} /> - </button> - {altLayout ? ( - <button className="docCreatorMenu-zoom-button trash" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this._savedLayouts.splice(this._savedLayouts.indexOf(altLayout), 1)))}> - <FontAwesomeIcon icon={'trash'} /> - </button> - ) : null} - </div> - { - <div - id={String(id) ?? undefined} - className={`docCreatorMenu-layout-preview-window ${small ? 'small' : ''}`} - style={{ - gridTemplateColumns: `repeat(${altLayout ? altLayout.columns : this.columnsCount}, ${scaledDown(docWidth)}px`, - gridTemplateRows: `${scaledDown(docHeight)}px`, - gridAutoRows: `${scaledDown(docHeight)}px`, - rowGap: `${scaledDown(layout.yMargin)}px`, - columnGap: `${scaledDown(layout.xMargin)}px`, - }}> - {this._layout.type === LayoutType.Stacked ? ( - <div - className="docCreatorMenu-layout-preview-item" - style={{ - width: scaledDown(docWidth), - height: scaledDown(docHeight), - fontSize: fontSize, - }}> - All - </div> - ) : ( - this.docsToRender.map(num => ( - <div - onMouseEnter={() => this._dataViz?.setSpecialHighlightedRow(num)} - onMouseLeave={() => this._dataViz?.setSpecialHighlightedRow(undefined)} - className="docCreatorMenu-layout-preview-item" - style={{ - width: scaledDown(docWidth), - height: scaledDown(docHeight), - fontSize: fontSize, - }}> - {num} - </div> - )) - )} - </div> - } - </div> - ); - }; - - get optionsMenuContents() { - const layoutEquals = (layout: DataVizTemplateLayout) => {}; //TODO: ADD LATER - - const layoutOption = (option: LayoutType, optStyle?: {}, specialFunc?: Function) => { - return ( - <div - className="docCreatorMenu-dropdown-option" - style={optStyle} - onPointerDown={e => - this.setUpButtonClick(e, () => { - specialFunc?.(); - runInAction(() => (this._layout.type = option)); - }) - }> - {option} - </div> - ); - }; - - const selectionBox = (width: number, height: number, icon: string, specClass?: string, options?: JSX.Element[], manual?: boolean): JSX.Element => { - return ( - <div className="docCreatorMenu-option-container"> - <div className={`docCreatorMenu-option-title config ${specClass}`} style={{ width: width * 0.4, height: height }}> - <FontAwesomeIcon icon={icon as any} /> - </div> - {manual ? ( - <input className={`docCreatorMenu-input config ${specClass}`} style={{ width: width * 0.6, height: height }} /> - ) : ( - <select className={`docCreatorMenu-input config ${specClass}`} style={{ width: width * 0.6, height: height }}> - {options} - </select> - )} - </div> - ); - }; - - const repeatOptions = [0, 1, 2, 3, 4, 5]; - - return ( - <div className="docCreatorMenu-menu-container"> - <div className="docCreatorMenu-option-container layout"> - <div className="docCreatorMenu-dropdown-hoverable"> - <div className="docCreatorMenu-option-title">{this._layout.type ? this._layout.type.toUpperCase() : 'Choose Layout'}</div> - <div className="docCreatorMenu-dropdown-content"> - {layoutOption(LayoutType.Stacked)} - {layoutOption(LayoutType.Grid, undefined, () => { - if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length)); - })} - {layoutOption(LayoutType.Row)} - {layoutOption(LayoutType.Column)} - {layoutOption(LayoutType.Custom, { borderBottom: `0px` })} - </div> - </div> - <button className="docCreatorMenu-menu-button preview-toggle" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._layoutPreview = !this._layoutPreview)))}> - <FontAwesomeIcon icon={this._layoutPreview ? 'minus' : 'magnifying-glass'} /> - </button> - </div> - {this._layout.type ? this.layoutConfigOptions : null} - {this._layoutPreview ? this.layoutPreviewContents(this._menuDimensions.width * 0.75) : null} - {selectionBox( - 60, - 20, - 'repeat', - undefined, - repeatOptions.map(num => <option onPointerDown={e => (this._layout.repeat = num)}>{`${num}x`}</option>) - )} - <hr className="docCreatorMenu-option-divider" /> - <div className="docCreatorMenu-general-options-container"> - <button - className="docCreatorMenu-save-layout-button" - onPointerDown={e => - setupMoveUpEvents( - this, - e, - returnFalse, - emptyFunction, - undoable(clickEv => { - clickEv.stopPropagation(); - if (!this._selectedTemplate) return; - const layout: DataVizTemplateLayout = { - template: this._selectedTemplate, - layout: { type: this._layout.type, xMargin: this._layout.xMargin, yMargin: this._layout.yMargin, repeat: 0 }, - columns: this.columnsCount, - rows: this.rowsCount, - docsNumList: this.docsToRender, - }; - if (!this._savedLayouts.includes(layout)) { - this._savedLayouts.push(layout); - } - }, 'make docs') - ) - }> - <FontAwesomeIcon icon="floppy-disk" /> - </button> - <button - className="docCreatorMenu-create-docs-button" - style={{ backgroundColor: this.canMakeDocs ? '' : 'rgb(155, 155, 155)', border: this.canMakeDocs ? '' : 'solid 2px rgb(180, 180, 180)' }} - onPointerDown={e => - setupMoveUpEvents( - this, - e, - returnFalse, - emptyFunction, - undoable(clickEv => { - clickEv.stopPropagation(); - if (!this._selectedTemplate) return; - const templateInfo: DataVizTemplateInfo = { doc: this._selectedTemplate, layout: this._layout, referencePos: { x: this._pageX + 450, y: this._pageY }, columns: this.columnsCount }; - this._dataViz?.createDocsFromTemplate(templateInfo); - }, 'make docs') - ) - }> - <FontAwesomeIcon icon="plus" /> - </button> - </div> - </div> - ); - } - - get dashboardContents() { - const sizes: string[] = ['tiny', 'small', 'medium', 'large', 'huge']; - - const fieldPanel = (field: Col) => { - return ( - <div className="field-panel"> - <div className="top-bar"> - <span className="field-title">{`${field.title} Field`}</span> - <button className="docCreatorMenu-menu-button section-reveal-options no-margin" onPointerDown={e => this.setUpButtonClick(e, () => this.removeField(field))} style={{ position: 'absolute', right: '0px' }}> - <FontAwesomeIcon icon="minus" /> - </button> - </div> - <div className="opts-bar"> - <div className="opt-box"> - <div className="top-bar"> Title </div> - <textarea className="content" style={{ width: '100%', height: 'calc(100% - 20px)' }} defaultValue={field.title} placeholder={'Enter title'} onChange={e => this.setColTitle(field, e.target.value)} /> - </div> - <div className="opt-box"> - <div className="top-bar"> Type </div> - <div className="content"> - <span className="type-display">{field.type === TemplateFieldType.TEXT ? 'Text Field' : field.type === TemplateFieldType.VISUAL ? 'File Field' : ''}</span> - <div className="bubbles"> - <input - className="bubble" - type="radio" - name="type" - onClick={() => { - this.setColType(field, TemplateFieldType.TEXT); - }} - /> - <div className="text">Text</div> - <input - className="bubble" - type="radio" - name="type" - onClick={() => { - this.setColType(field, TemplateFieldType.VISUAL); - }} - /> - <div className="text">File</div> - </div> - </div> - </div> - </div> - <div className="sizes-box"> - <div className="top-bar"> Valid Sizes </div> - <div className="content"> - <div className="bubbles"> - {sizes.map(size => ( - <> - <input - className="bubble" - type="checkbox" - name="type" - checked={field.sizes.includes(size as TemplateFieldSize)} - onChange={e => { - this.modifyColSizes(field, size as TemplateFieldSize, e.target.checked); - }} - /> - <div className="text">{size}</div> - </> - ))} - </div> - </div> - </div> - <div className="desc-box"> - <div className="top-bar"> Prompt </div> - <textarea - className="content" - onChange={e => this.setColDesc(field, e.target.value)} - defaultValue={field.desc === this._dataViz?.GPTSummary?.get(field.title)?.desc ? '' : field.desc} - placeholder={this._dataViz?.GPTSummary?.get(field.title)?.desc ?? 'Add a description/prompt to help with template generation.'} - /> - </div> - </div> - ); - }; - - return ( - <div className="docCreatorMenu-dashboard-view"> - <div className="topbar"> - <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, this.addField)}> - <FontAwesomeIcon icon="plus" /> - </button> - <button className="docCreatorMenu-menu-button section-reveal-options float-right" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._menuContent = 'templates')))}> - <FontAwesomeIcon icon="arrow-left" /> - </button> - </div> - <div className="panels-container">{this.fieldsInfos.map(field => fieldPanel(field))}</div> - </div> - ); - } - - get renderSelectedViewType() { - switch (this._menuContent) { - case 'templates': - return this.templatesPreviewContents; - case 'options': - return this.optionsMenuContents; - case 'saved': - return this.savedLayoutsPreviewContents; - case 'dashboard': - return this.dashboardContents; - default: - return undefined; - } - } - - get resizePanes() { - const ref = this._ref?.getBoundingClientRect(); - const height: number = ref?.height ?? 0; - const width: number = ref?.width ?? 0; - - return [ - <div className='docCreatorMenu-resizer top' onPointerDown={this.onResizePointerDown} style={{width: width, left: 0, top: -7}}/>, - <div className='docCreatorMenu-resizer right' onPointerDown={this.onResizePointerDown} style={{height: height, left: width - 3, top: 0}}/>, - <div className='docCreatorMenu-resizer bottom' onPointerDown={this.onResizePointerDown} style={{width: width, left: 0, top: height - 3}}/>, - <div className='docCreatorMenu-resizer left' onPointerDown={this.onResizePointerDown} style={{height: height, left: -7, top: 0}}/>, - <div className='docCreatorMenu-resizer topRight' onPointerDown={this.onResizePointerDown} style={{left: width - 5, top: -10, cursor: 'nesw-resize'}}/>, - <div className='docCreatorMenu-resizer topLeft' onPointerDown={this.onResizePointerDown} style={{left: -10, top: -10, cursor: 'nwse-resize'}}/>, - <div className='docCreatorMenu-resizer bottomRight' onPointerDown={this.onResizePointerDown} style={{left: width - 5, top: height - 5, cursor: 'nwse-resize'}}/>, - <div className='docCreatorMenu-resizer bottomLeft' onPointerDown={this.onResizePointerDown} style={{left: -10, top: height - 5, cursor: 'nesw-resize'}}/> - ]; //prettier-ignore - } - - render() { - const topButton = (icon: string, opt: string, func: Function, tag: string) => { - return ( - <div className={`top-button-container ${tag} ${opt === this._menuContent ? 'selected' : ''}`}> - <div - className="top-button-content" - onPointerDown={e => - this.setUpButtonClick(e, () => - runInAction(() => { - func(); - }) - ) - }> - <FontAwesomeIcon icon={icon as any} /> - </div> - </div> - ); - }; - - const onPreviewSelected = () => { - this._menuContent = 'templates'; - }; - const onSavedSelected = () => { - this._menuContent = 'dashboard'; - }; - const onOptionsSelected = () => { - this._menuContent = 'options'; - if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length)); - }; - - return ( - <div className="docCreatorMenu"> - {!this._shouldDisplay ? undefined : ( - <div - className="docCreatorMenu-cont" - ref={r => (this._ref = r)} - style={{ - display: '', - left: this._pageX, - top: this._pageY, - width: this._menuDimensions.width, - height: this._menuDimensions.height, - background: SnappingManager.userBackgroundColor, - color: SnappingManager.userColor, - }}> - {this.resizePanes} - <div - className="docCreatorMenu-menu" - onPointerDown={e => - setupMoveUpEvents( - this, - e, - e => { - this._dragging = true; - this._startPos = { x: 0, y: 0 }; - this._startPos.x = e.pageX - (this._ref?.getBoundingClientRect().left ?? 0); - this._startPos.y = e.pageY - (this._ref?.getBoundingClientRect().top ?? 0); - document.addEventListener('pointermove', this.onDrag); - return true; - }, - emptyFunction, - undoable(clickEv => { - clickEv.stopPropagation(); - }, 'drag menu') - ) - }> - <div className="docCreatorMenu-top-buttons-container"> - {topButton('table-cells', 'templates', onPreviewSelected, 'left')} - {topButton('bars', 'options', onOptionsSelected, 'middle')} - {topButton('floppy-disk', 'saved', onSavedSelected, 'right')} - </div> - <button className="docCreatorMenu-menu-button close-menu" onPointerDown={e => this.setUpButtonClick(e, this.closeMenu)}> - <FontAwesomeIcon icon={'minus'} /> - </button> - </div> - {this.renderSelectedViewType} - </div> - )} - </div> - ); - } -} - -export interface DataVizTemplateInfo { - doc: Doc; - layout: { type: LayoutType; xMargin: number; yMargin: number; repeat: number }; - columns: number; - referencePos: { x: number; y: number }; -} - -export interface DataVizTemplateLayout { - template: Doc; - docsNumList: number[]; - layout: { type: LayoutType; xMargin: number; yMargin: number; repeat: number }; - columns: number; - rows: number; -} - -export enum TemplateFieldType { - TEXT = 'text', - VISUAL = 'visual', - UNSET = 'unset', -} - -export enum TemplateFieldSize { - TINY = 'tiny', - SMALL = 'small', - MEDIUM = 'medium', - LARGE = 'large', - HUGE = 'huge', -} - -export type Col = { - sizes: TemplateFieldSize[]; - desc: string; - title: string; - type: TemplateFieldType; - defaultContent?: string; -}; -export interface FieldOpts { - backgroundColor?: string; - color?: string; - cornerRounding?: number; - borderWidth?: string; - borderColor?: string; - contentXCentering?: 'h-left' | 'h-center' | 'h-right'; - contentYCentering?: 'top' | 'center' | 'bottom'; - opacity?: number; - rotation?: number; - //animation?: boolean; - fontBold?: boolean; - fontTransform?: 'uppercase' | 'lowercase'; - fieldViewType?: 'freeform' | 'stacked'; -} - -export type Field = { - tl: [number, number]; - br: [number, number]; - opts: FieldOpts; - subfields?: Field[]; - types?: TemplateFieldType[]; - sizes?: TemplateFieldSize[]; - isDecoration?: boolean; - description?: string; -}; - -// class ContentField implements Field { -// tl: [number, number]; -// br: [number, number]; -// opts: FieldOpts; -// subfields?: Field[]; -// types?: TemplateFieldType[]; -// sizes?: TemplateFieldSize[]; -// description?: string; - -// constructor( tl: [number, number], br: [number, number], -// opts: FieldOpts, subfields?: Field[], -// types?: TemplateFieldType[], -// sizes?: TemplateFieldSize[], -// description?: string) { -// this.tl = tl; -// this.br = br; -// this.opts = opts; -// this.subfields = subfields; -// this.types = types; -// this.sizes = sizes; -// this.description = description; -// } - -// render = (content: any): Doc => { -// return new Doc; -// } -// } - -type DecorationField = Field; - -type InkDecoration = {}; - -type TemplateDecorations = Field | InkDecoration; - -interface TemplateOpts extends FieldOpts {} - -export interface TemplateDocInfos { - title: string; - height: number; - width: number; - opts: TemplateOpts; - fields: Field[]; - decorations: Field[]; -} - -export class TemplateLayouts { - public static get allTemplates(): TemplateDocInfos[] { - return Object.values(TemplateLayouts).filter(value => typeof value === 'object' && value !== null && 'title' in value) as TemplateDocInfos[]; - } - - public static getTemplateByTitle = (title: string): TemplateDocInfos | undefined => { - switch (title) { - case 'fourfield1': - return TemplateLayouts.FourField001; - case 'fourfield2': - return TemplateLayouts.FourField002; - // case 'fourfield3': - // return TemplateLayouts.FourField003; - case 'fourfield4': - return TemplateLayouts.FourField004; - case 'threefield1': - return TemplateLayouts.ThreeField001; - case 'threefield2': - return TemplateLayouts.ThreeField002; - default: - break; - } - - return undefined; - }; - - public static FourField001: TemplateDocInfos = { - title: 'fourfield1', - width: 416, - height: 700, - opts: { - backgroundColor: '#C0B887', - cornerRounding: 20, - borderColor: '#6B461F', - borderWidth: '12', - }, - fields: [ - { - tl: [-0.95, -1], - br: [0.95, -0.85], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.TINY], - description: 'A title field for very short text that contextualizes the content.', - opts: { - backgroundColor: 'transparent', - color: '#F1F0E9', - contentXCentering: 'h-center', - fontBold: true, - }, - }, - { - tl: [-0.87, -0.83], - br: [0.87, 0.2], - types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL], - sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], - description: 'The main focus of the template; could be an image, long text, etc.', - opts: { - cornerRounding: 20, - borderColor: '#8F5B25', - borderWidth: '6', - backgroundColor: '#CECAB9', - }, - }, - { - tl: [-0.8, 0.2], - br: [0.8, 0.3], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], - description: 'A caption for field #2, very short to short text that contextualizes the content of field #2', - opts: { - backgroundColor: 'transparent', - contentXCentering: 'h-center', - color: '#F1F0E9', - }, - }, - { - tl: [-0.87, 0.37], - br: [0.87, 0.96], - types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL], - sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], - description: 'A medium-sized field for medium/long text.', - opts: { - cornerRounding: 15, - borderColor: '#8F5B25', - borderWidth: '6', - backgroundColor: '#CECAB9', - }, - }, - ], - decorations: [], - }; - - public static FourField002: TemplateDocInfos = { - title: 'fourfield2', - width: 425, - height: 778, - opts: { - backgroundColor: '#242425', - }, - fields: [ - { - tl: [-0.83, -0.95], - br: [0.83, -0.2], - types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], - description: 'A medium to large-sized field suitable for an image or longer text that should be the main focus.', - opts: { - borderWidth: '8', - borderColor: '#F8E71C', - }, - }, - { - tl: [-0.65, -0.2], - br: [0.65, -0.02], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.TINY], - description: 'A tiny field for just a word or two of plain text.', - opts: { - backgroundColor: 'transparent', - color: 'white', - contentXCentering: 'h-center', - fontTransform: 'uppercase', - }, - }, - { - tl: [-0.65, 0], - br: [0.65, 0.18], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.TINY], - description: 'A tiny field for just a word or two of plain text.', - opts: { - backgroundColor: 'transparent', - color: 'white', - contentXCentering: 'h-center', - fontTransform: 'uppercase', - }, - }, - { - tl: [-0.83, 0.2], - br: [0.83, 0.95], - types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL], - sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], - description: 'A medium to large-sized field suitable for an image or longer text that should be the main focus, or share focus with field 1.', - opts: { - borderWidth: '8', - borderColor: '#F8E71C', - color: 'white', - backgroundColor: '#242425', - }, - }, - ], - decorations: [ - { - tl: [-0.8, -0.075], - br: [-0.525, 0.075], - opts: { - backgroundColor: '#F8E71C', - rotation: 45, - }, - }, - { - tl: [-0.3075, -0.0245], - br: [-0.2175, 0.0245], - opts: { - backgroundColor: '#F8E71C', - rotation: 45, - }, - }, - { - tl: [-0.045, -0.0245], - br: [0.045, 0.0245], - opts: { - backgroundColor: '#F8E71C', - rotation: 45, - }, - }, - { - tl: [0.2175, -0.0245], - br: [0.3075, 0.0245], - opts: { - backgroundColor: '#F8E71C', - rotation: 45, - }, - }, - { - tl: [0.525, -0.075], - br: [0.8, 0.075], - opts: { - backgroundColor: '#F8E71C', - rotation: 45, - }, - }, - ], - }; - - // public static FourField003: TemplateDocInfos = { - // title: 'fourfield3', - // width: 477, - // height: 662, - // opts: { - // backgroundColor: '#9E9C95' - // }, - // fields: [{ - // tl: [-.875, -.9], - // br: [.875, .7], - // types: [TemplateFieldType.VISUAL], - // sizes: [TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], - // description: '', - // opts: { - // borderWidth: '15', - // borderColor: '#E0E0DA', - // } - // }, { - // tl: [-.95, .8], - // br: [-.1, .95], - // types: [TemplateFieldType.TEXT], - // sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], - // description: '', - // opts: { - // backgroundColor: 'transparent', - // color: 'white', - // contentXCentering: 'h-right', - // } - // }, { - // tl: [.1, .8], - // br: [.95, .95], - // types: [TemplateFieldType.TEXT], - // sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], - // description: '', - // opts: { - // backgroundColor: 'transparent', - // color: 'red', - // fontTransform: 'uppercase', - // contentXCentering: 'h-left' - // } - // }, { - // tl: [0, -.9], - // br: [.85, -.66], - // types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL], - // sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], - // description: '', - // opts: { - // backgroundColor: 'transparent', - // contentXCentering: 'h-right' - // } - // }], - // decorations: [{ - // tl: [-.025, .8], - // br: [.025, .95], - // opts: { - // backgroundColor: '#E0E0DA', - // } - // }] - // }; - - public static FourField004: TemplateDocInfos = { - title: 'fourfield4', - width: 414, - height: 583, - opts: { - backgroundColor: '#6CCAF0', - borderColor: '#1088C3', - borderWidth: '10', - }, - fields: [ - { - tl: [-0.86, -0.92], - br: [-0.075, -0.77], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.TINY], - description: 'A tiny field for just a word or two of plain text.', - opts: { - backgroundColor: '#E2B4F5', - borderWidth: '9', - borderColor: '#9222F1', - contentXCentering: 'h-center', - }, - }, - { - tl: [0.075, -0.92], - br: [0.86, -0.77], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.TINY], - description: 'A tiny field for just a word or two of plain text.', - opts: { - backgroundColor: '#F5B4DD', - borderWidth: '9', - borderColor: '#E260F3', - contentXCentering: 'h-center', - }, - }, - { - tl: [-0.81, -0.64], - br: [0.81, 0.48], - types: [TemplateFieldType.VISUAL], - sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], - description: 'A large to huge field for visual content that is the main content of the template.', - opts: { - borderWidth: '16', - borderColor: '#A2BD77', - }, - }, - { - tl: [-0.86, 0.6], - br: [0.86, 0.92], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], - description: 'A medium to large field for text that describes the visual content above', - opts: { - borderWidth: '9', - borderColor: '#F0D601', - backgroundColor: '#F3F57D', - }, - }, - ], - decorations: [ - { - tl: [-0.852, -0.67], - br: [0.852, 0.51], - opts: { - backgroundColor: 'transparent', - borderColor: '#007C0C', - borderWidth: '10', - }, - }, - ], - }; - - public static ThreeField001: TemplateDocInfos = { - title: 'threefield1', - width: 575, - height: 770, - opts: { - backgroundColor: '#DDD3A9', - }, - fields: [ - { - tl: [-0.66, -0.747], - br: [0.66, 0.247], - types: [TemplateFieldType.VISUAL], - sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], - description: 'A medium to large field for visual content that is the central focus.', - opts: { - borderColor: 'yellow', - borderWidth: '8', - rotation: 45, - }, - }, - { - tl: [-0.7, 0.2], - br: [0.7, 0.46], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], - description: 'A very small text field for one to a few words. A good caption for the image.', - opts: { - backgroundColor: 'transparent', - contentXCentering: 'h-center', - }, - }, - { - tl: [-0.95, 0.5], - br: [0.95, 0.95], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], - description: 'A medium to large text field for a thorough description of the image. ', - opts: { - backgroundColor: 'transparent', - color: 'white', - }, - }, - ], - decorations: [ - { - tl: [0.2, -1.32], - br: [1.8, -0.66], - opts: { - backgroundColor: '#CEB155', - rotation: 45, - }, - }, - { - tl: [-1.8, -1.32], - br: [-0.2, -0.66], - opts: { - backgroundColor: '#CEB155', - rotation: 135, - }, - }, - { - tl: [0.33, 0.75], - br: [1.66, 1.25], - opts: { - backgroundColor: '#CEB155', - rotation: 135, - }, - }, - { - tl: [-1.66, 0.75], - br: [-0.33, 1.25], - opts: { - backgroundColor: '#CEB155', - rotation: 45, - }, - }, - ], - }; - - public static ThreeField002: TemplateDocInfos = { - title: 'threefield2', - width: 477, - height: 662, - opts: { - backgroundColor: '#9E9C95', - }, - fields: [ - { - tl: [-0.875, -0.9], - br: [0.875, 0.7], - types: [TemplateFieldType.VISUAL], - sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], - description: 'A medium to large visual field for the main content of the template', - opts: { - borderWidth: '15', - borderColor: '#E0E0DA', - }, - }, - { - tl: [0.1, 0.775], - br: [0.95, 0.975], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], - description: 'A very small text field for one to a few words. The content should represent a general categorization of the image.', - opts: { - backgroundColor: 'transparent', - color: '#AF0D0D', - fontTransform: 'uppercase', - fontBold: true, - contentXCentering: 'h-left', - }, - }, - { - tl: [-0.95, 0.775], - br: [-0.1, 0.975], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], - description: 'A very small text field for one to a few words. The content should contextualize field 2.', - opts: { - backgroundColor: 'transparent', - color: 'black', - contentXCentering: 'h-right', - }, - }, - ], - decorations: [ - { - tl: [-0.025, 0.8], - br: [0.025, 0.95], - opts: { - backgroundColor: '#E0E0DA', - }, - }, - ], - }; -} - -export class FieldUtils { - public static contentFields = (fields: Field[]) => { - let toRet: Field[] = []; - fields.forEach(field => { - if (!field.isDecoration) { - toRet.push(field); - } - toRet = toRet.concat(FieldUtils.contentFields(field.subfields ?? [])); - }); - - return toRet; - }; - - public static calculateFontSize = (contWidth: number, contHeight: number, text: string, uppercase: boolean): number => { - const words: string[] = text.split(/\s+/).filter(Boolean); - - let currFontSize = 1; - let rowsCount = 1; - let currTextHeight = currFontSize * rowsCount * 2; - - while (currTextHeight <= contHeight) { - let wordIndex = 0; - let currentRowWidth = 0; - let wordsInCurrRow = 0; - rowsCount = 1; - - while (wordIndex < words.length) { - const word = words[wordIndex]; - const wordWidth = word.length * currFontSize * 0.5; - //console.log(wordWidth) - - if (currentRowWidth + wordWidth <= contWidth) { - currentRowWidth += wordWidth; - ++wordsInCurrRow; - } else { - if (words.length !== 1 && words.length > wordsInCurrRow) { - rowsCount++; - currentRowWidth = wordWidth; - wordsInCurrRow = 1; - } else { - break; - } - } - - wordIndex++; - } - - currTextHeight = rowsCount * currFontSize * 2; - //console.log(rowsCount, currFontSize, currTextHeight) - - currFontSize += 1; - } - - return currFontSize - 1; - }; - - private static getDimensions = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number): { width: number; height: number; coord: { x: number; y: number } } => { - const l = (coords.tl[0] * parentHeight) / 2; - const t = coords.tl[1] * parentWidth / 2; //prettier-ignore - const r = (coords.br[0] * parentHeight) / 2; - const b = coords.br[1] * parentWidth / 2; //prettier-ignore - const width = r - l; - const height = b - t; - const coord = { x: l, y: t }; - //console.log(coords, parentWidth, parentHeight, height); - return { width, height, coord }; - }; - - public static FreeformField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, content: string, opts: FieldOpts) => { - const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight); - - const docWithBasicOpts = Docs.Create.FreeformDocument([], { - isDefaultTemplateDoc: true, - _height: height, - _width: width, - title: title, - x: coord.x, - y: coord.y, - backgroundColor: opts.backgroundColor ?? '', - _layout_borderRounding: `${opts.cornerRounding ?? 0}px`, - borderColor: opts.borderColor, - borderWidth: opts.borderWidth, - opacity: opts.opacity, - hCentering: opts.contentXCentering, - _rotation: opts.rotation, - }); - - return docWithBasicOpts; - }; - - public static TextField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, content: string, opts: FieldOpts) => { - const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight); - - const docWithBasicOpts = Docs.Create.TextDocument(content, { - isDefaultTemplateDoc: true, - _height: height, - _width: width, - title: title, - x: coord.x, - y: coord.y, - text_fontSize: `${FieldUtils.calculateFontSize(width, height, content, true)}`, - backgroundColor: opts.backgroundColor ?? '', - text_fontColor: opts.color, - contentBold: opts.fontBold, - text_transform: opts.fontTransform, - color: opts.color, - _layout_borderRounding: `${opts.cornerRounding ?? 0}px`, - borderColor: opts.borderColor, - borderWidth: opts.borderWidth, - opacity: opts.opacity, - hCentering: opts.contentXCentering, - _rotation: opts.rotation, - }); - - docWithBasicOpts._layout_hideScroll = true; - - return docWithBasicOpts; - }; - - public static ImageField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, content: string, opts: FieldOpts) => { - const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight); - - const doc = Docs.Create.ImageDocument(content, { - isDefaultTemplateDoc: true, - _height: height, - _width: width, - title: title, - x: coord.x, - y: coord.y, - _layout_fitWidth: false, - backgroundColor: opts.backgroundColor ?? '', - _layout_borderRounding: `${opts.cornerRounding ?? 0}px`, - borderColor: opts.borderColor, - borderWidth: opts.borderWidth, - opacity: opts.opacity, - _rotation: opts.rotation, - }); - - //setTimeout(() => {doc._height = height; doc._width = width}, 10); - - return doc; - }; - - public static CarouselField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, fields: Doc[]) => { - const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight); - - const doc = Docs.Create.Carousel3DDocument(fields, { _height: height, _width: width, title: title, x: coord.x, y: coord.y, text_fontSize: `${height / 2}` }); - - return doc; - }; -} - -// public static FourField002: TemplateDocInfos = { -// width: 450, -// height: 600, -// fields: [{ -// tl: [-.6, -.9], -// br: [.6, -.8], -// types: [FieldType.TEXT], -// sizes: [FieldSize.TINY] -// }, { -// tl: [-.9, -.7], -// br: [.9, .2], -// types: [FieldType.TEXT, FieldType.VISUAL], -// sizes: [FieldSize.MEDIUM, FieldSize.LARGE, FieldSize.HUGE] -// }, { -// tl: [-.9, .3], -// br: [-.05, .9], -// types: [FieldType.TEXT], -// sizes: [FieldSize.TINY] -// }, { -// tl: [.05, .3], -// br: [.9, .9], -// types: [FieldType.TEXT, FieldType.VISUAL], -// sizes: [FieldSize.MEDIUM, FieldSize.LARGE, FieldSize.HUGE] -// }] -// }; - -// public static TwoFieldPlusCarousel: TemplateDocInfos = { -// width: 500, -// height: 600, -// fields: [{ -// tl: [-.9, -.99], -// br: [.9, -.7], -// types: [FieldType.TEXT], -// sizes: [FieldSize.TINY] -// }, { -// tl: [-.9, -.65], -// br: [.9, .35], -// types: [], -// sizes: [] -// }, { -// tl: [-.9, .4], -// br: [.9, .95], -// types: [FieldType.TEXT], -// sizes: [FieldSize.TINY] -// }] -// }; -// } diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu.scss b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss index 4ea904b8e..57f4a1e94 100644 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu.scss +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss @@ -7,7 +7,7 @@ .docCreatorMenu-cont { position: absolute; - z-index: 100000; + z-index: 1000; // box-shadow: 0px 3px 4px rgba(0, 0, 0, 30%); // background: whitesmoke; // color: black; @@ -45,9 +45,10 @@ font-size: 12px; width: 18px; height: 18px; - border-radius: 2px; font-size: 12px; margin-left: auto; + margin-right: 5px; + margin-bottom: 3px; } &.options { @@ -292,6 +293,7 @@ display: flex; flex-direction: column; justify-content: flex-start; + color: black; position: relative; width: 100%; height: 100%; @@ -324,6 +326,7 @@ height: 113px; margin-top: 10px; margin-left: 10px; + color: none; border: 1px solid rgb(163, 163, 163); border-radius: 5px; box-shadow: 5px 5px rgb(29, 29, 31); @@ -350,6 +353,7 @@ border: 0px; padding: 0px; font-size: 15px; + z-index: 1000; &.right { position: absolute; @@ -422,6 +426,7 @@ align-items: center; overflow-y: scroll; position: relative; + color: black; height: 125px; width: calc(100% - 10px); -ms-overflow-style: none; @@ -524,8 +529,6 @@ background: whitesmoke; background-color: rgb(34, 34, 37); border-radius: 5px; - border-top-right-radius: 0px; - border-bottom-right-radius: 0px; border: 1px solid rgb(180, 180, 180); padding: 0px; font-size: 12px; @@ -634,6 +637,8 @@ height: calc(100% - 30px); border: 1px solid rgb(180, 180, 180); border-radius: 5px; + -ms-overflow-style: none; + scrollbar-width: none; .docCreatorMenu-option-container{ width: 180px; @@ -686,13 +691,24 @@ } .docCreatorMenu-layout-preview-window-wrapper { + flex: 0 0 auto; display: flex; justify-content: center; align-items: center; - width: 85%; - height: auto; + color: black; + width: calc(100% - 50px); + height: calc(100% - 50px); position: relative; - padding: 0px; + border: 1px solid rgb(180, 180, 180); + padding: 10px; + margin-left: 20px; + margin-right: 20px; + + &.loading { + width: 100px; + height: 100px; + border: none; + } &:hover .docCreatorMenu-zoom-button-container { display: block; diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx new file mode 100644 index 000000000..64416c26d --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx @@ -0,0 +1,1458 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Colors } from '@dash/components'; +import { action, computed, makeObservable, observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import { IDisposer } from 'mobx-utils'; +import * as React from 'react'; +import ReactLoading from 'react-loading'; +import { ClientUtils, returnEmptyFilter, returnFalse, setupMoveUpEvents } from '../../../../../ClientUtils'; +import { emptyFunction } from '../../../../../Utils'; +import { Doc, NumListCast, StrListCast, returnEmptyDoclist } from '../../../../../fields/Doc'; +import { Id } from '../../../../../fields/FieldSymbols'; +import { ImageCast, StrCast } from '../../../../../fields/Types'; +import { ImageField } from '../../../../../fields/URLField'; +import { Networking } from '../../../../Network'; +import { GPTCallType, gptAPICall, gptImageCall } from '../../../../apis/gpt/GPT'; +import { Docs, DocumentOptions } from '../../../../documents/Documents'; +import { DragManager } from '../../../../util/DragManager'; +import { SnappingManager } from '../../../../util/SnappingManager'; +import { UndoManager, undoable } from '../../../../util/UndoManager'; +import { ObservableReactComponent } from '../../../ObservableReactComponent'; +import { CollectionFreeFormView } from '../../../collections/collectionFreeForm/CollectionFreeFormView'; +import { DocumentView, DocumentViewInternal } from '../../DocumentView'; +import { OpenWhere } from '../../OpenWhere'; +import { DataVizBox } from '../DataVizBox'; +import './DocCreatorMenu.scss'; +import { DefaultStyleProvider } from '../../../StyleProvider'; +import { Transform } from '../../../../util/Transform'; +import { TemplateFieldSize, TemplateFieldType, TemplateLayouts } from './TemplateBackend'; +import { TemplateManager } from './TemplateManager'; +import { Template } from './Template'; +import { Field, FieldContentType } from './FieldTypes/Field'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { Upload } from '../../../../../server/SharedMediaTypes'; + +export enum LayoutType { + FREEFORM = 'Freeform', + CAROUSEL = 'Carousel', + CAROUSEL3D = '3D Carousel', + MASONRY = 'Masonry', + CARD = 'Card View', +} + +export interface DataVizTemplateInfo { + doc: Doc; + layout: { type: LayoutType; xMargin: number; yMargin: number; repeat: number }; + columns: number; + referencePos: { x: number; y: number }; +} + +export interface DataVizTemplateLayout { + template: Doc; + docsNumList: number[]; + layout: { type: LayoutType; xMargin: number; yMargin: number; repeat: number }; + columns: number; + rows: number; +} + +export type Col = { + sizes: TemplateFieldSize[]; + desc: string; + title: string; + type: TemplateFieldType; + defaultContent?: string; +}; + +interface DocCreateMenuProps { + addDocTab: (doc: Doc | Doc[], where: OpenWhere) => boolean; +} + +@observer +export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps> { + // eslint-disable-next-line no-use-before-define + static Instance: DocCreatorMenu; + + private _disposers: { [name: string]: IDisposer } = {}; + + private _ref: HTMLDivElement | null = null; + + private templateManager: TemplateManager; + + @observable _fullyRenderedDocs: Doc[] = []; + @observable _renderedDocCollectionPreview: Doc | undefined = undefined; + @observable _renderedDocCollection: Doc | undefined = undefined; + @observable _docsRendering: boolean = false; + + @observable _userTemplates: { template: Template; doc: Doc }[] = []; //!!! used to keep track of all templates, should be refactored to work with actual templates and not docs + @observable _selectedTemplate: Template | undefined = undefined; + @observable _currEditingTemplate: Template | undefined = undefined; + + @observable _userCreatedFields: Col[] = []; + @observable _selectedCols: { title: string; type: string; desc: string }[] | undefined = []; + + @observable _layout: { type: LayoutType; yMargin: number; xMargin: number; columns?: number; repeat: number } = { type: LayoutType.FREEFORM, yMargin: 10, xMargin: 10, columns: 3, repeat: 0 }; + @observable _layoutPreviewScale: number = 1; + @observable _savedLayouts: DataVizTemplateLayout[] = []; + @observable _expandedPreview: Doc | undefined = undefined; + + @observable _suggestedTemplates: Template[] = []; + @observable _suggestedTemplatePreviews: { doc: Doc; template: Template }[] = []; + @observable _GPTOpt: boolean = false; + @observable _callCount: number = 0; + @observable _GPTLoading: boolean = false; + + @observable _pageX: number = 0; + @observable _pageY: number = 0; + + @observable _hoveredLayoutPreview: number | undefined = undefined; + @observable _mouseX: number = -1; + @observable _mouseY: number = -1; + @observable _startPos?: { x: number; y: number }; + @observable _shouldDisplay: boolean = false; + + @observable _menuContent: 'templates' | 'options' | 'saved' | 'dashboard' = 'templates'; + @observable _dragging: boolean = false; + @observable _draggingIndicator: boolean = false; + @observable _dataViz?: DataVizBox; + @observable _interactionLock: boolean | undefined; + @observable _snapPt: { x: number; y: number } = { x: 0, y: 0 }; + @observable _resizeHdlId: string = ''; + @observable _resizing: boolean = false; + @observable _offset: { x: number; y: number } = { x: 0, y: 0 }; + @observable _resizeUndo: UndoManager.Batch | undefined = undefined; + @observable _initDimensions: { width: number; height: number; x?: number; y?: number } = { width: 300, height: 400, x: undefined, y: undefined }; + @observable _menuDimensions: { width: number; height: number } = { width: 400, height: 400 }; + @observable _editing: boolean = false; + + constructor(props: DocCreateMenuProps) { + super(props); + makeObservable(this); + DocCreatorMenu.Instance = this; + this.templateManager = new TemplateManager(TemplateLayouts.allTemplates); + } + + @action setDataViz = (dataViz: DataVizBox) => { + this._dataViz = dataViz; + this._selectedTemplate = undefined; + this._renderedDocCollection = undefined; + this._renderedDocCollectionPreview = undefined; + this._fullyRenderedDocs = []; + this._suggestedTemplatePreviews = []; + this._suggestedTemplates = []; + this._userCreatedFields = []; + }; + @action addUserTemplate = (template: Template) => { + this._userTemplates.push({ template: template.cloneBase(), doc: template.getRenderedDoc() }); + }; + @action removeUserTemplate = (template: Template) => { + this._userTemplates = this._userTemplates.filter(info => info.template !== template); + }; + @action updateTemplatePreview = (template: Template) => { + template.renderUpdates(); + const preview = { template: template, doc: template.getRenderedDoc() }; + this._suggestedTemplatePreviews = this._suggestedTemplatePreviews.map(t => { return t.template === preview.template ? preview : t }); //prettier-ignore + this._userTemplates = this._userTemplates.map(t => { return t.template === preview.template ? preview : t }); //prettier-ignore + }; + @action setSuggestedTemplates = (templates: Template[]) => { + this._suggestedTemplates = templates; + this._suggestedTemplatePreviews = templates.map(template => {return {template: template, doc: template.getRenderedDoc()}}); //prettier-ignore + }; + + @computed get docsToRender() { + return this._selectedTemplate ? NumListCast(this._dataViz?.layoutDoc.dataViz_selectedRows) : []; + } + + @computed get rowsCount() { + switch (this._layout.type) { + case LayoutType.FREEFORM: + return Math.ceil(this.docsToRender.length / (this._layout.columns ?? 1)) ?? 0; + case LayoutType.CAROUSEL3D: + return 1.8; + default: + return 1; + } + } + + @computed get columnsCount() { + switch (this._layout.type) { + case LayoutType.FREEFORM: + return this._layout.columns ?? 0; + case LayoutType.CAROUSEL3D: + return 3; + default: + return 1; + } + } + + @computed get selectedFields() { + return StrListCast(this._dataViz?.layoutDoc._dataViz_axes); + } + + @computed get fieldsInfos(): Col[] { + const colInfo = this._dataViz?.colsInfo; + return this.selectedFields + .map(field => { + const fieldInfo = colInfo?.get(field); + + const col: Col = { + title: field, + type: fieldInfo?.type ?? TemplateFieldType.UNSET, + desc: fieldInfo?.desc ?? '', + sizes: fieldInfo?.sizes ?? [TemplateFieldSize.MEDIUM], + }; + + if (fieldInfo?.defaultContent !== undefined) { + col.defaultContent = fieldInfo.defaultContent; + } + + return col; + }) + .concat(this._userCreatedFields); + } + + @computed get canMakeDocs() { + return this._selectedTemplate !== undefined && this._layout !== undefined; + } + + get bounds(): { t: number; b: number; l: number; r: number } { + const rect = this._ref?.getBoundingClientRect(); + const bounds = { t: rect?.top ?? 0, b: rect?.bottom ?? 0, l: rect?.left ?? 0, r: rect?.right ?? 0 }; + return bounds; + } + + setUpButtonClick = (e: React.PointerEvent, func: () => void) => { + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + undoable(clickEv => { + clickEv.stopPropagation(); + clickEv.preventDefault(); + func(); + }, 'create docs') + ); + }; + + @action + onPointerDown = (e: PointerEvent) => { + this._mouseX = e.clientX; + this._mouseY = e.clientY; + }; + + @action + onPointerUp = (e: PointerEvent) => { + if (this._resizing) { + this._initDimensions.width = this._menuDimensions.width; + this._initDimensions.height = this._menuDimensions.height; + this._initDimensions.x = this._pageX; + this._initDimensions.y = this._pageY; + document.removeEventListener('pointermove', this.onResize); + SnappingManager.SetIsResizing(undefined); + this._resizing = false; + } + if (this._dragging) { + document.removeEventListener('pointermove', this.onDrag); + this._dragging = false; + } + if (e.button !== 2 && !e.ctrlKey) return; + const curX = e.clientX; + const curY = e.clientY; + if (Math.abs(this._mouseX - curX) > 1 || Math.abs(this._mouseY - curY) > 1) { + this._shouldDisplay = false; + } + }; + + componentDidMount() { + document.addEventListener('pointerdown', this.onPointerDown, true); + document.addEventListener('pointerup', this.onPointerUp); + } + + componentWillUnmount() { + Object.values(this._disposers).forEach(disposer => disposer?.()); + document.removeEventListener('pointerdown', this.onPointerDown, true); + document.removeEventListener('pointerup', this.onPointerUp); + } + + @action + toggleDisplay = (x: number, y: number) => { + if (this._shouldDisplay) { + this._shouldDisplay = false; + } else { + this._pageX = x; + this._pageY = y; + this._shouldDisplay = true; + } + }; + + @action + closeMenu = () => { + this._shouldDisplay = false; + }; + + @action + openMenu = () => { + this._shouldDisplay = true; + }; + + @action + onResizePointerDown = (e: React.PointerEvent): void => { + this._resizing = true; + document.addEventListener('pointermove', this.onResize); + SnappingManager.SetIsResizing(DocumentView.Selected().lastElement()?.Document[Id]); // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them + e.stopPropagation(); + const id = (this._resizeHdlId = e.currentTarget.className); + const pad = id.includes('Left') || id.includes('Right') ? Number(getComputedStyle(e.target as HTMLElement).width.replace('px', '')) / 2 : 0; + const bounds = e.currentTarget.getBoundingClientRect(); + this._offset = { + x: id.toLowerCase().includes('left') ? bounds.right - e.clientX - pad : bounds.left - e.clientX + pad, // + y: id.toLowerCase().includes('top') ? bounds.bottom - e.clientY - pad : bounds.top - e.clientY + pad, + }; + this._resizeUndo = UndoManager.StartBatch('drag resizing'); + this._snapPt = { x: e.pageX, y: e.pageY }; + }; + + @action + onResize = (e: PointerEvent): boolean => { + const dragHdl = this._resizeHdlId.split(' ')[1]; + const thisPt = DragManager.snapDrag(e, -this._offset.x, -this._offset.y, this._offset.x, this._offset.y); + + const { scale, refPt, transl } = this.getResizeVals(thisPt, dragHdl); + !this._interactionLock && runInAction(async () => { // resize selected docs if we're not in the middle of a resize (ie, throttle input events to frame rate) + this._interactionLock = true; + const scaleAspect = {x: scale.x, y: scale.y}; + this.resizeView(refPt, scaleAspect, transl); // prettier-ignore + await new Promise<boolean | undefined>(res => { setTimeout(() => { res(this._interactionLock = undefined)})}); + }); // prettier-ignore + return true; + }; + + @action + onDrag = (e: PointerEvent): boolean => { + this._pageX = e.pageX - (this._startPos?.x ?? 0); + this._pageY = e.pageY - (this._startPos?.y ?? 0); + this._initDimensions.x = this._pageX; + this._initDimensions.y = this._pageY; + return true; + }; + + getResizeVals = (thisPt: { x: number; y: number }, dragHdl: string) => { + const [w, h] = [this._initDimensions.width, this._initDimensions.height]; + const [moveX, moveY] = [thisPt.x - this._snapPt!.x, thisPt.y - this._snapPt!.y]; + let vals: { scale: { x: number; y: number }; refPt: [number, number]; transl: { x: number; y: number } }; + switch (dragHdl) { + case 'topLeft': vals = { scale: { x: 1 - moveX / w, y: 1 -moveY / h }, refPt: [this.bounds.r, this.bounds.b], transl: {x: moveX, y: moveY } }; break; + case 'topRight': vals = { scale: { x: 1 + moveX / w, y: 1 -moveY / h }, refPt: [this.bounds.l, this.bounds.b], transl: {x: 0, y: moveY } }; break; + case 'top': vals = { scale: { x: 1, y: 1 -moveY / h }, refPt: [this.bounds.l, this.bounds.b], transl: {x: 0, y: moveY } }; break; + case 'left': vals = { scale: { x: 1 - moveX / w, y: 1 }, refPt: [this.bounds.r, this.bounds.t], transl: {x: moveX, y: 0 } }; break; + case 'bottomLeft': vals = { scale: { x: 1 - moveX / w, y: 1 + moveY / h }, refPt: [this.bounds.r, this.bounds.t], transl: {x: moveX, y: 0 } }; break; + case 'right': vals = { scale: { x: 1 + moveX / w, y: 1 }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break; + case 'bottomRight':vals = { scale: { x: 1 + moveX / w, y: 1 + moveY / h }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break; + case 'bottom': vals = { scale: { x: 1, y: 1 + moveY / h }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break; + default: vals = { scale: { x: 1, y: 1 }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break; + } // prettier-ignore + return vals; + }; + + resizeView = (refPt: number[], scale: { x: number; y: number }, translation: { x: number; y: number }) => { + if (this._initDimensions.x === undefined) this._initDimensions.x = this._pageX; + if (this._initDimensions.y === undefined) this._initDimensions.y = this._pageY; + const { height, width, x, y } = this._initDimensions; + + this._menuDimensions.width = Math.max(300, scale.x * width); + this._menuDimensions.height = Math.max(200, scale.y * height); + this._pageX = x + translation.x; + this._pageY = y + translation.y; + }; + + async getIcon(doc: Doc) { + const docView = DocumentView.getDocumentView(doc); + if (docView) { + docView.ComponentView?.updateIcon?.(); + return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 500)); + } + return undefined; + } + + @action updateSelectedTemplate = async (template: Template) => { + if (this._selectedTemplate === template) { + this._selectedTemplate = undefined; + return; + } else { + this._selectedTemplate = template; + template.renderUpdates(); + this._fullyRenderedDocs = (await this.createDocsFromTemplate(template)) ?? []; + this.updateRenderedDocCollection(); + } + }; + + @action updateSelectedSavedLayout = (layout: DataVizTemplateLayout) => { + this._layout.xMargin = layout.layout.xMargin; + this._layout.yMargin = layout.layout.yMargin; + this._layout.type = layout.layout.type; + this._layout.columns = layout.columns; + }; + + isSelectedLayout = (layout: DataVizTemplateLayout) => { + return this._layout.xMargin === layout.layout.xMargin && this._layout.yMargin === layout.layout.yMargin && this._layout.type === layout.layout.type && this._layout.columns === layout.columns; + }; + + editTemplate = (doc: Doc) => { + DocumentViewInternal.addDocTabFunc(doc, OpenWhere.addRight); + DocumentView.DeselectAll(); + Doc.UnBrushDoc(doc); + }; + + @action addField = () => { + const newFields: Col[] = this._userCreatedFields.concat([{ title: '', type: TemplateFieldType.UNSET, desc: '', sizes: [] }]); + this._userCreatedFields = newFields; + }; + + @action removeField = (field: { title: string; type: string; desc: string }) => { + if (this._dataViz?.axes.includes(field.title)) { + this._dataViz.selectAxes(this._dataViz.axes.filter(col => col !== field.title)); + } else { + const toRemove = this._userCreatedFields.filter(f => f === field); + if (!toRemove) return; + + if (toRemove.length > 1) { + while (toRemove.length > 1) { + toRemove.pop(); + } + } + + if (this._userCreatedFields.length === 1) { + this._userCreatedFields = []; + } else { + this._userCreatedFields.splice(this._userCreatedFields.indexOf(toRemove[0]), 1); + } + } + }; + + @action setColTitle = (column: Col, title: string) => { + if (this.selectedFields.includes(column.title)) { + this._dataViz?.setColumnTitle(column.title, title); + } else { + column.title = title; + } + this.forceUpdate(); + }; + + @action setColType = (column: Col, type: TemplateFieldType) => { + if (this.selectedFields.includes(column.title)) { + this._dataViz?.setColumnType(column.title, type); + } else { + column.type = type; + } + this.forceUpdate(); + }; + + modifyColSizes = (column: Col, size: TemplateFieldSize, valid: boolean) => { + if (this.selectedFields.includes(column.title)) { + this._dataViz?.modifyColumnSizes(column.title, size, valid); + } else { + if (!valid && column.sizes.includes(size)) { + column.sizes.splice(column.sizes.indexOf(size), 1); + } else if (valid && !column.sizes.includes(size)) { + column.sizes.push(size); + } + } + this.forceUpdate(); + }; + + setColDesc = (column: Col, desc: string) => { + if (this.selectedFields.includes(column.title)) { + this._dataViz?.setColumnDesc(column.title, desc); + } else { + column.desc = desc; + } + this.forceUpdate(); + }; + + generateGPTImage = async (prompt: string): Promise<string | undefined> => { + try { + const res = await gptImageCall(prompt); + + if (res) { + const result = (await Networking.PostToServer('/uploadRemoteImage', { sources: res })) as Upload.FileInformation[]; + const source = ClientUtils.prepend(result[0].accessPaths.agnostic.client); + return source; + } + } catch (e) { + console.log(e); + } + }; + + /** + * Populates a preset template framework with content from a datavizbox or any AI-generated content. + * @param template the preloaded template framework being filled in + * @param assignments a list of template field numbers (from top to bottom) and their assigned columns from the linked dataviz + * @returns a doc containing the fully rendered template + */ + applyGPTContentToTemplate = async (template: Template, assignments: { [field: string]: Col }): Promise<Template | undefined> => { + const GPTTextCalls = Object.entries(assignments).filter(([, col]) => col.type === TemplateFieldType.TEXT && this._userCreatedFields.includes(col)); + const GPTIMGCalls = Object.entries(assignments).filter(([, col]) => col.type === TemplateFieldType.VISUAL && this._userCreatedFields.includes(col)); + + if (GPTTextCalls.length) { + const promises = GPTTextCalls.map(([str, col]) => { + return this.renderGPTTextCall(template, col, Number(str)); + }); + + await Promise.all(promises); + } + + if (GPTIMGCalls.length) { + const promises = GPTIMGCalls.map(async ([fieldNum, col]) => { + return this.renderGPTImageCall(template, col, Number(fieldNum)); + }); + + await Promise.all(promises); + } + + return template; + }; + + compileFieldDescriptions = (templates: Template[]): string => { + let descriptions: string = ''; + templates.forEach(template => { + descriptions += `---------- NEW TEMPLATE TO INCLUDE: The title is: ${template.mainField.getTitle()}. Its fields are: `; + descriptions += template.descriptionSummary; + }); + + return descriptions; + }; + + compileColDescriptions = (cols: Col[]): string => { + let descriptions: string = ' ------------- COL DESCRIPTIONS START HERE:'; + cols.forEach(col => (descriptions += `{title: ${col.title}, sizes: ${String(col.sizes)}, type: ${col.type}, descreiption: ${col.desc}} `)); + + return descriptions; + }; + + getColByTitle = (title: string) => { + return this.fieldsInfos.filter(col => col.title === title)[0]; + }; + + @action + assignColsToFields = async (templates: Template[], cols: Col[]): Promise<[Template, { [field: number]: Col }][]> => { + const fieldDescriptions: string = this.compileFieldDescriptions(templates); + const colDescriptions: string = this.compileColDescriptions(cols); + + const inputText = fieldDescriptions.concat(colDescriptions); + + ++this._callCount; + const origCount = this._callCount; + + const prompt: string = `(${origCount}) ${inputText}`; + + this._GPTLoading = true; + + try { + const res = await gptAPICall(prompt, GPTCallType.TEMPLATE); + + if (res) { + const assignments: { [templateTitle: string]: { [fieldID: string]: string } } = JSON.parse(res); + const brokenDownAssignments: [Template, { [fieldID: number]: Col }][] = []; + + Object.entries(assignments).forEach(([tempTitle, assignment]) => { + const template = templates.filter(t => t.mainField.getTitle() === tempTitle)[0]; + if (!template) return; + const toObj = Object.entries(assignment).reduce( + (a, [fieldID, colTitle]) => { + const col = this.getColByTitle(colTitle); + if (!this._userCreatedFields.includes(col)) { + // do the following for any fields not added by the user; will change in the future, for now only GPT content works with user-added fields + const field = template.getFieldByID(Number(fieldID)); + field.setContent(col.defaultContent ?? '', col.type === TemplateFieldType.VISUAL ? FieldContentType.IMAGE : FieldContentType.STRING); + field.setTitle(col.title); + } else { + a[Number(fieldID)] = this.getColByTitle(colTitle); + } + return a; + }, + {} as { [field: number]: Col } + ); + brokenDownAssignments.push([template, toObj]); + }); + + return brokenDownAssignments; + } + } catch (err) { + console.error(err); + } + + return []; + }; + + generatePresetTemplates = async () => { + this._dataViz?.updateColDefaults(); + + const cols = this.fieldsInfos; + const templates = this.templateManager.getValidTemplates(cols); + + const assignments: [Template, { [field: number]: Col }][] = await this.assignColsToFields(templates, cols); + + const renderedTemplatePromises: Promise<Template | undefined>[] = assignments.map(([template, asns]) => this.applyGPTContentToTemplate(template, asns)); + + await Promise.all(renderedTemplatePromises); + + setTimeout(() => { + this.setSuggestedTemplates(templates); + this._GPTLoading = false; + }); + }; + + renderGPTImageCall = async (template: Template, col: Col, fieldNumber: number): Promise<boolean> => { + const generateAndLoadImage = async (fieldNum: string, column: Col, prompt: string) => { + const url = await this.generateGPTImage(prompt); + const field: Field = template.getFieldByID(Number(fieldNum)); + + field.setContent(url ?? '', FieldContentType.IMAGE); + field.setTitle(column.title); + }; + + const fieldContent: string = template.compiledContent; + + try { + const sysPrompt = + 'Your job is to create a prompt for an AI image generator to help it generate an image based on existing content in a template and a user prompt. Your prompt should focus heavily on visual elements to help the image generator; avoid unecessary info that might distract it. ONLY INCLUDE THE PROMPT, NO OTHER TEXT OR EXPLANATION. The existing content is as follows: ' + + fieldContent + + ' **** The user prompt is: ' + + col.desc; + + const prompt = await gptAPICall(sysPrompt, GPTCallType.COMPLETEPROMPT); + + await generateAndLoadImage(String(fieldNumber), col, prompt); + } catch (e) { + console.log(e); + } + return true; + }; + + renderGPTTextCall = async (template: Template, col: Col, fieldNum: number): Promise<boolean> => { + const wordLimit = (size: TemplateFieldSize) => { + switch (size) { + case TemplateFieldSize.TINY: + return 2; + case TemplateFieldSize.SMALL: + return 5; + case TemplateFieldSize.MEDIUM: + return 20; + case TemplateFieldSize.LARGE: + return 50; + case TemplateFieldSize.HUGE: + return 100; + default: + return 10; + } + }; + + const textAssignment = `--- title: ${col.title}, prompt: ${col.desc}, word limit: ${wordLimit(col.sizes[0])} words, assigned field: ${fieldNum} ---`; + + const fieldContent: string = template.compiledContent; + + try { + const prompt = fieldContent + textAssignment; + + const res = await gptAPICall(`${++this._callCount}: ${prompt}`, GPTCallType.FILL); + + if (res) { + const assignments: { [title: string]: { number: string; content: string } } = JSON.parse(res); + Object.entries(assignments).forEach(([title, info]) => { + const field: Field = template.getFieldByID(Number(info.number)); + const column = this.getColByTitle(title); + + field.setContent(info.content ?? '', FieldContentType.STRING); + field.setTitle(column.title); + }); + } + } catch (err) { + console.log(err); + } + + return true; + }; + + createDocsFromTemplate = async (template: Template) => { + const dv = this._dataViz; + + if (!dv) return; + + this._docsRendering = true; + + const fields: string[] = Array.from(Object.keys(dv.records[0])); + const selectedRows = NumListCast(dv.layoutDoc.dataViz_selectedRows); + + const rowContents: { [title: string]: string }[] = selectedRows.map(row => { + const values: { [title: string]: string } = {}; + fields.forEach(col => { + values[col] = dv.records[row][col]; + }); + + return values; + }); + + const processContent = async (content: { [title: string]: string }) => { + const templateCopy = template.cloneBase(); + + fields + .filter(title => title) + .forEach(title => { + const field = templateCopy.getFieldByTitle(title); + if (field === undefined) { + return; + } + field.setContent(content[title]); + }); + + const gptPromises = this._userCreatedFields + .filter(field => field.type === TemplateFieldType.TEXT) + .map(field => { + const title = field.title; + const templateField = templateCopy.getFieldByTitle(title); + if (templateField === undefined) { + return; + } + const templatefieldID = templateField.getID; + + return this.renderGPTTextCall(templateCopy, field, templatefieldID); + }); + + const imagePromises = this._userCreatedFields + .filter(field => field.type === TemplateFieldType.VISUAL) + .map(field => { + const title = field.title; + const templateField = templateCopy.getFieldByTitle(title); + if (templateField === undefined) { + return; + } + const templatefieldID = templateField.getID; + + return this.renderGPTImageCall(templateCopy, field, templatefieldID); + }); + + await Promise.all(gptPromises); + + await Promise.all(imagePromises); + + return templateCopy.getRenderedDoc(); + }; + + const promises = rowContents.map(content => processContent(content)); + + const renderedDocs = await Promise.all(promises); + + this._docsRendering = false; + + return renderedDocs; + }; + + addRenderedCollectionToMainview = () => { + const collection = this._renderedDocCollection; + if (!collection) return; + const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView; + collection.x = this._pageX - this._menuDimensions.width; + collection.y = this._pageY - this._menuDimensions.height; + mainCollection.addDocument(collection); + this.closeMenu(); + }; + + @action setExpandedView = (template: Template | undefined) => { + if (template) { + this._currEditingTemplate = template; + this._expandedPreview = template.mainField.renderedDoc(); //Docs.Create.FreeformDocument([doc], { _height: NumListCast(doc._height)[0], _width: NumListCast(doc._width)[0], title: ''}); + } else { + this._currEditingTemplate = undefined; + this._expandedPreview = undefined; + } + }; + + get editingWindow() { + const rendered = !this._expandedPreview ? null : ( + <div className="docCreatorMenu-expanded-template-preview"> + <DocumentView + Document={this._expandedPreview} + isContentActive={emptyFunction} + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + PanelWidth={() => this._menuDimensions.width - 10} + PanelHeight={() => this._menuDimensions.height - 60} + ScreenToLocalTransform={() => new Transform(-this._pageX - 5, -this._pageY - 35, 1)} + renderDepth={5} + whenChildContentsActiveChanged={emptyFunction} + focus={emptyFunction} + styleProvider={DefaultStyleProvider} + addDocTab={DocumentViewInternal.addDocTabFunc} + pinToPres={() => undefined} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + fitContentsToBox={returnFalse} + fitWidth={returnFalse} + /> + </div> + ); + + return ( + <div className="docCreatorMenu-expanded-template-preview"> + <div className="top-panel" /> + {rendered} + <div className="right-buttons-panel"> + <button + className="docCreatorMenu-menu-button section-reveal-options top-right" + onPointerDown={e => + this.setUpButtonClick(e, () => { + this._currEditingTemplate && this.updateTemplatePreview(this._currEditingTemplate); + this.setExpandedView(undefined); + }) + }> + <FontAwesomeIcon icon="minimize" /> + </button> + <button + className="docCreatorMenu-menu-button section-reveal-options top-right-lower" + onPointerDown={e => + this.setUpButtonClick(e, () => { + this._currEditingTemplate?.resetToBase(); + this.setExpandedView(this._currEditingTemplate); + }) + }> + <FontAwesomeIcon icon="arrows-rotate" color="white" /> + </button> + </div> + </div> + ); + } + + get templatesPreviewContents() { + const GPTOptions = <div></div>; + + return ( + <div className={`docCreatorMenu-templates-view`}> + {this._expandedPreview ? ( + this.editingWindow + ) : ( + <div> + <div className="docCreatorMenu-section" style={{ height: this._GPTOpt ? 200 : 200 }}> + <div className="docCreatorMenu-section-topbar"> + <div className="docCreatorMenu-section-title">Suggested Templates</div> + <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._menuContent = 'dashboard')))}> + <FontAwesomeIcon icon="gear" /> + </button> + </div> + <div className="docCreatorMenu-templates-preview-window" style={{ justifyContent: this._GPTLoading || this._menuDimensions.width > 400 ? 'center' : '' }}> + {this._GPTLoading ? ( + <div className="loading-spinner"> + <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} /> + </div> + ) : ( + this._suggestedTemplatePreviews.map(({ doc, template }) => ( + <div + className="docCreatorMenu-preview-window" + key="0" + style={{ + border: this._selectedTemplate === template ? `solid 3px ${Colors.MEDIUM_BLUE}` : '', + boxShadow: this._selectedTemplate === template ? `0 0 15px rgba(68, 118, 247, .8)` : '', + }} + onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(template)))}> + <button + className="option-button left" + onPointerDown={e => + this.setUpButtonClick(e, () => { + this.setExpandedView(template); + }) + }> + <FontAwesomeIcon icon="magnifying-glass" color="white" /> + </button> + <button className="option-button right" onPointerDown={e => this.setUpButtonClick(e, () => this.addUserTemplate(template))}> + <FontAwesomeIcon icon="plus" color="white" /> + </button> + <DocumentView + Document={doc} + isContentActive={emptyFunction} // !!! should be return false + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + PanelWidth={() => (this._selectedTemplate === template ? 104 : 111)} + PanelHeight={() => (this._selectedTemplate === template ? 104 : 111)} + ScreenToLocalTransform={() => new Transform(-this._pageX - 5, -this._pageY - 35, 1)} + renderDepth={1} + whenChildContentsActiveChanged={emptyFunction} + focus={emptyFunction} + styleProvider={DefaultStyleProvider} + addDocTab={this._props.addDocTab} + pinToPres={() => undefined} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + fitContentsToBox={returnFalse} + fitWidth={returnFalse} + hideDecorations={true} + /> + </div> + )) + )} + </div> + <div className="docCreatorMenu-GPT-options"> + <div className="docCreatorMenu-GPT-options-container"> + <button className="docCreatorMenu-menu-button" onPointerDown={e => this.setUpButtonClick(e, () => this.generatePresetTemplates())}> + <FontAwesomeIcon icon="arrows-rotate" /> + </button> + </div> + {this._GPTOpt ? GPTOptions : null} + </div> + </div> + <hr className="docCreatorMenu-option-divider full no-margin" /> + <div className="docCreatorMenu-section"> + <div className="docCreatorMenu-section-topbar"> + <div className="docCreatorMenu-section-title">Your Templates</div> + <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, () => (this._GPTOpt = !this._GPTOpt))}> + <FontAwesomeIcon icon="gear" /> + </button> + </div> + <div className="docCreatorMenu-templates-preview-window" style={{ justifyContent: this._menuDimensions.width > 400 ? 'center' : '' }}> + <div className="docCreatorMenu-preview-window empty"> + <FontAwesomeIcon icon="plus" color="rgb(160, 160, 160)" /> + </div> + {this._userTemplates.map(({ template, doc }) => ( + <div + className="docCreatorMenu-preview-window" + key="0" + style={{ + border: this._selectedTemplate === template ? `solid 3px ${Colors.MEDIUM_BLUE}` : '', + boxShadow: this._selectedTemplate === template ? `0 0 15px rgba(68, 118, 247, .8)` : '', + }} + onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(template)))}> + <button + className="option-button left" + onPointerDown={e => + this.setUpButtonClick(e, () => { + this.setExpandedView(template); + }) + }> + <FontAwesomeIcon icon="magnifying-glass" color="white" /> + </button> + <button className="option-button right" onPointerDown={e => this.setUpButtonClick(e, () => this.removeUserTemplate(template))}> + <FontAwesomeIcon icon="minus" color="white" /> + </button> + <DocumentView + Document={doc} + isContentActive={emptyFunction} // !!! should be return false + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + PanelWidth={() => (this._selectedTemplate === template ? 104 : 111)} + PanelHeight={() => (this._selectedTemplate === template ? 104 : 111)} + ScreenToLocalTransform={() => new Transform(-this._pageX - 5, -this._pageY - 35, 1)} + renderDepth={1} + whenChildContentsActiveChanged={emptyFunction} + focus={emptyFunction} + styleProvider={DefaultStyleProvider} + addDocTab={this._props.addDocTab} + pinToPres={() => undefined} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + fitContentsToBox={returnFalse} + fitWidth={returnFalse} + hideDecorations={true} + /> + </div> + ))} + </div> + </div> + </div> + )} + </div> + ); + } + + @action updateXMargin = (input: string) => { + this._layout.xMargin = Number(input); + setTimeout(() => { + if (!this._renderedDocCollection || !this._fullyRenderedDocs) return; + this.applyLayout(this._renderedDocCollection, this._fullyRenderedDocs); + }); + }; + @action updateYMargin = (input: string) => { + this._layout.yMargin = Number(input); + setTimeout(() => { + if (!this._renderedDocCollection || !this._fullyRenderedDocs) return; + this.applyLayout(this._renderedDocCollection, this._fullyRenderedDocs); + }); + }; + @action updateColumns = (input: string) => { + this._layout.columns = Number(input); + this.updateRenderedDocCollection(); + }; + + get layoutConfigOptions() { + const optionInput = (icon: string, func: (input: string) => void, def?: number, key?: string, noMargin?: boolean) => { + return ( + <div className="docCreatorMenu-option-container small no-margin" key={key} style={{ marginTop: noMargin ? '0px' : '' }}> + <div className="docCreatorMenu-option-title config layout-config"> + <FontAwesomeIcon icon={icon as IconProp} /> + </div> + <input defaultValue={def} onInput={e => func(e.currentTarget.value)} className="docCreatorMenu-input config layout-config" /> + </div> + ); + }; + + switch (this._layout.type) { + case LayoutType.FREEFORM: + return ( + <div className="docCreatorMenu-configuration-bar"> + {optionInput('arrows-up-down', this.updateYMargin, this._layout.xMargin, '2')} + {optionInput('arrows-left-right', this.updateXMargin, this._layout.xMargin, '3')} + {optionInput('table-columns', this.updateColumns, this._layout.columns, '4', true)} + </div> + ); + default: + break; + } + } + + applyLayout = (collection: Doc, docs: Doc[]) => { + const { horizontalSpan, verticalSpan } = this.previewInfo; + collection._height = verticalSpan; + collection._width = horizontalSpan; + + const layout = this._layout; + const columns: number = layout.columns ?? this.columnsCount; + const xGap: number = layout.xMargin; + const yGap: number = layout.yMargin; + // const repeat: number = templateInfo.layout.repeat; + const startX: number = -Number(collection._width) / 2; + const startY: number = -Number(collection._height) / 2; + const docHeight: number = Number(docs[0]._height); + const docWidth: number = Number(docs[0]._width); + + if (columns === 0 || docs.length === 0) { + return; + } + + let i: number = 0; + let docsChanged: number = 0; + let curX: number = startX; + let curY: number = startY; + + while (docsChanged < docs.length) { + while (i < columns && docsChanged < docs.length) { + docs[docsChanged].x = curX; + docs[docsChanged].y = curY; + curX += docWidth + xGap; + ++docsChanged; + ++i; + } + i = 0; + curX = startX; + curY += docHeight + yGap; + } + }; + + @computed + get previewInfo() { + const docHeight: number = Number(this._fullyRenderedDocs[0]._height); + const docWidth: number = Number(this._fullyRenderedDocs[0]._width); + const layout = this._layout; + return { + docHeight: docHeight, + docWidth: docWidth, + horizontalSpan: (docWidth + layout.xMargin) * this.columnsCount - layout.xMargin, + verticalSpan: (docHeight + layout.yMargin) * this.rowsCount - layout.yMargin, + }; + } + + /** + * Updates the preview that shows how all docs will be rendered in the chosen collection type. + @type the type of collection the docs should render to (ie. freeform, carousel, card) + */ + updateRenderedDocCollection = () => { + if (!this._fullyRenderedDocs) return; + + const { horizontalSpan, verticalSpan } = this.previewInfo; + + const collectionFactory = (): ((docs: Doc[], options: DocumentOptions) => Doc) => { + switch (this._layout.type) { + case LayoutType.CAROUSEL3D: + return Docs.Create.Carousel3DDocument; + case LayoutType.FREEFORM: + return Docs.Create.FreeformDocument; + case LayoutType.CARD: + return Docs.Create.CardDeckDocument; + case LayoutType.MASONRY: + return Docs.Create.MasonryDocument; + case LayoutType.CAROUSEL: + return Docs.Create.CarouselDocument; + default: + return Docs.Create.FreeformDocument; + } + }; + + const collection: Doc = collectionFactory()(this._fullyRenderedDocs, { + isDefaultTemplateDoc: true, + _height: verticalSpan, + _width: horizontalSpan, + title: 'title', + backgroundColor: 'gray', + }); + + this.applyLayout(collection, this._fullyRenderedDocs); + + this._renderedDocCollection = collection; + }; + + layoutPreviewContents = () => { + return this._docsRendering ? ( + <div className="docCreatorMenu-layout-preview-window-wrapper loading"> + <div className="loading-spinner"> + <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} /> + </div> + </div> + ) : !this._renderedDocCollection ? null : ( + <div className="docCreatorMenu-layout-preview-window-wrapper"> + <DocumentView + Document={this._renderedDocCollection} + isContentActive={emptyFunction} + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + PanelWidth={() => this._menuDimensions.width - 80} + PanelHeight={() => this._menuDimensions.height - 105} + ScreenToLocalTransform={() => new Transform(-this._pageX - 5, -this._pageY - 35, 1)} + renderDepth={5} + whenChildContentsActiveChanged={emptyFunction} + focus={emptyFunction} + styleProvider={DefaultStyleProvider} + addDocTab={this._props.addDocTab} + pinToPres={() => undefined} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + fitContentsToBox={returnFalse} + fitWidth={returnFalse} + hideDecorations={true} + /> + </div> + ); + }; + + get optionsMenuContents() { + const layoutOption = (option: LayoutType, optStyle?: object, specialFunc?: () => void) => { + return ( + <div + className="docCreatorMenu-dropdown-option" + style={optStyle} + onPointerDown={e => + this.setUpButtonClick(e, () => { + specialFunc?.(); + runInAction(() => { + this._layout.type = option; + this.updateRenderedDocCollection(); + }); + }) + }> + {option} + </div> + ); + }; + + const selectionBox = (width: number, height: number, icon: string, specClass?: string, options?: JSX.Element[], manual?: boolean): JSX.Element => { + return ( + <div className="docCreatorMenu-option-container"> + <div className={`docCreatorMenu-option-title config ${specClass}`} style={{ width: width * 0.4, height: height }}> + <FontAwesomeIcon icon={icon as IconProp} /> + </div> + {manual ? ( + <input className={`docCreatorMenu-input config ${specClass}`} style={{ width: width * 0.6, height: height }} /> + ) : ( + <select className={`docCreatorMenu-input config ${specClass}`} style={{ width: width * 0.6, height: height }}> + {options} + </select> + )} + </div> + ); + }; + + const repeatOptions = [0, 1, 2, 3, 4, 5]; + + return ( + <div className="docCreatorMenu-menu-container"> + <div className="docCreatorMenu-option-container layout"> + <div className="docCreatorMenu-dropdown-hoverable"> + <div className="docCreatorMenu-option-title">{this._layout.type ? this._layout.type.toUpperCase() : 'Choose Layout'}</div> + <div className="docCreatorMenu-dropdown-content"> + {layoutOption(LayoutType.FREEFORM, undefined, () => { + if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length)); + })} + {layoutOption(LayoutType.CAROUSEL)} + {layoutOption(LayoutType.CAROUSEL3D)} + {layoutOption(LayoutType.MASONRY)} + </div> + </div> + </div> + {this._layout.type ? this.layoutConfigOptions : null} + {this.layoutPreviewContents()} + {selectionBox( + 60, + 20, + 'repeat', + undefined, + repeatOptions.map(num => <option key={num} onPointerDown={() => (this._layout.repeat = num)}>{`${num}x`}</option>) + )} + <hr className="docCreatorMenu-option-divider" /> + <div className="docCreatorMenu-general-options-container"> + <button + className="docCreatorMenu-save-layout-button" + onPointerDown={e => + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + undoable(clickEv => { + clickEv.stopPropagation(); + if (!this._selectedTemplate) return; + const layout: DataVizTemplateLayout = { + template: this._selectedTemplate.getRenderedDoc(), + layout: { type: this._layout.type, xMargin: this._layout.xMargin, yMargin: this._layout.yMargin, repeat: 0 }, + columns: this.columnsCount, + rows: this.rowsCount, + docsNumList: this.docsToRender, + }; + if (!this._savedLayouts.includes(layout)) { + this._savedLayouts.push(layout); + } + }, 'make docs') + ) + }> + <FontAwesomeIcon icon="floppy-disk" /> + </button> + <button + className="docCreatorMenu-create-docs-button" + style={{ backgroundColor: this.canMakeDocs ? '' : 'rgb(155, 155, 155)', border: this.canMakeDocs ? '' : 'solid 2px rgb(180, 180, 180)' }} + onPointerDown={e => + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + undoable(clickEv => { + clickEv.stopPropagation(); + if (!this._selectedTemplate) return; + this.addRenderedCollectionToMainview(); + }, 'make docs') + ) + }> + <FontAwesomeIcon icon="plus" /> + </button> + </div> + </div> + ); + } + + get dashboardContents() { + const sizes: string[] = ['tiny', 'small', 'medium', 'large', 'huge']; + + const fieldPanel = (field: Col, id: number) => { + return ( + <div className="field-panel" key={id}> + <div className="top-bar"> + <span className="field-title">{`${field.title} Field`}</span> + <button className="docCreatorMenu-menu-button section-reveal-options no-margin" onPointerDown={e => this.setUpButtonClick(e, () => this.removeField(field))} style={{ position: 'absolute', right: '0px' }}> + <FontAwesomeIcon icon="minus" /> + </button> + </div> + <div className="opts-bar"> + <div className="opt-box"> + <div className="top-bar"> Title </div> + <textarea className="content" style={{ width: '100%', height: 'calc(100% - 20px)' }} value={field.title} placeholder={'Enter title'} onChange={e => this.setColTitle(field, e.target.value)} /> + </div> + <div className="opt-box"> + <div className="top-bar"> Type </div> + <div className="content"> + <span className="type-display">{field.type === TemplateFieldType.TEXT ? 'Text Field' : field.type === TemplateFieldType.VISUAL ? 'File Field' : ''}</span> + <div className="bubbles"> + <input + className="bubble" + type="radio" + name="type" + onClick={() => { + this.setColType(field, TemplateFieldType.TEXT); + }} + /> + <div className="text">Text</div> + <input + className="bubble" + type="radio" + name="type" + onClick={() => { + this.setColType(field, TemplateFieldType.VISUAL); + }} + /> + <div className="text">File</div> + </div> + </div> + </div> + </div> + <div className="sizes-box"> + <div className="top-bar"> Valid Sizes </div> + <div className="content"> + <div className="bubbles"> + {sizes.map(size => ( + <> + <input + className="bubble" + type="checkbox" + name="type" + checked={field.sizes.includes(size as TemplateFieldSize)} + onChange={e => { + this.modifyColSizes(field, size as TemplateFieldSize, e.target.checked); + }} + /> + <div className="text">{size}</div> + </> + ))} + </div> + </div> + </div> + <div className="desc-box"> + <div className="top-bar"> Prompt </div> + <textarea + className="content" + onChange={e => this.setColDesc(field, e.target.value)} + defaultValue={field.desc === this._dataViz?.GPTSummary?.get(field.title)?.desc ? '' : field.desc} + placeholder={this._dataViz?.GPTSummary?.get(field.title)?.desc ?? 'Add a description/prompt to help with template generation.'} + /> + </div> + </div> + ); + }; + + return ( + <div className="docCreatorMenu-dashboard-view"> + <div className="topbar"> + <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, this.addField)}> + <FontAwesomeIcon icon="plus" /> + </button> + <button className="docCreatorMenu-menu-button section-reveal-options float-right" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._menuContent = 'templates')))}> + <FontAwesomeIcon icon="arrow-left" /> + </button> + </div> + <div className="panels-container">{this.fieldsInfos.map((field, i) => fieldPanel(field, i))}</div> + </div> + ); + } + + get renderSelectedViewType() { + switch (this._menuContent) { + case 'templates': + return this.templatesPreviewContents; + case 'options': + return this.optionsMenuContents; + case 'dashboard': + return this.dashboardContents; + default: + return undefined; + } + } + + get resizePanes() { + const ref = this._ref?.getBoundingClientRect(); + const height: number = ref?.height ?? 0; + const width: number = ref?.width ?? 0; + + return [ + <div className='docCreatorMenu-resizer top' key='0' onPointerDown={this.onResizePointerDown} style={{width: width, left: 0, top: -7}}/>, + <div className='docCreatorMenu-resizer left' key='1' onPointerDown={this.onResizePointerDown} style={{height: height, left: -7, top: 0}}/>, + <div className='docCreatorMenu-resizer right' key='2' onPointerDown={this.onResizePointerDown} style={{height: height, left: width - 3, top: 0}}/>, + <div className='docCreatorMenu-resizer bottom' key='3' onPointerDown={this.onResizePointerDown} style={{width: width, left: 0, top: height - 3}}/>, + <div className='docCreatorMenu-resizer topLeft' key='4' onPointerDown={this.onResizePointerDown} style={{left: -10, top: -10, cursor: 'nwse-resize'}}/>, + <div className='docCreatorMenu-resizer topRight' key='5' onPointerDown={this.onResizePointerDown} style={{left: width - 5, top: -10, cursor: 'nesw-resize'}}/>, + <div className='docCreatorMenu-resizer bottomLeft' key='6' onPointerDown={this.onResizePointerDown} style={{left: -10, top: height - 5, cursor: 'nesw-resize'}}/>, + <div className='docCreatorMenu-resizer bottomRight' key='7' onPointerDown={this.onResizePointerDown} style={{left: width - 5, top: height - 5, cursor: 'nwse-resize'}}/>, + ]; //prettier-ignore + } + + render() { + const topButton = (icon: string, opt: string, func: () => void, tag: string) => { + return ( + <div className={`top-button-container ${tag} ${opt === this._menuContent ? 'selected' : ''}`}> + <div + className="top-button-content" + onPointerDown={e => + this.setUpButtonClick(e, () => + runInAction(() => { + func(); + }) + ) + }> + <FontAwesomeIcon icon={icon as IconProp} /> + </div> + </div> + ); + }; + + const onPreviewSelected = () => { + this._menuContent = 'templates'; + }; + const onSavedSelected = () => { + this._menuContent = 'dashboard'; + }; + const onOptionsSelected = () => { + this._menuContent = 'options'; + if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length)); + }; + + return ( + <div className="docCreatorMenu"> + {!this._shouldDisplay ? undefined : ( + <div + className="docCreatorMenu-cont" + ref={r => (this._ref = r)} + style={{ + display: '', + left: this._pageX, + top: this._pageY, + width: this._menuDimensions.width, + height: this._menuDimensions.height, + background: SnappingManager.userBackgroundColor, + color: SnappingManager.userColor, + }}> + {this.resizePanes} + <div + className="docCreatorMenu-menu" + onPointerDown={e => + setupMoveUpEvents( + this, + e, + event => { + this._dragging = true; + this._startPos = { x: 0, y: 0 }; + this._startPos.x = event.pageX - (this._ref?.getBoundingClientRect().left ?? 0); + this._startPos.y = event.pageY - (this._ref?.getBoundingClientRect().top ?? 0); + document.addEventListener('pointermove', this.onDrag); + return true; + }, + emptyFunction, + undoable(clickEv => { + clickEv.stopPropagation(); + }, 'drag menu') + ) + }> + <div className="docCreatorMenu-top-buttons-container"> + {topButton('lightbulb', 'templates', onPreviewSelected, 'left')} + {topButton('magnifying-glass', 'options', onOptionsSelected, 'middle')} + {topButton('bars', 'saved', onSavedSelected, 'right')} + </div> + <button className="docCreatorMenu-menu-button close-menu" onPointerDown={e => this.setUpButtonClick(e, this.closeMenu)}> + <FontAwesomeIcon icon={'minus'} /> + </button> + </div> + {this.renderSelectedViewType} + </div> + )} + </div> + ); + } +} diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx new file mode 100644 index 000000000..c5254c17d --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx @@ -0,0 +1,117 @@ +import { Doc } from "../../../../../../fields/Doc"; +import { Docs } from "../../../../../documents/Documents"; +import { Field, FieldDimensions, FieldSettings, ViewType } from "./Field"; +import { FieldUtils } from "./FieldUtils"; +import { StaticField } from "./StaticField"; + +export class DynamicField implements Field { + private subfields: Field[] = []; + + private id: number; + private settings: FieldSettings; + private title: string = ''; + + private parent: Field; + private dimensions: FieldDimensions; + + constructor(settings: FieldSettings, id: number, parent?: Field) { + this.id = id; + this.settings = settings; + if (settings.title) { this.title = settings.title }; + if (!parent) { + this.parent = this; + this.dimensions = {width: this.settings.br[0] - this.settings.tl[0], height: this.settings.br[1] - this.settings.tl[1], coord: {x: this.settings.tl[0], y: this.settings.tl[1]}}; + } else { + this.parent = parent; + this.dimensions = FieldUtils.getLocalDimensions({tl: settings.tl, br: settings.br}, this.parent.getDimensions); + } + this.subfields = this.setupSubfields(); + } + + setContent = () => { return }; + getContent = () => { return '' }; + + setTitle = (title: string) => { this.title = title }; + getTitle = () => { return this.title }; + + get getSubfields() { return this.subfields }; + get getAllSubfields() { + let fields: Field[] = []; + this.subfields?.forEach(field => { + fields.push(field); + fields = fields.concat(field.getAllSubfields) + }); + return fields; + }; + + get getDimensions() { return this.dimensions }; + get getID() { return this.id }; + get getViewType() { return this.settings.viewType }; + + get getDescription(): string { + return this.settings.description ?? ''; + } + + matches = (): Array<number> => { + return []; + } + + updateRenderedDoc = () => { + return new Doc(); + } + + setupSubfields = (): Field[] => { + const fields: Field[] = []; + this.settings.subfields?.forEach((fieldSettings, index) => { + let field: Field; + const type = fieldSettings.viewType; + + const id = Number(String(this.id) + String(index)); + + if (type == ViewType.CAROUSEL3D || type === ViewType.FREEFORM) { + field = new DynamicField(fieldSettings, id, this); + } else { + field = new StaticField(fieldSettings, this, id); + } + fields.push(field); + }); + return fields; + } + + applyAttributes = (field: Field) => { + field.setTitle(this.title); + field.updateRenderedDoc(this.renderedDoc()); + } + + getChildDimensions = (coords: { tl: [number, number]; br: [number, number] }): FieldDimensions => { + const l = (coords.tl[0] * this.dimensions.height) / 2; + const t = coords.tl[1] * this.dimensions.width / 2; //prettier-ignore + const r = (coords.br[0] * this.dimensions.height) / 2; + const b = coords.br[1] * this.dimensions.width / 2; //prettier-ignore + const width = r - l; + const height = b - t; + const coord = { x: l, y: t }; + return { width, height, coord }; + }; + + renderedDoc = (): Doc => { + let doc: Doc; + switch (this.settings.viewType) { + case ViewType.CAROUSEL3D: + doc = Docs.Create.Carousel3DDocument(this.subfields.map(field => field.renderedDoc()), { + title: this.title, + }); + FieldUtils.applyBasicOpts(doc, this.dimensions, this.settings); + return doc; + case ViewType.FREEFORM: + doc = Docs.Create.FreeformDocument(this.subfields.map(field => field.renderedDoc()), { + title: this.title, + }); + FieldUtils.applyBasicOpts(doc, this.dimensions, this.settings); + return doc; + default: + return new Doc(); + } + } + +} diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/Field.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/Field.tsx new file mode 100644 index 000000000..ea9b566b3 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/Field.tsx @@ -0,0 +1,66 @@ +import { Doc } from "../../../../../../fields/Doc"; +import { Col } from "../DocCreatorMenu"; +import { TemplateFieldSize, TemplateFieldType } from "../TemplateBackend"; + +export enum FieldContentType { + STRING = 'string', + IMAGE = 'image', +} + +export enum ViewType { + CAROUSEL3D = 'carousel3d', + FREEFORM = 'freeform', + STATIC = 'static', + DEC = 'decoration' +} + +export type FieldDimensions = { + width: number; + height: number; + coord: {x: number, y: number}; +} + +export interface FieldOpts { + backgroundColor?: string; + color?: string; + cornerRounding?: number; + borderWidth?: string; + borderColor?: string; + contentXCentering?: 'h-left' | 'h-center' | 'h-right'; + contentYCentering?: 'top' | 'center' | 'bottom'; + opacity?: number; + rotation?: number; + fontBold?: boolean; + fontTransform?: 'uppercase' | 'lowercase'; + fieldViewType?: 'freeform' | 'stacked'; +} + +export type FieldSettings = { + tl: [number, number]; + br: [number, number]; + opts: FieldOpts; + viewType: ViewType; + title?: string; + subfields?: FieldSettings[]; + types?: TemplateFieldType[]; + sizes?: TemplateFieldSize[]; + description?: string; +}; + +export interface Field { + getContent: () => string; + setContent: (content: string, type?: FieldContentType) => void; + getDimensions: FieldDimensions; + getSubfields: Field[]; + getAllSubfields: Field[]; + getID: number; + getViewType: ViewType; + getDescription: string; + getTitle: () => string; + setTitle: (title: string) => void; + setupSubfields: () => Field[]; + applyAttributes: (field: Field) => void; + renderedDoc: () => Doc; + matches: (cols: Col[]) => number[]; + updateRenderedDoc: (oldDoc?: Doc) => Doc; +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/FieldUtils.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/FieldUtils.tsx new file mode 100644 index 000000000..3886774d2 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/FieldUtils.tsx @@ -0,0 +1,79 @@ +import { Doc } from "../../../../../../fields/Doc"; +import { ComputedField, ScriptField } from "../../../../../../fields/ScriptField"; +import { Col } from "../DocCreatorMenu"; +import { TemplateFieldSize, TemplateFieldType, TemplateLayouts } from "../TemplateBackend"; +import { FieldDimensions, FieldSettings } from "./Field"; + +export class FieldUtils { + public static getLocalDimensions = (coords: { tl: [number, number]; br: [number, number] }, parentDimensions: FieldDimensions): FieldDimensions => { + const l = (coords.tl[0] * parentDimensions.width) / 2; + const t = coords.tl[1] * parentDimensions.height / 2; //prettier-ignore + const r = (coords.br[0] * parentDimensions.width) / 2; + const b = coords.br[1] * parentDimensions.height / 2; //prettier-ignore + const width = r - l; + const height = b - t; + const coord = { x: l, y: t }; + return { width, height, coord }; + }; + + public static applyBasicOpts = (doc: Doc, parentDimensions: FieldDimensions, settings: FieldSettings, oldDoc?: Doc) => { + const opts = settings.opts; + doc.isDefaultTemplateDoc = oldDoc ? oldDoc.isDefaultTemplateDoc : true; + doc._layout_hideScroll = oldDoc ? oldDoc._layout_hideScroll : true; + doc.x = oldDoc ? oldDoc.x : parentDimensions.coord.x; + doc.y = oldDoc ? oldDoc.y : parentDimensions.coord.y; + doc._height = oldDoc ? oldDoc.height : parentDimensions.height; + doc._width = oldDoc ? oldDoc.width : parentDimensions.width; + doc.backgroundColor = oldDoc ? oldDoc.backgroundColor : opts.backgroundColor ?? ''; + doc._layout_borderRounding = !opts.cornerRounding ? '0px' : ScriptField.MakeFunction(`${opts.cornerRounding} * this.width + 'px'`); + doc.borderColor = oldDoc ? oldDoc.borderColor : opts.borderColor; + doc.borderWidth = oldDoc ? oldDoc.borderWidth : opts.borderWidth; + doc.opacity = oldDoc ? oldDoc.opacity : opts.opacity; + doc._rotation = oldDoc ? oldDoc._rotation : opts.rotation; + doc.hCentering = oldDoc ? oldDoc.hCentering : opts.contentXCentering; + doc.nativeWidth = parentDimensions.width; + doc.nativeHeight = parentDimensions.height; + doc._layout_nativeDimEditable = true; + }; + + public static calculateFontSize = (contWidth: number, contHeight: number, text: string, uppercase: boolean): number => { + const words: string[] = text.split(/\s+/).filter(Boolean); + + let currFontSize = 1; + let rowsCount = 1; + let currTextHeight = currFontSize * rowsCount * 2; + + while (currTextHeight <= contHeight) { + let wordIndex = 0; + let currentRowWidth = 0; + let wordsInCurrRow = 0; + rowsCount = 1; + + while (wordIndex < words.length) { + const word = words[wordIndex]; + const wordWidth = word.length * currFontSize * 0.7; + + if (currentRowWidth + wordWidth <= contWidth) { + currentRowWidth += wordWidth; + ++wordsInCurrRow; + } else { + if (words.length !== 1 && words.length > wordsInCurrRow) { + rowsCount++; + currentRowWidth = wordWidth; + wordsInCurrRow = 1; + } else { + break; + } + } + + wordIndex++; + } + + currTextHeight = rowsCount * currFontSize * 2; + + currFontSize += 1; + } + + return currFontSize - 1; + }; +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/StaticField.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/StaticField.tsx new file mode 100644 index 000000000..47b43f051 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/StaticField.tsx @@ -0,0 +1,147 @@ +import { Doc } from "../../../../../../fields/Doc"; +import { Docs } from "../../../../../documents/Documents"; +import { Col } from "../DocCreatorMenu"; +import { DynamicField } from "./DynamicField"; +import { FieldUtils } from "./FieldUtils"; +import { Field, FieldContentType, FieldDimensions, FieldSettings, ViewType } from "./Field"; + +export class StaticField { + private content: string; + private contentType: FieldContentType | undefined; + private subfields: Field[] = []; + private renderedDocument: Doc; + + private id: number; + private title: string = ''; + + private settings: FieldSettings; + + private parent: Field; + private dimensions: FieldDimensions; + + constructor(settings: FieldSettings, parent: Field, id: number) { + this.settings = settings; + if (settings.title) { this.title = settings.title }; + this.id = id; + this.parent = parent; + this.dimensions = FieldUtils.getLocalDimensions({tl: settings.tl, br: settings.br}, this.parent.getDimensions); + this.content = ''; + this.subfields = this.setupSubfields(); + this.renderedDocument = this.updateRenderedDoc(); + }; + + get getSubfields(): Field[] { return this.subfields ?? []; }; + + get getAllSubfields(): Field[] { + let fields: Field[] = []; + this.subfields?.forEach(field => { + fields.push(field); + fields = fields.concat(field.getAllSubfields); + }); + return fields; + }; + + get getDimensions() { return this.dimensions }; + get getID() { return this.id }; + get getViewType() { return this.settings.viewType }; + + get getDescription(): string { + return this.settings.description ?? ''; + } + + renderedDoc = () => { + return this.renderedDocument; + } + + setContent = (newContent: string, type?: FieldContentType) => { + this.content = newContent; + if (type) this.contentType = type; + this.updateRenderedDoc(this.renderedDocument); + }; + getContent() { return this.content }; + + setTitle = (title: string) => { + this.title = title; + this.renderedDocument.title = title; + this.updateRenderedDoc(this.renderedDocument); + }; + getTitle = () => { return this.title }; + + applyAttributes = (field: Field) => { //!!! can be updated later for more robust clonign; this is all ythat's needed now + field.setTitle(this.title); + field.setContent('', this.contentType); + field.updateRenderedDoc(this.renderedDoc()); + } + + setupSubfields = (): Field[] => { + const fields: Field[] = []; + this.settings.subfields?.forEach((fieldSettings, index) => { + let field: Field; + const type = fieldSettings.viewType; + + const id = Number(String(this.id) + String(index)); + + if (type === ViewType.FREEFORM || type === ViewType.CAROUSEL3D) { + field = new DynamicField(fieldSettings, id, this); + } else { + field = new StaticField(fieldSettings, this, id); + }; + + fields.push(field); + }); + return fields; + }; + + matches = (cols: Col[]): number[] => { + const colMatchesField = (col: Col) => { + const isMatch: boolean = ( + this.settings.sizes?.some(size => col.sizes?.includes(size)) + && this.settings.types?.includes(col.type)) + ?? false; + return isMatch; + } + + const matches: Array<number> = []; + + cols.forEach((col, v) => { + if (colMatchesField(col)) { + matches.push(v); + } + }); + + return matches; + }; + + updateRenderedDoc = (oldDoc?: Doc): Doc => { + const opts = this.settings.opts; + + if (!this.contentType) { this.contentType = FieldContentType.STRING }; + + let doc: Doc; + + switch (this.contentType) { + case FieldContentType.STRING: + doc = Docs.Create.TextDocument(String(this.content), { + title: this.title, + text_fontColor: oldDoc ? String(oldDoc.color) : opts.color, + contentBold: oldDoc ? Boolean(oldDoc.fontBold) : opts.fontBold, + textTransform: oldDoc ? String(oldDoc.fontTransform) : opts.fontTransform, + color: oldDoc ? String(oldDoc.color) : opts.color, + _text_fontSize: `${FieldUtils.calculateFontSize(this.dimensions.width, this.dimensions.height, String(this.content), true)}` + }); + FieldUtils.applyBasicOpts(doc, this.dimensions, this.settings, oldDoc); + break; + case FieldContentType.IMAGE: + doc = Docs.Create.ImageDocument(String(this.content), { + title: this.title, + _layout_fitWidth: false, + }); + FieldUtils.applyBasicOpts(doc, this.dimensions, this.settings, oldDoc); + break; + } + + this.renderedDocument = doc; + + return doc; + }; +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx new file mode 100644 index 000000000..0a5097d4a --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx @@ -0,0 +1,139 @@ +import { Doc, FieldType } from "../../../../../fields/Doc"; +import { Col } from "./DocCreatorMenu"; +import { DynamicField } from "./FieldTypes/DynamicField"; +import { Field, FieldSettings, ViewType } from "./FieldTypes/Field"; +import { } from "./FieldTypes/FieldUtils"; +import { } from "./FieldTypes/StaticField"; + +export class Template { + + mainField: DynamicField; + settings: FieldSettings; + + constructor(templateInfo: FieldSettings) { + this.mainField = this.setupMainField(templateInfo); + this.settings = templateInfo; + } + + get childFields(): Field[] { return this.mainField.getSubfields }; + get allFields(): Field[] { return this.mainField.getAllSubfields }; + get contentFields(): Field[] { return this.allFields.filter(field => field.getViewType === ViewType.STATIC) }; + get doc(){ return this.mainField.renderedDoc(); }; + + cloneBase = () => { + const clone: Template = new Template(this.settings); + clone.allFields.forEach(field => { + const matchingField: Field = this.allFields.filter(f => f.getID === field.getID)[0]; + matchingField.applyAttributes(field); + }) + return clone; + } + + getRenderedDoc = () => { + const doc: Doc = this.mainField.renderedDoc(); + this.contentFields.forEach(field => { + const title: string = field.getTitle(); + const val: FieldType = field.getContent() as FieldType; + if (!title || !val) return; + doc[title] = val; + }); + return doc; + } + + getFieldByID = (id: number): Field => { + return this.allFields.filter(field => field.getID === id)[0]; + } + + getFieldByTitle = (title: string) => { + return this.allFields.filter(field => field.getTitle() === title)[0]; + } + + setupMainField = (templateInfo: FieldSettings) => { + return new DynamicField(templateInfo, 1); + } + + get descriptionSummary(): string { + let summary: string = ''; + this.contentFields.forEach(field => { + summary += `--- Field #${field.getID} (title: ${field.getTitle()}): ${field.getDescription ?? ''} ---`; + }); + return summary; + } + + get compiledContent(): string { + let summary: string = ''; + this.contentFields.forEach(field => { + summary += `--- Field #${field.getID} (title: ${field.getTitle()}): ${field.getContent() ?? ''} ---`; + }); + return summary; + } + + renderUpdates = () => { + this.allFields.forEach(field => { + field.updateRenderedDoc(field.renderedDoc()); + }); + }; + + resetToBase = () => { + this.allFields.forEach(field => { + field.updateRenderedDoc(); + }) + } + + isValidTemplate = (cols: Col[]) => { + const matches: number[][] = this.getMatches(cols); + const maxMatches: number = this.maxMatches(matches); + return maxMatches === this.contentFields.length; + } + + getMatches = (cols: Col[]): number[][] => { + const numFields = this.contentFields.length; + + if (cols.length !== numFields) return []; + + const matches: number[][] = Array(numFields) + .fill([]) + .map(() => []); + + this.contentFields.forEach((field, i) => { + matches[i] = (field.matches(cols)); + }); + + return matches; + } + + maxMatches = (matches: number[][]) => { + if (matches.length === 0) return 0; + + const fieldsCt = this.contentFields.length; + const used: boolean[] = Array(fieldsCt).fill(false); + const mt: number[] = Array(fieldsCt).fill(-1); + + const augmentingPath = (v: number): boolean => { + if (used[v]) return false; + used[v] = true; + + for (const to of matches[v]) { + if (mt[to] === -1 || augmentingPath(mt[to])) { + mt[to] = v; + return true; + } + } + return false; + }; + + for (let v = 0; v < fieldsCt; ++v) { + used.fill(false); + augmentingPath(v); + } + + let count: number = 0; + + for (let i = 0; i < fieldsCt; ++i) { + if (mt[i] !== -1) ++count; + } + + return count; + }; + +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.tsx new file mode 100644 index 000000000..d3282eda3 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.tsx @@ -0,0 +1,752 @@ +import { FieldSettings, ViewType } from "./FieldTypes/Field"; +import { } from "./FieldTypes/StaticField"; + +export enum TemplateFieldType { + TEXT = 'text', + VISUAL = 'visual', + UNSET = 'unset', +} + +export enum TemplateFieldSize { + TINY = 'tiny', + SMALL = 'small', + MEDIUM = 'medium', + LARGE = 'large', + HUGE = 'huge', +} + +export class TemplateLayouts { + public static get allTemplates(): FieldSettings[] { + return Object.values(TemplateLayouts); + } + + public static FourField001: FieldSettings = { + title: 'fourfield001', + tl: [0, 0], + br: [416, 700], + viewType: ViewType.FREEFORM, + opts: { + backgroundColor: '#C0B887', + cornerRounding: .05, + //borderColor: '#6B461F', + //borderWidth: '12', + }, + subfields: [ + { + viewType: ViewType.STATIC, + tl: [-0.95, -1], + br: [0.95, -0.85], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY], + description: 'A title field for very short text that contextualizes the content.', + opts: { + backgroundColor: 'transparent', + color: '#F1F0E9', + contentXCentering: 'h-center', + fontBold: true, + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.87, -0.83], + br: [0.87, 0.2], + types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'The main focus of the template; could be an image, long text, etc.', + opts: { + cornerRounding: .05, + borderColor: '#8F5B25', + borderWidth: '6', + backgroundColor: '#CECAB9', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.8, 0.2], + br: [0.8, 0.3], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + description: 'A caption for field #2, very short text.', + opts: { + backgroundColor: 'transparent', + contentXCentering: 'h-center', + color: '#F1F0E9', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.87, 0.37], + br: [0.87, 0.96], + types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A medium-sized field for medium/long text.', + opts: { + cornerRounding: .05, + borderColor: '#8F5B25', + borderWidth: '6', + backgroundColor: '#CECAB9', + }, + }, + ], + }; + + public static FourField002: FieldSettings = { + title: 'fourfield002', + viewType: ViewType.FREEFORM, + tl: [0,0], + br: [425, 778], + opts: { + backgroundColor: '#242425', + }, + subfields: [ + { + viewType: ViewType.STATIC, + tl: [-0.83, -0.95], + br: [0.83, -0.2], + types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], + description: 'A medium to large-sized field suitable for an image or longer text that should be the main focus.', + opts: { + borderWidth: '8', + borderColor: '#F8E71C', + backgroundColor: '#242425', + color: 'white', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.65, -0.2], + br: [0.65, -0.02], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY], + description: 'A tiny field for just a word or two of plain text.', + opts: { + backgroundColor: 'transparent', + color: 'white', + contentXCentering: 'h-center', + fontTransform: 'uppercase', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.65, 0], + br: [0.65, 0.18], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY], + description: 'A tiny field for just a word or two of plain text.', + opts: { + backgroundColor: 'transparent', + color: 'white', + contentXCentering: 'h-center', + fontTransform: 'uppercase', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.83, 0.2], + br: [0.83, 0.95], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A medium to large-sized field suitable for longer text that should contextualize field 1.', + opts: { + borderWidth: '8', + borderColor: '#F8E71C', + color: 'white', + backgroundColor: '#242425', + }, + }, + { + viewType: ViewType.DEC, + tl: [-0.8, -0.075], + br: [-0.525, 0.075], + opts: { + backgroundColor: '#F8E71C', + rotation: 45, + }, + }, + { + viewType: ViewType.DEC, + tl: [-0.3075, -0.0245], + br: [-0.2175, 0.0245], + opts: { + backgroundColor: '#F8E71C', + rotation: 45, + }, + }, + { + viewType: ViewType.DEC, + tl: [-0.045, -0.0245], + br: [0.045, 0.0245], + opts: { + backgroundColor: '#F8E71C', + rotation: 45, + }, + }, + { + viewType: ViewType.DEC, + tl: [0.2175, -0.0245], + br: [0.3075, 0.0245], + opts: { + backgroundColor: '#F8E71C', + rotation: 45, + }, + }, + { + viewType: ViewType.DEC, + tl: [0.525, -0.075], + br: [0.8, 0.075], + opts: { + backgroundColor: '#F8E71C', + rotation: 45, + }, + }, + ], + }; + + // public static FourField003: TemplateDocInfos = { + // title: 'fourfield3', + // width: 477, + // height: 662, + // opts: { + // backgroundColor: '#9E9C95' + // }, + // fields: [{ + // tl: [-.875, -.9], + // br: [.875, .7], + // types: [TemplateFieldType.VISUAL], + // sizes: [TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + // description: '', + // opts: { + // borderWidth: '15', + // borderColor: '#E0E0DA', + // } + // }, { + // tl: [-.95, .8], + // br: [-.1, .95], + // types: [TemplateFieldType.TEXT], + // sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + // description: '', + // opts: { + // backgroundColor: 'transparent', + // color: 'white', + // contentXCentering: 'h-right', + // } + // }, { + // tl: [.1, .8], + // br: [.95, .95], + // types: [TemplateFieldType.TEXT], + // sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + // description: '', + // opts: { + // backgroundColor: 'transparent', + // color: 'red', + // fontTransform: 'uppercase', + // contentXCentering: 'h-left' + // } + // }, { + // tl: [0, -.9], + // br: [.85, -.66], + // types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL], + // sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + // description: '', + // opts: { + // backgroundColor: 'transparent', + // contentXCentering: 'h-right' + // } + // }], + // decorations: [{ + // tl: [-.025, .8], + // br: [.025, .95], + // opts: { + // backgroundColor: '#E0E0DA', + // } + // }] + // }; + + public static FourField004: FieldSettings = { + title: 'fourfield04', + viewType: ViewType.FREEFORM, + tl: [0,0], + br: [414,583], + opts: { + backgroundColor: '#6CCAF0', + //borderColor: '#1088C3', + //borderWidth: '10', + }, + subfields: [ + { + viewType: ViewType.STATIC, + tl: [-0.86, -0.92], + br: [-0.075, -0.77], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY], + description: 'A tiny field for just a word or two of plain text.', + opts: { + backgroundColor: '#E2B4F5', + borderWidth: '9', + borderColor: '#9222F1', + contentXCentering: 'h-center', + }, + }, + { + viewType: ViewType.STATIC, + tl: [0.075, -0.92], + br: [0.86, -0.77], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY], + description: 'A tiny field for just a word or two of plain text.', + opts: { + backgroundColor: '#F5B4DD', + borderWidth: '9', + borderColor: '#E260F3', + contentXCentering: 'h-center', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.81, -0.64], + br: [0.81, 0.48], + types: [TemplateFieldType.VISUAL], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A large to huge field for visual content that is the main content of the template.', + opts: { + borderWidth: '16', + borderColor: '#A2BD77', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.86, 0.6], + br: [0.86, 0.92], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], + description: 'A medium to large field for text that describes the visual content above', + opts: { + borderWidth: '9', + borderColor: '#F0D601', + backgroundColor: '#F3F57D', + }, + }, + { + viewType: ViewType.DEC, + tl: [-0.852, -0.67], + br: [0.852, 0.51], + opts: { + backgroundColor: 'transparent', + borderColor: '#007C0C', + borderWidth: '10', + }, + }, + ], + }; + + public static FourField005: FieldSettings = { + title: 'fourfield05', + viewType: ViewType.FREEFORM, + tl: [0,0], + br: [400,550], + opts: { + backgroundColor: '#95A575', + }, + subfields: [ + { + viewType: ViewType.STATIC, + tl: [-0.9, -.925], + br: [-.075, -.775], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + description: 'A small text field for a title or word(s) that categorize the rest of the content.', + opts: { + borderColor: '#3B4A2C', + borderWidth: '8', + contentXCentering: "h-center", + backgroundColor: '#B8DC90', + }, + }, + { + viewType: ViewType.STATIC, + tl: [.075, -.925], + br: [.9, -.775], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + description: 'A small text field for a title that categorizes the rest of the content.', + opts: { + borderColor: '#3B4A2C', + borderWidth: '8', + contentXCentering: "h-center", + backgroundColor: '#B8DC90', + }, + }, + { + viewType: ViewType.DEC, + tl: [-.82, -.4], + br: [-.5, -.2], + opts: { + backgroundColor: '#94B058', + borderColor: '#3B4A2C', + borderWidth: '8', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.66, -.65], + br: [0.66, .25], + types: [TemplateFieldType.VISUAL], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], + description: 'A medium to large field in the center of the template, for the main visual content.', + opts: { + borderColor: '#3B4A2C', + borderWidth: '8', + backgroundColor: '#B8DC90', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-.875, .425], + br: [0.875, .925], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], + description: 'A medium to large field at the bottom of the template, for the main text content.', + opts: { + borderColor: '#3B4A2C', + borderWidth: '8', + contentXCentering: "h-center", + backgroundColor: '#B8DC90', + }, + }, + { + viewType: ViewType.DEC, + tl: [-1.1, -.62], + br: [-.9, -.5], + opts: { + backgroundColor: '#7A9D31', + borderColor: '#3B4A2C', + borderWidth: '8', + }, + }, + { + viewType: ViewType.DEC, + tl: [-1.1, 0], + br: [-.9, .15], + opts: { + backgroundColor: '#94B058', + borderColor: '#3B4A2C', + borderWidth: '8', + }, + }, + { + viewType: ViewType.DEC, + tl: [-.93, -.265], + br: [-.715, -.125], + opts: { + backgroundColor: '#728745', + borderColor: '#3B4A2C', + borderWidth: '8', + }, + }, + { + viewType: ViewType.DEC, + tl: [.7, -.45], + br: [.85, -.3], + opts: { + backgroundColor: '#7A9D31', + borderColor: '#3B4A2C', + borderWidth: '8', + }, + }, + { + viewType: ViewType.DEC, + tl: [.8, .03], + br: [1.2, .33], + opts: { + backgroundColor: '#728745', + borderColor: '#3B4A2C', + borderWidth: '8', + }, + }, + { + viewType: ViewType.DEC, + tl: [.875, -.13], + br: [1.2, .12], + opts: { + backgroundColor: '#94B058', + borderColor: '#3B4A2C', + borderWidth: '8', + }, + }, + ] + } + + public static FourFieldCarousel: FieldSettings = { + title: 'title_fourfieldcarousel', + viewType: ViewType.FREEFORM, + tl:[0,0], + br:[500, 600], + opts: { + backgroundColor: '#DDD3A9', + }, + subfields: [ + { + viewType: ViewType.STATIC, + tl: [-0.8, -.9], + br: [0.8, -.5], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + description: 'A small text field for a title that categorizes the rest of the content.', + opts: { + borderColor: 'yellow', + borderWidth: '8', + contentXCentering: "h-center", + backgroundColor: 'transparent', + }, + }, + { + viewType: ViewType.CAROUSEL3D, + tl: [-0.9, -.3], + br: [0.9, .9], + opts: { + borderColor: 'yellow', + borderWidth: '8', + backgroundColor: 'transparent', + }, + subfields: [ + { + viewType: ViewType.STATIC, + tl: [-.3, -.6], + br: [.3, .6], + types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A medium to large field for content that will share central focus with other content in the carousel.', + opts: { + borderColor: 'yellow', + borderWidth: '8', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-.3, -.6], + br: [.3, .6], + types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A medium to large field for content that will share central focus with other content in the carousel.', + opts: { + borderColor: 'black', + borderWidth: '8', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-.3, -.6], + br: [.3, .6], + types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A medium to large field for content that will share central focus with other content in the carousel.', + opts: { + borderColor: 'yellow', + borderWidth: '8', + }, + }, + ] + }, + ] + } + + public static ThreeField001: FieldSettings = { + title: 'threefield001', + viewType: ViewType.FREEFORM, + tl: [0,0], + br: [575, 770], + opts: { + backgroundColor: '#DDD3A9', + }, + subfields: [ + { + viewType: ViewType.FREEFORM, + tl: [-0.66, -0.747], + br: [0.66, 0.247], + description: 'A medium to large field for visual content that is the central focus.', + opts: { + borderColor: 'yellow', + borderWidth: '8', + backgroundColor: '#DDD3A9', + rotation: 45, + }, + subfields: [ + { + viewType: ViewType.STATIC, + tl: [-1.25, -1.25], + br: [1.25, 1.25], + types: [TemplateFieldType.VISUAL], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A medium to large field for visual content that is the central focus.', + opts: { + rotation: -45, + }, + }, + ] + }, + { + viewType: ViewType.STATIC, + tl: [-0.7, 0.2], + br: [0.7, 0.46], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + description: 'A very small text field for one to a few words. A good caption for the image.', + opts: { + backgroundColor: 'transparent', + contentXCentering: 'h-center', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.95, 0.5], + br: [0.95, 0.95], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], + description: 'A medium to large text field for a thorough description of the image. ', + opts: { + backgroundColor: 'transparent', + color: 'white', + }, + }, + { + viewType: ViewType.FREEFORM, + tl: [0.2, -1.32], + br: [1.8, -0.66], + opts: { + backgroundColor: '#CEB155', + rotation: 45, + }, + subfields: [ + { + viewType: ViewType.DEC, + tl: [-1, -.7], + br: [1, -.625], + opts: { + backgroundColor: 'yellow', + }, + }, + ] + }, + { + viewType: ViewType.FREEFORM, + tl: [-1.8, -1.32], + br: [-0.2, -0.66], + opts: { + backgroundColor: '#CEB155', + rotation: 135, + }, + subfields: [ + { + viewType: ViewType.DEC, + tl: [-1, -.7], + br: [1, -.625], + opts: { + backgroundColor: 'yellow', + }, + }, + ] + }, + { + viewType: ViewType.FREEFORM, + tl: [0.33, 0.75], + br: [1.66, 1.25], + opts: { + backgroundColor: '#CEB155', + rotation: 135, + }, + subfields: [ + { + viewType: ViewType.DEC, + tl: [-1, -.7], + br: [1, -.625], + opts: { + backgroundColor: 'yellow', + }, + }, + ] + }, + { + viewType: ViewType.FREEFORM, + tl: [-1.66, 0.75], + br: [-0.33, 1.25], + opts: { + backgroundColor: '#CEB155', + rotation: 45, + }, + subfields: [ + { + viewType: ViewType.DEC, + tl: [-1, -.7], + br: [1, -.625], + opts: { + backgroundColor: 'yellow', + }, + }, + ] + }, + ], + }; + + public static ThreeField002: FieldSettings = { + title: 'threefield002', + viewType: ViewType.FREEFORM, + tl: [0,0], + br: [477, 662], + opts: { + backgroundColor: '#9E9C95', + }, + subfields: [ + { + viewType: ViewType.STATIC, + tl: [-0.875, -0.9], + br: [0.875, 0.7], + types: [TemplateFieldType.VISUAL], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A medium to large visual field for the main content of the template', + opts: { + borderWidth: '15', + borderColor: '#E0E0DA', + }, + }, + { + viewType: ViewType.STATIC, + tl: [0.1, 0.775], + br: [0.95, 0.975], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + description: 'A very small text field for one to a few words. The content should represent a general categorization of the image.', + opts: { + backgroundColor: 'transparent', + color: '#AF0D0D', + fontTransform: 'uppercase', + fontBold: true, + contentXCentering: 'h-left', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.95, 0.775], + br: [-0.1, 0.975], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + description: 'A very small text field for one to a few words. The content should contextualize field 2.', + opts: { + backgroundColor: 'transparent', + color: 'black', + contentXCentering: 'h-right', + }, + }, + { + viewType: ViewType.DEC, + tl: [-0.025, 0.8], + br: [0.025, 0.95], + opts: { + backgroundColor: '#E0E0DA', + }, + }, + ], + }; +} + + diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx new file mode 100644 index 000000000..50ae4d72a --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx @@ -0,0 +1,22 @@ +import { Col } from "./DocCreatorMenu"; +import { FieldSettings } from "./FieldTypes/Field"; +import { Template } from "./Template"; + +export class TemplateManager { + + templates: Template[] = []; + + constructor(templateSettings: FieldSettings[]) { + this.templates = this.initializeTemplates(templateSettings); + } + + initializeTemplates = (templateSettings: FieldSettings[]): Template[] => { + const initializedTemplates: Template[] = []; + templateSettings.forEach(settings => initializedTemplates.push(new Template(settings))); + return initializedTemplates; + } + + getValidTemplates = (cols: Col[]): Template[] => { + return this.templates.filter(template => template.isValidTemplate(cols)); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss index 0eb27b65b..ff1fa343d 100644 --- a/src/client/views/nodes/DataVizBox/components/Chart.scss +++ b/src/client/views/nodes/DataVizBox/components/Chart.scss @@ -1,4 +1,4 @@ -@import '../../../global/globalCssVariables.module.scss'; +@use '../../../global/globalCssVariables.module.scss' as global; .chart-container { display: flex; flex-direction: column; @@ -108,7 +108,7 @@ } } tr td { - height: $DATA_VIZ_TABLE_ROW_HEIGHT !important; // bcz: hack. you can't set a <tr> height directly, but you can set the height of all of it's <td>s. So this is the height of a tableBox row. + height: global.$DATA_VIZ_TABLE_ROW_HEIGHT !important; // bcz: hack. you can't set a <tr> height directly, but you can set the height of all of it's <td>s. So this is the height of a tableBox row. padding: 0 !important; vertical-align: middle !important; } @@ -135,7 +135,7 @@ } .tableBox-filterPopup { - background: $light-gray; + background: global.$light-gray; position: absolute; min-width: 235px; top: 60px; @@ -152,7 +152,7 @@ .tableBox-filterPopup-selectColumn-each { margin-left: 25px; border-radius: 3px; - background: $light-gray; + background: global.$light-gray; } } .tableBox-filterPopup-setValue { @@ -162,7 +162,7 @@ .tableBox-filterPopup-setValue-each { margin-right: 5px; border-radius: 3px; - background: $light-gray; + background: global.$light-gray; } .tableBox-filterPopup-setValue-input { margin: 5px; diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss index b32b27e65..e1b83dc59 100644 --- a/src/client/views/nodes/DocumentLinksButton.scss +++ b/src/client/views/nodes/DocumentLinksButton.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables.module.scss'; +@use '../global/globalCssVariables.module.scss' as global; .documentLinksButton-wrapper { transform-origin: top left; @@ -29,7 +29,7 @@ pointer-events: auto; display: flex; align-items: center; - background-color: $light-blue; + background-color: global.$light-blue; color: black; } .documentLinksButton, @@ -59,30 +59,30 @@ } } .documentLinksButton { - background-color: $dark-gray; - color: $white; + background-color: global.$dark-gray; + color: global.$white; font-weight: bold; font-size: 100%; font-family: 'Roboto'; transition: 0.2s ease all; &:hover { - background-color: $black; + background-color: global.$black; } } .documentLinksButton.startLink { - background-color: $medium-blue; + background-color: global.$medium-blue; width: 75%; height: 75%; - color: $white; + color: global.$white; font-weight: bold; font-size: 100%; transition: 0.2s ease all; } .documentLinksButton-endLink { - border: $medium-blue 2px dashed; - color: $medium-blue; + border: global.$medium-blue 2px dashed; + color: global.$medium-blue; background-color: none !important; font-size: 100%; transition: 0.2s ease all; diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 7e5507586..dd5fd0d0c 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables.module.scss'; +@use '../global/globalCssVariables.module.scss' as global; .documentView-effectsWrapper { border-radius: inherit; @@ -28,7 +28,7 @@ // overflow: hidden; // need this so that title will be clipped when borderRadius is set // transition: outline 0.3s linear; - // background: $white; //overflow: hidden; + // background: global.$white; //overflow: hidden; transform-origin: center; &.minimized { @@ -180,7 +180,7 @@ .documentView-titleWrapper, .documentView-titleWrapper-hover { - color: $black; + color: global.$black; transform-origin: top left; top: 0; width: 100%; @@ -291,6 +291,7 @@ justify-items: center; background-color: rgb(223, 223, 223); transform-origin: top left; + background: transparent; .documentView-editorView-resizer { height: 5px; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 0193fd328..595abc7f8 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -131,6 +131,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document style = (doc: Doc, sprop: StyleProp | string) => this._props.styleProvider?.(doc, this._props, sprop); @computed get opacity() { return this.style(this.layoutDoc, StyleProp.Opacity) as number; } // prettier-ignore @computed get boxShadow() { return this.style(this.layoutDoc, StyleProp.BoxShadow) as string; } // prettier-ignore + @computed get border() { return this.style(this.layoutDoc, StyleProp.Border) as string || ""; } // prettier-ignore @computed get borderRounding() { return this.style(this.layoutDoc, StyleProp.BorderRounding) as string; } // prettier-ignore @computed get widgetDecorations() { return this.style(this.layoutDoc, StyleProp.Decorations) as JSX.Element; } // prettier-ignore @computed get backgroundBoxColor(){ return this.style(this.layoutDoc, StyleProp.BackgroundColor + ':docView') as string; } // prettier-ignore @@ -278,16 +279,17 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document setTimeout(() => this._titleRef.current?.setIsFocused(true)); // use timeout in case title wasn't shown to allow re-render so that titleref will be defined }; onBrowseClick = (e: React.MouseEvent) => { - const browseTransitionTime = 500; + //const browseTransitionTime = 500; DocumentView.DeselectAll(); DocumentView.showDocument(this.Document, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => { - const options: FocusViewOptions = { pointFocus: { X: e.clientX, Y: e.clientY }, zoomTime: browseTransitionTime }; + // const options: FocusViewOptions = { pointFocus: { X: e.clientX, Y: e.clientY }, zoomTime: browseTransitionTime }; if (!focused && this._docView) { - this._docView - .docViewPath() - .reverse() - .forEach(cont => cont.ComponentView?.focus?.(cont.Document, options)); - Doc.linkFollowHighlight(this.Document, false); + DocumentView.showDocument(this.Document, { zoomScale: 0.3, willZoomCentered: true }); + // this._docView + // .docViewPath() + // .reverse() + // .forEach(cont => cont.ComponentView?.focus?.(cont.Document, options)); + // Doc.linkFollowHighlight(this.Document, false); } }); e.stopPropagation(); @@ -684,14 +686,14 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document }; rootSelected = () => this._rootSelected; - panelHeight = () => this._props.PanelHeight() - this.headerMargin; + panelHeight = () => this._props.PanelHeight() - this.headerMargin - 2 * NumCast(this.Document.borderWidth); screenToLocalContent = () => this._props .ScreenToLocalTransform() - .translate(0, -this.headerMargin) + .translate(-NumCast(this.Document.borderWidth), -this.headerMargin - NumCast(this.Document.borderWidth)) .scale(this._props.showAIEditor ? (this._props.PanelHeight() || 1) / this.aiContentsHeight() : 1); onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHdlr; - setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height)); } // prettier-ignore + setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height + 2 * NumCast(this.Document.borderWidth))); } // prettier-ignore setContentView = action((view: ViewBoxInterface<FieldViewProps>) => { this._componentView = view; }); // prettier-ignore isContentActive = (): boolean | undefined => this._isContentActive; childFilters = () => [...this._props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)]; @@ -777,9 +779,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document style={{ width: `${100 / this.uiBtnScaling}%`, // transform: `scale(${this.uiBtnScaling})`, - bottom: this.maxWidgetSize, + bottom: Number.isNaN(this.maxWidgetSize) ? undefined : this.maxWidgetSize, }}> - {this._props.DocumentView?.() ? <TagsView Views={[this._props.DocumentView?.()]} /> : null} + {this._props.DocumentView?.() && !this._props.docViewPath().slice(-2)[0].ComponentView?.isUnstyledView?.() ? <TagsView Views={[this._props.DocumentView?.()]} /> : null} </div> ) : ( <> @@ -796,6 +798,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document <div className="documentView-editorView" style={{ + background: SnappingManager.userVariantColor, width: `${100 / this.uiBtnScaling}%`, // transform: `scale(${this.uiBtnScaling})`, }} @@ -989,15 +992,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document highlightStroke: undefined, }; const { clipPath, jsx } = (borderPath as { clipPath: string; jsx: JSX.Element }) ?? { clipPath: undefined, jsx: undefined }; - const boxShadow = !highlighting - ? this.boxShadow - : highlighting && this.borderRounding && highlightStyle !== 'dashed' - ? `0 0 0 ${highlightIndex}px ${highlightColor}` - : this.boxShadow || (this.Document.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined); + const boxShadow = this.boxShadow; const renderDoc = this.renderDoc({ borderRadius: this.borderRounding, - outline: highlighting && !this.borderRounding && !highlightStroke ? `${highlightColor} ${highlightStyle} ${highlightIndex}px` : 'solid 0px', - border: highlighting && this.borderRounding && highlightStyle === 'dashed' ? `${highlightStyle} ${highlightColor} ${highlightIndex}px` : undefined, + outline: highlighting && !highlightStroke ? `${highlightColor} ${highlightStyle} ${highlightIndex}px` : 'solid 0px', + border: this._componentView?.isUnstyledView?.() ? undefined : this.border, boxShadow, clipPath, }); @@ -1013,7 +1012,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document onPointerOver={() => (!SnappingManager.IsDragging || SnappingManager.CanEmbed) && Doc.BrushDoc(this.Document)} onPointerLeave={e => !isParentOf(this._contentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.Document)} style={{ - borderRadius: this.borderRounding, + borderRadius: this._componentView?.isUnstyledView?.() ? undefined : this.borderRounding, pointerEvents: this._pointerEvents === 'visiblePainted' ? 'none' : this._pointerEvents, // visible painted means that the underlying doc contents are irregular and will process their own pointer events (otherwise, the contents are expected to fill the entire doc view box so we can handle pointer events here) }}> {this._componentView?.isUnstyledView?.() || this.Document.type === DocumentType.CONFIG || !renderDoc ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.Document[Animation], this.Document)} @@ -1487,7 +1486,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { ShouldNotScale = () => this.shouldNotScale; NativeWidth = () => this.effectiveNativeWidth; NativeHeight = () => this.effectiveNativeHeight; - PanelWidth = () => this.panelWidth; + PanelWidth = () => this.panelWidth - 2 * NumCast(this.Document.borderWidth); PanelHeight = () => this.panelHeight; ReducedPanelWidth = () => this.panelWidth / 2; ReducedPanelHeight = () => this.panelWidth / 2; diff --git a/src/client/views/nodes/EquationBox.scss b/src/client/views/nodes/EquationBox.scss index 55e0f5184..bcbb44e68 100644 --- a/src/client/views/nodes/EquationBox.scss +++ b/src/client/views/nodes/EquationBox.scss @@ -1,5 +1,3 @@ -@import '../global/globalCssVariables.module.scss'; - .equationBox-cont { transform-origin: center; width: fit-content; diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.scss b/src/client/views/nodes/FontIconBox/FontIconBox.scss index 2405889cf..186d24e92 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.scss +++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss @@ -1,4 +1,4 @@ -@import '../../global/globalCssVariables.module.scss'; +@use '../../global/globalCssVariables.module.scss' as global; // bcz: something's messed up with the IconButton css. this mostly fixes the fit-all button, the color buttons, the undo +/- expander and the dropdown doc type list (eg 'text') .iconButton-container { @@ -23,7 +23,7 @@ justify-content: center; align-items: center; font-size: 80%; - border-radius: $standard-border-radius; + border-radius: global.$standard-border-radius; transition: 0.15s; .menuButton-wrap { @@ -34,7 +34,7 @@ } .fontIconBox-label { - color: $white; + color: global.$white; bottom: -1; position: absolute; text-align: center; @@ -124,17 +124,17 @@ width: 21px; left: 2px; bottom: 2px; - background-color: $white; + background-color: global.$white; -webkit-transition: 0.4s; transition: 0.4s; } input:checked + .slider { - background-color: $medium-blue; + background-color: global.$medium-blue; } input:focus + .slider { - box-shadow: 0 0 1px $medium-blue; + box-shadow: 0 0 1px global.$medium-blue; } input:checked + .slider:before { @@ -145,11 +145,11 @@ /* Rounded sliders */ .slider.round { - border-radius: $standard-border-radius; + border-radius: global.$standard-border-radius; } .slider.round:before { - border-radius: $standard-border-radius; + border-radius: global.$standard-border-radius; } } @@ -259,12 +259,12 @@ height: fit-content; top: 100%; z-index: 21; - background-color: $white; + background-color: global.$white; box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); padding: 1px; .list-item { - color: $black; + color: global.$black; width: 100%; height: 25px; font-weight: 400; @@ -285,7 +285,7 @@ background: transparent; &.slider { - color: $white; + color: global.$white; cursor: pointer; flex-direction: column; background: transparent; @@ -302,7 +302,7 @@ z-index: 21; background-color: #e3e3e3; box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); - border-radius: $standard-border-radius; + border-radius: global.$standard-border-radius; .menu-slider { height: 10px; @@ -340,7 +340,7 @@ border: none; text-align: right; width: 100%; - color: $white; + color: global.$white; height: 100%; text-align: center; } @@ -354,7 +354,7 @@ &.list { width: 100%; justify-content: space-around; - border: $standard-border; + border: global.$standard-border; .menuButton-dropdownList { position: absolute; @@ -365,12 +365,12 @@ overflow-y: scroll; top: 100%; z-index: 21; - background-color: $white; + background-color: global.$white; box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); padding: 1px; .list-item { - color: $black; + color: global.$black; width: 100%; height: 25px; font-weight: 400; @@ -394,7 +394,7 @@ padding-left: 10px; justify-content: flex-start; color: black; - background-color: $light-gray; + background-color: global.$light-gray; padding: 5px; padding-left: 10px; width: 100%; @@ -417,7 +417,7 @@ top: 100%; background-color: #e3e3e3; box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); - border-radius: $standard-border-radius; + border-radius: global.$standard-border-radius; } } diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index f58862028..7e0236b74 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -1,11 +1,13 @@ +import { Button, ColorPicker, Dropdown, DropdownType, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from '@dash/components'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, ColorPicker, Dropdown, DropdownType, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from '@dash/components'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, DashColor, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; +import { InkTool } from '../../../../fields/InkField'; +import { ScriptField } from '../../../../fields/ScriptField'; import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { emptyFunction } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; @@ -21,8 +23,6 @@ import { FieldView, FieldViewProps } from '../FieldView'; import { OpenWhere } from '../OpenWhere'; import './FontIconBox.scss'; import TrailsIcon from './TrailsIcon'; -import { InkTool } from '../../../../fields/InkField'; -import { ScriptField } from '../../../../fields/ScriptField'; export enum ButtonType { TextButton = 'textBtn', @@ -134,6 +134,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { min={NumCast(this.dataDoc.numBtnMin, 0)} max={NumCast(this.dataDoc.numBtnMax, 100)} number={checkResult} + size={Size.XSMALL} setNumber={undoable(value => numScript(value), `${this.Document.title} button set from list`)} fillWidth /> diff --git a/src/client/views/nodes/IconTagBox.scss b/src/client/views/nodes/IconTagBox.scss index c79d662f4..202b0c701 100644 --- a/src/client/views/nodes/IconTagBox.scss +++ b/src/client/views/nodes/IconTagBox.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables.module.scss'; +@use '../global/globalCssVariables.module.scss' as global; .card-button-container { display: flex; @@ -18,7 +18,7 @@ margin: auto; padding: 0; border-radius: 50%; - background-color: $dark-gray; + background-color: global.$dark-gray; background-color: transparent; } } diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index fe4f0b1a2..3d6942e6f 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -40,7 +40,8 @@ max-height: 100%; pointer-events: inherit; background: transparent; - z-index: -10000; + z-index: 0; + // z-index: -10000; // bcz: not sure why this was here. it broke dropping images on the image box alternate bullseye icon. img { height: auto; @@ -103,6 +104,10 @@ margin: 0 auto; display: flex; height: 100%; + img { + object-fit: contain; + height: 100%; + } .imageBox-fadeBlocker, .imageBox-fadeBlocker-hover { @@ -122,6 +127,7 @@ } } } +.imageBox-regenerateDropTarget, .imageBox-alternateDropTarget { position: absolute; color: white; @@ -129,7 +135,19 @@ right: 0; bottom: 0; z-index: 2; + transform-origin: bottom right; cursor: default; + > svg { + width: 100%; + height: 100%; + } +} +.imageBox-regenerateDropTarget { + right: 30; + border-radius: 50%; + svg { + border-radius: 50%; + } } .imageBox-fader img { @@ -182,8 +200,9 @@ flex-direction: row; gap: 5px; width: 100%; + padding: 0 10; .imageBox-aiView-regenerate-createBtn { - max-width: 10%; + max-width: 20%; .button-container { width: 100% !important; justify-content: left !important; @@ -194,7 +213,7 @@ .imageBox-aiView-firefly { overflow: hidden; text-overflow: ellipsis; - max-width: 10%; + max-width: 15%; width: 100%; } .imageBox-aiView-regenerate-send { @@ -205,7 +224,7 @@ text-align: center; align-items: center; display: flex; - max-width: 25%; + max-width: 90%; width: 100%; .imageBox-aiView-similarity { max-width: 65; @@ -223,5 +242,6 @@ text-overflow: ellipsis; max-width: 65%; width: 100%; + color: black; } } diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index caefbf542..017ef7191 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,11 +1,12 @@ +import { Button, Colors, Size, Type } from '@dash/components'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Slider, Tooltip } from '@mui/material'; import axios from 'axios'; -import { Colors, Button, Type, Size } from '@dash/components'; import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { extname } from 'path'; import * as React from 'react'; +import { AiOutlineSend } from 'react-icons/ai'; import ReactLoading from 'react-loading'; import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; @@ -16,12 +17,14 @@ import { ObjectField } from '../../../fields/ObjectField'; import { Cast, DocCast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; +import { Upload } from '../../../server/SharedMediaTypes'; import { emptyFunction } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocUtils, FollowLinkScript } from '../../documents/DocUtils'; import { Networking } from '../../Network'; import { DragManager } from '../../util/DragManager'; +import { SettingsManager } from '../../util/SettingsManager'; import { SnappingManager } from '../../util/SnappingManager'; import { undoable, undoBatch } from '../../util/UndoManager'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; @@ -32,6 +35,9 @@ import { MarqueeAnnotator } from '../MarqueeAnnotator'; import { OverlayView } from '../OverlayView'; import { AnchorMenu } from '../pdf/AnchorMenu'; import { PinDocView, PinProps } from '../PinFuncs'; +import { DrawingFillHandler } from '../smartdraw/DrawingFillHandler'; +import { FireflyImageData, isFireflyImageData } from '../smartdraw/FireflyConstants'; +import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler'; import { StickerPalette } from '../smartdraw/StickerPalette'; import { StyleProp } from '../StyleProp'; import { DocumentView } from './DocumentView'; @@ -39,12 +45,7 @@ import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; import './ImageBox.scss'; import { OpenWhere } from './OpenWhere'; -import { Upload } from '../../../server/SharedMediaTypes'; -import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler'; -import { SettingsManager } from '../../util/SettingsManager'; -import { AiOutlineSend } from 'react-icons/ai'; -import { FireflyImageData } from '../smartdraw/FireflyConstants'; -import { DrawingFillHandler } from '../smartdraw/DrawingFillHandler'; +import { RichTextField } from '../../../fields/RichTextField'; export class ImageEditorData { // eslint-disable-next-line no-use-before-define @@ -83,7 +84,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { private _disposers: { [name: string]: IReactionDisposer } = {}; private _getAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = () => undefined; private _overlayIconRef = React.createRef<HTMLDivElement>(); - private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + private _regenerateIconRef = React.createRef<HTMLDivElement>(); + private _mainCont: HTMLDivElement | null = null; private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); imageRef: HTMLImageElement | null = null; // <video> ref marqueeref = React.createRef<MarqueeAnnotator>(); @@ -108,6 +110,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } protected createDropTarget = (ele: HTMLDivElement) => { + this._mainCont = ele; this._dropDisposer?.(); ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.Document)); }; @@ -135,11 +138,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this._disposers.sizer = reaction( () => ({ forceFull: this._props.renderDepth < 1 || this.layoutDoc._showFullRes, - scrSize: (this.ScreenToLocalBoxXf().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth) * NumCast(this.layoutDoc._freeform_scale, 1), + scrSize: (NumCast(this.layoutDoc._freeform_scale, 1) / (this._props.DocumentView?.().screenToLocalScale() ?? 1)) * this._props.PanelWidth(), selected: this._props.isSelected(), }), ({ forceFull, scrSize, selected }) => { - this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 ? '_l' : '_o'; + this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 100 ? '_s' : scrSize < 400 ? '_m' : scrSize < 800 ? '_l' : '_o'; }, { fireImmediately: true, delay: 1000 } ); @@ -147,7 +150,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this._disposers.path = reaction( () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width) }), ({ nativeSize, width }) => { - if (layoutDoc === this.layoutDoc || !this.layoutDoc._height) { + if ((layoutDoc === this.layoutDoc && !this.layoutDoc._layout_nativeDimEditable) || !this.layoutDoc._height) { this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth; } }, @@ -157,8 +160,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { () => this.layoutDoc.layout_scrollTop, sTop => { this._forcedScroll = true; - !this._ignoreScroll && this._mainCont.current && (this._mainCont.current.scrollTop = NumCast(sTop)); - this._mainCont.current?.scrollTo({ top: NumCast(sTop) }); + !this._ignoreScroll && this._mainCont && (this._mainCont.scrollTop = NumCast(sTop)); + this._mainCont?.scrollTo({ top: NumCast(sTop) }); this._forcedScroll = false; }, { fireImmediately: true } @@ -195,36 +198,47 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this._searchInput = selection; }; - drop = undoable((e: Event, de: DragManager.DropEvent) => { - if (de.complete.docDragData) { - let added: boolean | undefined; - const targetIsBullseye = (ele: HTMLElement): boolean => { - if (!ele) return false; - if (ele === this._overlayIconRef.current) return true; - return targetIsBullseye(ele.parentElement as HTMLElement); - }; - if (de.metaKey || targetIsBullseye(e.target as HTMLElement)) { - added = de.complete.docDragData.droppedDocuments.reduce((last: boolean, drop: Doc) => { - this.layoutDoc[this.fieldKey + '_usePath'] = 'alternate:hover'; - return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', drop); - }, true); - } else if (de.altKey || !this.dataDoc[this.fieldKey]) { - const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; - const targetField = Doc.LayoutFieldKey(layoutDoc); - const targetDoc = layoutDoc[DocData]; - if (targetDoc[targetField] instanceof ImageField) { - added = true; - this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField); - Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc), this.fieldKey); - Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(targetDoc), this.fieldKey); + drop = undoable( + action((e: Event, de: DragManager.DropEvent) => { + if (de.complete.docDragData) { + let added: boolean | undefined; + const hitDropTarget = (ele: HTMLElement, dropTarget: HTMLDivElement | null): boolean => { + if (!ele) return false; + if (ele === dropTarget) return true; + return hitDropTarget(ele.parentElement as HTMLElement, dropTarget); + }; + if (de.metaKey || hitDropTarget(e.target as HTMLElement, this._overlayIconRef.current)) { + added = de.complete.docDragData.droppedDocuments.reduce((last: boolean, drop: Doc) => { + this.layoutDoc[this.fieldKey + '_usePath'] = 'alternate:hover'; + return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', drop); + }, true); + } else if (hitDropTarget(e.target as HTMLElement, this._regenerateIconRef.current)) { + this._regenerateLoading = true; + const drag = de.complete.docDragData.draggedDocuments.lastElement(); + const dragField = drag[Doc.LayoutFieldKey(drag)]; + const oldPrompt = StrCast(this.Document.ai_firefly_prompt, StrCast(this.Document.title)); + const newPrompt = (text: string) => (oldPrompt ? `${oldPrompt} ~~~ ${text}` : text); + DrawingFillHandler.drawingToImage(this.Document, 100, newPrompt(dragField instanceof RichTextField ? dragField.Text : ''), drag)?.then(action(() => (this._regenerateLoading = false))); + added = false; + } else if (de.altKey || !this.dataDoc[this.fieldKey]) { + const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; + const targetField = Doc.LayoutFieldKey(layoutDoc); + const targetDoc = layoutDoc[DocData]; + if (targetDoc[targetField] instanceof ImageField) { + added = true; + this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField); + Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc), this.fieldKey); + Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(targetDoc), this.fieldKey); + } } + added === false && e.preventDefault(); + added !== undefined && e.stopPropagation(); + return added; } - added === false && e.preventDefault(); - added !== undefined && e.stopPropagation(); - return added; - } - return false; - }, 'image drop'); + return false; + }), + 'image drop' + ); @undoBatch resolution = () => { @@ -315,6 +329,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { return cropping; }; + docEditorView = action(() => { + const field = Cast(this.dataDoc[this.fieldKey], ImageField); + if (field) { + ImageEditorData.Open = true; + ImageEditorData.Source = this.choosePath(field.url); + ImageEditorData.AddDoc = this._props.addDocument; + ImageEditorData.RootDoc = this.Document; + } + }); + specificContextMenu = (): void => { const field = Cast(this.dataDoc[this.fieldKey], ImageField); if (field) { @@ -343,7 +367,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const ext = extname(file); return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href), - }).then((info: Upload.ImageInformation) => { + }).then(res => { + const info = res as Upload.ImageInformation; const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title }); DocUtils.assignImageInfo(info, img); this._props.addDocTab(img, OpenWhere.addRight); @@ -352,21 +377,17 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { icon: 'expand-arrows-alt', }); funcs.push({ description: 'Copy path', event: () => ClientUtils.CopyText(this.choosePath(field.url)), icon: 'copy' }); - funcs.push({ - description: 'Open Image Editor', - event: action(() => { - ImageEditorData.Open = true; - ImageEditorData.Source = this.choosePath(field.url); - ImageEditorData.AddDoc = this._props.addDocument; - ImageEditorData.RootDoc = this.Document; - }), - icon: 'pencil-alt', - }); + funcs.push({ description: 'Open Image Editor', event: this.docEditorView, icon: 'pencil-alt' }); this.layoutDoc.ai && funcs.push({ description: 'Regenerate AI Image', - event: action(e => { - !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(e?.x || 0, e?.y || 0) : SmartDrawHandler.Instance.hideRegenerate(); + event: action(() => { + if (!SmartDrawHandler.Instance.ShowRegenerate && this.DocumentView) { + const [x, y] = this.DocumentView().screenToViewTransform().inverse().transformPoint(NumCast(this.Document.width), 0); + this._props.docViewPath().slice(-2)[0]?.ComponentView?.showSmartDraw?.(x, y, true); + } else { + SmartDrawHandler.Instance.hideRegenerate(); + } }), icon: 'pen-to-square', }); @@ -381,7 +402,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { // updateIcon = () => new Promise<void>(res => res()); updateIcon = (usePanelDimensions?: boolean) => { - const contentDiv = this._mainCont.current; + const contentDiv = this._mainCont; return !contentDiv ? new Promise<void>(res => res()) : UpdateIcon( @@ -415,6 +436,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; getScrollHeight = () => (this._props.fitWidth?.(this.Document) !== false && NumCast(this.layoutDoc._freeform_scale, 1) === NumCast(this.dataDoc._freeform_scaleMin, 1) ? this.nativeSize.nativeHeight : undefined); + @computed get usingAlternate() { + const usePath = StrCast(this.Document[this.fieldKey + '_usePath']); + return 'alternate' === usePath || ('alternate:hover' === usePath && this._isHovering) || (':hover' === usePath && !this._isHovering); + } + @computed get nativeSize() { TraceMobx(); if (this.paths.length && this.paths[0].includes('icon-hi')) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0 }; @@ -423,6 +449,20 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '_nativeOrientation'], 1); return { nativeWidth, nativeHeight, nativeOrientation }; } + private _sideBtnWidth = 35; + /** + * How much the content of the view is being scaled based on its nesting and its fit-to-width settings + */ + @computed get viewScaling() { return this.ScreenToLocalBoxXf().Scale * ( this._props.NativeDimScaling?.() || 1); } // prettier-ignore + /** + * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size. + */ + @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth, 0.5 * Math.min(NumCast(this.Document.width)))* this.viewScaling; } // prettier-ignore + /** + * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content + */ + @computed get uiBtnScaling() { return Math.min(this.maxWidgetSize / this._sideBtnWidth, 1); } // prettier-ignore + @computed get overlayImageIcon() { const usePath = this.layoutDoc[`_${this.fieldKey}_usePath`]; return ( @@ -436,10 +476,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { <span style={{ color: usePath === 'alternate' ? 'black' : undefined }}> <em>alternate, </em> </span> - and show <span style={{ color: usePath === 'alternate:hover' ? 'black' : undefined }}> <em> alternate on hover</em> </span> + and show + <span style={{ color: usePath === ':hover' ? 'black' : undefined }}> + <em> primary on hover</em> + </span> </div> }> <div @@ -447,13 +490,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { ref={this._overlayIconRef} onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => { - this.layoutDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined; + this.layoutDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : usePath === 'alternate:hover' ? ':hover' : undefined; }) } style={{ - display: (this._props.isContentActive() !== false && SnappingManager.CanEmbed) || this.dataDoc[this.fieldKey + '_alternates'] ? 'block' : 'none', - width: 'min(10%, 25px)', - height: 'min(10%, 25px)', + display: this._props.isContentActive() && (SnappingManager.CanEmbed || this.dataDoc[this.fieldKey + '_alternates']) ? 'block' : 'none', + transform: `scale(${this.uiBtnScaling})`, + width: this._sideBtnWidth, + height: this._sideBtnWidth, background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray', color: usePath === undefined ? 'black' : 'white', }}> @@ -462,6 +506,24 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { </Tooltip> ); } + @computed get regenerateImageIcon() { + return ( + <div + className="imageBox-regenerateDropTarget" + ref={this._regenerateIconRef} + onClick={() => DocumentView.showDocument(DocCast(this.Document.ai_firefly_generatedDocs), { openLocation: OpenWhere.addRight })} + style={{ + display: (this._props.isContentActive() && (SnappingManager.CanEmbed || this.Document.ai_firefly_generatedDocs)) || this._regenerateLoading ? 'block' : 'none', + transform: `scale(${this.uiBtnScaling})`, + width: this._sideBtnWidth, + height: this._sideBtnWidth, + background: 'transparent', + // color: SettingsManager.userBackgroundColor, + }}> + {this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width="100%" height="100%" /> : <FontAwesomeIcon icon="portrait" color={SettingsManager.userColor} size="lg" />} + </div> + ); + } @computed get paths() { const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey])); // retrieve the primary image URL that is being rendered from the data doc @@ -473,7 +535,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { .filter(url => url) .map(url => this.choosePath(url)) ?? []; // acc ess the primary layout data of the alternate documents const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; - return paths.length ? paths : [defaultUrl.href]; + return paths.length ? paths.reverse() : [defaultUrl.href]; } @computed get content() { @@ -498,7 +560,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { transformOrigin = 'right top'; transform = `translate(-100%, 0%) rotate(${rotation}deg) scale(${aspect})`; } - const usePath = this.layoutDoc[`_${this.fieldKey}_usePath`]; return ( <div @@ -510,7 +571,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this._isHovering = false; })} key={this.layoutDoc[Id]} - ref={this.createDropTarget} onPointerDown={this.marqueeDown}> <div className="imageBox-fader" style={{ opacity: backAlpha }}> <img @@ -518,7 +578,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { ref={action((r: HTMLImageElement | null) => (this.imageRef = r))} key="paths" src={srcpath} - style={{ transform, transformOrigin, objectFit: 'fill', height: '100%' }} + style={{ transform, transformOrigin }} onError={action(e => { this._error = e.toString(); })} @@ -526,12 +586,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { width={nativeWidth} /> {fadepath === srcpath ? null : ( - <div className={`imageBox-fadeBlocker${(this._isHovering && usePath === 'alternate:hover') || usePath === 'alternate' ? '-hover' : ''}`} style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms') }}> + <div className={`imageBox-fadeBlocker${this.usingAlternate ? '-hover' : ''}`} style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms') }}> <img alt="" className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} /> </div> )} </div> - {this.overlayImageIcon} </div> ); } @@ -567,123 +626,72 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { return ( <div className="imageBox-aiView"> <div className="imageBox-aiView-regenerate"> - <span className="imageBox-aiView-firefly">Firefly:</span> + <span className="imageBox-aiView-firefly" style={{ color: SnappingManager.userColor }}> + Firefly: + </span> <input + style={{ color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }} className="imageBox-aiView-input" aria-label="Edit instructions input" type="text" - value={this._regenInput} + value={this._regenInput || StrCast(this.Document.title)} onChange={action(e => this._canInteract && (this._regenInput = e.target.value))} placeholder={this._regenInput || StrCast(this.Document.title)} /> - <div className="imageBox-aiView-strength"> - <span className="imageBox-aiView-similarity">Similarity</span> - <Slider - className="imageBox-aiView-slider" - sx={{ - '& .MuiSlider-track': { color: SettingsManager.userVariantColor }, - '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor }, - '& .MuiSlider-thumb': { color: SettingsManager.userVariantColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } }, - }} - min={0} - max={100} - step={1} - size="small" - value={this._fireflyRefStrength} - onChange={action((e, val) => this._canInteract && (this._fireflyRefStrength = val as number))} - valueLabelDisplay="auto" - /> - </div> <div className="imageBox-aiView-regenerate-createBtn"> <Button text="Create" - type={Type.SEC} + type={Type.TERT} + color={SnappingManager.userColor} + background={SnappingManager.userBackgroundColor} // style={{ alignSelf: 'flex-end' }} icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />} iconPlacement="right" onClick={action(async () => { this._regenerateLoading = true; if (this._fireflyRefStrength) { - DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._regenInput || StrCast(this.Document.title), this.Document)?.then( - action(() => { - this._regenerateLoading = false; - }) - ); - } else + DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._regenInput || StrCast(this.Document.title), this.Document)?.then(action(() => (this._regenerateLoading = false))); + } else { SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput || StrCast(this.Document.title), true).then( action(newImgs => { - if (newImgs[0]) { - const url = newImgs[0].pathname; + const firstImg = newImgs[0]; + if (isFireflyImageData(firstImg)) { + const url = firstImg.pathname; const imgField = new ImageField(url); this._prevImgs.length === 0 && this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field.url.pathname }); - this._prevImgs.unshift({ prompt: newImgs[0].prompt, seed: newImgs[0].seed, pathname: url }); + this._prevImgs.unshift({ prompt: firstImg.prompt, seed: firstImg.seed, pathname: url }); this.dataDoc.ai_firefly_history = JSON.stringify(this._prevImgs); - this.dataDoc.ai_firefly_prompt = newImgs[0].prompt; + this.dataDoc.ai_firefly_prompt = firstImg.prompt; this.dataDoc[this.fieldKey] = imgField; this._regenerateLoading = false; this._regenInput = ''; } }) ); + } })} /> </div> </div> - <div className="imageBox-aiView-options"> - <span className="imageBox-aiView-subtitle"> More: </span> - <Button - type={Type.TERT} - text="Get Text" - icon={<FontAwesomeIcon icon="font" />} - color={SettingsManager.userBackgroundColor} - iconPlacement="right" - onClick={() => { - Networking.PostToServer('/queryFireflyImageText', { - file: (file => { - const ext = extname(file); - return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); - })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href), - }).then(text => alert(text)); - }} - /> - <Button - type={Type.TERT} - text="Generative Fill" - icon={<FontAwesomeIcon icon="fill" />} - color={SettingsManager.userBackgroundColor} - iconPlacement="right" - onClick={action(() => { - ImageEditorData.Open = true; - ImageEditorData.Source = (field && this.choosePath(field.url)) || ''; - ImageEditorData.AddDoc = this._props.addDocument; - ImageEditorData.RootDoc = this.Document; - })} - /> - <Button - type={Type.TERT} - text="Expand" - icon={<FontAwesomeIcon icon="expand" />} - color={SettingsManager.userBackgroundColor} - iconPlacement="right" - onClick={() => { - Networking.PostToServer('/expandImage', { - prompt: 'sunny skies', - file: (file => { - const ext = extname(file); - return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); - })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href), - }).then((info: Upload.ImageInformation) => { - const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title }); - DocUtils.assignImageInfo(info, img); - const genratedDocs = this.Document.generatedDocs - ? DocCast(this.Document.generatedDocs) - : Docs.Create.MasonryDocument([], { _width: 400, _height: 400, x: NumCast(this.Document.x) + NumCast(this.Document.width), y: NumCast(this.Document.y) }); - Doc.AddDocToList(genratedDocs, undefined, img); - this.Document[DocData].generatedDocs = genratedDocs; - if (!DocumentView.getFirstDocumentView(genratedDocs)) this._props.addDocTab(genratedDocs, OpenWhere.addRight); - }); + <div className="imageBox-aiView-strength"> + <span className="imageBox-aiView-similarity" style={{ color: SnappingManager.userColor }}> + Similarity + </span> + <Slider + className="imageBox-aiView-slider" + sx={{ + '& .MuiSlider-track': { color: SettingsManager.userColor }, + '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor }, + '& .MuiSlider-thumb': { color: SettingsManager.userColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } }, }} + min={0} + max={100} + step={1} + size="small" + value={this._fireflyRefStrength} + onChange={action((e, val) => this._canInteract && (this._fireflyRefStrength = val as number))} + valueLabelDisplay="auto" /> </div> </div> @@ -726,25 +734,27 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { focus = (anchor: Doc, options: FocusViewOptions) => (anchor.type === DocumentType.CONFIG ? undefined : this._ffref.current?.focus(anchor, options)); renderedPixelDimensions = async () => { - const { nativeWidth: width, nativeHeight: height } = await Networking.PostToServer('/inspectImage', { source: this.paths[0] }); + const res = await Networking.PostToServer('/inspectImage', { source: this.paths[0] }); + const { nativeWidth: width, nativeHeight: height } = res as { nativeWidth: number; nativeHeight: number }; return { width, height }; }; savedAnnotations = () => this._savedAnnotations; - render() { TraceMobx(); const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string; const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / (this._props.NativeDimScaling?.() || 1)}px` : borderRad; + const alts = DocListCast(this.dataDoc[this.fieldKey + '_alternates']); + const doc = this.usingAlternate ? (alts.lastElement() ?? this.Document) : this.Document; return ( <div className="imageBox" onContextMenu={this.specificContextMenu} - ref={this._mainCont} + ref={this.createDropTarget} onScroll={action(() => { if (!this._forcedScroll) { - if (this.layoutDoc._layout_scrollTop || this._mainCont.current?.scrollTop) { + if (this.layoutDoc._layout_scrollTop || this._mainCont?.scrollTop) { this._ignoreScroll = true; - this.layoutDoc._layout_scrollTop = this._mainCont.current?.scrollTop; + this.layoutDoc._layout_scrollTop = this._mainCont?.scrollTop; this._ignoreScroll = false; } } @@ -759,6 +769,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { <CollectionFreeFormView ref={this._ffref} {...this._props} + Document={doc} setContentViewBox={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} @@ -786,8 +797,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { <ReactLoading type="spin" height={50} width={50} color={'blue'} /> </div> ) : null} + {this.regenerateImageIcon} + {this.overlayImageIcon} {this.annotationLayer} - {!this._mainCont.current || !this.DocumentView || !this._annotationLayer.current ? null : ( + {!this._mainCont || !this.DocumentView || !this._annotationLayer.current ? null : ( <MarqueeAnnotator Document={this.Document} ref={this.marqueeref} @@ -802,7 +815,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { savedAnnotations={this.savedAnnotations} selectionText={returnEmptyString} annotationLayer={this._annotationLayer.current} - marqueeContainer={this._mainCont.current} + marqueeContainer={this._mainCont} highlightDragSrcColor="" anchorMenuCrop={this.crop} // anchorMenuFlashcard={() => this.getImageDesc()} @@ -839,5 +852,5 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { Docs.Prototypes.TemplateMap.set(DocumentType.IMG, { layout: { view: ImageBox, dataField: 'data' }, - options: { acl: '', freeform: '', systemIcon: 'BsFileEarmarkImageFill' }, + options: { acl: '', freeform: '', _layout_nativeDimEditable: true, systemIcon: 'BsFileEarmarkImageFill' }, }); diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss index a44f614b2..441fceba4 100644 --- a/src/client/views/nodes/KeyValueBox.scss +++ b/src/client/views/nodes/KeyValueBox.scss @@ -1,11 +1,11 @@ -@import '../global/globalCssVariables.module.scss'; +@use '../global/globalCssVariables.module.scss' as global; .keyValueBox-cont { overflow-y: scroll; width: 100%; height: 100%; - background-color: $white; - border: 1px solid $medium-gray; - border-radius: $border-radius; + background-color: global.$white; + border: 1px solid global.$medium-gray; + border-radius: global.$border-radius; box-sizing: border-box; display: inline-block; cursor: default; @@ -56,8 +56,8 @@ $header-height: 30px; width: 100%; position: relative; display: inline-block; - background: $medium-gray; - color: $white; + background: global.$medium-gray; + color: global.$white; text-transform: uppercase; letter-spacing: 2px; font-size: 12px; @@ -66,7 +66,7 @@ $header-height: 30px; th { font-weight: normal; &:first-child { - border-right: 1px solid $white; + border-right: 1px solid global.$white; } } } @@ -76,9 +76,9 @@ $header-height: 30px; display: flex; width: 100%; height: $header-height; - background: $white; + background: global.$white; .formattedTextBox-cont { - background: $white; + background: global.$white; } } .keyValueBox-cont { @@ -116,8 +116,8 @@ $header-height: 30px; display: flex; width: 100%; height: 30px; - background: $light-gray; + background: global.$light-gray; .formattedTextBox-cont { - background: $light-gray; + background: global.$light-gray; } } diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss index 46ea9c18e..913ab641c 100644 --- a/src/client/views/nodes/KeyValuePair.scss +++ b/src/client/views/nodes/KeyValuePair.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables.module.scss'; +@use '../global/globalCssVariables.module.scss' as global; .keyValuePair-td-key { display: inline-block; diff --git a/src/client/views/nodes/LinkDescriptionPopup.scss b/src/client/views/nodes/LinkDescriptionPopup.scss index 104301656..b44b69af5 100644 --- a/src/client/views/nodes/LinkDescriptionPopup.scss +++ b/src/client/views/nodes/LinkDescriptionPopup.scss @@ -1,12 +1,12 @@ -@import '../global/globalCssVariables.module.scss'; +@use '../global/globalCssVariables.module.scss' as global; .linkDescriptionPopup { display: flex; flex-direction: row; justify-content: center; align-items: center; - border: 2px solid $medium-blue; - background-color: $white; + border: 2px solid global.$medium-blue; + background-color: global.$white; width: auto; position: absolute; @@ -35,7 +35,7 @@ white-space: nowrap; padding: 5px; vertical-align: middle; - background-color: $close-red; + background-color: global.$close-red; border-radius: 3px; color: black; } @@ -46,7 +46,7 @@ white-space: nowrap; padding: 5px; vertical-align: middle; - background-color: $light-blue; + background-color: global.$light-blue; border-radius: 3px; color: black; } diff --git a/src/client/views/nodes/MapBox/AnimationUtility.ts b/src/client/views/nodes/MapBox/AnimationUtility.ts index f4bae66bb..a3ac68b99 100644 --- a/src/client/views/nodes/MapBox/AnimationUtility.ts +++ b/src/client/views/nodes/MapBox/AnimationUtility.ts @@ -1,11 +1,12 @@ import * as turf from '@turf/turf'; -import { Position } from '@turf/turf'; import * as d3 from 'd3'; -import { Feature, GeoJsonProperties, Geometry } from 'geojson'; -import mapboxgl, { MercatorCoordinate } from 'mapbox-gl'; +import { Feature, GeoJsonProperties, Geometry, LineString } from 'geojson'; +import { MercatorCoordinate } from 'mapbox-gl'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { MapRef } from 'react-map-gl'; +export type Position = [number, number]; + export enum AnimationStatus { START = 'start', RESUME = 'resume', @@ -23,7 +24,7 @@ export class AnimationUtility { private ROUTE_COORDINATES: Position[] = []; @observable - private PATH?: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties> = undefined; + private PATH?: Feature<LineString> = undefined; // turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties> = undefined; private PATH_DISTANCE: number = 0; private FLY_IN_START_PITCH = 40; @@ -65,7 +66,7 @@ export class AnimationUtility { const coords: mapboxgl.LngLatLike = [this.previousLngLat.lng, this.previousLngLat.lat]; // console.log('MAP REF: ', this.MAP_REF) // console.log("current elevation: ", this.MAP_REF?.queryTerrainElevation(coords)); - let altitude = this.MAP_REF ? this.MAP_REF.queryTerrainElevation(coords) ?? 0 : 0; + let altitude = this.MAP_REF ? (this.MAP_REF.queryTerrainElevation(coords) ?? 0) : 0; if (altitude === 0) { altitude += 50; } @@ -165,7 +166,8 @@ export class AnimationUtility { } @action - public setPath = (path: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties>) => { + public setPath = (path: Feature<LineString>) => { + // turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties>) => { this.PATH = path; }; @@ -178,7 +180,7 @@ export class AnimationUtility { this.ROUTE_COORDINATES = routeCoordinates; this.PATH = turf.lineString(routeCoordinates); - this.PATH_DISTANCE = turf.lineDistance(this.PATH); + this.PATH_DISTANCE = turf.length(this.PATH as Feature<LineString>); this.terrainDisplayed = terrainDisplayed; const bearing = this.calculateBearing( @@ -232,7 +234,7 @@ export class AnimationUtility { if (!this.PATH) return; // calculate the distance along the path based on the animationPhase - const alongPath = turf.along(this.PATH, this.PATH_DISTANCE * animationPhase).geometry.coordinates; + const alongPath = turf.along(this.PATH as Feature<LineString>, this.PATH_DISTANCE * animationPhase).geometry.coordinates; const lngLat = { lng: alongPath[0], diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx index cef202256..8079d96ea 100644 --- a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx @@ -1,9 +1,7 @@ -/* eslint-disable react/button-has-type */ import { IconLookup, faAdd, faArrowDown, faArrowLeft, faArrowsRotate, faBicycle, faCalendarDays, faCar, faDiamondTurnRight, faEdit, faPersonWalking, faRoute } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Autocomplete, Checkbox, FormControlLabel, TextField } from '@mui/material'; import { IconButton } from '@dash/components'; -import { Position } from 'geojson'; import { IReactionDisposer, ObservableMap, action, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -19,6 +17,8 @@ import { DocumentView } from '../DocumentView'; import './MapAnchorMenu.scss'; import { MapboxApiUtility, TransportationType } from './MapboxApiUtility'; import { MarkerIcons } from './MarkerIcons'; +import { LngLatLike } from 'mapbox-gl'; +import { Position } from './AnimationUtility'; // import { GPTPopup, GPTPopupMode } from './../../GPTPopup/GPTPopup'; type MapAnchorMenuType = 'standard' | 'routeCreation' | 'calendar' | 'customize' | 'route'; @@ -44,10 +44,9 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { // public MakeTargetToggle: () => void = unimplementedFunction; // public ShowTargetTrail: () => void = unimplementedFunction; public IsTargetToggler: () => boolean = returnFalse; - - public DisplayRoute: (routeInfoMap: Record<TransportationType, any> | undefined, type: TransportationType) => void = unimplementedFunction; - public AddNewRouteToMap: (coordinates: Position[], origin: string, destination: any, createPinForDestination: boolean) => void = unimplementedFunction; - public CreatePin: (feature: any) => void = unimplementedFunction; + public DisplayRoute: (routeInfoMap: Record<TransportationType, { coordinates: Position[] }> | undefined, type: TransportationType) => void = unimplementedFunction; + public AddNewRouteToMap: (coordinates: Position[], origin: string, destination: { place_name: string; center: number[] }, createPinForDestination: boolean) => void = unimplementedFunction; + public CreatePin: (feature: { place_name: string; center: LngLatLike; properties: { wikiData: unknown } }) => void = unimplementedFunction; public UpdateMarkerColor: (color: string) => void = unimplementedFunction; public UpdateMarkerIcon: (iconKey: string) => void = unimplementedFunction; @@ -212,19 +211,19 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { }; @observable - destinationFeatures: any[] = []; + destinationFeatures: { place_name: string; center: number[] }[] = []; @observable destinationSelected: boolean = false; @observable - selectedDestinationFeature: any = undefined; + selectedDestinationFeature?: { place_name: string; center: number[] } = undefined; @observable createPinForDestination: boolean = true; @observable - currentRouteInfoMap: Record<TransportationType, any> | undefined = undefined; + currentRouteInfoMap: Record<TransportationType, { coordinates: Position[]; duration: number; distance: number }> | undefined = undefined; @observable selectedTransportationType: TransportationType = 'driving'; @@ -238,7 +237,7 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { }; @action - handleSelectedDestinationFeature = (destinationFeature: any) => { + handleSelectedDestinationFeature = (destinationFeature?: { place_name: string; center: number[] }) => { this.selectedDestinationFeature = destinationFeature; }; @@ -258,7 +257,7 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { } }; - getRoutes = async (destinationFeature: any) => { + getRoutes = async (destinationFeature: { center: number[] }) => { const currentPinLong: number = NumCast(this.pinDoc?.longitude); const currentPinLat: number = NumCast(this.pinDoc?.latitude); @@ -280,8 +279,6 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { HandleAddRouteClick = () => { if (this.currentRouteInfoMap && this.selectedTransportationType && this.selectedDestinationFeature) { const { coordinates } = this.currentRouteInfoMap[this.selectedTransportationType]; - console.log(coordinates); - console.log(this.selectedDestinationFeature); this.AddNewRouteToMap(coordinates, this.title ?? '', this.selectedDestinationFeature, this.createPinForDestination); } }; @@ -441,27 +438,26 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { <Autocomplete fullWidth id="route-destination-searcher" - onInputChange={(e: any, searchText: any) => this.handleDestinationSearchChange(searchText)} - onChange={(e: any, feature: any, reason: any) => { + onInputChange={(e, searchText) => this.handleDestinationSearchChange(searchText)} + onChange={(e, feature: unknown, reason: unknown) => { if (reason === 'clear') { this.handleSelectedDestinationFeature(undefined); } else if (reason === 'selectOption') { - this.handleSelectedDestinationFeature(feature); + this.handleSelectedDestinationFeature(feature as { place_name: string; center: number[] }); } }} options={this.destinationFeatures.filter(feature => feature.place_name).map(feature => feature)} - getOptionLabel={(feature: any) => feature.place_name} - // eslint-disable-next-line react/jsx-props-no-spreading - renderInput={(params: any) => <TextField {...params} placeholder="Enter a destination" />} + getOptionLabel={(feature: unknown) => (feature as { place_name: string }).place_name} + renderInput={params => <TextField {...params} placeholder="Enter a destination" />} /> {!this.selectedDestinationFeature ? null - : !this.allMapPinDocs.some(pinDoc => pinDoc.title === this.selectedDestinationFeature.place_name) && ( + : !this.allMapPinDocs.some(pinDoc => pinDoc.title === this.selectedDestinationFeature?.place_name) && ( <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '5px' }}> <FormControlLabel label="Create pin for destination?" control={<Checkbox color="success" checked={this.createPinForDestination} onChange={this.toggleCreatePinForDestinationCheckbox} />} /> </div> )} - <button id="get-routes-button" disabled={!this.selectedDestinationFeature} onClick={() => this.getRoutes(this.selectedDestinationFeature)}> + <button id="get-routes-button" disabled={!this.selectedDestinationFeature} onClick={() => this.selectedDestinationFeature && this.getRoutes(this.selectedDestinationFeature)}> Get routes </button> diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss index 25b4587a5..fdd8a29d7 100644 --- a/src/client/views/nodes/MapBox/MapBox.scss +++ b/src/client/views/nodes/MapBox/MapBox.scss @@ -1,4 +1,6 @@ -@import '../../global/globalCssVariables.module.scss'; +@use 'sass:color'; +@use '../../global/globalCssVariables.module.scss' as global; + .mapBox { width: 100%; height: 100%; @@ -25,14 +27,6 @@ gap: 5px; align-items: center; width: calc(100% - 40px); - - // .editableText-container { - // width: 100%; - // font-size: 16px !important; - // } - // input { - // width: 100%; - // } } .mapbox-settings-panel { @@ -83,7 +77,7 @@ width: 100%; padding: 10px; &:hover { - background-color: lighten(rgb(187, 187, 187), 10%); + background-color: color.adjust(rgb(187, 187, 187), $lightness: 10%); } } } @@ -167,7 +161,7 @@ pointer-events: all; z-index: 1; // so it appears on top of the document's title, if shown - box-shadow: $standard-box-shadow; + box-shadow: global.$standard-box-shadow; transition: 0.2s; &:hover { diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index c4bb7c47d..541b41bf7 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -4,14 +4,13 @@ import { Checkbox, FormControlLabel, TextField } from '@mui/material'; import * as turf from '@turf/turf'; import { IconButton, Size, Type } from '@dash/components'; import * as d3 from 'd3'; -import { Feature, FeatureCollection, GeoJsonProperties, Geometry, LineString, Position } from 'geojson'; -import mapboxgl, { LngLatBoundsLike, MapLayerMouseEvent } from 'mapbox-gl'; +import { Feature, FeatureCollection, GeoJsonProperties, Geometry, LineString } from 'geojson'; +import { LngLatBoundsLike, LngLatLike, MapLayerMouseEvent } from 'mapbox-gl'; import { IReactionDisposer, ObservableMap, action, autorun, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { CirclePicker, ColorResult } from 'react-color'; import { Layer, MapProvider, MapRef, Map as MapboxMap, Marker, Source, ViewState, ViewStateChangeEvent } from 'react-map-gl'; -import { MarkerEvent } from 'react-map-gl/dist/esm/types'; import { ClientUtils, setupMoveUpEvents } from '../../../../ClientUtils'; import { emptyFunction } from '../../../../Utils'; import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc'; @@ -30,11 +29,12 @@ import { DocumentView } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; import { FocusViewOptions } from '../FocusViewOptions'; import { fastSpeedIcon, mediumSpeedIcon, slowSpeedIcon } from './AnimationSpeedIcons'; -import { AnimationSpeed, AnimationStatus, AnimationUtility } from './AnimationUtility'; +import { AnimationSpeed, AnimationStatus, AnimationUtility, Position } from './AnimationUtility'; import { MapAnchorMenu } from './MapAnchorMenu'; import './MapBox.scss'; import { MapboxApiUtility, TransportationType } from './MapboxApiUtility'; import { MarkerIcons } from './MarkerIcons'; +import { RichTextField } from '../../../../fields/RichTextField'; // import { GeocoderControl } from './GeocoderControl'; // amongus @@ -76,7 +76,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { makeObservable(this); } - @observable _featuresFromGeocodeResults: any[] = []; + @observable _featuresFromGeocodeResults: { place_name: string; center: LngLatLike | undefined }[] = []; @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @observable _selectedPinOrRoute: Doc | undefined = undefined; // The pin that is selected @observable _mapReady = false; @@ -100,7 +100,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { geometry: { type: 'LineString', coordinates: [] }, }; - @observable path: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties> = { + @observable path: Feature<LineString> = { + // turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties> = { type: 'Feature', geometry: { type: 'LineString', coordinates: [] }, properties: {}, @@ -168,7 +169,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { autorun(() => { const animationUtil = this._animationUtility; const concattedCoordinates = geometry.coordinates.concat(originalCoordinates.slice(endIndex)); - const newFeature: Feature<LineString, turf.Properties> = { + const newFeature: Feature<LineString> = { type: 'Feature', properties: {}, geometry: { @@ -445,7 +446,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { /// this should use SELECTED pushpin for lat/long if there is a selection, otherwise CENTER const anchor = Docs.Create.ConfigDocument({ title: 'MapAnchor:' + this.Document.title, - text: (StrCast(this._selectedPinOrRoute?.map) || StrCast(this.Document.map) || 'map location') as any, + text: (StrCast(this._selectedPinOrRoute?.map) || StrCast(this.Document.map) || 'map location') as unknown as RichTextField, // strings are allowed for text config_latitude: NumCast((existingPin ?? this._selectedPinOrRoute)?.latitude ?? this.dataDoc.latitude), config_longitude: NumCast((existingPin ?? this._selectedPinOrRoute)?.longitude ?? this.dataDoc.longitude), config_map_zoom: NumCast(this.dataDoc.map_zoom), @@ -464,7 +465,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { return this.Document; }; - map_docToPinMap = new Map<Doc, any>(); + map_docToPinMap = new Map<Doc, unknown>(); map_pinHighlighted = new Map<Doc, boolean>(); /* @@ -541,15 +542,17 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { * Creates Pushpin doc and adds it to the list of annotations */ @action - createPushpin = undoable((latitude: number, longitude: number, location?: string, wikiData?: string) => { + createPushpin = undoable((center: LngLatLike, location?: string, wikiData?: string) => { + const lat = 'lat' in center ? center.lat : center[0]; + const lon = 'lng' in center ? center.lng : 'lon' in center ? center.lon : center[1]; // Stores the pushpin as a MapMarkerDocument const pushpin = Docs.Create.PushpinDocument( - NumCast(latitude), - NumCast(longitude), + lat, + lon, false, [], { - title: location ?? `lat=${NumCast(latitude)},lng=${NumCast(longitude)}`, + title: location ?? `lat=${lat},lng=${lon}`, map: location, description: '', wikiData: wikiData, @@ -567,7 +570,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }, 'createpin'); @action - createMapRoute = undoable((coordinates: Position[], originName: string, destination: any, createPinForDestination: boolean) => { + createMapRoute = undoable((coordinates: Position[], originName: string, destination: { place_name: string; center: number[] }, createPinForDestination: boolean) => { if (originName !== destination.place_name) { const mapRoute = Docs.Create.MapRouteDocument(false, [], { title: `${originName} --> ${destination.place_name}`, routeCoordinates: JSON.stringify(coordinates) }); this.addDocument(mapRoute, this.annotationKey); @@ -586,23 +589,21 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }, 'createmaproute'); @action - searchbarKeyDown = (e: any) => { + searchbarKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && this._featuresFromGeocodeResults) { - const center = this._featuresFromGeocodeResults[0]?.center; + const center = this._featuresFromGeocodeResults[0]; this._featuresFromGeocodeResults = []; - setTimeout(() => center && this._mapRef.current?.flyTo({ center })); + setTimeout(() => center && this._mapRef.current?.flyTo(center)); } }; @action - addMarkerForFeature = (feature: any) => { + addMarkerForFeature = (feature: { place_name: string; center: LngLatLike | undefined; properties?: { wikiData: unknown } }) => { const location = feature.place_name; if (feature.center) { - const longitude = feature.center[0]; - const latitude = feature.center[1]; const wikiData = feature.properties?.wikiData; - this.createPushpin(latitude, longitude, location, wikiData); + this.createPushpin(feature.center, location, wikiData); if (this._mapRef.current) { this._mapRef.current.flyTo({ @@ -727,7 +728,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; @action - handleMarkerClick = (e: MarkerEvent<mapboxgl.Marker, MouseEvent>, pinDoc: Doc) => { + handleMarkerClick = (clientX: number, clientY: number, pinDoc: Doc) => { this._featuresFromGeocodeResults = []; this.deselectPinOrRoute(); // TODO: check this method this._selectedPinOrRoute = pinDoc; @@ -758,7 +759,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { // MapAnchorMenu.Instance.jumpTo(NumCast(pinDoc.longitude), NumCast(pinDoc.latitude)-3, true); - MapAnchorMenu.Instance.jumpTo(e.originalEvent.clientX, e.originalEvent.clientY, true); + MapAnchorMenu.Instance.jumpTo(clientX, clientY, true); document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true); @@ -768,7 +769,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; @action - displayRoute = (routeInfoMap: Record<TransportationType, any> | undefined, type: TransportationType) => { + displayRoute = (routeInfoMap: Record<TransportationType, { coordinates: Position[] }> | undefined, type: TransportationType) => { if (routeInfoMap) { const newTempRouteSource: FeatureCollection = { type: 'FeatureCollection', @@ -1052,7 +1053,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { <div id="divider">|</div> <div style={{ display: 'flex', alignItems: 'center' }}> <div>Select Line Color: </div> - <CirclePicker circleSize={12} circleSpacing={5} width="100%" colors={['#ffff00', '#03a9f4', '#ff0000', '#ff5722', '#000000', '#673ab7']} onChange={(color: any) => this.setAnimationLineColor(color)} /> + <CirclePicker circleSize={12} circleSpacing={5} width="100%" colors={['#ffff00', '#03a9f4', '#ff0000', '#ff5722', '#000000', '#673ab7']} onChange={color => this.setAnimationLineColor(color)} /> </div> </div> </> @@ -1147,7 +1148,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { return MarkerIcons.getFontAwesomeIcon(markerType, '2x', markerColor) ?? null; }; - _textRef = React.createRef<any>(); render() { const scale = this._props.NativeDimScaling?.() || 1; const parscale = scale === 1 ? 1 : (this.ScreenToLocalBoxXf().Scale ?? 1); @@ -1161,7 +1161,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { style={{ transformOrigin: 'top left', transform: `scale(${scale})`, width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> {!this._routeToAnimate && ( <div className="mapBox-searchbar" style={{ width: `${100 / scale}%`, zIndex: 1, position: 'relative', background: 'lightGray' }}> - <TextField ref={this._textRef} fullWidth placeholder="Enter a location" onKeyDown={this.searchbarKeyDown} onChange={(e: any) => this.handleSearchChange(e.target.value)} /> + <TextField fullWidth placeholder="Enter a location" onKeyDown={this.searchbarKeyDown} onChange={e => this.handleSearchChange(e.target.value)} /> <IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={() => this.toggleSettings()} /> <div style={{ opacity: 0 }}> <IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={() => this.toggleSettings()} /> @@ -1217,7 +1217,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { .filter(feature => feature.place_name) .map((feature, idx) => ( <div - // eslint-disable-next-line react/no-array-index-key key={idx} className="search-result-container" onClick={() => { @@ -1321,8 +1320,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this._animationPhase === 0 && this.allPushpins // .filter(anno => !anno.layout_unrendered) .map((pushpin, idx) => ( - // eslint-disable-next-line react/no-array-index-key - <Marker key={idx} longitude={NumCast(pushpin.longitude)} latitude={NumCast(pushpin.latitude)} anchor="bottom" onClick={(e: MarkerEvent<mapboxgl.Marker, MouseEvent>) => this.handleMarkerClick(e, pushpin)}> + <Marker key={idx} longitude={NumCast(pushpin.longitude)} latitude={NumCast(pushpin.latitude)} anchor="bottom" onClick={e => this.handleMarkerClick(e.originalEvent.clientX, e.originalEvent.clientY, pushpin)}> {this.getMarkerIcon(pushpin)} </Marker> ))} @@ -1336,7 +1334,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { <div className="mapBox-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> <SidebarAnnos ref={this._sidebarRef} - // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} fieldKey={this.fieldKey} Document={this.Document} diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index f6908d5fd..f2160feb7 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables.module.scss'; +@use '../global/globalCssVariables.module.scss' as global; .pdfBox, .pdfBox-interactive { @@ -22,11 +22,11 @@ // glr: This should really be the same component as text and PDFs .pdfBox-sidebarBtn { - background: $black; + background: global.$black; height: 25px; width: 25px; right: 5px; - color: $white; + color: global.$white; display: flex; position: absolute; align-items: center; @@ -35,7 +35,7 @@ pointer-events: all; z-index: 1; // so it appears on top of the document's title, if shown - box-shadow: $standard-box-shadow; + box-shadow: global.$standard-box-shadow; transition: 0.2s; &:hover { diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index 37ffca2d6..e7a6193d4 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react/button-has-type */ import * as React from 'react'; import { useEffect, useRef, useState } from 'react'; import { IconContext } from 'react-icons'; @@ -72,7 +71,7 @@ export function RecordingView(props: IRecordingViewProps) { const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles.map(file => ({ file })))).map(res => (res.result instanceof Error ? '' : res.result.accessPaths.agnostic.server)); // concat the segments together using post call - const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths); + const result = (await Networking.PostToServer('/concatVideos', serverPaths)) as Upload.AccessPathInfo | Error; !(result instanceof Error) ? props.setResult(result, concatPres || undefined) : console.error('video conversion failed'); })(); } diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index 460155446..b5405f0fb 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables.module.scss'; +@use '../global/globalCssVariables.module.scss' as global; .mini-viewer { cursor: grab; @@ -22,7 +22,7 @@ height: 100%; border-radius: inherit; opacity: 0.99; // hack! overcomes some kind of Chrome weirdness where buttons (e.g., snapshot) disappear at some point as the video is resized larger - background: $dark-gray; + background: global.$dark-gray; } .inkingCanvas-paths-markers { @@ -93,7 +93,7 @@ align-items: center; justify-content: center; display: flex; - background-color: $dark-gray; + background-color: global.$dark-gray; color: white; border-radius: 100px; height: 40px; @@ -128,13 +128,13 @@ width: 25px; height: 25px; border-radius: 50%; - background: $dark-gray; + background: global.$dark-gray; display: flex; align-items: center; justify-content: center; &:hover { - background: $black; + background: global.$black; } svg { @@ -157,7 +157,7 @@ cursor: pointer; &:hover { - background-color: $medium-gray; + background-color: global.$medium-gray; } } @@ -198,7 +198,7 @@ input[type='range']::-webkit-slider-runnable-track { height: 10px; cursor: pointer; box-shadow: 0; - background: $light-gray; + background: global.$light-gray; border-radius: 10px; } @@ -208,7 +208,7 @@ input[type='range']::-webkit-slider-thumb { height: 12px; width: 12px; border-radius: 10px; - background: $medium-blue; + background: global.$medium-blue; cursor: pointer; -webkit-appearance: none; margin-top: -1px; diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index a1686adaf..05d5babf9 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables.module.scss'; +@use '../global/globalCssVariables.module.scss' as global; .webBox { height: 100%; @@ -120,7 +120,7 @@ pointer-events: all; z-index: 1; // so it appears on top of the document's title, if shown - box-shadow: $standard-box-shadow; + box-shadow: global.$standard-box-shadow; transition: 0.2s; &:hover { diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 6026d9ca7..e7a10cc29 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -383,7 +383,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this._textAnnotationCreator = () => this.createTextAnnotation(sel, !sel.isCollapsed ? sel.getRangeAt(0) : undefined); AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._layout_scrollTop) * scale); // Changing which document to add the annotation to (the currently selected WebBox) - GPTPopup.Instance.setSidebarId(`${this._props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`); + GPTPopup.Instance.setSidebarFieldKey(`${this._props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`); GPTPopup.Instance.addDoc = this.sidebarAddDocument; } } else { @@ -446,7 +446,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this._textAnnotationCreator = () => this.createTextAnnotation(sel, selRange); (!sel.isCollapsed || this.marqueeing) && AnchorMenu.Instance.jumpTo(e.clientX, e.clientY); // Changing which document to add the annotation to (the currently selected WebBox) - GPTPopup.Instance.setSidebarId(`${this._props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`); + GPTPopup.Instance.setSidebarFieldKey(`${this._props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`); GPTPopup.Instance.addDoc = this.sidebarAddDocument; } }; diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx index d38cb5423..009eb82cd 100644 --- a/src/client/views/nodes/calendarBox/CalendarBox.tsx +++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx @@ -1,4 +1,4 @@ -import { Calendar, EventClickArg, EventSourceInput } from '@fullcalendar/core'; +import { Calendar, EventClickArg, EventDropArg, EventSourceInput } from '@fullcalendar/core'; import dayGridPlugin from '@fullcalendar/daygrid'; import multiMonthPlugin from '@fullcalendar/multimonth'; import timeGrid from '@fullcalendar/timegrid'; @@ -17,6 +17,7 @@ import { DocumentView } from '../DocumentView'; import { OpenWhere } from '../OpenWhere'; import { DragManager } from '../../../util/DragManager'; import { DocData } from '../../../../fields/DocSymbols'; +import { ContextMenu } from '../../ContextMenu'; type CalendarView = 'multiMonth' | 'dayGridMonth' | 'timeGridWeek' | 'timeGridDay'; @@ -104,32 +105,44 @@ export class CalendarBox extends CollectionSubView() { } // TODO: Return a different color based on the event type - eventToColor(event: Doc): string { + eventToColor = (event: Doc): string => { return 'red'; - } + }; - internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData) { + internalDocDrop = (e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData) => { if (!super.onInternalDrop(e, de)) return false; de.complete.docDragData?.droppedDocuments.forEach(doc => { const today = new Date().toISOString(); if (!doc.date_range) doc[DocData].date_range = `${today}|${today}`; }); return true; - } + }; onInternalDrop = (e: Event, de: DragManager.DropEvent): boolean => { if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData); return false; }; + handleEventDrop = (arg: EventDropArg) => { + const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? ''); + doc && arg.event.start && (doc.date_range = arg.event.start?.toString() + '|' + (arg.event.end ?? arg.event.start).toString()); + }; + handleEventClick = (arg: EventClickArg) => { const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? ''); - DocumentView.DeselectAll(); if (doc) { DocumentView.showDocument(doc, { openLocation: OpenWhere.lightboxAlways }); arg.jsEvent.stopPropagation(); } }; + handleEventContextMenu = (pageX: number, pageY: number, docid: string) => { + const doc = DocServer.GetCachedRefField(docid ?? ''); + if (doc) { + const cm = ContextMenu.Instance; + cm.addItem({ description: 'Show Metadata', event: () => this._props.addDocTab(doc, OpenWhere.addRightKeyvalue), icon: 'table-columns' }); + cm.displayMenu(pageX - 15, pageY - 15, undefined, undefined); + } + }; // https://fullcalendar.io renderCalendar = () => { @@ -157,6 +170,25 @@ export class CalendarBox extends CollectionSubView() { aspectRatio: NumCast(this.Document.width) / NumCast(this.Document.height), events: this.calendarEvents, eventClick: this.handleEventClick, + eventDrop: this.handleEventDrop, + eventDidMount: arg => { + arg.el.addEventListener('pointerdown', ev => { + ev.button && ev.stopPropagation(); + }); + if (navigator.userAgent.includes('Macintosh')) { + arg.el.addEventListener('pointerup', ev => { + ev.button && ev.stopPropagation(); + ev.button && this.handleEventContextMenu(ev.pageX, ev.pageY, arg.event._def.groupId); + }); + } + arg.el.addEventListener('contextmenu', ev => { + if (!navigator.userAgent.includes('Macintosh')) { + this.handleEventContextMenu(ev.pageX, ev.pageY, arg.event._def.groupId); + } + ev.stopPropagation(); + ev.preventDefault(); + }); + }, })); cal?.render(); setTimeout(() => cal?.view.calendar.select(this.dateSelect.start, this.dateSelect.end)); diff --git a/src/client/views/nodes/chatbot/agentsystem/Agent.ts b/src/client/views/nodes/chatbot/agentsystem/Agent.ts index b2b0c9aea..e93fb87db 100644 --- a/src/client/views/nodes/chatbot/agentsystem/Agent.ts +++ b/src/client/views/nodes/chatbot/agentsystem/Agent.ts @@ -22,6 +22,8 @@ import { ChatCompletionMessageParam } from 'openai/resources'; import { Doc } from '../../../../../fields/Doc'; import { parsedDoc } from '../chatboxcomponents/ChatBox'; import { WebsiteInfoScraperTool } from '../tools/WebsiteInfoScraperTool'; +import { Upload } from '../../../../../server/SharedMediaTypes'; +import { RAGTool } from '../tools/RAGTool'; //import { CreateTextDocTool } from '../tools/CreateTextDocumentTool'; dotenv.config(); @@ -61,7 +63,7 @@ export class Agent { history: () => string, csvData: () => { filename: string; id: string; text: string }[], addLinkedUrlDoc: (url: string, id: string) => void, - createImage: (result: any, options: DocumentOptions) => void, + createImage: (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => void, addLinkedDoc: (doc: parsedDoc) => Doc | undefined, // eslint-disable-next-line @typescript-eslint/no-unused-vars createCSVInDash: (url: string, title: string, id: string, data: string) => void @@ -76,7 +78,7 @@ export class Agent { // Define available tools for the assistant this.tools = { calculate: new CalculateTool(), - // rag: new RAGTool(this.vectorstore), + rag: new RAGTool(this.vectorstore), dataAnalysis: new DataAnalysisTool(csvData), websiteInfoScraper: new WebsiteInfoScraperTool(addLinkedUrlDoc), searchTool: new SearchTool(addLinkedUrlDoc), diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss index 9cf760a12..3d27fa887 100644 --- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss +++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss @@ -1,3 +1,4 @@ +@use 'sass:color'; @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap'); $primary-color: #3f51b5; @@ -68,7 +69,7 @@ $transition: all 0.2s ease-in-out; &:focus { outline: none; border-color: $primary-color; - box-shadow: 0 0 0 2px rgba($primary-color, 0.2); + box-shadow: 0 0 0 2px color.adjust($primary-color, $alpha: -0.8); } &:disabled { @@ -92,11 +93,11 @@ $transition: all 0.2s ease-in-out; transition: $transition; &:hover { - background-color: darken($primary-color, 10%); + background-color: color.adjust($primary-color, $lightness: -10%); } &:disabled { - background-color: lighten($primary-color, 20%); + background-color: color.adjust($primary-color, $lightness: 20%); cursor: not-allowed; } @@ -178,7 +179,7 @@ $transition: all 0.2s ease-in-out; margin-bottom: 16px; &:hover { - background-color: rgba($primary-color, 0.1); + background-color: color.adjust($primary-color, $alpha: -0.9); } } @@ -220,7 +221,7 @@ $transition: all 0.2s ease-in-out; transition: $transition; &:hover { - background-color: rgba($primary-color, 0.2); + background-color: color.adjust($primary-color, $alpha: -0.8); color: #fff; } } diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx index 16da360fc..6e9307d37 100644 --- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx +++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx @@ -42,6 +42,7 @@ import './ChatBox.scss'; import MessageComponentBox from './MessageComponent'; import { ProgressBar } from './ProgressBar'; import { OpenWhere } from '../../OpenWhere'; +import { Upload } from '../../../../../server/SharedMediaTypes'; dotenv.config(); @@ -412,7 +413,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }); @action - createImageInDash = async (result: any, options: DocumentOptions) => { + createImageInDash = async (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => { const newImgSrc = result.accessPaths.agnostic.client.indexOf('dashblobstore') === -1 // ? ClientUtils.prepend(result.accessPaths.agnostic.client) diff --git a/src/client/views/nodes/chatbot/tools/CreateAnyDocTool.ts b/src/client/views/nodes/chatbot/tools/CreateAnyDocTool.ts index ef4bbbc47..754d230c8 100644 --- a/src/client/views/nodes/chatbot/tools/CreateAnyDocTool.ts +++ b/src/client/views/nodes/chatbot/tools/CreateAnyDocTool.ts @@ -13,63 +13,63 @@ const standardOptions = ['title', 'backgroundColor']; * Description of document options and data field for each type. */ const documentTypesInfo: { [key in supportedDocTypes]: { options: string[]; dataDescription: string } } = { - [supportedDocumentTypes.flashcard]: { + [supportedDocTypes.flashcard]: { options: [...standardOptions, 'fontColor', 'text_align'], dataDescription: 'an array of two strings. the first string contains a question, and the second string contains an answer', }, - [supportedDocumentTypes.text]: { + [supportedDocTypes.text]: { options: [...standardOptions, 'fontColor', 'text_align'], dataDescription: 'The text content of the document.', }, - [supportedDocumentTypes.html]: { + [supportedDocTypes.html]: { options: [], dataDescription: 'The HTML-formatted text content of the document.', }, - [supportedDocumentTypes.equation]: { + [supportedDocTypes.equation]: { options: [...standardOptions, 'fontColor'], dataDescription: 'The equation content as a string.', }, - [supportedDocumentTypes.functionplot]: { + [supportedDocTypes.functionplot]: { options: [...standardOptions, 'function_definition'], dataDescription: 'The function definition(s) for plotting. Provide as a string or array of function definitions.', }, - [supportedDocumentTypes.dataviz]: { + [supportedDocTypes.dataviz]: { options: [...standardOptions, 'chartType'], dataDescription: 'A string of comma-separated values representing the CSV data.', }, - [supportedDocumentTypes.notetaking]: { + [supportedDocTypes.notetaking]: { options: standardOptions, dataDescription: 'The initial content or structure for note-taking.', }, - [supportedDocumentTypes.rtf]: { + [supportedDocTypes.rtf]: { options: standardOptions, dataDescription: 'The rich text content in RTF format.', }, - [supportedDocumentTypes.image]: { + [supportedDocTypes.image]: { options: standardOptions, dataDescription: 'The image content as an image file URL.', }, - [supportedDocumentTypes.pdf]: { + [supportedDocTypes.pdf]: { options: standardOptions, dataDescription: 'the pdf content as a PDF file url.', }, - [supportedDocumentTypes.audio]: { + [supportedDocTypes.audio]: { options: standardOptions, dataDescription: 'The audio content as a file url.', }, - [supportedDocumentTypes.video]: { + [supportedDocTypes.video]: { options: standardOptions, dataDescription: 'The video content as a file url.', }, - [supportedDocumentTypes.message]: { + [supportedDocTypes.message]: { options: standardOptions, dataDescription: 'The message content of the document.', }, - [supportedDocumentTypes.diagram]: { + [supportedDocTypes.diagram]: { options: ['title', 'backgroundColor'], dataDescription: 'diagram content as a text string in Mermaid format.', }, - [supportedDocumentTypes.script]: { + [supportedDocTypes.script]: { options: ['title', 'backgroundColor'], dataDescription: 'The compilable JavaScript code. Use this for creating scripts.', }, diff --git a/src/client/views/nodes/chatbot/tools/CreateCSVTool.ts b/src/client/views/nodes/chatbot/tools/CreateCSVTool.ts index e8ef3fbfe..290c48d6c 100644 --- a/src/client/views/nodes/chatbot/tools/CreateCSVTool.ts +++ b/src/client/views/nodes/chatbot/tools/CreateCSVTool.ts @@ -38,10 +38,10 @@ export class CreateCSVTool extends BaseTool<CreateCSVToolParamsType> { async execute(args: ParametersType<CreateCSVToolParamsType>): Promise<Observation[]> { try { console.log('Creating CSV file:', args.filename, ' with data:', args.csvData); - const { fileUrl, id } = await Networking.PostToServer('/createCSV', { + const { fileUrl, id } = (await Networking.PostToServer('/createCSV', { filename: args.filename, data: args.csvData, - }); + })) as { fileUrl: string; id: string }; this._handleCSVResult(fileUrl, args.filename, id, args.csvData); diff --git a/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts b/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts index 6dc36b0d1..284879a4a 100644 --- a/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts +++ b/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts @@ -263,79 +263,79 @@ const standardOptions = ['title', 'backgroundColor']; * Description of document options and data field for each type. */ const documentTypesInfo: { [key in supportedDocTypes]: { options: string[]; dataDescription: string } } = { - [supportedDocTypes.comparison]: { + comparison: { options: [...standardOptions, 'fontColor', 'text_align'], dataDescription: 'an array of two documents of any kind that can be compared.', }, - [supportedDocTypes.deck]: { + deck: { options: [...standardOptions, 'fontColor', 'text_align'], dataDescription: 'an array of flashcard docs', }, - [supportedDocTypes.flashcard]: { + flashcard: { options: [...standardOptions, 'fontColor', 'text_align'], dataDescription: 'an array of two strings. the first string contains a question, and the second string contains an answer', }, - [supportedDocTypes.text]: { + text: { options: [...standardOptions, 'fontColor', 'text_align'], dataDescription: 'The text content of the document.', }, - [supportedDocTypes.web]: { + web: { options: [], dataDescription: 'A URL to a webpage. Example: https://en.wikipedia.org/wiki/Brown_University', }, - [supportedDocTypes.html]: { + html: { options: [], dataDescription: 'The HTML-formatted text content of the document.', }, - [supportedDocTypes.equation]: { + equation: { options: [...standardOptions, 'fontColor'], dataDescription: 'The equation content represented as a MathML string.', }, - [supportedDocTypes.functionplot]: { + functionplot: { options: [...standardOptions, 'function_definition'], dataDescription: 'The function definition(s) for plotting. Provide as a string or array of function definitions.', }, - [supportedDocTypes.dataviz]: { + dataviz: { options: [...standardOptions, 'chartType'], dataDescription: 'A string of comma-separated values representing the CSV data.', }, - [supportedDocTypes.notetaking]: { + notetaking: { options: standardOptions, dataDescription: 'An array of related text documents with small amounts of text.', }, - [supportedDocTypes.rtf]: { + rtf: { options: standardOptions, dataDescription: 'The rich text content in RTF format.', }, - [supportedDocTypes.image]: { + image: { options: standardOptions, dataDescription: `A url string that must end with '.png', '.jpeg', '.gif', or '.jpg'`, }, - [supportedDocTypes.pdf]: { + pdf: { options: standardOptions, dataDescription: 'the pdf content as a PDF file url.', }, - [supportedDocTypes.audio]: { + audio: { options: standardOptions, dataDescription: 'The audio content as a file url.', }, - [supportedDocTypes.video]: { + video: { options: standardOptions, dataDescription: 'The video content as a file url.', }, - [supportedDocTypes.message]: { + message: { options: standardOptions, dataDescription: 'The message content of the document.', }, - [supportedDocTypes.diagram]: { + diagram: { options: standardOptions, dataDescription: 'diagram content as a text string in Mermaid format.', }, - [supportedDocTypes.script]: { + script: { options: standardOptions, dataDescription: 'The compilable JavaScript code. Use this for creating scripts.', }, - [supportedDocTypes.collection]: { + collection: { options: [...standardOptions, 'type_collection'], dataDescription: 'A collection of Docs represented as an array.', }, diff --git a/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts b/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts index 177552c5c..37907fd4f 100644 --- a/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts +++ b/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts @@ -1,10 +1,11 @@ -import { v4 as uuidv4 } from 'uuid'; import { RTFCast } from '../../../../../fields/Types'; import { DocumentOptions } from '../../../../documents/Documents'; import { Networking } from '../../../../Network'; import { ParametersType, ToolInfo } from '../types/tool_types'; import { Observation } from '../types/types'; import { BaseTool } from './BaseTool'; +import { Upload } from '../../../../../server/SharedMediaTypes'; +import { List } from '../../../../../fields/List'; const imageCreationToolParams = [ { @@ -25,8 +26,8 @@ const imageCreationToolInfo: ToolInfo<ImageCreationToolParamsType> = { }; export class ImageCreationTool extends BaseTool<ImageCreationToolParamsType> { - private _createImage: (result: any, options: DocumentOptions) => void; - constructor(createImage: (result: any, options: DocumentOptions) => void) { + private _createImage: (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => void; + constructor(createImage: (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => void) { super(imageCreationToolInfo); this._createImage = createImage; } @@ -37,28 +38,24 @@ export class ImageCreationTool extends BaseTool<ImageCreationToolParamsType> { console.log(`Generating image for prompt: ${image_prompt}`); // Create an array of promises, each one handling a search for a query try { - const { result, url } = await Networking.PostToServer('/generateImage', { + const { result, url } = (await Networking.PostToServer('/generateImage', { image_prompt, - }); + })) as { result: Upload.FileInformation & Upload.InspectionResults; url: string }; console.log('Image generation result:', result); - this._createImage(result, { text: RTFCast(image_prompt) }); - if (url) { - const id = uuidv4(); - - return [ - { - type: 'image_url', - image_url: { url }, - }, - ]; - } else { - return [ - { - type: 'text', - text: `An error occurred while generating image.`, - }, - ]; - } + this._createImage(result, { text: RTFCast(image_prompt), ai: 'dall-e-3', tags: new List<string>(['@ai']) }); + return url + ? [ + { + type: 'image_url', + image_url: { url }, + }, + ] + : [ + { + type: 'text', + text: `An error occurred while generating image.`, + }, + ]; } catch (error) { console.log(error); return [ diff --git a/src/client/views/nodes/chatbot/tools/RAGTool.ts b/src/client/views/nodes/chatbot/tools/RAGTool.ts index 2db61c768..ef374ed22 100644 --- a/src/client/views/nodes/chatbot/tools/RAGTool.ts +++ b/src/client/views/nodes/chatbot/tools/RAGTool.ts @@ -75,7 +75,7 @@ export class RAGTool extends BaseTool<RAGToolParamsType> { async getFormattedChunks(relevantChunks: RAGChunk[]): Promise<Observation[]> { try { - const { formattedChunks } = await Networking.PostToServer('/formatChunks', { relevantChunks }); + const { formattedChunks } = await Networking.PostToServer('/formatChunks', { relevantChunks }) as { formattedChunks: Observation[]} if (!formattedChunks) { throw new Error('Failed to format chunks'); diff --git a/src/client/views/nodes/chatbot/tools/SearchTool.ts b/src/client/views/nodes/chatbot/tools/SearchTool.ts index 5fc6ab768..6a11407a5 100644 --- a/src/client/views/nodes/chatbot/tools/SearchTool.ts +++ b/src/client/views/nodes/chatbot/tools/SearchTool.ts @@ -41,15 +41,15 @@ export class SearchTool extends BaseTool<SearchToolParamsType> { // Create an array of promises, each one handling a search for a query const searchPromises = queries.map(async query => { try { - const { results } = await Networking.PostToServer('/getWebSearchResults', { + const { results } = (await Networking.PostToServer('/getWebSearchResults', { query, max_results: this._max_results, - }); + })) as { results: { url: string; snippet: string }[] }; const data = results.map((result: { url: string; snippet: string }) => { const id = uuidv4(); this._addLinkedUrlDoc(result.url, id); return { - type: 'text', + type: 'text' as const, text: `<chunk chunk_id="${id}" chunk_type="url"><url>${result.url}</url><overview>${result.snippet}</overview></chunk>`, }; }); @@ -58,7 +58,7 @@ export class SearchTool extends BaseTool<SearchToolParamsType> { console.log(error); return [ { - type: 'text', + type: 'text' as const, text: `An error occurred while performing the web search for query: ${query}`, }, ]; diff --git a/src/client/views/nodes/chatbot/types/types.ts b/src/client/views/nodes/chatbot/types/types.ts index 995ac531d..882e74ebb 100644 --- a/src/client/views/nodes/chatbot/types/types.ts +++ b/src/client/views/nodes/chatbot/types/types.ts @@ -1,6 +1,3 @@ -import { indexes } from 'd3'; -import { AnyLayer } from 'react-map-gl'; - export enum ASSISTANT_ROLE { USER = 'user', ASSISTANT = 'assistant', @@ -122,9 +119,8 @@ export interface AI_Document { type: string; } +export type Observation = { type: 'text'; text: string } | { type: 'image_url'; image_url: { url: string } }; export interface AgentMessage { role: 'system' | 'user' | 'assistant'; content: string | Observation[]; } - -export type Observation = { type: 'text'; text: string } | { type: 'image_url'; image_url: { url: string } }; diff --git a/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts b/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts index ef24e59bc..afd34f28d 100644 --- a/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts +++ b/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts @@ -1,13 +1,11 @@ /** * @file Vectorstore.ts - * @description This file defines the Vectorstore class, which integrates with Pinecone for vector-based document indexing and Cohere for text embeddings. + * @description This file defines the Vectorstore class, which integrates with Pinecone for vector-based document indexing and OpenAI text-embedding-3-large for text embeddings. * It manages AI document handling, including adding documents, processing media files, combining document chunks, indexing documents, * and retrieving relevant sections based on user queries. */ import { Index, IndexList, Pinecone, PineconeRecord, QueryResponse, RecordMetadata } from '@pinecone-database/pinecone'; -import { CohereClient } from 'cohere-ai'; -import { EmbedResponse } from 'cohere-ai/api'; import dotenv from 'dotenv'; import path from 'path'; import { v4 as uuidv4 } from 'uuid'; @@ -15,17 +13,20 @@ import { Doc } from '../../../../../fields/Doc'; import { AudioCast, CsvCast, PDFCast, StrCast, VideoCast } from '../../../../../fields/Types'; import { Networking } from '../../../../Network'; import { AI_Document, CHUNK_TYPE, RAGChunk } from '../types/types'; +import OpenAI from 'openai'; +import { Embedding } from 'openai/resources'; +import { PineconeEnvironmentVarsNotSupportedError } from '@pinecone-database/pinecone/dist/errors'; dotenv.config(); /** * The Vectorstore class integrates with Pinecone for vector-based document indexing and retrieval, - * and Cohere for text embedding. It handles AI document management, uploads, and query-based retrieval. + * and OpenAI text-embedding-3-large for text embedding. It handles AI document management, uploads, and query-based retrieval. */ export class Vectorstore { private pinecone: Pinecone; // Pinecone client for managing the vector index. private index!: Index; // The specific Pinecone index used for document chunks. - private cohere: CohereClient; // Cohere client for generating embeddings. + private openai: OpenAI; // OpenAI client for generating embeddings. private indexName: string = 'pdf-chatbot'; // Default name for the index. private _id: string; // Unique ID for the Vectorstore instance. private _doc_ids: () => string[]; // List of document IDs handled by this instance. @@ -33,20 +34,20 @@ export class Vectorstore { documents: AI_Document[] = []; // Store the documents indexed in the vectorstore. /** - * Initializes the Pinecone and Cohere clients, sets up the document ID list, + * Initializes the Pinecone and OpenAI clients, sets up the document ID list, * and initializes the Pinecone index. * @param id The unique identifier for the vectorstore instance. * @param doc_ids A function that returns a list of document IDs. */ constructor(id: string, doc_ids: () => string[]) { - const pineconeApiKey = '51738e9a-bea2-4c11-b6bf-48a825e774dc'; + const pineconeApiKey = process.env.PINECONE_API_KEY; if (!pineconeApiKey) { throw new Error('PINECONE_API_KEY is not defined.'); } - // Initialize Pinecone and Cohere clients with API keys from the environment. + // Initialize Pinecone and OpenAI clients with API keys from the environment. this.pinecone = new Pinecone({ apiKey: pineconeApiKey }); - // this.cohere = new CohereClient({ token: process.env.COHERE_API_KEY }); + this.openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, dangerouslyAllowBrowser: true }); this._id = id; this._doc_ids = doc_ids; this.initializeIndex(); @@ -63,7 +64,7 @@ export class Vectorstore { if (!indexList.indexes?.some(index => index.name === this.indexName)) { await this.pinecone.createIndex({ name: this.indexName, - dimension: 1024, + dimension: 3072, metric: 'cosine', spec: { serverless: { @@ -119,23 +120,12 @@ export class Vectorstore { const texts = segmentedTranscript.map((chunk: any) => chunk.text); try { - const embeddingsResponse = await this.cohere.v2.embed({ - model: 'embed-english-v3.0', - inputType: 'classification', - embeddingTypes: ['float'], // Specify that embeddings should be floats - texts, // Pass the array of chunk texts + const embeddingsResponse = await this.openai.embeddings.create({ + model: 'text-embedding-3-large', + input: texts, + encoding_format: 'float', }); - if (!embeddingsResponse.embeddings.float || embeddingsResponse.embeddings.float.length !== texts.length) { - throw new Error('Mismatch between embeddings and the number of chunks'); - } - - // Assign embeddings to each chunk - segmentedTranscript.forEach((chunk: any, index: number) => { - if (!embeddingsResponse.embeddings || !embeddingsResponse.embeddings.float) { - throw new Error('Invalid embeddings response'); - } - }); doc.original_segments = JSON.stringify(response.full); doc.ai_type = local_file_path.endsWith('.mp3') ? 'audio' : 'video'; const doc_id = uuidv4(); @@ -149,7 +139,7 @@ export class Vectorstore { summary: '', chunks: segmentedTranscript.map((chunk: any, index: number) => ({ id: uuidv4(), - values: (embeddingsResponse.embeddings.float as number[][])[index], // Assign embedding + values: (embeddingsResponse.data as Embedding[])[index].embedding, // Assign embedding metadata: { indexes: chunk.indexes, original_document: local_file_path, @@ -291,7 +281,7 @@ export class Vectorstore { /** * Retrieves the most relevant document chunks for a given query. - * Uses Cohere for embedding the query and Pinecone for vector similarity matching. + * Uses OpenAI for embedding the query and Pinecone for vector similarity matching. * @param query The search query string. * @param topK The number of top results to return (default is 10). * @returns A list of document chunks that match the query. @@ -299,27 +289,17 @@ export class Vectorstore { async retrieve(query: string, topK: number = 10): Promise<RAGChunk[]> { console.log(`Retrieving chunks for query: ${query}`); try { - // Generate an embedding for the query using Cohere. - const queryEmbeddingResponse: EmbedResponse = await this.cohere.embed({ - texts: [query], - model: 'embed-english-v3.0', - inputType: 'search_query', + // Generate an embedding for the query using OpenAI. + const queryEmbeddingResponse = await this.openai.embeddings.create({ + model: 'text-embedding-3-large', + input: query, + encoding_format: 'float', }); - let queryEmbedding: number[]; + let queryEmbedding = queryEmbeddingResponse.data[0].embedding; // Extract the embedding from the response. - if (Array.isArray(queryEmbeddingResponse.embeddings)) { - queryEmbedding = queryEmbeddingResponse.embeddings[0]; - } else if (queryEmbeddingResponse.embeddings && 'embeddings' in queryEmbeddingResponse.embeddings) { - queryEmbedding = (queryEmbeddingResponse.embeddings as { embeddings: number[][] }).embeddings[0]; - } else { - throw new Error('Invalid embedding response format'); - } - if (!Array.isArray(queryEmbedding)) { - throw new Error('Query embedding is not an array'); - } console.log(this._doc_ids()); // Query the Pinecone index using the embedding and filter by document IDs. const queryResponse: QueryResponse = await this.index.query({ diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss index d79df4272..78bbb520e 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.scss +++ b/src/client/views/nodes/formattedText/DashFieldView.scss @@ -1,4 +1,4 @@ -@import '../../global/globalCssVariables.module.scss'; +@use '../../global/globalCssVariables.module.scss' as global; .dashFieldView-active, .dashFieldView { @@ -64,5 +64,5 @@ } .ProseMirror-selectedNode { - outline: solid 1px $light-blue !important; + outline: solid 1px global.$light-blue !important; } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 84859b94d..f9de4ab5a 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -1,4 +1,4 @@ -@import '../../global/globalCssVariables.module.scss'; +@use '../../global/globalCssVariables.module.scss' as global; .ProseMirror { width: 100%; @@ -22,7 +22,7 @@ &.h-left * { display: flex; - justify-content: flex-start; + justify-content: flex-start; } &.h-right * { @@ -32,7 +32,7 @@ &.template * { ::-webkit-scrollbar-track { - background: none; + background: none; } } @@ -64,7 +64,7 @@ audiotag:hover { background: inherit; padding: 0; border-width: 0px; - border-color: $medium-gray; + border-color: global.$medium-gray; box-sizing: border-box; background-color: inherit; border-style: solid; @@ -79,7 +79,6 @@ audiotag:hover { transform-origin: left top; top: 0; left: 0; - } .formattedTextBox-cont { @@ -88,7 +87,7 @@ audiotag:hover { padding: 0; border-width: 0px; border-radius: inherit; - border-color: $medium-gray; + border-color: global.$medium-gray; box-sizing: border-box; background-color: inherit; border-style: solid; @@ -147,13 +146,13 @@ audiotag:hover { font-size: 11px; border-radius: 3px; color: white; - background: $medium-gray; + background: global.$medium-gray; border-radius: 5px; display: flex; justify-content: center; align-items: center; cursor: grabbing; - box-shadow: $standard-box-shadow; + box-shadow: global.$standard-box-shadow; // transition: 0.2s; opacity: 0.3; &:hover { @@ -646,7 +645,7 @@ footnote::before { } @media only screen and (max-width: 1000px) { - @import '../../global/globalCssVariables.module.scss'; + // @import '../../global/globalCssVariables.module.scss'; .ProseMirror { width: 100%; @@ -664,7 +663,7 @@ footnote::before { padding: 0; border-width: 0px; border-radius: inherit; - border-color: $medium-gray; + border-color: global.$medium-gray; box-sizing: border-box; background-color: inherit; border-style: solid; @@ -1074,4 +1073,3 @@ footnote::before { } } } - diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index eb1f9d07b..c2a2caecf 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -135,7 +135,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB /** * ApplyingChange - Marks whether an interactive text edit is currently in the process of being written to the database. - * This is needed to distinguish changes to text fields caused by editing vs those caused by changes to + * This is needed to distinguish changes to text fields caused by editing vs those caused by changes to * the prototype or other external edits */ public ApplyingChange: string = ''; @@ -977,7 +977,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }, icon: 'star', }); - optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' }); + optionItems.push({ description: `Generate Dall-E Image`, event: this.generateImage, icon: 'star' }); // optionItems.push({ description: `Make AI Flashcards`, event: () => this.makeAIFlashcards(), icon: 'lightbulb' }); optionItems.push({ description: `Ask GPT-3`, event: this.askGPT, icon: 'lightbulb' }); this._props.renderDepth && @@ -1043,7 +1043,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB askGPT = action(async () => { try { - GPTPopup.Instance.setSidebarId(this.sidebarKey); + GPTPopup.Instance.setSidebarFieldKey(this.sidebarKey); GPTPopup.Instance.addDoc = this.sidebarAddDocument; const res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION); if (!res) { @@ -1061,12 +1061,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } }); - generateImage = async () => { + generateImage = () => { GPTPopup.Instance?.setTextAnchor(this.getAnchor(false)); - GPTPopup.Instance?.setImgTargetDoc(this.Document); - GPTPopup.Instance.addToCollection = this._props.addDocument; - GPTPopup.Instance.setImgDesc((this.dataDoc.text as RichTextField)?.Text); - GPTPopup.Instance.generateImage(); + GPTPopup.Instance.generateImage((this.dataDoc.text as RichTextField)?.Text, this.Document, this._props.addDocument); }; breakupDictation = () => { @@ -1285,14 +1282,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ); this._disposers.componentHeights = reaction( // set the document height when one of the component heights changes and layout_autoHeight is on - () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }), - ({ sidebarHeight, textHeight, layoutAutoHeight, marginsHeight }) => { + () => ({ border: this._props.PanelHeight(), sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }), + ({ border, sidebarHeight, textHeight, layoutAutoHeight, marginsHeight }) => { const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight)); if ( (!Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') || this._props.isSelected()) && // layoutAutoHeight && newHeight && - newHeight !== this.layoutDoc.height && + (newHeight !== this.layoutDoc.height || border < NumCast(this.layoutDoc.height)) && !this._props.dontRegisterView ) { this._props.setHeight?.(newHeight); @@ -1660,7 +1657,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } }; onSelectEnd = () => { - GPTPopup.Instance.setSidebarId(this.sidebarKey); + GPTPopup.Instance.setSidebarFieldKey(this.sidebarKey); GPTPopup.Instance.addDoc = this.sidebarAddDocument; document.removeEventListener('pointerup', this.onSelectEnd); }; @@ -1672,7 +1669,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB for (let target: HTMLElement | Element | null = clickTarget as HTMLElement; target instanceof HTMLElement && !target.dataset?.targethrefs; target = target.parentElement); while (clickTarget instanceof HTMLElement && !clickTarget.dataset?.targethrefs) clickTarget = clickTarget.parentElement; const dataset = clickTarget instanceof HTMLElement ? clickTarget?.dataset : undefined; - FormattedTextBoxComment.update(this, this.EditorView!, undefined, dataset?.targethrefs, dataset?.linkdoc, dataset?.nopreview === 'true'); + + if (dataset?.targethrefs) + window + .open( + dataset?.targethrefs + ?.trim() + .split(' ') + .filter(h => h) + .lastElement(), + '_blank' + ) + ?.focus(); + else FormattedTextBoxComment.update(this, this.EditorView!, undefined, dataset?.targethrefs, dataset?.linkdoc, dataset?.nopreview === 'true'); } }; @action diff --git a/src/client/views/nodes/formattedText/RichTextMenu.scss b/src/client/views/nodes/formattedText/RichTextMenu.scss index d6ed5ebee..fcc816447 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.scss +++ b/src/client/views/nodes/formattedText/RichTextMenu.scss @@ -1,4 +1,4 @@ -@import '../../global/globalCssVariables.module.scss'; +@use '../../global/globalCssVariables.module.scss' as global; .button-dropdown-wrapper { position: relative; @@ -25,7 +25,7 @@ top: 35px; left: 0; background-color: #323232; - color: $light-gray; + color: global.$light-gray; border: 1px solid #4d4d4d; border-radius: 0 6px 6px 6px; box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx index 6b1d05031..657e689bb 100644 --- a/src/client/views/nodes/imageEditor/ImageEditor.tsx +++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx @@ -90,18 +90,8 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc * @param type The new tool type we are changing to */ const changeTool = (type: ImageToolType) => { - switch (type) { - case ImageToolType.GenerativeFill: - setCurrTool(genFillTool); - setCursorData(prev => ({ ...prev, width: genFillTool.sliderDefault as number })); - break; - case ImageToolType.Cut: - setCurrTool(cutTool); - setCursorData(prev => ({ ...prev, width: cutTool.sliderDefault as number })); - break; - default: - break; - } + setCurrToolType(type); + setCursorData(prev => ({ ...prev, width: currTool().sliderDefault as number })); }; // Undo and Redo const handleUndo = () => { @@ -171,9 +161,8 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc // handles brushing on pointer movement useEffect(() => { - if (!isBrushing) return undefined; const canvas = canvasRef.current; - if (!canvas) return undefined; + if (!isBrushing || !canvas) return undefined; const ctx = ImageUtility.getCanvasContext(canvasRef); if (!ctx) return undefined; @@ -188,33 +177,29 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc }; drawingAreaRef.current?.addEventListener('pointermove', handlePointerMove); - return () => { - drawingAreaRef.current?.removeEventListener('pointermove', handlePointerMove); - }; + return () => drawingAreaRef.current?.removeEventListener('pointermove', handlePointerMove); }, [isBrushing]); // first load useEffect(() => { - const loadInitial = async () => { - if (!imageEditorSource || imageEditorSource === '') return; - const img = new Image(); - const res = await ImageUtility.urlToBase64(imageEditorSource); - if (!res) return; - img.src = `data:image/png;base64,${res}`; - - img.onload = () => { - currImg.current = img; - originalImg.current = img; - const imgWidth = img.naturalWidth; - const imgHeight = img.naturalHeight; - const scale = Math.min(canvasSize / imgWidth, canvasSize / imgHeight); - const width = imgWidth * scale; - const height = imgHeight * scale; - setCanvasDims({ width, height }); - }; - }; - - loadInitial(); + if (imageEditorSource && imageEditorSource) { + ImageUtility.urlToBase64(imageEditorSource).then(res => { + if (res) { + const img = new Image(); + img.src = `data:image/png;base64,${res}`; + img.onload = () => { + currImg.current = img; + originalImg.current = img; + const imgWidth = img.naturalWidth; + const imgHeight = img.naturalHeight; + const scale = Math.min(canvasSize / imgWidth, canvasSize / imgHeight); + const width = imgWidth * scale; + const height = imgHeight * scale; + setCanvasDims({ width, height }); + }; + } + }); + } // cleanup return () => { @@ -300,7 +285,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc if (!canvasMask) return; const maskBlob = await ImageUtility.canvasToBlob(canvasMask); const imgBlob = await ImageUtility.canvasToBlob(canvasOriginalImg); - const res = await ImageUtility.getEdit(imgBlob, maskBlob, input !== '' ? input + ' in the same style' : 'Fill in the image in the same style', 2); + const res = await ImageUtility.getEdit(imgBlob, maskBlob, input || 'Fill in the image in the same style', 2); // create first image if (!newCollectionRef.current) { @@ -569,11 +554,15 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc setIsFirstDoc(true); }; + function currTool() { + return imageEditTools.find(tool => tool.type === currToolType) ?? genFillTool; + } + // defines the tools and sets current tool - const genFillTool: ImageEditTool = { type: ImageToolType.GenerativeFill, name: 'Generative Fill', btnText: 'GET EDITS', icon: 'fill', applyFunc: getEdit, sliderMin: 25, sliderMax: 500, sliderDefault: 150 }; - const cutTool: ImageEditTool = { type: ImageToolType.Cut, name: 'Cut', btnText: 'CUT IMAGE', icon: 'scissors', applyFunc: cutImage, sliderMin: 1, sliderMax: 50, sliderDefault: 5 }; + const genFillTool: ImageEditTool = { type: ImageToolType.GenerativeFill, btnText: 'GET EDITS', icon: 'fill', applyFunc: getEdit, sliderMin: 25, sliderMax: 500, sliderDefault: 150 }; + const cutTool: ImageEditTool = { type: ImageToolType.Cut, btnText: 'CUT IMAGE', icon: 'scissors', applyFunc: cutImage, sliderMin: 1, sliderMax: 50, sliderDefault: 5 }; const imageEditTools: ImageEditTool[] = [genFillTool, cutTool]; - const [currTool, setCurrTool] = useState<ImageEditTool>(genFillTool); + const [currToolType, setCurrToolType] = useState<ImageToolType>(ImageToolType.GenerativeFill); // the top controls for making a new collection, resetting, and applying edits, function renderControls() { @@ -595,7 +584,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc labelPlacement="end" sx={{ whiteSpace: 'nowrap' }} /> - <ApplyFuncButtons onClick={() => currTool.applyFunc(cutType, cursorData.width, edits, isFirstDoc)} loading={loading} onReset={handleReset} btnText={currTool.btnText} /> + <ApplyFuncButtons onClick={() => currTool().applyFunc(cutType, cursorData.width, edits, isFirstDoc)} loading={loading} onReset={handleReset} btnText={currTool().btnText} /> <IconButton color={activeColor} tooltip="close" icon={<CgClose size="16px" />} onClick={handleViewClose} /> </div> </div> @@ -607,8 +596,8 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc return ( <div className="sideControlsContainer" style={{ backgroundColor: bgColor }}> <div className="sideControls"> - <div className="imageToolsContainer">{imageEditTools.map(tool => ImageToolButton(tool, tool.type === currTool.type, changeTool))}</div> - {currTool.type == ImageToolType.Cut && ( + <div className="imageToolsContainer">{imageEditTools.map(tool => ImageToolButton(tool, tool.type === currTool().type, changeTool))}</div> + {currTool().type == ImageToolType.Cut && ( <div className="cutToolsContainer"> <Button style={{ width: '100%' }} text="Keep in" type={Type.TERT} color={cutType == CutMode.IN ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.IN)} /> <Button style={{ width: '100%' }} text="Keep out" type={Type.TERT} color={cutType == CutMode.OUT ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.OUT)} /> @@ -617,11 +606,13 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc </div> )} <div className="sliderContainer" onPointerDown={e => e.stopPropagation()}> - {currTool.type === ImageToolType.GenerativeFill && ( + {currTool().type === ImageToolType.GenerativeFill && ( <Slider sx={{ '& input[type="range"]': { - WebkitAppearance: 'slider-vertical', + writingMode: 'vertical-lr', + direction: 'rtl', + // WebkitAppearance: 'slider-vertical', }, }} orientation="vertical" @@ -633,11 +624,13 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc onChange={(e, val) => setCursorData(prev => ({ ...prev, width: val as number }))} /> )} - {currTool.type === ImageToolType.Cut && ( + {currTool().type === ImageToolType.Cut && ( <Slider sx={{ '& input[type="range"]': { - WebkitAppearance: 'slider-vertical', + writingMode: 'vertical-lr', + direction: 'rtl', + // WebkitAppearance: 'slider-vertical', }, }} orientation="vertical" @@ -780,7 +773,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc {renderSideIcons()} {renderEditThumbnails()} </div> - {currTool.type === ImageToolType.GenerativeFill && renderPromptBox()} + {currTool().type === ImageToolType.GenerativeFill && renderPromptBox()} </div> ); }; diff --git a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx index 985dc914f..3eaa251f2 100644 --- a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx +++ b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx @@ -53,10 +53,10 @@ export function ApplyFuncButtons({ loading, onClick: getEdit, onReset, btnText } export function ImageToolButton(tool: ImageEditTool, isActive: boolean, selectTool: (type: ImageToolType) => void) { return ( - <div key={tool.name} className="imageEditorButtonContainer"> + <div key={tool.type} className="imageEditorButtonContainer"> <Button style={{ width: '100%' }} - text={tool.name} + text={tool.type} type={Type.TERT} color={isActive ? SettingsManager.userVariantColor : bgColor} icon={<FontAwesomeIcon icon={tool.icon} />} diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts index ece0f4d7f..1c6a38a24 100644 --- a/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts +++ b/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts @@ -87,7 +87,6 @@ export class ImageUtility { body: fd, }); const data = await res.json(); - console.log(data.data); return { status: 'success', urls: (data.data as { b64_json: string }[]).map(urlData => `data:image/png;base64,${urlData.b64_json}`), diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts index a14b55439..02dbc0312 100644 --- a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts +++ b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts @@ -13,8 +13,8 @@ export interface Point { } export enum ImageToolType { - GenerativeFill = 'genFill', - Cut = 'cut', + GenerativeFill = 'Generative Fill', + Cut = 'Cut', } export enum CutMode { @@ -26,7 +26,6 @@ export enum CutMode { export interface ImageEditTool { type: ImageToolType; - name: string; btnText: string; icon: IconProp; // this is the function that the image tool applies, so it can be defined depending on the tool diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss index e34e1b380..e24b47bd1 100644 --- a/src/client/views/nodes/trails/PresBox.scss +++ b/src/client/views/nodes/trails/PresBox.scss @@ -1,4 +1,4 @@ -@import '../../global/globalCssVariables.module.scss'; +@use '../../global/globalCssVariables.module.scss' as global; .presBox-gpt-chat { padding: 16px; @@ -203,8 +203,8 @@ align-items: center; height: 30px; width: 100%; - color: $white; - background-color: $dark-gray; + color: global.$white; + background-color: global.$dark-gray; .toolbar-button { cursor: pointer; @@ -218,7 +218,7 @@ } .toolbar-button.active { - color: $light-blue; + color: global.$light-blue; background-color: white; border-radius: 100%; } @@ -266,7 +266,7 @@ } .toolbar-divider { - border-left: solid $medium-gray 0.5px; + border-left: solid global.$medium-gray 0.5px; height: 20px; } } @@ -274,13 +274,13 @@ .dropdown { font-size: 10; margin-left: 5px; - color: $medium-gray; + color: global.$medium-gray; transition: 0.5s ease; } .dropdown.active { transform: rotate(180deg); - color: $light-blue; + color: global.$light-blue; opacity: 0.7; } @@ -340,7 +340,7 @@ .ribbon-colorBox { cursor: pointer; - border: solid 1px $black; + border: solid 1px global.$black; display: flex; margin-left: 5px; margin-top: 5px; @@ -387,7 +387,7 @@ } .ribbon-propertyUpDownItem:hover { - background: $medium-gray; + background: global.$medium-gray; transform: scale(1.05); } } @@ -413,7 +413,7 @@ @media screen and (-webkit-min-device-pixel-ratio: 0) { .multiThumb-slider { display: grid; - background-color: $white; + background-color: global.$white; height: 10px; border-radius: 10px; overflow: hidden; @@ -431,8 +431,8 @@ -webkit-appearance: none; height: 10px; cursor: ew-resize; - background: $medium-blue; - box-shadow: -100vw 0 0 100vw $white; + background: global.$medium-blue; + box-shadow: -100vw 0 0 100vw global.$white; } .toolbar-slider.end::-webkit-slider-thumb { @@ -441,8 +441,8 @@ -webkit-appearance: none; height: 10px; cursor: ew-resize; - background: $medium-blue; - box-shadow: -100vw 0 0 100vw $light-blue; + background: global.$medium-blue; + box-shadow: -100vw 0 0 100vw global.$light-blue; } } @@ -456,7 +456,7 @@ height: 10px; border-radius: 10px; -webkit-appearance: none; - background-color: $white; + background-color: global.$white; } .toolbar-slider:focus { @@ -476,8 +476,8 @@ -webkit-appearance: none; height: 10px; cursor: ew-resize; - background: $medium-blue; - box-shadow: -100vw 0 0 100vw $light-blue; + background: global.$medium-blue; + box-shadow: -100vw 0 0 100vw global.$light-blue; } .presBox-checkbox { @@ -493,7 +493,7 @@ width: 15px; min-width: 15px; cursor: pointer; - background: $white; + background: global.$white; } .presBox-checkbox:focus { @@ -501,11 +501,11 @@ } .presBox-checkbox:hover { - background: $light-gray; + background: global.$light-gray; } .presBox-checkbox:checked { - background: $light-blue; + background: global.$light-blue; } } @@ -554,9 +554,9 @@ text-align: center; font-size: 16; width: 90%; - color: $black; + color: global.$black; transform: translate(5%, 0px); - border-bottom: solid 2px $medium-gray; + border-bottom: solid 2px global.$medium-gray; } .ribbon-textInput { @@ -568,8 +568,8 @@ justify-self: left; margin-top: 5px; padding-left: 10px; - background-color: $white; - border: solid 1px $black; + background-color: global.$white; + border: solid 1px global.$black; min-width: 80px; max-width: 200px; width: 100%; @@ -590,7 +590,7 @@ } .ribbon-frameSelector { - border: $black solid 1px; + border: global.$black solid 1px; width: 60px; height: 20px; margin-top: 5px; @@ -607,12 +607,12 @@ cursor: pointer; position: relative; height: 100%; - background: $white; + background: global.$white; display: flex; align-items: center; justify-content: center; text-align: center; - color: $black; + color: global.$black; } .numKeyframe { @@ -620,7 +620,7 @@ font-size: 10; font-weight: 600; position: relative; - color: $black; + color: global.$black; display: flex; width: 100%; height: 100%; @@ -662,7 +662,7 @@ padding-left: 10; padding-right: 10; border-radius: 10px; - background-color: $medium-gray; + background-color: global.$medium-gray; } .ribbon-final-button:hover { @@ -681,13 +681,13 @@ align-items: center; margin-bottom: 5px; height: 25px; - color: $light-gray; + color: global.$light-gray; width: 100%; max-width: 120; padding-left: 10; padding-right: 10; border-radius: 10px; - background-color: $black; + background-color: global.$black; } .ribbon-final-button-hidden:hover { @@ -698,15 +698,15 @@ .ribbon-frameList { width: calc(100% - 5px); height: 50px; - background-color: $white; - border: 1px solid $medium-gray; + background-color: global.$white; + border: 1px solid global.$medium-gray; grid-template-rows: max-content; .frameList-header { display: grid; width: 100%; height: 20px; - background-color: $medium-gray; + background-color: global.$medium-gray; .frameList-headerButtons { display: flex; @@ -761,7 +761,7 @@ font-size: 10.5; font-weight: 300; height: 20; - background-color: $medium-gray; + background-color: global.$medium-gray; color: white; display: flex; margin-top: 5px; @@ -780,8 +780,8 @@ transition: all 0.4s; font-weight: 400; opacity: 1; - color: $white; - background-color: $black; + color: global.$white; + background-color: global.$black; } .ribbon-toggle { @@ -789,9 +789,9 @@ font-size: 10.5; font-weight: 200; height: 20; - background-color: $white; + background-color: global.$white; display: inline-flex; - color: $black; + color: global.$black; border-radius: 5px; width: max-content; justify-content: center; @@ -831,13 +831,13 @@ position: relative; font-size: 13; padding-bottom: 10px; - border-bottom: solid 1px $dark-gray; + border-bottom: solid 1px global.$dark-gray; .presBox-dropdown:hover { - border: solid 1px $medium-blue; + border: solid 1px global.$medium-blue; .presBox-dropdownIcon { - color: $medium-blue; + color: global.$medium-blue; } } @@ -846,12 +846,12 @@ display: grid; grid-template-columns: auto 20%; position: relative; - border: solid 1px $black; - background-color: $light-gray; + border: solid 1px global.$black; + background-color: global.$light-gray; border-radius: 5px; font-size: 10; height: 25; - color: $black; + color: global.$black; padding-left: 5px; align-items: center; margin-top: 5px; @@ -917,7 +917,7 @@ height: 100px; padding-top: 5px; padding-bottom: 5px; - border: solid 1px $black; + border: solid 1px global.$black; // overflow: auto; ::-webkit-scrollbar { @@ -967,7 +967,7 @@ cursor: pointer; position: relative; text-align: center; - border-left: solid 1px $medium-gray; + border-left: solid 1px global.$medium-gray; width: 20%; height: 100%; display: flex; @@ -998,7 +998,7 @@ box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.8); z-index: 200; background-color: white; - color: $black; + color: global.$black; position: absolute; overflow: hidden; } @@ -1014,12 +1014,12 @@ align-items: center; justify-content: center; transform: translate(0px, -1px); - background-color: $white; + background-color: global.$white; width: 40px; height: 15px; align-self: center; justify-self: center; - border: solid 1px $black; + border: solid 1px global.$black; border-top: 0px; border-bottom-right-radius: 7px; border-bottom-left-radius: 7px; @@ -1028,15 +1028,15 @@ .layout-container { padding: 5px; display: grid; - background-color: $white; + background-color: global.$white; grid-template-columns: repeat(auto-fit, minmax(90px, 100px)); width: 100%; - border: solid 1px $black; + border: solid 1px global.$black; min-width: 100px; overflow: hidden; .layout:hover { - border: solid 2px $medium-blue; + border: solid 2px global.$medium-blue; } .layout { @@ -1051,7 +1051,7 @@ width: 90px; overflow: hidden; background-color: white; - border: solid $medium-gray 1px; + border: solid global.$medium-gray 1px; display: grid; grid-template-rows: auto; align-items: center; @@ -1066,7 +1066,7 @@ height: 13; font-size: 12; display: flex; - background-color: $white; + background-color: global.$white; } .subtitle { @@ -1079,7 +1079,7 @@ height: 13; font-size: 9; display: flex; - background-color: $white; + background-color: global.$white; } .content { @@ -1092,7 +1092,7 @@ height: 13; font-size: 10; display: flex; - background-color: $white; + background-color: global.$white; height: 33; text-align: left; font-size: 8px; @@ -1103,7 +1103,7 @@ .presBox-buttons { position: relative; width: 100%; - background: $medium-gray; + background: global.$medium-gray; min-height: 35px; padding-top: 5px; padding-bottom: 5px; @@ -1137,8 +1137,8 @@ } select { - background: $dark-gray; - color: $white; + background: global.$dark-gray; + color: global.$white; } .presBox-button { @@ -1152,8 +1152,8 @@ text-align: center; letter-spacing: normal; width: inherit; - background: $dark-gray; - color: $white; + background: global.$dark-gray; + color: global.$white; } .presBox-button.active { @@ -1161,7 +1161,7 @@ } .presBox-button.active:hover { - background-color: $medium-blue; + background-color: global.$medium-blue; } .presBox-button.edit { @@ -1238,8 +1238,8 @@ font-size: 100; display: flex; align-items: center; - background: $dark-gray; - color: $white; + background: global.$dark-gray; + color: global.$white; } .presBox-viewPicker { @@ -1273,7 +1273,7 @@ left: 0; opacity: 0.5; transition: all 0.4s; - color: $white; + color: global.$white; width: 100%; height: 100%; } @@ -1283,8 +1283,8 @@ } .presPanelOverlay { - background-color: $dark-gray; - color: $white; + background-color: global.$dark-gray; + color: global.$white; border-radius: 5px; grid-template-rows: 100%; height: 100%; @@ -1316,7 +1316,7 @@ .presPanel-divider { width: 0.5px; height: 80%; - border-right: solid 1px $medium-gray; + border-right: solid 1px global.$medium-gray; } .presPanel-button-frame { @@ -1348,12 +1348,12 @@ } .presPanel-button:hover { - background-color: $medium-gray; + background-color: global.$medium-gray; transform: scale(1.2); } .presPanel-button-text:hover { - background-color: $medium-gray; + background-color: global.$medium-gray; } } diff --git a/src/client/views/nodes/trails/SlideEffect.tsx b/src/client/views/nodes/trails/SlideEffect.tsx index a114c231f..89abdd12d 100644 --- a/src/client/views/nodes/trails/SlideEffect.tsx +++ b/src/client/views/nodes/trails/SlideEffect.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react/require-default-props */ import { animated, to, useInView, useSpring } from '@react-spring/web'; import React, { useEffect } from 'react'; import { Doc } from '../../../../fields/Doc'; |
