diff options
18 files changed, 203 insertions, 115 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index a13edec77..1d7c73306 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -175,7 +175,7 @@ class DateInfo extends FInfo { } class RtfInfo extends FInfo { constructor(d: string, filterable?: boolean) { - super(d, true); + super(d); this.filterable = filterable; } fieldType? = FInfoFieldType.rtf; diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts index 40c3f76fb..359140732 100644 --- a/src/client/util/SnappingManager.ts +++ b/src/client/util/SnappingManager.ts @@ -9,6 +9,7 @@ export class SnappingManager { @observable _shiftKey = false; @observable _ctrlKey = false; + @observable _metaKey = false; @observable _isLinkFollowing = false; @observable _isDragging: boolean = false; @observable _isResizing: Doc | undefined = undefined; @@ -32,6 +33,7 @@ export class SnappingManager { public static get VertSnapLines() { return this.Instance._vertSnapLines; } // prettier-ignore public static get ShiftKey() { return this.Instance._shiftKey; } // prettier-ignore public static get CtrlKey() { return this.Instance._ctrlKey; } // prettier-ignore + public static get MetaKey() { return this.Instance._metaKey; } // prettier-ignore public static get IsLinkFollowing(){ return this.Instance._isLinkFollowing; } // prettier-ignore public static get IsDragging() { return this.Instance._isDragging; } // prettier-ignore public static get IsResizing() { return this.Instance._isResizing; } // prettier-ignore @@ -39,7 +41,8 @@ export class SnappingManager { public static get ExploreMode() { return this.Instance._exploreMode; } // prettier-ignore public static SetShiftKey = (down: boolean) => runInAction(() => (this.Instance._shiftKey = down)); // prettier-ignore public static SetCtrlKey = (down: boolean) => runInAction(() => (this.Instance._ctrlKey = down)); // prettier-ignore - public static SetIsLinkFollowing= (follow: boolean) => runInAction(() => (this.Instance._isLinkFollowing = follow)); // prettier-ignore + public static SetMetaKey = (down: boolean) => runInAction(() => (this.Instance._metaKey = down)); // prettier-ignore + public static SetIsLinkFollowing= (follow:boolean)=> runInAction(() => (this.Instance._isLinkFollowing = follow)); // prettier-ignore public static SetIsDragging = (drag: boolean) => runInAction(() => (this.Instance._isDragging = drag)); // prettier-ignore public static SetIsResizing = (doc: Opt<Doc>) => runInAction(() => (this.Instance._isResizing = doc)); // prettier-ignore public static SetCanEmbed = (embed:boolean) => runInAction(() => (this.Instance._canEmbed = embed)); // prettier-ignore diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index e800798ca..667d8dbb0 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -55,10 +55,12 @@ export class KeyManager { public handleModifiers = action((e: KeyboardEvent) => { if (e.shiftKey) SnappingManager.SetShiftKey(true); if (e.ctrlKey) SnappingManager.SetCtrlKey(true); + if (e.metaKey) SnappingManager.SetMetaKey(true); }); public unhandleModifiers = action((e: KeyboardEvent) => { if (!e.shiftKey) SnappingManager.SetShiftKey(false); if (!e.ctrlKey) SnappingManager.SetCtrlKey(false); + if (!e.metaKey) SnappingManager.SetMetaKey(false); }); public handle = action((e: KeyboardEvent) => { diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index ea1caf58f..2b23935eb 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -225,6 +225,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection componentWillUnmount() { super.componentWillUnmount(); + this.observer.disconnect(); Object.keys(this._disposers).forEach(key => this._disposers[key]()); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 7bfbbf3f9..b2fb5848e 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -249,7 +249,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // this search order, for example, allows icons of cropped images to find the panx/pany/zoom on the cropped image's data doc instead of the usual layout doc because the zoom/panX/panY define the cropped image 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], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.[this.scaleFieldKey], 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)`; ScreenToContentsXf = () => this.screenToFreeformContentsXf.copy(); diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index 12f0ad5e9..4a0ca8fe5 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -1,5 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, makeObservable, observable, ObservableMap, observe, trace } from 'mobx'; +import { Popup, PopupTrigger, Type } from 'browndash-components'; +import { action, computed, makeObservable, observable, ObservableMap, observe } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, Field, NumListCast, Opt, StrListCast } from '../../../../fields/Doc'; @@ -12,12 +13,13 @@ import { Docs, DocumentOptions, DocUtils, FInfo } from '../../../documents/Docum import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager, dropActionType } from '../../../util/DragManager'; import { SelectionManager } from '../../../util/SelectionManager'; +import { SettingsManager } from '../../../util/SettingsManager'; import { undoable, undoBatch } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; import { EditableView } from '../../EditableView'; import { Colors } from '../../global/globalEnums'; import { DocumentView } from '../../nodes/DocumentView'; -import { FocusViewOptions, FieldViewProps } from '../../nodes/FieldView'; +import { FieldViewProps, FocusViewOptions } from '../../nodes/FieldView'; import { KeyValueBox } from '../../nodes/KeyValueBox'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { DefaultStyleProvider, StyleProp } from '../../StyleProvider'; @@ -25,8 +27,7 @@ import { CollectionSubView } from '../CollectionSubView'; import './CollectionSchemaView.scss'; import { SchemaColumnHeader } from './SchemaColumnHeader'; import { SchemaRowBox } from './SchemaRowBox'; -import { Popup, PopupTrigger, Type } from 'browndash-components'; -import { SettingsManager } from '../../../util/SettingsManager'; +import { DocData } from '../../../../fields/DocSymbols'; const { default: { SCHEMA_NEW_NODE_HEIGHT } } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore export enum ColumnType { @@ -284,7 +285,7 @@ export class CollectionSchemaView extends CollectionSubView() { }; @action - addNewKey = (key: string, defaultVal: any) => this.childDocs.forEach(doc => (doc[key] = defaultVal)); + addNewKey = (key: string, defaultVal: any) => this.childDocs.forEach(doc => (doc[DocData][key] = defaultVal)); @undoBatch removeColumn = (index: number) => { diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx index ed1b519b4..711ef507c 100644 --- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx @@ -1,8 +1,11 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Popup, Size, Type } from 'browndash-components'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import { extname } from 'path'; import * as React from 'react'; import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; import Select from 'react-select'; import { Utils, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../../Utils'; import { DateField } from '../../../../fields/DateField'; @@ -12,6 +15,8 @@ import { BoolCast, Cast, DateCast, DocCast, FieldValue, StrCast } from '../../.. import { ImageField } from '../../../../fields/URLField'; import { FInfo, FInfoFieldType } from '../../../documents/Documents'; import { DocFocusOrOpen } from '../../../util/DocumentManager'; +import { dropActionType } from '../../../util/DragManager'; +import { SettingsManager } from '../../../util/SettingsManager'; import { Transform } from '../../../util/Transform'; import { undoBatch, undoable } from '../../../util/UndoManager'; import { EditableView } from '../../EditableView'; @@ -24,12 +29,7 @@ import { KeyValueBox } from '../../nodes/KeyValueBox'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { ColumnType, FInfotoColType } from './CollectionSchemaView'; import './CollectionSchemaView.scss'; -import 'react-datepicker/dist/react-datepicker.css'; -import { Popup, Size, Type } from 'browndash-components'; -import { IconLookup, faCaretDown } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { SettingsManager } from '../../../util/SettingsManager'; -import { dropActionType } from '../../../util/DragManager'; +import { SnappingManager } from '../../../util/SnappingManager'; export interface SchemaTableCellProps { Document: Doc; @@ -129,10 +129,12 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro contents={undefined} fieldContents={fieldProps} editing={this.selected ? undefined : false} - GetValue={() => Field.toKeyValueString(this._props.Document, this._props.fieldKey)} + GetValue={() => Field.toKeyValueString(this._props.Document, this._props.fieldKey, SnappingManager.MetaKey)} SetValue={undoable((value: string, shiftDown?: boolean, enterKey?: boolean) => { if (shiftDown && enterKey) { this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), value); + this._props.finishEdit?.(); + return true; } const ret = KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), value, Doc.IsDataProto(this._props.Document) ? true : undefined); this._props.finishEdit?.(); @@ -345,8 +347,7 @@ export class SchemaBoolCell extends ObservableReactComponent<SchemaTableCellProp onChange={undoBatch((value: React.ChangeEvent<HTMLInputElement> | undefined) => { if ((value?.nativeEvent as any).shiftKey) { this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString()); - } - KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString()); + } else KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString()); })} /> <EditableView @@ -357,8 +358,10 @@ export class SchemaBoolCell extends ObservableReactComponent<SchemaTableCellProp SetValue={undoBatch((value: string, shiftDown?: boolean, enterKey?: boolean) => { if (shiftDown && enterKey) { this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), value); + this._props.finishEdit?.(); + return true; } - const set = KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), value); + const set = KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), value, Doc.IsDataProto(this._props.Document) ? true : undefined); this._props.finishEdit?.(); return set; })} diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index e759030f5..715b23fb6 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -4,6 +4,7 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; +import { RichTextField } from '../../../fields/RichTextField'; import { DocCast, NumCast, RTFCast, StrCast } from '../../../fields/Types'; import { DocUtils, Docs } from '../../documents/Documents'; import { DragManager, dropActionType } from '../../util/DragManager'; @@ -13,11 +14,9 @@ import { StyleProp } from '../StyleProvider'; import './ComparisonBox.scss'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; -import { PinProps, PresBox } from './trails'; +import { KeyValueBox } from './KeyValueBox'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; -import { RichTextField } from '../../../fields/RichTextField'; -import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT'; -import { DocData } from '../../../fields/DocSymbols'; +import { PinProps, PresBox } from './trails'; @observer export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { @@ -173,10 +172,17 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() </div> ); }; + + /** + * Display the Docs in the before/after fields of the comparison. This also supports a GPT flash card use case + * where if there are no Docs in the slots, but the main fieldKey contains text, then + * @param which + * @returns + */ const displayDoc = (which: string) => { const whichDoc = DocCast(this.dataDoc[which]); const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc); - const subjectText = RTFCast(this.Document[this.fieldKey])?.Text; + const subjectText = RTFCast(this.Document[this.fieldKey])?.Text.trim(); // if there is no Doc in the first comparison slot, but the comparison box's fieldKey slot has a RichTextField, then render a text box to show the contents of the document's field key slot // of if there is no Doc in the second comparison slot, but the second slot has a RichTextField, then render a text box to show the contents of the document's field key slot const layoutTemplateString = !targetDoc @@ -188,15 +194,18 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() : undefined; // A bit hacky to try out the concept of using GPT to fill in flashcards -- this whole process should probably be packaged into a script to be more generic. - // If the second slot doesn't have anything in it, but the fieldKey slot has text - // and the fieldKey + "_alternate" has a text that incldues "--TEXT--", then - // treat the fieldKey + "_altenrate" text as a GPT query parameterized by the fieldKey text - // Call GPT to fill in an "answer" value in the second slot. + // If the second slot doesn't have anything in it, but the fieldKey slot has text (e.g., this.text is a string) + // and the fieldKey + "_alternate" has text, then treat the _alternate's text as a GPT query (indicated by (( && )) ) that is parameterized (optionally) + // by the field references in the text (eg., this.text_alternate is + // "((Provide a one sentence definition for (this) that doesn't use any word in (this.excludeWords) ))" + // where (this) is replaced by the text in the fieldKey slot abd this.excludeWords is repalced by the conetnts of the excludeWords field + // A GPT call will put the "answer" in the second slot of the comparison (eg., text_2) if (which.endsWith('2') && !layoutTemplateString && !targetDoc) { - const queryText = RTFCast(this.Document[this.fieldKey + '_alternate'])?.Text; - if (queryText?.includes('--TEXT--') && subjectText) { - this.Document[DocData][this.fieldKey + '_2'] = ''; - gptAPICall(queryText?.replace('--TEXT--', subjectText), GPTCallType.COMPLETION).then(value => (this.Document[DocData][this.fieldKey + '_2'] = value.trim())); + var queryText = RTFCast(this.Document[this.fieldKey + '_alternate']) + ?.Text.replace('(this)', subjectText) // TODO: this should be done in KeyValueBox.setField but it doesn't know about the fieldKey ... + .trim(); + if (subjectText && queryText.match(/\(\(.*\)\)/)) { + KeyValueBox.SetField(this.Document, which, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt } } return targetDoc || layoutTemplateString ? ( diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index bbeacef88..9848f18e0 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1215,7 +1215,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { if (layout_fieldKey && layout_fieldKey !== 'layout' && layout_fieldKey !== 'layout_icon') this.Document.deiconifyLayout = layout_fieldKey.replace('layout_', ''); } else { const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null); - this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished); + this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished, true); this.Document.deiconifyLayout = undefined; this._props.bringToFront?.(this.Document); } diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 8a49b4757..4ecaaa283 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -11,6 +11,7 @@ import { ViewBoxInterface } from '../DocComponent'; import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView'; import { DocumentView, OpenWhere } from './DocumentView'; import { PinProps } from './trails'; +import { computed } from 'mobx'; export interface FocusViewOptions { willPan?: boolean; // determines whether to pan to target document @@ -121,9 +122,12 @@ export class FieldView extends React.Component<FieldViewProps> { public static LayoutString(fieldType: { name: string }, fieldStr: string) { return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={'data'} />" } + @computed get fieldval() { + return this.props.Document[this.props.fieldKey]; + } render() { - const field = this.props.Document[this.props.fieldKey]; + const field = this.fieldval; // prettier-ignore if (field instanceof Doc) return <p> <b>{field.title?.toString()}</b></p>; if (field === undefined) return <p>{'<null>'}</p>; diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 89a5ac0b8..2257e6455 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -6,7 +6,7 @@ import { Doc, Field, FieldResult } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { RichTextField } from '../../../fields/RichTextField'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; -import { DocCast } from '../../../fields/Types'; +import { DocCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { Docs } from '../../documents/Documents'; import { SetupDrag } from '../../util/DragManager'; @@ -71,34 +71,51 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> { } } }; - public static CompileKVPScript(value: string): KVPScript | undefined { - const eq = value.startsWith('='); - value = eq ? value.substring(1) : value; - const dubEq = value.startsWith(':=') ? 'computed' : value.startsWith('$=') ? 'script' : false; - value = dubEq ? value.substring(2) : value; - const options: ScriptOptions = { addReturn: true, typecheck: false, params: { this: Doc.name, self: Doc.name, documentView: 'any', _last_: 'any', _readOnly_: 'boolean' }, editable: true }; - if (dubEq) options.typecheck = false; - const script = CompileScript(value, { ...options, transformer: DocumentIconContainer.getTransformer() }); - return !script.compiled ? undefined : { script, type: dubEq, onDelegate: eq }; + /** + * this compiles a string as a script after parsing off initial characters that determine script parameters + * if the script starts with '=', then it will be stored on the delegate of the Doc, otherise on the data doc + * if the script then starts with a ':=', then it will be treated as ComputedField, + * '$=', then it will just be a Script + * @param value + * @returns + */ + public static CompileKVPScript(rawvalue: string): KVPScript | undefined { + const onDelegate = rawvalue.startsWith('='); + rawvalue = onDelegate ? rawvalue.substring(1) : rawvalue; + const type: 'computed' | 'script' | false = rawvalue.startsWith(':=') ? 'computed' : rawvalue.startsWith('$=') ? 'script' : false; + rawvalue = type ? rawvalue.substring(2) : rawvalue; + rawvalue = rawvalue.replace(/.*\(\((.*)\)\)/, 'dashCallChat(_setCacheResult_, this, "$1")'); + const value = ["'", '"', '`'].includes(rawvalue.length ? rawvalue[0] : '') || !isNaN(rawvalue as any) ? rawvalue : '`' + rawvalue + '`'; + + var script = ScriptField.CompileScript(rawvalue, {}, true, undefined, DocumentIconContainer.getTransformer()); + if (!script.compiled) { + script = ScriptField.CompileScript(value, {}, true, undefined, DocumentIconContainer.getTransformer()); + } + return !script.compiled ? undefined : { script, type, onDelegate }; } - public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean): boolean { + public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) { const { script, type, onDelegate } = kvpScript; //const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : DocCast(doc.proto, doc); - let field: Field; - if (type === 'computed') { - field = new ComputedField(script); - } else if (type === 'script') { - field = new ScriptField(script); - } else { - const res = script.run({ this: Doc.Layout(doc), self: doc }, console.log); - if (!res.success) { - target[key] = script.originalScript; - return true; + let field: Field | undefined; + switch (type) { + case 'computed': field = new ComputedField(script); break; // prettier-ignore + case 'script': field = new ScriptField(script); break; // prettier-ignore + default: { + const _setCacheResult_ = (value: FieldResult) => { + field = value as Field; + setResult?.(value); + }; + const res = script.run({ this: Doc.Layout(doc), self: doc, _setCacheResult_ }, console.log); + if (!res.success) { + if (key) target[key] = script.originalScript; + return false; + } + field === undefined && (field = res.result); } - field = res.result; } + if (!key) return field; if (Field.IsField(field, true) && (key !== 'proto' || field !== target)) { target[key] = field; return true; @@ -107,10 +124,10 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> { } @undoBatch - public static SetField(doc: Doc, key: string, value: string, forceOnDelegate?: boolean) { + public static SetField(doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) { const script = this.CompileKVPScript(value); if (!script) return false; - return this.ApplyKVPScript(doc, key, script, forceOnDelegate); + return this.ApplyKVPScript(doc, key, script, forceOnDelegate, setResult); } onPointerDown = (e: React.PointerEvent): void => { diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index f9e8ce4f3..d59489a78 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -125,7 +125,7 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> { pinToPres: returnZero, }} GetValue={() => Field.toKeyValueString(this._props.doc, this._props.keyName)} - SetValue={(value: string) => KeyValueBox.SetField(this._props.doc, this._props.keyName, value)} + SetValue={(value: string) => (KeyValueBox.SetField(this._props.doc, this._props.keyName, value) ? true : false)} /> </div> </td> diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 5c4d850ad..62cb460c2 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -137,7 +137,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi col={0} deselectCell={emptyFunction} selectCell={emptyFunction} - maxWidth={this._props.hideKey ? undefined : this.return100} + maxWidth={this._props.hideKey ? undefined : this._props.tbox._props.PanelWidth} columnWidth={this._props.hideKey ? () => this._props.tbox._props.PanelWidth() - 20 : returnZero} selectedCell={() => [this._dashDoc!, 0]} fieldKey={this._fieldKey} diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index d5c91fc09..c798ae4b3 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -1,6 +1,6 @@ import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from 'prosemirror-inputrules'; import { NodeSelection, TextSelection } from 'prosemirror-state'; -import { Doc, StrListCast } from '../../../../fields/Doc'; +import { Doc, FieldResult, StrListCast } from '../../../../fields/Doc'; import { DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; @@ -8,13 +8,14 @@ import { NumCast, StrCast } from '../../../../fields/Types'; import { Utils } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; +import { CollectionViewType } from '../../../documents/DocumentTypes'; +import { CollectionView } from '../../collections/CollectionView'; +import { ContextMenu } from '../../ContextMenu'; +import { KeyValueBox } from '../KeyValueBox'; import { FormattedTextBox } from './FormattedTextBox'; import { wrappingInputRule } from './prosemirrorPatches'; import { RichTextMenu } from './RichTextMenu'; import { schema } from './schema_rts'; -import { CollectionView } from '../../collections/CollectionView'; -import { CollectionViewType } from '../../../documents/DocumentTypes'; -import { ContextMenu } from '../../ContextMenu'; export class RichTextRules { public Document: Doc; @@ -282,18 +283,28 @@ export class RichTextRules { : tr; }), + new InputRule(new RegExp(/(^|[^=])(\(\(.*\)\))/), (state, match, start, end) => { + var count = 0; // ignore first return value which will be the notation that chat is pending a result + KeyValueBox.SetField(this.Document, '', match[2], false, (gptval: FieldResult) => { + count && this.TextBox.EditorView?.dispatch(this.TextBox.EditorView!.state.tr.insertText(' ' + (gptval as string))); + count++; + }); + return null; + }), + // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document // [[<fieldKey> : <Doc>]] // [[:docTitle]] => hyperlink // [[fieldKey]] => show field - // [[fieldKey=value]] => show field and also set its value + // [[fieldKey{:,=:}=value]] => show field and also set its value // [[fieldKey:docTitle]] => show field of doc new InputRule( - new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-z,A-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), + new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)((=:|:)?=)([a-z,A-Z_@\?+\-*/\ 0-9\(\)]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), (state, match, start, end) => { const fieldKey = match[1]; - const docTitle = match[3]?.replace(':', ''); - const value = match[2]?.substring(1); + const assign = match[2] === '=' ? '' : match[2]; + const value = match[4]; + const docTitle = match[5]?.replace(':', ''); const linkToDoc = (target: Doc) => { const rstate = this.TextBox.EditorView?.state; const selection = rstate?.selection.$from.pos; @@ -325,13 +336,14 @@ export class RichTextRules { } return state.tr; } - if (value?.includes(',')) { + // if the value has commas assume its an array (unless it's part of a chat gpt call indicated by '((' ) + if (value?.includes(',') && !value.startsWith('((')) { const values = value.split(','); const strs = values.some(v => !v.match(/^[-]?[0-9.]$/)); this.Document[DocData][fieldKey] = strs ? new List<string>(values) : new List<number>(values.map(v => Number(v))); - } else if (value !== '' && value !== undefined) { - const num = value.match(/^[0-9.]$/); - this.Document[DocData][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value; + } else if (value) { + KeyValueBox.SetField(this.Document, fieldKey, assign + value, Doc.IsDataProto(this.Document) ? true : undefined, assign ? undefined: + (gptval: FieldResult) => this.Document[DocData][fieldKey] = gptval as string ); // prettier-ignore } const target = getTitledDoc(docTitle); const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false }); diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index 4706a97fa..c9115be90 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -1,4 +1,3 @@ -import * as React from 'react'; import { DOMOutputSpec, Node, NodeSpec } from 'prosemirror-model'; import { listItem, orderedList } from 'prosemirror-schema-list'; import { ParagraphNodeSpec, toParagraphDOM, getParagraphNodeAttrs } from './ParagraphNodeSpec'; diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 921d7aa5d..30f5f716c 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -34,14 +34,29 @@ import * as JSZip from 'jszip'; import { FieldViewProps } from '../client/views/nodes/FieldView'; export const LinkedTo = '-linkedTo'; export namespace Field { - export function toKeyValueString(doc: Doc, key: string): string { - const onDelegate = Object.keys(doc).includes(key.replace(/^_/, '')); + /** + * Converts a field to its equivalent input string in the key value box such that if the string + * is entered into a keyValueBox it will create an equivalent field (except if showComputedValue is set). + * @param doc doc containing key + * @param key field key to display + * @param showComputedValue whether copmuted function should display its value instead of its function + * @returns string representation of the field + */ + export function toKeyValueString(doc: Doc, key: string, showComputedValue?: boolean): string { + const onDelegate = !Doc.IsDataProto(doc) && Object.keys(doc).includes(key.replace(/^_/, '')); const field = ComputedField.WithoutComputed(() => FieldValue(doc[key])); return !Field.IsField(field) ? key.startsWith('_') ? '=' : '' - : (onDelegate ? '=' : '') + (field instanceof ComputedField ? `:=${field.script.originalScript}` : field instanceof ScriptField ? `$=${field.script.originalScript}` : Field.toScriptString(field)); + : (onDelegate ? '=' : '') + + (field instanceof ComputedField && showComputedValue + ? field._lastComputedResult + : field instanceof ComputedField + ? `:=${field.script.originalScript}` + : field instanceof ScriptField + ? `$=${field.script.originalScript}` + : Field.toScriptString(field)); } export function toScriptString(field: Field) { switch (typeof field) { diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index c7fe72ca6..9021c8896 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -1,15 +1,17 @@ +import { action, makeObservable, observable } from 'mobx'; import { computedFn } from 'mobx-utils'; import { createSimpleSchema, custom, map, object, primitive, PropSchema, serializable, SKIP } from 'serializr'; import { DocServer } from '../client/DocServer'; -import { CompiledScript, CompileScript, ScriptOptions } from '../client/util/Scripting'; +import { CompiledScript, CompileScript, ScriptOptions, Transformer } from '../client/util/Scripting'; import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { autoObject, Deserializable } from '../client/util/SerializationHelper'; import { numberRange } from '../Utils'; -import { Doc, Field, Opt } from './Doc'; -import { Copy, Id, ToJavascriptString, ToScriptString, ToString, ToValue } from './FieldSymbols'; +import { Doc, Field, FieldResult, Opt } from './Doc'; +import { Copy, FieldChanged, Id, ToJavascriptString, ToScriptString, ToString, ToValue } from './FieldSymbols'; import { List } from './List'; import { ObjectField } from './ObjectField'; import { Cast, StrCast } from './Types'; +import { GPTCallType, gptAPICall } from '../client/apis/gpt/GPT'; function optional(propSchema: PropSchema) { return custom( @@ -85,6 +87,13 @@ export class ScriptField extends ObjectField { readonly script: CompiledScript; @serializable(object(scriptSchema)) readonly setterscript: CompiledScript | undefined; + @serializable + @observable + _cachedResult: FieldResult = undefined; + setCacheResult = action((value: FieldResult) => { + this._cachedResult = value; + this[FieldChanged]?.(); + }); @serializable(autoObject()) captures?: List<string>; @@ -122,21 +131,25 @@ export class ScriptField extends ObjectField { [ToString]() { return this.script.originalScript; } - public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { + public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Doc | string | number | boolean }, transformer?: Transformer) { return CompileScript(script, { params: { this: Doc?.name || 'Doc', // this is the doc that executes the script self: Doc?.name || 'Doc', // self is the root doc of the doc that executes the script + documentView: 'any', _last_: 'any', // _last_ is the previous value of a computed field when it is being triggered to re-run. + _setCacheResult_: 'any', // set the cached value of the function _readOnly_: 'boolean', // _readOnly_ is set when a computed field is executed to indicate that it should not have mobx side-effects. used for checking the value of a set function (see FontIconBox) ...params, }, + transformer, typecheck: false, editable: true, addReturn: addReturn, capturedVariables, }); } + public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { const compiled = ScriptField.CompileScript(script, params, true, capturedVariables); return compiled.compiled ? new ScriptField(compiled) : undefined; @@ -146,29 +159,62 @@ export class ScriptField extends ObjectField { const compiled = ScriptField.CompileScript(script, params, false, capturedVariables); return compiled.compiled ? new ScriptField(compiled) : undefined; } + public static CallGpt(queryText: string, setVal: (val: FieldResult) => void, target: Doc) { + if (typeof queryText === 'string' && setVal) { + while (queryText.match(/\(this\.[a-zA-Z_]*\)/)?.length) { + const fieldRef = queryText.split('(this.')[1].replace(/\).*/, ''); + queryText = queryText.replace(/\(this\.[a-zA-Z_]*\)/, Field.toString(target[fieldRef] as Field)); + } + setVal(`Chat Pending: ${queryText}`); + gptAPICall(queryText, GPTCallType.COMPLETION).then(result => { + if (queryText.includes('#')) { + const matches = result.match(/-?[0-9][0-9,]+[.]?[0-9]*/); + if (matches?.length) setVal(Number(matches[0].replace(/,/g, ''))); + } else setVal(result.trim()); + }); + } + } } @scriptingGlobal @Deserializable('computed', deserializeScript) export class ComputedField extends ScriptField { - _lastComputedResult: any; - //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc - value = computedFn((doc: Doc) => this._valueOutsideReaction(doc)); - _valueOutsideReaction = (doc: Doc) => (this._lastComputedResult = this.script.compiled && this.script.run({ this: doc, self: doc, value: '', _last_: this._lastComputedResult, _readOnly_: true }, console.log).result); - - [ToValue](doc: Doc) { - return ComputedField.toValue(doc, this); + static undefined = '__undefined'; + static useComputed = true; + static DisableComputedFields() { this.useComputed = false; } // prettier-ignore + static EnableComputedFields() { this.useComputed = true; } // prettier-ignore + static WithoutComputed<T>(fn: () => T) { + this.DisableComputedFields(); + try { + return fn(); + } finally { + this.EnableComputedFields(); + } } - [Copy](): ObjectField { - return new ComputedField(this.script, this.setterscript, this.rawscript); + + constructor(script: CompiledScript | undefined, setterscript?: CompiledScript, rawscript?: string) { + super(script, setterscript, rawscript); + makeObservable(this); } + _lastComputedResult: FieldResult; + value = computedFn((doc: Doc) => this._valueOutsideReaction(doc)); + _valueOutsideReaction = (doc: Doc) => { + this._lastComputedResult = + this._cachedResult ?? (this.script.compiled && this.script.run({ this: doc, self: doc, value: '', _setCacheResult_: this.setCacheResult, _last_: this._lastComputedResult, _readOnly_: true }, console.log).result); + return this._lastComputedResult; + }; + + [ToValue](doc: Doc) { if (ComputedField.useComputed) return { value: this._valueOutsideReaction(doc) }; } // prettier-ignore + [Copy](): ObjectField { return new ComputedField(this.script, this.setterscript, this.rawscript); } // prettier-ignore + public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }, setterscript?: string) { const compiled = ScriptField.CompileScript(script, params, true, { value: '', ...capturedVariables }); const compiledsetter = setterscript ? ScriptField.CompileScript(setterscript, { ...params, value: 'any' }, false, capturedVariables) : undefined; const compiledsetscript = compiledsetter?.compiled ? compiledsetter : undefined; return compiled.compiled ? new ComputedField(compiled, compiledsetscript) : undefined; } + public static MakeInterpolatedNumber(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number, defaultVal: Opt<number>) { if (!doc[`${fieldKey}_indexed`]) { const flist = new List<number>(numberRange(curTimecode + 1).map(i => undefined) as any as number[]); @@ -206,33 +252,6 @@ export class ComputedField extends ScriptField { return (doc[`${fieldKey}`] = getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined); } } -export namespace ComputedField { - let useComputed = true; - export function DisableComputedFields() { - useComputed = false; - } - - export function EnableComputedFields() { - useComputed = true; - } - - export const undefined = '__undefined'; - - export function WithoutComputed<T>(fn: () => T) { - DisableComputedFields(); - try { - return fn(); - } finally { - EnableComputedFields(); - } - } - - export function toValue(doc: any, value: any) { - if (useComputed) { - return { value: value._valueOutsideReaction(doc) }; - } - } -} ScriptingGlobals.add( function setIndexVal(list: any[], index: number, value: any) { @@ -258,3 +277,6 @@ ScriptingGlobals.add( 'returns the value at a given index of a list', '(list: any[], index: number)' ); +ScriptingGlobals.add(function dashCallChat(setVal: (val: FieldResult) => void, target: Doc, queryText: string) { + ScriptField.CallGpt(queryText, setVal, target); +}, 'calls chat gpt for the query string and then calls setVal with the result'); diff --git a/src/server/public/assets/documentation.png b/src/server/public/assets/documentation.png Binary files differnew file mode 100644 index 000000000..95c76b198 --- /dev/null +++ b/src/server/public/assets/documentation.png |