diff options
Diffstat (limited to 'src')
24 files changed, 648 insertions, 208 deletions
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index fb51278ae..30194f9f8 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -1,9 +1,11 @@ import { ClientOptions, OpenAI } from 'openai'; +import { ChatCompletionMessageParam } from 'openai/resources'; enum GPTCallType { SUMMARY = 'summary', COMPLETION = 'completion', EDIT = 'edit', + DATA = 'data', } type GPTCallOpts = { @@ -13,10 +15,16 @@ type GPTCallOpts = { prompt: string; }; +/** + * Replace completions (deprecated) with chat + */ + const callTypeMap: { [type: string]: GPTCallOpts } = { - summary: { model: 'gpt-3.5-turbo-instruct', maxTokens: 256, temp: 0.5, prompt: 'Summarize this text in simpler terms: ' }, - edit: { model: 'gpt-3.5-turbo-instruct', maxTokens: 256, temp: 0.5, prompt: 'Reword this: ' }, - completion: { model: 'gpt-3.5-turbo-instruct', maxTokens: 256, temp: 0.5, prompt: '' }, + // newest model: gpt-4 + summary: { model: 'gpt-3.5-turbo', maxTokens: 256, temp: 0.5, prompt: 'Summarize the text given in simpler terms.' }, + edit: { model: 'gpt-3.5-turbo', maxTokens: 256, temp: 0.5, prompt: 'Reword the text.' }, + completion: { model: 'gpt-3.5-turbo', maxTokens: 256, temp: 0.5, prompt: "You are a helpful assistant. Answer the user's prompt." }, + data: { model: 'gpt-3.5-turbo', maxTokens: 256, temp: 0.5, prompt: "You are a helpful resarch assistant. Analyze the user's data to find meaningful patterns and/or correlation. Please keep your response short and to the point." }, }; /** @@ -25,7 +33,7 @@ const callTypeMap: { [type: string]: GPTCallOpts } = { * @param inputText Text to process * @returns AI Output */ -const gptAPICall = async (inputText: string, callType: GPTCallType) => { +const gptAPICall = async (inputText: string, callType: GPTCallType, prompt?: any) => { if (callType === GPTCallType.SUMMARY) inputText += '.'; const opts: GPTCallOpts = callTypeMap[callType]; try { @@ -34,13 +42,21 @@ const gptAPICall = async (inputText: string, callType: GPTCallType) => { dangerouslyAllowBrowser: true, }; const openai = new OpenAI(configuration); - const response = await openai.completions.create({ + + let usePrompt = prompt ? opts.prompt + prompt : opts.prompt; + let messages: ChatCompletionMessageParam[] = [ + { role: 'system', content: usePrompt }, + { role: 'user', content: inputText }, + ]; + + const response = await openai.chat.completions.create({ model: opts.model, max_tokens: opts.maxTokens, temperature: opts.temp, - prompt: `${opts.prompt}${inputText}`, + messages, }); - return response.choices[0].text; + const content = response.choices[0].message.content; + return content; } catch (err) { console.log(err); return 'Error connecting with API.'; @@ -60,8 +76,7 @@ const gptImageCall = async (prompt: string, n?: number) => { n: n ?? 1, size: '1024x1024', }); - return response.data.map((data: any) => data.url); - // return response.data.data[0].url; + return response.data.map(data => data.url); } catch (err) { console.error(err); return; diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 38ebc86e7..081115879 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -816,7 +816,7 @@ pie title Minerals in my tap water } /// initializes a context menu button for the top bar context menu - static setupContextMenuButton(params:Button, btnDoc?:Doc) { + static setupContextMenuButton(params:Button, btnDoc?:Doc, btnContainer?:Doc) { const reqdOpts:DocumentOptions = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, color: Colors.WHITE, isSystem: true, @@ -824,6 +824,7 @@ pie title Minerals in my tap water _height: 30, _nativeHeight: 30, linearBtnWidth: params.linearBtnWidth, toolType: params.toolType, expertMode: params.expertMode, _dragOnlyWithinContainer: true, _lockedPosition: true, + _embedContainer: btnContainer }; const reqdFuncs:{[key:string]:any} = { ...params.funcs, @@ -833,15 +834,15 @@ pie title Minerals in my tap water static setupContextMenuBtn(params:Button, menuDoc:Doc):Doc { - const menuBtnDoc = DocListCast(menuDoc?.data).find(doc => doc.title === params.title); + const menuBtnDoc = DocListCast(menuDoc?.data).find( doc => doc.title === params.title); const subMenu = params.subMenu; if (!subMenu) { // button does not have a sub menu - return this.setupContextMenuButton(params, menuBtnDoc); + return this.setupContextMenuButton(params, menuBtnDoc, menuDoc); } // linear view const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List<string>(['width', "linearView_IsOpen"]), childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: params.scripts?.onClick ? false : true, - linearView_SubMenu: true, linearView_Expandable: true}; + linearView_SubMenu: true, linearView_Expandable: true, embedContainer: menuDoc}; const items = (menuBtnDoc?:Doc) => !menuBtnDoc ? [] : subMenu.map(sub => this.setupContextMenuBtn(sub, menuBtnDoc) ); const creator = params.btnType === ButtonType.MultiToggleButton ? this.multiToggleList : this.linearButtonList; diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss index b21828aa7..840df41e7 100644 --- a/src/client/views/PropertiesView.scss +++ b/src/client/views/PropertiesView.scss @@ -242,14 +242,15 @@ font-weight: bold; width: 95px; overflow-x: hidden; - display: inline-block; text-overflow: ellipsis; white-space: nowrap; + display: flex; + align-items: center; } .propertiesView-sharingTable-item-permission { display: flex; - align-items: flex-end; + align-items: center; text-align: right; margin-left: auto; margin-right: -12px; diff --git a/src/client/views/collections/CollectionNoteTakingView.scss b/src/client/views/collections/CollectionNoteTakingView.scss index 4c2dcf9ab..95fda7b0a 100644 --- a/src/client/views/collections/CollectionNoteTakingView.scss +++ b/src/client/views/collections/CollectionNoteTakingView.scss @@ -1,7 +1,7 @@ @import '../global/globalCssVariables.module.scss'; .collectionNoteTakingView-DocumentButtons { - display: none; + opacity: 0; justify-content: space-between; margin: auto; } @@ -15,7 +15,6 @@ .editableView-container-editing-oneLine, .editableView-container-editing { - color: grey; padding: 10px; width: 100%; } @@ -29,7 +28,6 @@ .editableView-input { outline-color: black; letter-spacing: 2px; - color: grey; border: 0px; padding: 12px 10px 11px 10px; } @@ -41,7 +39,6 @@ .editableView-input { outline-color: black; letter-spacing: 2px; - color: grey; border: 0px; padding: 12px 10px 11px 10px; } @@ -51,9 +48,9 @@ display: flex; } -.collectionNoteTakingViewFieldColumn:hover { +.collectionNoteTakingViewFieldColumnHover:hover { .collectionNoteTakingView-DocumentButtons { - display: flex; + opacity: 1; } } @@ -100,6 +97,9 @@ flex-direction: column; height: max-content; } + .collectionNoteTakingViewFieldColumnHover { + min-height: 100%; // if we use this, then we can't have autoHeight + } .collectionSchemaView-previewDoc { height: 100%; @@ -250,7 +250,6 @@ overflow: visible; width: 100%; display: flex; - color: gray; align-items: center; } } @@ -262,10 +261,6 @@ background: $medium-gray; // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong - .editableView-input { - color: $dark-gray; - } - .editableView-input:hover, .editableView-container-editing:hover, .editableView-container-editing-oneLine:hover { @@ -288,7 +283,6 @@ .editableView-container-editing-oneLine, .editableView-container-editing { - color: grey; padding: 10px; } @@ -301,7 +295,6 @@ .editableView-input { padding: 12px 10px 11px 10px; border: 0px; - color: grey; text-align: center; letter-spacing: 2px; outline-color: black; @@ -409,7 +402,6 @@ .editableView-container-editing-oneLine, .editableView-container-editing { - color: grey; padding: 10px; width: 100%; } @@ -423,13 +415,16 @@ .editableView-input { outline-color: black; letter-spacing: 2px; - color: grey; border: 0px; padding: 12px 10px 11px 10px; + &::placeholder { + color: black; + } } } .collectionNoteTakingView-addDocumentButton { + opacity: 0.5; font-size: 75%; letter-spacing: 2px; cursor: pointer; @@ -437,10 +432,12 @@ .editableView-input { outline-color: black; letter-spacing: 2px; - color: grey; border: 0px; padding: 12px 10px 11px 10px; } + &:hover { + opacity: unset; + } } .collectionNoteTakingView-addGroupButton { @@ -497,6 +494,24 @@ .rc-switch-checked .rc-switch-inner { left: 8px; } + + .collectionNoteTaking-pivotField { + display: none; + } + &:hover { + .collectionNoteTaking-pivotField { + display: unset; + } + } +} + +.collectionNoteTakingViewLight { + .collectionNoteTakingView-addDocumentButton, + .collectionNoteTakingView-addGroupButton { + .editableView-input::placeholder { + color: white; + } + } } @media only screen and (max-device-width: 480px) { diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index 5b91216cb..d8a0aebb1 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -9,14 +9,15 @@ import { listSpec } from '../../../fields/Schema'; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { DivHeight, emptyFunction, returnZero, smoothScroll, Utils } from '../../../Utils'; +import { DivHeight, emptyFunction, lightOrDark, returnZero, smoothScroll, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DragManager, dropActionType } from '../../util/DragManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; -import { undoBatch } from '../../util/UndoManager'; +import { undoable, undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; +import { Colors } from '../global/globalEnums'; import { LightboxView } from '../LightboxView'; import { DocumentView } from '../nodes/DocumentView'; import { FieldViewProps, FocusViewOptions } from '../nodes/FieldView'; @@ -26,6 +27,7 @@ import './CollectionNoteTakingView.scss'; import { CollectionNoteTakingViewColumn } from './CollectionNoteTakingViewColumn'; import { CollectionNoteTakingViewDivider } from './CollectionNoteTakingViewDivider'; import { CollectionSubView } from './CollectionSubView'; +import { FieldsDropdown } from '../FieldsDropdown'; const _global = (window /* browser */ || global) /* node */ as any; /** @@ -40,7 +42,9 @@ export class CollectionNoteTakingView extends CollectionSubView() { _disposers: { [key: string]: IReactionDisposer } = {}; _masonryGridRef: HTMLDivElement | null = null; _draggerRef = React.createRef<HTMLDivElement>(); - notetakingCategoryField = 'NotetakingCategory'; + @computed get notetakingCategoryField() { + return StrCast(this.dataDoc.notetaking_column, StrCast(this.layoutDoc.pivotField, 'notetaking_column')); + } public DividerWidth = 16; @observable docsDraggedRowCol: number[] = []; @observable _scroll = 0; @@ -152,25 +156,34 @@ export class CollectionNoteTakingView extends CollectionSubView() { ); }; + @computed get allFieldValues() { + return new Set(this.childDocs.map(doc => StrCast(doc[this.notetakingCategoryField]))); + } + componentDidMount() { super.componentDidMount?.(); document.addEventListener('pointerup', this.removeDocDragHighlight, true); - this._disposers.layout_autoHeight = reaction( - () => this.layoutDoc._layout_autoHeight, - layout_autoHeight => layout_autoHeight && this._props.setHeight?.(this.headerMargin + Math.max(...this._refList.map(DivHeight))) + + this._disposers.autoColumns = reaction( + () => (this.layoutDoc._notetaking_columns_autoCreate ? Array.from(this.allFieldValues) : undefined), + columns => undoable(() => columns?.filter(col => !this.colHeaderData.some(h => h.heading === col)).forEach(col => this.addColumn(col)), 'adding columns')(), + { fireImmediately: true } ); this._disposers.refList = reaction( () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !LightboxView.Contains(this.DocumentView?.()) }), ({ refList, autoHeight }) => { - if (autoHeight) refList.forEach(r => this.observer.observe(r)); - else this.observer.disconnect(); + if (autoHeight) { + refList.forEach(r => this.observer.observe(r)); + this._props.setHeight?.(this.headerMargin + Math.max(...this._refList.map(DivHeight))); + } else this.observer.disconnect(); }, { fireImmediately: true } ); } componentWillUnmount() { + this.observer.disconnect(); document.removeEventListener('pointerup', this.removeDocDragHighlight, true); super.componentWillUnmount(); Object.keys(this._disposers).forEach(key => this._disposers[key]()); @@ -297,7 +310,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { getDocWidth(d: Doc) { const heading = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as Field); const existingHeader = this.colHeaderData.find(sh => sh.heading === heading); - const existingWidth = existingHeader?.width ? existingHeader.width : 0; + const existingWidth = this.layoutDoc._notetaking_columns_autoSize ? 1 / (this.colHeaderData.length ?? 1) : existingHeader?.width ? existingHeader.width : 0; const maxWidth = existingWidth > 0 ? existingWidth * this.availableWidth : this.maxColWidth; const width = d.layout_fitWidth ? maxWidth : NumCast(d._width); return Math.min(maxWidth - CollectionNoteTakingViewColumn.ColumnMargin, width < maxWidth ? width : maxWidth); @@ -369,7 +382,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { // we alter the pivot fields of the docs in case they are moved to a new column. const colIndex = this.getColumnFromXCoord(xCoord); const colHeader = colIndex === undefined ? 'unset' : StrCast(this.colHeaderData[colIndex].heading); - DragManager.docsBeingDragged.forEach(d => (d[this.notetakingCategoryField] = colHeader)); + DragManager.docsBeingDragged.map(doc => doc[DocData]).forEach(d => (d[this.notetakingCategoryField] = colHeader)); // used to notify sections to re-render this.docsDraggedRowCol.length = 0; const columnFromCoord = this.getColumnFromXCoord(xCoord); @@ -499,11 +512,12 @@ export class CollectionNoteTakingView extends CollectionSubView() { editableViewProps = () => ({ GetValue: () => '', - SetValue: this.addGroup, - contents: '+ New Column', + SetValue: this.addColumn, + contents: '+ Column', }); refList = () => this._refList; + backgroundColor = () => this._props.DocumentView?.().backgroundColor(); // sectionNoteTaking returns a CollectionNoteTakingViewColumn (which is an individual column) sectionNoteTaking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => ( @@ -511,7 +525,9 @@ export class CollectionNoteTakingView extends CollectionSubView() { key={heading?.heading ?? 'unset'} PanelWidth={this._props.PanelWidth} refList={this._refList} + backgroundColor={this.backgroundColor} select={this._props.select} + isContentActive={this.isContentActive} addDocument={this.addDocument} chromeHidden={this.chromeHidden} colHeaderData={this.colHeaderData} @@ -537,18 +553,26 @@ export class CollectionNoteTakingView extends CollectionSubView() { /> ); + @undoBatch + remColumn = (value: SchemaHeaderField) => { + const colHdrData = Array.from(Cast(this._props.Document[this._props.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null)); + if (value) { + const index = colHdrData.indexOf(value); + index !== -1 && colHdrData.splice(index, 1); + this.resizeColumns(colHdrData); + } + }; + // addGroup is called when adding a new columnHeader, adding a SchemaHeaderField to our list of // columnHeaders and resizing the existing columns to make room for our new one. @undoBatch - addGroup = (value: string) => { - if (this.colHeaderData) { - for (const header of this.colHeaderData) { - if (header.heading === value) { - alert('You cannot use an existing column name. Please try a new column name'); - return value; - } + addColumn = (value: string) => { + this.colHeaderData.forEach(header => { + if (header.heading === value) { + alert('You cannot use an existing column name. Please try a new column name'); + return value; } - } + }); const columnHeaders = Array.from(Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null)); const newColWidth = 1 / (this.numGroupColumns + 1); columnHeaders.push(new SchemaHeaderField(value, undefined, undefined, newColWidth)); @@ -556,11 +580,25 @@ export class CollectionNoteTakingView extends CollectionSubView() { return true; }; + removeEmptyColumns = undoable(() => { + this.colHeaderData.filter(h => !this.allFieldValues.has(h.heading)).forEach(this.remColumn); + }, 'remove empty Columns'); + onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout if (!e.isPropagationStopped()) { const subItems: ContextMenuProps[] = []; - subItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' }); + subItems.push({ + description: `${this.layoutDoc._notetaking_columns_autoCreate ? 'Manually' : 'Automatically'} Create columns`, + event: () => (this.layoutDoc._notetaking_columns_autoCreate = !this.layoutDoc._notetaking_columns_autoCreate), + icon: 'computer', + }); + subItems.push({ description: 'Remove Empty Columns', event: this.removeEmptyColumns, icon: 'computer' }); + subItems.push({ + description: `${this.layoutDoc._notetaking_columns_autoSize ? 'Variable Size' : 'Autosize'} Columns`, + event: () => (this.layoutDoc._notetaking_columns_autoSize = !this.layoutDoc._notetaking_columns_autoSize), + icon: 'plus', + }); subItems.push({ description: `${this.layoutDoc._layout_autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight), icon: 'plus' }); subItems.push({ description: 'Clear All', event: () => (this.dataDoc.data = new List([])), icon: 'times' }); ContextMenu.Instance.addItem({ description: 'Options...', subitems: subItems, icon: 'eye' }); @@ -613,12 +651,11 @@ export class CollectionNoteTakingView extends CollectionSubView() { TraceMobx(); return ( <div - className="collectionNoteTakingView" + className={`collectionNoteTakingView ${lightOrDark(this.backgroundColor()) === Colors.WHITE ? 'collectionNoteTakingViewLight' : ''}`} ref={this.createRef} - key="notes" style={{ - overflowY: this._props.isContentActive() ? 'auto' : 'hidden', - background: this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor), + overflowY: this.isContentActive() ? 'auto' : 'hidden', + background: this.backgroundColor(), pointerEvents: this.backgroundEvents, }} onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))} @@ -629,6 +666,16 @@ export class CollectionNoteTakingView extends CollectionSubView() { onContextMenu={this.onContextMenu} onWheel={e => this._props.isContentActive() && e.stopPropagation()}> <>{this.renderedSections}</> + <div className="collectionNotetaking-pivotField" style={{ right: 0, top: 0, position: 'absolute' }}> + <FieldsDropdown + Document={this.Document} + selectFunc={undoable(fieldKey => { + this.layoutDoc._pivotField = fieldKey; + this.removeEmptyColumns(); + }, 'change pivot field')} + placeholder={StrCast(this.layoutDoc._pivotField)} + /> + </div> </div> ); } diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx index db178d500..448b11b05 100644 --- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx +++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx @@ -1,10 +1,9 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable } from 'mobx'; +import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { returnEmptyString } from '../../../Utils'; +import { lightOrDark, returnEmptyString } from '../../../Utils'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; -import { Id } from '../../../fields/FieldSymbols'; import { RichTextField } from '../../../fields/RichTextField'; import { listSpec } from '../../../fields/Schema'; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; @@ -26,6 +25,7 @@ import './CollectionNoteTakingView.scss'; interface CSVFieldColumnProps { Document: Doc; TemplateDataDocument: Opt<Doc>; + backgroundColor?: (() => string) | undefined; docList: Doc[]; heading: string; pivotField: string; @@ -38,6 +38,7 @@ interface CSVFieldColumnProps { gridGap: number; headings: () => object[]; select: (ctrlPressed: boolean) => void; + isContentActive: () => boolean | undefined; renderChildren: (docs: Doc[]) => JSX.Element[]; addDocument: (doc: Doc | Doc[]) => boolean; createDropTarget: (ele: HTMLDivElement) => void; @@ -57,10 +58,16 @@ interface CSVFieldColumnProps { */ @observer export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSVFieldColumnProps> { - @observable private _background = 'inherit'; + @observable private _hover = false; + + constructor(props: CSVFieldColumnProps) { + super(props); + makeObservable(this); + } // columnWidth returns the width of a column in absolute pixels @computed get columnWidth() { + if (this._props.Document._notetaking_columns_autoSize) return this._props.availableWidth / (this._props.colHeaderData?.length || 1); if (!this._props.colHeaderData || !this._props.headingObject || this._props.colHeaderData.length === 1) return `${(this._props.availableWidth / this._props.PanelWidth()) * 100}%`; const i = this._props.colHeaderData.findIndex(hd => hd.heading === this._props.headingObject?.heading && hd.color === this._props.headingObject.color); return ((this._props.colHeaderData[i].width * this._props.availableWidth) / this._props.PanelWidth()) * 100 + '%'; @@ -122,8 +129,8 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV return false; }; - @action pointerEntered = () => SnappingManager.IsDragging && (this._background = '#b4b4b4'); - @action pointerLeave = () => (this._background = 'inherit'); + @action pointerEntered = () => (this._hover = true); + @action pointerLeave = () => (this._hover = false); @undoBatch addTextNote = (char: string) => this.addNewTextDoc('-typed text-', false, true); @@ -147,7 +154,7 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV deleteColumn = () => { const colHdrData = Array.from(Cast(this._props.Document[this._props.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null)); if (this._props.headingObject) { - this._props.docList.forEach(d => (d[this._props.pivotField] = undefined)); + // this._props.docList.forEach(d => (d[DocData][this._props.pivotField] = undefined)); colHdrData.splice(colHdrData.indexOf(this._props.headingObject), 1); this._props.resizeColumns(colHdrData); } @@ -273,11 +280,11 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV </div> {!this._props.chromeHidden ? ( - <div className="collectionNoteTakingView-DocumentButtons" style={{ marginBottom: 10 }}> - <div key={`${heading}-add-document`} className="collectionNoteTakingView-addDocumentButton"> - <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.addTextNote} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} /> + <div className="collectionNoteTakingView-DocumentButtons" style={{ display: this._props.isContentActive() ? 'flex' : 'none', marginBottom: 10 }}> + <div className="collectionNoteTakingView-addDocumentButton" style={{ color: lightOrDark(this._props.backgroundColor?.()) }}> + <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.addTextNote} placeholder={"Type ':' for commands"} contents={'+ Node'} menuCallback={this.menuCallback} /> </div> - <div key={`${this._props.Document[Id]}-addGroup`} className="collectionNoteTakingView-addDocumentButton"> + <div className="collectionNoteTakingView-addDocumentButton" style={{ color: lightOrDark(this._props.backgroundColor?.()) }}> <EditableView {...this._props.editableViewProps()} /> </div> </div> @@ -292,17 +299,17 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV TraceMobx(); return ( <div - className="collectionNoteTakingViewFieldColumn" - key={this._heading} + className="collectionNoteTakingViewFieldColumnHover" + onPointerEnter={this.pointerEntered} + onPointerLeave={this.pointerLeave} style={{ width: this.columnWidth, - background: this._background, + background: this._hover && SnappingManager.IsDragging ? '#b4b4b4' : 'inherit', marginLeft: this._props.headings().findIndex((h: any) => h[0] === this._props.headingObject) === 0 ? NumCast(this._props.Document.xMargin) : 0, - }} - ref={this.createColumnDropRef} - onPointerEnter={this.pointerEntered} - onPointerLeave={this.pointerLeave}> - {this.innards} + }}> + <div className="collectionNoteTakingViewFieldColumn" key={this._heading} ref={this.createColumnDropRef}> + {this.innards} + </div> </div> ); } diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 3a62a53d7..bf0393883 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -316,7 +316,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection <DocumentView ref={action((r: DocumentView) => r?.ContentDiv && this.docRefs.set(doc, r))} Document={doc} - TemplateDataDocument={dataDoc ?? (Doc.AreProtosEqual(doc[DocData], doc) ? undefined : doc[DocData])} + TemplateDataDocument={dataDoc} renderDepth={this._props.renderDepth + 1} PanelWidth={panelWidth} PanelHeight={panelHeight} diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 641e01b81..c5292f880 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -12,7 +12,7 @@ import { TraceMobx } from '../../../fields/util'; import { DivHeight, DivWidth, emptyFunction, returnEmptyString, setupMoveUpEvents } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; -import { DragManager } from '../../util/DragManager'; +import { DragManager, dropActionType } from '../../util/DragManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; @@ -66,11 +66,26 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< _ele: HTMLElement | null = null; + protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) => { + const dragData = de.complete.docDragData; + if (dragData) { + const sourceDragAction = dragData.dropAction; + const sameCollection = !dragData.draggedDocuments.some(d => !this._props.docList.includes(d)); + dragData.dropAction = !sameCollection // if doc from another tree + ? sourceDragAction || targetDropAction // then use the source's dragAction otherwise the target's + : sourceDragAction === dropActionType.inPlace // if source drag is inPlace + ? sourceDragAction // keep the doc in place + : dropActionType.same; // otherwise use same tree semantics to move within tree + + e.stopPropagation(); + } + }; + // This is likely similar to what we will be doing. Why do we need to make these refs? // is that the only way to have drop targets? createColumnDropRef = (ele: HTMLDivElement | null) => { this.dropDisposer?.(); - if (ele) this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this), this._props.Document); + if (ele) this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this), this._props.Document, this.onInternalPreDrop.bind(this)); else if (this._ele) this.props.refList.splice(this.props.refList.indexOf(this._ele), 1); this._ele = ele; }; @@ -345,7 +360,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< <div> <div key={`${heading}-stack`} - className={`collectionStackingView-masonrySingle`} + className="collectionStackingView-masonrySingle" style={{ padding: `${columnYMargin}px ${0}px ${this._props.yMargin}px ${0}px`, margin: 'auto', diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index ecc2aea31..32198e3a2 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -4,7 +4,7 @@ import * as rp from 'request-promise'; import { Utils, returnFalse } from '../../../Utils'; import CursorField from '../../../fields/CursorField'; import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc'; -import { AclPrivate } from '../../../fields/DocSymbols'; +import { AclPrivate, DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; @@ -196,13 +196,16 @@ export function CollectionSubView<X>(moreProps?: X) { @undoBatch protected onGesture(e: Event, ge: GestureUtils.GestureEvent) {} - protected onInternalPreDrop(e: Event, de: DragManager.DropEvent, dropAction: dropActionType) { - if (de.complete.docDragData) { - // if the dropEvent's dragAction is, say 'embed', but we're just dragging within a collection, we may not actually want to make an embedding. - // so we check if our collection has a dropAction set on it and if so, we use that instead. - if (dropAction && !de.complete.docDragData.draggedDocuments.some(d => d.embedContainer === this.Document && this.childDocs.includes(d))) { - de.complete.docDragData.dropAction = dropAction; - } + protected onInternalPreDrop(e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) { + const dragData = de.complete.docDragData; + if (dragData) { + const sourceDragAction = dragData.dropAction; + const sameCollection = !dragData.draggedDocuments.some(d => d.embedContainer !== this._props.Document); + dragData.dropAction = !sameCollection // if doc from another tree + ? sourceDragAction || targetDropAction // then use the source's dragAction otherwise the target's + : sourceDragAction === dropActionType.inPlace // if source drag is inPlace + ? sourceDragAction // keep the doc in place + : dropActionType.same; // otherwise use same tree semantics to move within tree e.stopPropagation(); } } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 293c79119..5741fc29b 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -157,14 +157,12 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree const dragData = de.complete.docDragData; if (dragData) { const sourceDragAction = dragData.dropAction; - const sameTree = () => Doc.AreProtosEqual(dragData.treeViewDoc, this.Document); - const isAlreadyInTree = () => sameTree || dragData.draggedDocuments.some(d => d.embedContainer === this.Document && this.childDocs.includes(d)); - dragData.dropAction = - targetDropAction && !isAlreadyInTree() // if dropped document is not in the tree - ? targetDropAction // then use the target's drop action if it's specified - : !sameTree() || sourceDragAction === dropActionType.inPlace // if doc from another tree, or a non inPlace source drag action is specified - ? sourceDragAction // use the source dragAction - : dropActionType.same; // otherwise use same tree semantics to move within tree + const sameTree = dragData.treeViewDoc?.[DocData] === this.dataDoc; + dragData.dropAction = !sameTree // if doc from another tree + ? sourceDragAction || targetDropAction // then use the source's dragAction otherwise the target's + : sourceDragAction === dropActionType.inPlace // if source drag is inPlace + ? sourceDragAction // keep the doc in place + : dropActionType.same; // otherwise use same tree semantics to move within tree e.stopPropagation(); } }; diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 8c29c3ada..4fd49f8fe 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -449,7 +449,9 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { const addDoc = inside ? this.localAdd : parentAddDoc; const canAdd = !StrCast((inside ? this.Document : this._props.treeViewParent)?.treeView_FreezeChildren).includes('add') || forceAdd; if (canAdd && (dropAction !== dropActionType.inPlace || droppedDocuments.every(d => d.embedContainer === this._props.parentTreeView?.Document))) { - const move = (!dropAction || canEmbed || dropAction === dropActionType.proto || dropAction === dropActionType.move || dropAction === dropActionType.same || dropAction === dropActionType.inPlace) && moveDocument; + const move = + (!dropAction || (canEmbed && dropAction !== dropActionType.copy) || dropAction === dropActionType.proto || dropAction === dropActionType.move || dropAction === dropActionType.same || dropAction === dropActionType.inPlace) && + moveDocument; this._props.parentTreeView instanceof TreeView && (this._props.parentTreeView.dropping = true); const res = droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === dropActionType.proto ? addDoc(d) : false) : addDoc(d)) || added, false); this._props.parentTreeView instanceof TreeView && (this._props.parentTreeView.dropping = false); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormBackgroundGrid.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormBackgroundGrid.tsx index 08dfb32ad..0acc99360 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormBackgroundGrid.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormBackgroundGrid.tsx @@ -14,8 +14,8 @@ export interface CollectionFreeFormViewBackgroundGridProps { nativeDimScaling: () => number; zoomScaling: () => number; layoutDoc: Doc; - cachedCenteringShiftX: number; - cachedCenteringShiftY: number; + centeringShiftX: number; + centeringShiftY: number; } @observer export class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFormViewBackgroundGridProps> { @@ -32,7 +32,7 @@ export class CollectionFreeFormBackgroundGrid extends React.Component<Collection const w = this.props.PanelWidth() / this.props.nativeDimScaling() + 2 * renderGridSpace; const h = this.props.PanelHeight() / this.props.nativeDimScaling() + 2 * renderGridSpace; const strokeStyle = this.props.color(); - return !this.props.nativeDimScaling() ? null : ( + return ( <canvas className="collectionFreeFormView-grid" width={w} @@ -41,8 +41,8 @@ export class CollectionFreeFormBackgroundGrid extends React.Component<Collection ref={el => { const ctx = el?.getContext('2d'); if (ctx) { - const Cx = this.props.cachedCenteringShiftX % renderGridSpace; - const Cy = this.props.cachedCenteringShiftY % renderGridSpace; + const Cx = this.props.centeringShiftX % renderGridSpace; + const Cy = this.props.centeringShiftY % renderGridSpace; ctx.lineWidth = Math.min(1, Math.max(0.5, this.props.zoomScaling())); ctx.setLineDash(gridSpace > 50 ? [3, 3] : [1, 5]); ctx.clearRect(0, 0, w, h); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 958117b91..88fe10bfd 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -179,17 +179,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @computed get nativeHeight() { return this._props.NativeHeight?.() || Doc.NativeHeight(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null)); } - @computed get cachedCenteringShiftX(): number { - const scaling = !this.nativeDimScaling ? 1 : this.nativeDimScaling; + @computed get centeringShiftX(): number { + const scaling = this.nativeDimScaling; return this._props.isAnnotationOverlay || this._props.originTopLeft ? 0 : this._props.PanelWidth() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections } - @computed get cachedCenteringShiftY(): number { + @computed get centeringShiftY(): number { + const panLocAtCenter = !(this._props.isAnnotationOverlay || this._props.originTopLeft); + if (!panLocAtCenter) return 0; const dv = this.DocumentView?.(); - const fitWidth = this._props.layout_fitWidth?.(this.Document) ?? dv?.layoutDoc.layout_fitWidth; - const scaling = !this.nativeDimScaling ? 1 : this.nativeDimScaling; + const aspect = !(this._props.layout_fitWidth?.(this.Document) ?? dv?.layoutDoc.layout_fitWidth) && dv?.nativeWidth && dv?.nativeHeight; + const scaling = this.nativeDimScaling; // if freeform has a native aspect, then the panel height needs to be adjusted to match it - const aspect = dv?.nativeWidth && dv?.nativeHeight && !fitWidth ? dv.nativeHeight / dv.nativeWidth : this._props.PanelHeight() / this._props.PanelWidth(); - return this._props.isAnnotationOverlay || this._props.originTopLeft ? 0 : (aspect * this._props.PanelWidth()) / 2 / scaling; // shift so pan position is at center of window for non-overlay collections + const height = aspect ? (dv.nativeHeight / dv.nativeWidth) * this._props.PanelWidth() : this._props.PanelHeight(); + return height / 2 / scaling; // shift so pan position is at center of window for non-overlay collections } @computed get panZoomXf() { return new Transform(this.panX(), this.panY(), 1 / this.zoomScaling()); @@ -197,7 +199,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @computed get screenToFreeformContentsXf() { return this._props .ScreenToLocalTransform() // - .translate(-this.cachedCenteringShiftX, -this.cachedCenteringShiftY) + .translate(-this.centeringShiftX, -this.centeringShiftY) .transform(this.panZoomXf); } @@ -260,8 +262,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document[this.panXFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panX, 1)); panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document[this.panYFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panY, 1)); zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], 1); //, NumCast(DocCast(this.Document.resolvedDataDoc)?.[this.scaleFieldKey], 1)); - PanZoomCenterXf = () => - this._props.isAnnotationOverlay && this.zoomScaling() === 1 ? `` : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`; + PanZoomCenterXf = () => (this._props.isAnnotationOverlay && this.zoomScaling() === 1 ? `` : `translate(${this.centeringShiftX}px, ${this.centeringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`); ScreenToContentsXf = () => this.screenToFreeformContentsXf.copy(); getActiveDocuments = () => this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); isAnyChildContentActive = () => this._props.isAnyChildContentActive(); @@ -1131,9 +1132,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (scale !== undefined) { const maxZoom = 5; // sets the limit for how far we will zoom. this is useful for preventing small text boxes from filling the screen. So probably needs to be more sophisticated to consider more about the target and context const newScale = - scale === 0 - ? NumCast(this.layoutDoc[this.scaleFieldKey]) - : Math.min(maxZoom, (1 / (this.nativeDimScaling || 1)) * scale * Math.min(this._props.PanelWidth() / Math.abs(bounds.width), this._props.PanelHeight() / Math.abs(bounds.height))); + scale === 0 ? NumCast(this.layoutDoc[this.scaleFieldKey]) : Math.min(maxZoom, (1 / this.nativeDimScaling) * scale * Math.min(this._props.PanelWidth() / Math.abs(bounds.width), this._props.PanelHeight() / Math.abs(bounds.height))); return { panX: this._props.isAnnotationOverlay ? bounds.left - (Doc.NativeWidth(this.layoutDoc) / newScale - bounds.width) / 2 : (bounds.left + bounds.right) / 2, panY: this._props.isAnnotationOverlay ? bounds.top - (Doc.NativeHeight(this.layoutDoc) / newScale - bounds.height) / 2 : (bounds.top + bounds.bot) / 2, @@ -1430,7 +1429,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection presentation_transition: 500, annotationOn: this.Document, }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !this.Document.isGroup, type_collection: true, filters: true } }, this.Document); + PresBox.pinDocView( + anchor, + { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ? { ...pinProps.pinData, poslayoutview: pinProps.pinData.dataview } : {}), pannable: !this.Document.isGroup, type_collection: true, filters: true } }, + this.Document + ); if (addAsAnnotation) { if (Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), null) !== undefined) { @@ -1822,8 +1825,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection zoomScaling={this.zoomScaling} layoutDoc={this.layoutDoc} isAnnotationOverlay={this.isAnnotationOverlay} - cachedCenteringShiftX={this.cachedCenteringShiftX} - cachedCenteringShiftY={this.cachedCenteringShiftY} + centeringShiftX={this.centeringShiftX} + centeringShiftY={this.centeringShiftY} /> </div> ); @@ -1872,7 +1875,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } @computed get nativeDimScaling() { - if (this._firstRender || (this._props.isAnnotationOverlay && !this._props.annotationLayerHostsContent)) return 0; + if (this._firstRender || (this._props.isAnnotationOverlay && !this._props.annotationLayerHostsContent)) return 1; const nw = this.nativeWidth; const nh = this.nativeHeight; const hscale = nh ? this._props.PanelHeight() / nh : 1; @@ -1926,9 +1929,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection style={{ pointerEvents: this._props.isContentActive() && SnappingManager.IsDragging ? 'all' : (this._props.pointerEvents?.() as any), textAlign: this.isAnnotationOverlay ? 'initial' : undefined, - transform: `scale(${this.nativeDimScaling || 1})`, - width: `${100 / (this.nativeDimScaling || 1)}%`, - height: this._props.getScrollHeight?.() ?? `${100 / (this.nativeDimScaling || 1)}%`, + transform: `scale(${this.nativeDimScaling})`, + width: `${100 / this.nativeDimScaling}%`, + height: this._props.getScrollHeight?.() ?? `${100 / this.nativeDimScaling}%`, }}> {this.paintFunc ? ( <FormattedTextBox {...this.props} /> // need this so that any live dashfieldviews will update the underlying text that the code eval reads diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss index 6b5738790..e9a346fbe 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.scss +++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss @@ -32,6 +32,10 @@ .liveSchema-checkBox { margin-bottom: -35px; } + + .displaySchemaLive { + margin-bottom: 20px; + } .dataviz-sidebar { position: absolute; diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index 22f1f7b79..60c5fdba2 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -18,7 +18,7 @@ import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponen import { MarqueeAnnotator } from '../../MarqueeAnnotator'; import { SidebarAnnos } from '../../SidebarAnnos'; import { AnchorMenu } from '../../pdf/AnchorMenu'; -import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup'; +import { GPTPopup, GPTPopupMode } from '../../pdf/GPTPopup/GPTPopup'; import { DocumentView } from '../DocumentView'; import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView'; import { PinProps } from '../trails'; @@ -28,6 +28,7 @@ import { LineChart } from './components/LineChart'; import { PieChart } from './components/PieChart'; import { TableBox } from './components/TableBox'; import { Checkbox } from '@mui/material'; +import { ContextMenu } from '../../ContextMenu'; export enum DataVizView { TABLE = 'table', @@ -43,6 +44,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _disposers: { [name: string]: IReactionDisposer } = {}; anchorMenuClick?: () => undefined | ((anchor: Doc) => void); + sidebarAddDoc: ((doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean) | undefined; crop: ((region: Doc | undefined, addCrop?: boolean) => Doc | undefined) | undefined; @observable _marqueeing: number[] | undefined = undefined; @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @@ -400,8 +402,28 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im @action changeLiveSchemaCheckbox = () => { - this.layoutDoc.dataViz_schemaLive = !this.layoutDoc.dataViz_schemaLive; - }; + this.layoutDoc.dataViz_schemaLive = !this.layoutDoc.dataViz_schemaLive + } + + specificContextMenu = (e: React.MouseEvent): void => { + const cm = ContextMenu.Instance; + const options = cm.findByDescription('Options...'); + const optionItems = options && 'subitems' in options ? options.subitems : []; + optionItems.push({ description: `Analyze with AI`, event: () => this.askGPT(), icon: 'lightbulb' }); + !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); + } + + + askGPT = action(async () => { + GPTPopup.Instance.setSidebarId('data_sidebar'); + GPTPopup.Instance.addDoc = this.sidebarAddDocument; + GPTPopup.Instance.setDataJson(""); + GPTPopup.Instance.setMode(GPTPopupMode.DATA); + let data = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href); + let input = JSON.stringify(data); + GPTPopup.Instance.setDataJson(input); + GPTPopup.Instance.generateDataAnalysis(); + }); render() { const scale = this._props.NativeDimScaling?.() || 1; @@ -419,6 +441,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im transform: `scale(${scale})`, position: 'absolute', }} + onContextMenu={this.specificContextMenu} onWheel={e => e.stopPropagation()} ref={this._mainCont}> <div className="datatype-button"> @@ -428,11 +451,13 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im <Toggle text={'PIE CHART'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.PIECHART)} toggleStatus={this.layoutDoc._dataViz == -DataVizView.PIECHART} /> </div> - {this.layoutDoc && this.layoutDoc.dataViz_asSchema ? ( - <div className={'liveSchema-checkBox'} style={{ width: this._props.width }}> + {(this.layoutDoc && this.layoutDoc.dataViz_asSchema)?( + <div className={'displaySchemaLive'}> + <div className={'liveSchema-checkBox'} style={{ width: this._props.width }}> <Checkbox color="primary" onChange={this.changeLiveSchemaCheckbox} checked={this.layoutDoc.dataViz_schemaLive as boolean} /> Display Live Updates to Canvas </div> + </div> ) : null} {this.renderVizView} diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss index 41ce637ac..cf0007cfd 100644 --- a/src/client/views/nodes/DataVizBox/components/Chart.scss +++ b/src/client/views/nodes/DataVizBox/components/Chart.scss @@ -120,11 +120,62 @@ } } } -.selectAll-buttons { - display: flex; - flex-direction: row; - justify-content: flex-end; +.tableBox-selectButtons { margin-top: 5px; - margin-right: 10px; - float: right; + margin-left: 25px; + display: inline-block; + padding: 2px; + .tableBox-selectTitle { + display: inline-flex; + flex-direction: row; + } + .tableBox-filtering { + display: flex; + flex-direction: row; + float: right; + margin-right: 10px; + .tableBox-filterAll { + min-width: 75px; + } + } +} + +.tableBox-filterPopup { + background: $light-gray; + position: absolute; + min-width: 235px; + top: 60px; + display: flex; + flex-direction: column; + align-items: flex-start; + z-index: 2; + padding: 7px; + border-radius: 5px; + margin: 3px; + .tableBox-filterPopup-selectColumn { + margin-top: 5px; + flex-direction: row; + .tableBox-filterPopup-selectColumn-each { + margin-left: 25px; + border-radius: 3px; + background: $light-gray; + } + } + .tableBox-filterPopup-setValue { + margin-top: 5px; + display: flex; + flex-direction: row; + .tableBox-filterPopup-setValue-each { + margin-right: 5px; + border-radius: 3px; + background: $light-gray; + } + .tableBox-filterPopup-setValue-input { + margin: 5px; + } + } + .tableBox-filterPopup-setFilter { + margin-top: 5px; + align-self: center; + } } diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index 53d1869d9..67e1c67bd 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -12,6 +12,7 @@ import { ObservableReactComponent } from '../../../ObservableReactComponent'; import { DocumentView } from '../../DocumentView'; import { DataVizView } from '../DataVizBox'; import './Chart.scss'; +import { undoable } from '../../../../util/UndoManager'; const { DATA_VIZ_TABLE_ROW_HEIGHT } = require('../../../global/globalCssVariables.module.scss'); // prettier-ignore interface TableBoxProps { Document: Doc; @@ -37,6 +38,13 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { _inputChangedDisposer?: IReactionDisposer; _containerRef: HTMLDivElement | null = null; + @observable settingTitle: boolean = false; // true when setting a title column + @observable hasRowsToFilter: boolean = false; // true when any rows are selected + @observable filtering: boolean = false; // true when the filtering menu is open + @observable filteringColumn: any = ''; // column to filter + @observable filteringType: string = 'Value'; // "Value" or "Range" + filteringVal: any[] = ['', '']; // value or range to filter the column with + @observable _scrollTop = -1; @observable _tableHeight = 0; @observable _tableContainerHeight = 0; @@ -49,6 +57,8 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { // if the tableData changes (ie., when records are selected by the parent (input) visulization), // then we need to remove any selected rows that are no longer part of the visualized dataset. this._inputChangedDisposer = reaction(() => this._tableData.slice(), this.filterSelectedRowsDown, { fireImmediately: true }); + const selected = NumListCast(this._props.layoutDoc.dataViz_selectedRows); + if (selected.length > 0) this.hasRowsToFilter = true; this.handleScroll(); } componentWillUnmount() { @@ -64,9 +74,6 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { @computed get parentViz() { return DocCast(this._props.Document.dataViz_parentViz); - // return LinkManager.Instance.getAllRelatedLinks(this._props.Document) // out of all links - // .filter(link => link.link_anchor_1 == this._props.Document.dataViz_parentViz) // get links where this chart doc is the target of the link - // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link } @computed get columns() { @@ -115,6 +122,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { } else selected?.push(rowId); } e.stopPropagation(); + this.hasRowsToFilter = selected.length > 0 ? true : false; }; columnPointerDown = (e: React.PointerEvent, col: string) => { @@ -155,7 +163,8 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { }, emptyFunction, action(e => { - if (e.shiftKey) { + if (e.shiftKey || this.settingTitle) { + if (this.settingTitle) this.settingTitle = false; if (this._props.titleCol == col) this._props.titleCol = ''; else this._props.titleCol = col; this._props.selectTitleCol(this._props.titleCol); @@ -170,6 +179,134 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { ); }; + /** + * These functions handle the filtering popup for when the "filter" button is pressed to select rows + */ + filter = undoable((e: any) => { + var start: any; + var end: any; + if (this.filteringType == 'Range') { + start = (this.filteringVal[0] as Number) ? Number(this.filteringVal[0]) : this.filteringVal[0]; + end = (this.filteringVal[1] as Number) ? Number(this.filteringVal[1]) : this.filteringVal[0]; + } + + this._tableDataIds.forEach(rowID => { + if (this.filteringType == 'Value') { + if (this._props.records[rowID][this.filteringColumn] == this.filteringVal[0]) { + if (!NumListCast(this._props.layoutDoc.dataViz_selectedRows).includes(rowID)) { + this.tableRowClick(e, rowID); + } + } + } else { + let compare = this._props.records[rowID][this.filteringColumn]; + if (compare as Number) compare = Number(compare); + if (start <= compare && compare <= end) { + if (!NumListCast(this._props.layoutDoc.dataViz_selectedRows).includes(rowID)) { + this.tableRowClick(e, rowID); + } + } + } + }); + this.filtering = false; + this.filteringColumn = ''; + this.filteringVal = ['', '']; + }, 'filter table'); + @action + setFilterColumn = (e: any) => { + this.filteringColumn = e.currentTarget.value; + }; + @action + setFilterType = (e: any) => { + this.filteringType = e.currentTarget.value; + }; + changeFilterValue = action((e: React.ChangeEvent<HTMLInputElement>) => { + this.filteringVal[0] = e.target.value; + }); + changeFilterRange0 = action((e: React.ChangeEvent<HTMLInputElement>) => { + this.filteringVal[0] = e.target.value; + }); + changeFilterRange1 = action((e: React.ChangeEvent<HTMLInputElement>) => { + this.filteringVal[1] = e.target.value; + }); + @computed get renderFiltering() { + if (this.filteringColumn === '') this.filteringColumn = this.columns[0]; + return ( + <div className="tableBox-filterPopup" style={{ right: this._props.width * 0.05 }}> + <div className="tableBox-filterPopup-selectColumn"> + Column: + <select className="tableBox-filterPopup-selectColumn-each" value={this.filteringColumn != '' ? this.filteringColumn : this.columns[0]} onChange={e => this.setFilterColumn(e)}> + {this.columns.map(column => ( + <option className="" key={column} value={column}> + {' '} + {column}{' '} + </option> + ))} + </select> + </div> + <div className="tableBox-filterPopup-setValue"> + <select className="tableBox-filterPopup-setValue-each" value={this.filteringType} onChange={e => this.setFilterType(e)}> + <option className="" key={'Value'} value={'Value'}> + {' '} + {'Value'}{' '} + </option> + <option className="" key={'Range'} value={'Range'}> + {' '} + {'Range'}{' '} + </option> + </select> + : + {this.filteringType == 'Value' ? ( + <input + className="tableBox-filterPopup-setValue-input" + defaultValue="" + autoComplete="off" + onChange={this.changeFilterValue} + onKeyDown={e => { + e.stopPropagation(); + }} + type="text" + placeholder="" + id="search-input" + /> + ) : ( + <div> + <input + className="tableBox-filterPopup-setValue-input" + defaultValue="" + autoComplete="off" + onChange={this.changeFilterRange0} + onKeyDown={e => { + e.stopPropagation(); + }} + type="text" + placeholder="" + id="search-input" + style={{ width: this._props.width * 0.15 }} + /> + to + <input + className="tableBox-filterPopup-setValue-input" + defaultValue="" + autoComplete="off" + onChange={this.changeFilterRange1} + onKeyDown={e => { + e.stopPropagation(); + }} + type="text" + placeholder="" + id="search-input" + style={{ width: this._props.width * 0.15 }} + /> + </div> + )} + </div> + <div className="tableBox-filterPopup-setFilter"> + <Button onClick={action(e => this.filter(e))} text="Set Filter" type={Type.SEC} color={'black'} /> + </div> + </div> + ); + } + render() { if (this._tableData.length > 0) { return ( @@ -183,9 +320,39 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { this._props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds); } }}> - <div className="selectAll-buttons"> - <Button onClick={action(() => (this._props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds)))} text="Select All" type={Type.SEC} color={'black'} /> - <Button onClick={action(() => (this._props.layoutDoc.dataViz_selectedRows = new List<number>()))} text="Deselect All" type={Type.SEC} color={'black'} /> + <div className="tableBox-selectButtons"> + <div className="tableBox-selectTitle"> + <Button onClick={action(() => (this.settingTitle = !this.settingTitle))} text="Select Title Column" type={Type.SEC} color={'black'} /> + </div> + <div className="tableBox-filtering"> + {this.filtering ? this.renderFiltering : null} + <Button onClick={action(() => (this.filtering = !this.filtering))} text="Filter" type={Type.SEC} color={'black'} /> + <div className="tableBox-filterAll"> + {this.hasRowsToFilter ? ( + <Button + onClick={action(() => { + this._props.layoutDoc.dataViz_selectedRows = new List<number>(); + this.hasRowsToFilter = false; + })} + text="Deselect All" + type={Type.SEC} + color={'black'} + tooltip="Select rows to be displayed in any DataViz boxes dragged off of this one." + /> + ) : ( + <Button + onClick={action(() => { + this._props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds); + this.hasRowsToFilter = true; + })} + text="Select All" + type={Type.SEC} + color={'black'} + tooltip="Select rows to be displayed in any DataViz boxes dragged off of this one." + /> + )} + </div> + </div> </div> <div className={`tableBox-container ${this.columns[0]}`} @@ -227,17 +394,15 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] == col) ? 'darkblue' : undefined, - background: - this._props.axes.slice().reverse().lastElement() === col - ? '#E3fbdb' - : this._props.axes.length > 2 && this._props.axes.lastElement() === col - ? '#Fbdbdb' - : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] == col) - ? '#c6ebf7' - : undefined, - // blue: #ADD8E6 - // green: #E3fbdb - // red: #Fbdbdb + background: this.settingTitle + ? 'lightgrey' + : this._props.axes.slice().reverse().lastElement() === col + ? '#E3fbdb' + : this._props.axes.length > 2 && this._props.axes.lastElement() === col + ? '#Fbdbdb' + : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] == col) + ? '#c6ebf7' + : undefined, fontWeight: 'bolder', border: '3px solid black', }} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index b372c1927..5897dc105 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1417,6 +1417,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { } } }; + backgroundColor = () => this._docViewInternal?.backgroundBoxColor; DataTransition = () => this._props.DataTransition?.() || StrCast(this.Document.dataTransition); ShouldNotScale = () => this.shouldNotScale; NativeWidth = () => this.effectiveNativeWidth; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index f07c2d191..34f03399a 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -8,7 +8,7 @@ import { history } from 'prosemirror-history'; import { inputRules } from 'prosemirror-inputrules'; import { keymap } from 'prosemirror-keymap'; import { Fragment, Mark, Node, Slice } from 'prosemirror-model'; -import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state'; +import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transaction } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import * as React from 'react'; import { BsMarkdownFill } from 'react-icons/bs'; @@ -985,10 +985,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB animateRes = (resIndex: number, newText: string) => { if (resIndex < newText.length) { const marks = this._editorView?.state.storedMarks ?? []; - this._editorView?.dispatch(this._editorView.state.tr.setStoredMarks(marks).insertText(newText[resIndex]).setStoredMarks(marks)); - setTimeout(() => { - this.animateRes(resIndex + 1, newText); - }, 20); + this._editorView?.dispatch(this._editorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(marks)); + setTimeout(() => this.animateRes(resIndex + 1, newText), 20); } }; @@ -997,8 +995,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION); if (!res) { this.animateRes(0, 'Something went wrong.'); - } else { - this.animateRes(0, res); + } else if (this._editorView) { + const { dispatch, state } = this._editorView; + // for no animation, use: dispatch(state.tr.insertText(res)); + // for animted response starting at end of text, use: + dispatch(state.tr.setSelection(Selection.atEnd(state.doc))); + this.animateRes(0, '\n\n' + res); } } catch (err) { console.error(err); @@ -1509,6 +1511,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); } else if (curText && !FormattedTextBox.DontSelectInitialText) { selectAll(this._editorView.state, this._editorView?.dispatch); + this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data } } if (selectOnLoad) { diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 632636c85..cbbba1690 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -544,8 +544,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // Case 2: Last slide and presLoop is toggled ON or it is in Edit mode this.nextSlide(0); progressiveReveal(true); // shows first progressive document, but without a transition effect + return 0; } - return 0; + return false; } return this.itemIndex; }; @@ -1186,7 +1187,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const func = () => { const delay = NumCast(this.activeItem.presentation_duration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presentation_transition); this._presTimer = setTimeout(() => { - if (!this.next()) this.layoutDoc.presentation_status = this._exitTrail?.() ?? PresStatus.Manual; + if (this.next() === false) this.layoutDoc.presentation_status = this._exitTrail?.() ?? PresStatus.Manual; this.layoutDoc.presentation_status === PresStatus.Autoplay && func(); }, delay); }; @@ -1288,7 +1289,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } } else if (doc.type !== DocumentType.PRES) { if (!doc.presentation_targetDoc) doc.title = doc.title + ' - Slide'; - doc.presentation_targetDoc = doc.createdFrom; // dropped document will be a new embedding of an embedded document somewhere else. + doc.presentation_targetDoc = doc.createdFrom ?? doc; // dropped document will be a new embedding of an embedded document somewhere else. doc.presentation_movement = PresMovement.Zoom; if (this._expandBoolean) doc.presentation_expandInlineButton = true; } diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 1c6b38aeb..f78e29821 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -61,7 +61,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { // Since this node is being rendered with a template, this method retrieves // the actual slide being rendered from the auto-generated rendering template @computed get slideDoc() { - return this._props.TemplateDataDocument ?? this.Document; + return DocCast(this.Document.rootDocument, this.Document); } // this is the document in the workspaces that is targeted by the slide diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index 59f191af0..0b3ba81d3 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -6,7 +6,6 @@ import * as React from 'react'; import { ColorResult } from 'react-color'; import { Utils, returnFalse, setupMoveUpEvents, unimplementedFunction } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; -import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT'; import { DocumentType } from '../../documents/DocumentTypes'; import { SelectionManager } from '../../util/SelectionManager'; import { SettingsManager } from '../../util/SettingsManager'; @@ -73,18 +72,8 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { * @param e pointer down event */ gptSummarize = async (e: React.PointerEvent) => { - // move this logic to gptpopup, need to implement generate again - GPTPopup.Instance.setVisible(true); - GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY); - GPTPopup.Instance.setLoading(true); - - try { - const res = await gptAPICall(this.selectedText, GPTCallType.SUMMARY); - GPTPopup.Instance.setText(res || 'Something went wrong.'); - } catch (err) { - console.error(err); - } - GPTPopup.Instance.setLoading(false); + GPTPopup.Instance?.setSelectedText(this.selectedText); + GPTPopup.Instance.generateSummary(); }; pointerDown = (e: React.PointerEvent) => { diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index cb5137c83..e312441ed 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, IconButton, Type } from 'browndash-components'; +import { Button, EditableText, IconButton, Size, Type } from 'browndash-components'; import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -10,7 +10,7 @@ import { Utils } from '../../../../Utils'; import { Doc } from '../../../../fields/Doc'; import { NumCast, StrCast } from '../../../../fields/Types'; import { Networking } from '../../../Network'; -import { gptImageCall } from '../../../apis/gpt/GPT'; +import { GPTCallType, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT'; import { DocUtils, Docs } from '../../../documents/Documents'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { AnchorMenu } from '../AnchorMenu'; @@ -20,6 +20,7 @@ export enum GPTPopupMode { SUMMARY, EDIT, IMAGE, + DATA, } interface GPTPopupProps {} @@ -27,6 +28,7 @@ interface GPTPopupProps {} @observer export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { static Instance: GPTPopup; + @observable private chatMode: boolean = false; @observable public visible: boolean = false; @@ -46,6 +48,20 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { public setText = (text: string) => { this.text = text; }; + @observable + public selectedText: string = ''; + @action + public setSelectedText = (text: string) => { + this.selectedText = text; + }; + @observable + public dataJson: string = ''; + public dataChatPrompt: string | null = null; + @action + public setDataJson = (text: string) => { + if (text == '') this.dataChatPrompt = ''; + this.dataJson = text; + }; @observable public imgDesc: string = ''; @@ -79,6 +95,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { @action public setDone = (done: boolean) => { this.done = done; + this.chatMode = false; }; // change what can be a ref into a ref @@ -118,20 +135,47 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { try { let image_urls = await gptImageCall(this.imgDesc); + console.log('Image urls: ', image_urls); if (image_urls && image_urls[0]) { // need to fix this const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [image_urls[0]] }); - console.log('Result', result); - console.log('Client', result.accessPaths.agnostic.client); + console.log('Upload result: ', result); const source = Utils.prepend(result.accessPaths.agnostic.client); + console.log('Upload source: ', source); this.setImgUrls([[image_urls[0], source]]); } } catch (err) { - console.log(err); + console.error(err); } this.setLoading(false); }; + generateSummary = async () => { + GPTPopup.Instance.setVisible(true); + GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY); + GPTPopup.Instance.setLoading(true); + + try { + const res = await gptAPICall(this.selectedText, GPTCallType.SUMMARY); + GPTPopup.Instance.setText(res || 'Something went wrong.'); + } catch (err) { + console.error(err); + } + GPTPopup.Instance.setLoading(false); + }; + + generateDataAnalysis = async () => { + GPTPopup.Instance.setVisible(true); + GPTPopup.Instance.setLoading(true); + try { + let res = await gptAPICall(this.dataJson, GPTCallType.DATA, this.dataChatPrompt); + GPTPopup.Instance.setText(res || 'Something went wrong.'); + } catch (err) { + console.error(err); + } + GPTPopup.Instance.setLoading(false); + }; + /** * Transfers the summarization text to a sidebar annotation text document. */ @@ -177,6 +221,16 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { DocUtils.MakeLink(textAnchor, newDoc, { link_relationship: 'Image Prompt' }); }; + /** + * Creates a chatbox for analyzing data so that users can ask specific questions. + */ + private chatWithAI = () => { + this.chatMode = true; + }; + dataPromptChanged = action((e: React.ChangeEvent<HTMLInputElement>) => { + this.dataChatPrompt = e.target.value; + }); + private getPreviewUrl = (source: string) => source.split('.').join('_m.'); constructor(props: GPTPopupProps) { @@ -216,31 +270,6 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { ); }; - data = () => { - return ( - <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> - {this.heading('GENERATED IMAGE')} - <div className="image-content-wrapper"> - {this.imgUrls.map(rawSrc => ( - <div className="img-wrapper"> - <div className="img-container"> - <img key={rawSrc[0]} src={rawSrc[0]} width={150} height={150} alt="dalle generation" /> - </div> - <div className="btn-container"> - <Button text="Save Image" onClick={() => this.transferToImage(rawSrc[1])} color={StrCast(Doc.UserDoc().userColor)} type={Type.TERT} /> - </div> - </div> - ))} - </div> - {!this.loading && ( - <> - <IconButton tooltip="Generate Again" onClick={this.generateImage} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} /> - </> - )} - </div> - ); - }; - summaryBox = () => ( <> <div> @@ -258,7 +287,6 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { }, 500); }, ]} - //cursor={{ hideWhenDone: true }} /> ) : ( this.text @@ -269,7 +297,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { <div className="btns-wrapper"> {this.done ? ( <> - <IconButton tooltip="Generate Again" onClick={this.callSummaryApi} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} /> + <IconButton tooltip="Generate Again" onClick={this.generateSummary} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} /> <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} /> </> ) : ( @@ -291,6 +319,72 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { </> ); + dataAnalysisBox = () => ( + <> + <div> + {this.heading('ANALYSIS')} + <div className="content-wrapper"> + {!this.loading && + (!this.done ? ( + <TypeAnimation + speed={50} + sequence={[ + this.text, + () => { + setTimeout(() => { + this.setDone(true); + }, 500); + }, + ]} + /> + ) : ( + this.text + ))} + </div> + </div> + {!this.loading && ( + <div className="btns-wrapper"> + {this.done ? ( + this.chatMode ? ( + <input + defaultValue="" + autoComplete="off" + onChange={this.dataPromptChanged} + onKeyDown={e => { + e.key === 'Enter' ? this.generateDataAnalysis() : null; + e.stopPropagation(); + }} + type="text" + placeholder="Ask GPT a question about the data..." + id="search-input" + className="searchBox-input" + style={{ width: '100%' }} + /> + ) : ( + <> + <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} /> + <Button tooltip="Chat with AI" text="Chat with AI" onClick={this.chatWithAI} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} /> + </> + ) + ) : ( + <div className="summarizing"> + <span>Summarizing</span> + <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} /> + <Button + text="Stop Animation" + onClick={() => { + this.setDone(true); + }} + color={StrCast(Doc.UserDoc().userVariantColor)} + type={Type.TERT} + /> + </div> + )} + </div> + )} + </> + ); + aiWarning = () => this.done ? ( <div className="ai-warning"> @@ -311,7 +405,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { render() { return ( <div className="summary-box" style={{ display: this.visible ? 'flex' : 'none' }}> - {this.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : <></>} + {this.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.mode === GPTPopupMode.DATA ? this.dataAnalysisBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : <></>} </div> ); } diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index a582f8004..aaff2a342 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -30,7 +30,7 @@ const _global = (window /* browser */ || global) /* node */ as any; //pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`; // The workerSrc property shall be specified. -Pdfjs.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@4.0.379/build/pdf.worker.mjs'; +Pdfjs.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@4.1.392/build/pdf.worker.mjs'; interface IViewerProps extends FieldViewProps { pdfBox: PDFBox; |