From 26cd21b5148d07e0be39cd6997723b34a70ebab6 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 5 Mar 2024 12:16:53 -0500
Subject: added mermaid and poltly to tools list

---
 src/client/views/nodes/ComparisonBox.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

(limited to 'src/client/views/nodes/ComparisonBox.tsx')

diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 116dc48a6..ef8c045cc 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -6,7 +6,7 @@ import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents }
 import { Doc, Opt } from '../../../fields/Doc';
 import { DocCast, NumCast, StrCast } from '../../../fields/Types';
 import { DocUtils, Docs } from '../../documents/Documents';
-import { DragManager } from '../../util/DragManager';
+import { DragManager, dropActionType } from '../../util/DragManager';
 import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
 import { StyleProp } from '../StyleProvider';
@@ -135,7 +135,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             this,
             e,
             e => {
-                const de = new DragManager.DocumentDragData([DocCast(this.dataDoc[which])], 'move');
+                const de = new DragManager.DocumentDragData([DocCast(this.dataDoc[which])], dropActionType.move);
                 de.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
                     this.clearDoc(which);
                     return addDocument(doc);
-- 
cgit v1.2.3-70-g09d2


From c563aec906c5728a5563fefac6ab573a31375641 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 12 Mar 2024 09:09:59 -0400
Subject: made text templates be both layout templates and prototypes of new
 text documents.  fixed onPaint funcs to be undoable.  fixed comparisonBox to
 render a text box if it's fieldKey has a richtext field - this makes
 flashcard templates much easier.  fixed right-click on hyperlinks to bring up
 menu.   fixed layout_centered to be settable on templates.  added enable
 flashcard property for text.

---
 src/client/documents/Documents.ts                  | 32 ++++++++++++----------
 src/client/util/CurrentUserUtils.ts                |  4 +--
 src/client/views/PropertiesButtons.tsx             | 21 ++++++++++++--
 src/client/views/StyleProvider.tsx                 |  2 +-
 src/client/views/nodes/ComparisonBox.tsx           | 13 ++++++---
 src/client/views/nodes/DocumentView.tsx            | 27 ++++++++++++++----
 src/client/views/nodes/KeyValueBox.tsx             |  2 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  4 +--
 .../views/nodes/formattedText/RichTextMenu.tsx     |  4 +--
 .../views/nodes/formattedText/RichTextRules.ts     |  2 +-
 10 files changed, 75 insertions(+), 36 deletions(-)

(limited to 'src/client/views/nodes/ComparisonBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 7a3b965fe..a13edec77 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1982,27 +1982,29 @@ export namespace DocUtils {
     }
 
     export function GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, annotationOn?: Doc, backgroundColor?: string) {
+        const defaultTextTemplate = DocCast(Doc.UserDoc().defaultTextLayout);
         const tbox = Docs.Create.TextDocument('', {
             annotationOn,
             backgroundColor,
-            _width: width || 200,
-            _height: 35,
-            x: x,
-            y: y,
-            _layout_centered: BoolCast(Doc.UserDoc().layout_centered),
-            _layout_fitWidth: true,
-            _layout_autoHeight: true,
-            _layout_enableAltContentUI: BoolCast(Doc.UserDoc().defaultToFlashcards),
+            x,
+            y,
             title,
+            ...(defaultTextTemplate
+                ? {} // if the new doc will inherit from a template, don't set any layout fields since that would block the inheritance
+                : {
+                      _width: width || 200,
+                      _height: 35,
+                      _layout_centered: BoolCast(Doc.UserDoc()._layout_centered),
+                      _layout_fitWidth: true,
+                      _layout_autoHeight: true,
+                      _layout_enableAltContentUI: BoolCast(Doc.UserDoc().defaultToFlashcards),
+                  }),
         });
 
-        const template = Doc.UserDoc().defaultTextLayout;
-        if (template instanceof Doc) {
-            // if a default text template is specified
-            tbox._width = NumCast(template._width);
-            tbox.layout_fieldKey = 'layout_' + StrCast(template.title);
-            Doc.GetProto(tbox)[StrCast(tbox.layout_fieldKey)] = template; // set the text doc's layout to render with the text template
-            tbox[DocData].proto = template; // and also set the text doc to inherit from the template (this allows the template to specify default field values)
+        if (defaultTextTemplate) {
+            tbox.layout_fieldKey = 'layout_' + StrCast(defaultTextTemplate.title);
+            Doc.GetProto(tbox)[StrCast(tbox.layout_fieldKey)] = defaultTextTemplate; // set the text doc's layout to render with the text template
+            tbox[DocData].proto = defaultTextTemplate; // and also set the text doc to inherit from the template (this allows the template to specify default field values)
         }
         return tbox;
     }
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index fc315fdbe..84a33500d 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -276,7 +276,7 @@ export class CurrentUserUtils {
             slide[DocData].text  = rtfield;
             slide[DocData].layout_textPainted = `<CollectionView {...props} fieldKey={'text'}/>`;
             slide[DocData]._type_collection = CollectionViewType.Freeform;
-            slide.onPaint = ScriptField.MakeScript(`toggleDetail(documentView, "textPainted", "")`, {documentView:"any"});
+            slide.onPaint = ScriptField.MakeScript(`toggleDetail(documentView, "textPainted")`, {documentView:"any"});
             return slide; 
         }
         const mermaidsApi = () => {
@@ -330,7 +330,7 @@ pie title Minerals in my tap water
             slide[DocData].text  = rtfield;
             slide[DocData].layout_textPainted = `<CollectionView {...props} fieldKey={'text'}/>`;
             slide[DocData]._type_collection = CollectionViewType.Freeform;
-            slide.onPaint = ScriptField.MakeScript(`toggleDetail(documentView, "textPainted", "")`, {documentView:"any"});
+            slide.onPaint = ScriptField.MakeScript(`toggleDetail(documentView, "textPainted")`, {documentView:"any"});
             return slide; 
         }
         const apis = [plotlyApi(), mermaidsApi()]
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 3cb835e39..02f288a68 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -13,7 +13,8 @@ import { RxWidth } from 'react-icons/rx';
 import { TbEditCircle, TbEditCircleOff, TbHandOff, TbHandStop, TbHighlight, TbHighlightOff } from 'react-icons/tb';
 import { TfiBarChart } from 'react-icons/tfi';
 import { Doc, DocListCast, Opt } from '../../fields/Doc';
-import { RichTextField } from '../../fields/RichTextField';
+import { DocData } from '../../fields/DocSymbols';
+import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, ScriptCast } from '../../fields/Types';
 import { ImageField } from '../../fields/URLField';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
@@ -27,6 +28,7 @@ import { InkingStroke } from './InkingStroke';
 import './PropertiesButtons.scss';
 import { Colors } from './global/globalEnums';
 import { DocumentView, OpenWhere } from './nodes/DocumentView';
+import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 
 @observer
 export class PropertiesButtons extends React.Component<{}, {}> {
@@ -174,6 +176,20 @@ export class PropertiesButtons extends React.Component<{}, {}> {
         );
     }
 
+    @computed get flashcardButton() {
+        return this.propertyToggleBtn(
+            on => (on ? 'DISABLE FLASHCARD' : 'ENABLE FLASHCARD'),
+            'layout_textPainted',
+            on => `${on ? 'Flashcard enabled' : 'Flashcard disabled'} `,
+            on => <MdTouchApp />,
+            (dv, doc) => {
+                const on = doc.onPaint ? true : false;
+                doc[DocData].onPaint = on ? undefined : ScriptField.MakeScript(`toggleDetail(documentView, "textPainted")`, { documentView: 'any' });
+                doc[DocData].layout_textPainted = on ? undefined : `<ComparisonBox {...props} fieldKey={'${dv?.LayoutFieldKey ?? 'text'}'}/>`;
+            }
+        );
+    }
+
     @computed get fitContentButton() {
         return this.propertyToggleBtn(
             on => (on ? 'PREVIOUS VIEW' : 'VIEW ALL'), //'View All',
@@ -481,7 +497,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
 
     render() {
         const layoutField = this.selectedDoc?.[Doc.LayoutFieldKey(this.selectedDoc)];
-        const isText = layoutField instanceof RichTextField;
+        const isText = SelectionManager.Views.lastElement()?.ComponentView instanceof FormattedTextBox;
         const isInk = this.selectedDoc?.layout_isSvg;
         const isImage = layoutField instanceof ImageField;
         const isMap = this.selectedDoc?.type === DocumentType.MAP;
@@ -507,6 +523,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                 {/* {toggle(this.freezeThumb)} */}
                 {toggle(this.forceActiveButton)}
                 {toggle(this.verticalAlignButton, { display: !isText ? 'none' : '' })}
+                {toggle(this.flashcardButton, { display: !isText ? 'none' : '' })}
                 {toggle(this.fitContentButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
                 {/* {toggle(this.isLightboxButton, { display: !isFreeForm && !isMap ? 'none' : '' })} */}
                 {toggle(this.layout_autoHeightButton, { display: !isText && !isStacking && !isTree ? 'none' : '' })}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 0794efe4c..ab811858a 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -61,7 +61,7 @@ function togglePaintView(e: React.MouseEvent, doc: Opt<Doc>, props: Opt<FieldVie
         value: undefined,
     };
     e.stopPropagation();
-    UndoManager.RunInBatch(() => doc && ScriptCast(doc.onPaint).script.run(scriptProps), 'togglePaintView');
+    ScriptCast(doc?.onPaint)?.script.run(scriptProps);
 }
 
 export function wavyBorderPath(pw: number, ph: number, inset: number = 0.05) {
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index ef8c045cc..2b57178f4 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -14,6 +14,8 @@ import './ComparisonBox.scss';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { PinProps, PresBox } from './trails';
+import { FormattedTextBox } from './formattedText/FormattedTextBox';
+import { RichTextField } from '../../../fields/RichTextField';
 
 @observer
 export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
@@ -172,12 +174,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         const displayDoc = (which: string) => {
             const whichDoc = DocCast(this.dataDoc[which]);
             const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc);
-            return targetDoc ? (
+            // 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
+            const layoutTemplateString = !targetDoc && which.endsWith('1') && this.Document[this.fieldKey] instanceof RichTextField ? FormattedTextBox.LayoutString(this.fieldKey) : undefined;
+            return targetDoc || layoutTemplateString ? (
                 <>
                     <DocumentView
                         {...this._props}
-                        Document={targetDoc}
-                        TemplateDataDocument={undefined}
+                        renderDepth={this.props.renderDepth + 1}
+                        LayoutTemplateString={layoutTemplateString}
+                        Document={layoutTemplateString ? this.Document : targetDoc}
                         containerViewPath={this.DocumentView?.().docViewPath}
                         moveDocument={which.endsWith('1') ? this.moveDoc1 : this.moveDoc2}
                         removeDocument={which.endsWith('1') ? this.remDoc1 : this.remDoc2}
@@ -190,7 +195,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         hideLinkButton={true}
                         pointerEvents={this._isAnyChildContentActive ? undefined : returnNone}
                     />
-                    {clearButton(which)}
+                    {layoutTemplateString ? null : clearButton(which)}
                 </> // placeholder image if doc is missing
             ) : (
                 <div className="placeholder">
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 29266bd8e..40592c2cd 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -457,11 +457,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
 
     deleteClicked = undoable(() => this._props.removeDocument?.(this.Document), 'delete doc');
     setToggleDetail = undoable(
-        (defaultLayout: string, scriptFieldKey: 'onClick') =>
+        (scriptFieldKey: 'onClick') =>
             (this.Document[scriptFieldKey] = ScriptField.MakeScript(
                 `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey)
                     .replace('layout_', '')
-                    .replace(/^layout$/, 'detail')}", "${defaultLayout}")`,
+                    .replace(/^layout$/, 'detail')}")`,
                 { documentView: 'any' }
             )),
         'set toggle detail'
@@ -1201,7 +1201,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     };
     public noOnClick = () => this._docViewInternal?.noOnClick();
     public toggleFollowLink = (zoom?: boolean, setTargetToggle?: boolean): void => this._docViewInternal?.toggleFollowLink(zoom, setTargetToggle);
-    public setToggleDetail = (defaultLayout = '', scriptFieldKey = 'onClick') => this._docViewInternal?.setToggleDetail(defaultLayout, scriptFieldKey);
+    public setToggleDetail = (scriptFieldKey = 'onClick') => this._docViewInternal?.setToggleDetail(scriptFieldKey);
     public onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => this._docViewInternal?.onContextMenu?.(e, pageX, pageY);
     public cleanupPointerEvents = () => this._docViewInternal?.cleanupPointerEvents();
     public startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this._docViewInternal?.startDragging(x, y, dropAction, hideSource);
@@ -1279,7 +1279,22 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         custom && DocUtils.makeCustomViewClicked(this.Document, Docs.Create.StackingDocument, layout, undefined);
     }, 'set custom view');
 
+    /**
+     * This switches between the current view of a Doc and a specified alternate layout view.
+     * The current view of the Doc is stored in the layout_default field so that it can be restored.
+     * If the current view of the Doc is already the specified alternate layout view, this will switch
+     * back to the original layout (stored in layout_default)
+     * @param detailLayoutKeySuffix the name of the alternate layout field key (NOTE: 'layout_' will be prepended to this string to get the actual field nam)
+     */
+    public toggleDetail = (detailLayoutKeySuffix: string) => {
+        const curLayout = StrCast(this.Document.layout_fieldKey).replace('layout_', '').replace('layout', '');
+        if (!this.Document.layout_default && curLayout !== detailLayoutKeySuffix) this.Document.layout_default = curLayout;
+        const defaultLayout = StrCast(this.Document.layout_default);
+        if (this.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) this.switchViews(defaultLayout ? true : false, defaultLayout, undefined, true);
+        else this.switchViews(true, detailLayoutKeySuffix, undefined, true);
+    };
     public switchViews = (custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => {
+        const batch = UndoManager.StartBatch('switchView:' + view);
         runInAction(() => this._docViewInternal && (this._docViewInternal._animateScalingTo = 0.1)); // shrink doc
         setTimeout(
             action(() => {
@@ -1292,6 +1307,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
                 setTimeout(
                     action(() => {
                         this._docViewInternal && (this._docViewInternal._animateScalingTo = 0);
+                        batch.end();
                         finished?.();
                     }),
                     Math.max(0, (this._docViewInternal?.animateScaleTime() ?? 0) - 10)
@@ -1451,9 +1467,8 @@ ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView
     LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightbox, 'layout'); //, 0);
 });
 
-ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string, defaultLayout = '') {
-    if (dv.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(defaultLayout ? true : false, defaultLayout, undefined, true);
-    else dv.switchViews(true, detailLayoutKeySuffix, undefined, true);
+ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) {
+    dv.toggleDetail(detailLayoutKeySuffix);
 });
 
 ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSource: Doc) {
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 39a45693e..89a5ac0b8 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -76,7 +76,7 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
         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, _last_: 'any', _readOnly_: 'boolean' }, editable: true };
+        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 };
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 1ff7274f8..1bd230891 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1574,7 +1574,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         }
         if (!state || !editor || !this.ProseRef?.children[0].className.includes('-focused')) return;
         if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu();
-        else if (this._props.isContentActive()) {
+        else if (this._props.isContentActive() && !e.button) {
             const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY });
             let xpos = pcords?.pos || 0;
             while (xpos > 0 && !state.doc.resolve(xpos).node()?.isTextblock) {
@@ -2074,7 +2074,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                         onScroll={this.onScroll}
                         onDrop={this.ondrop}>
                         <div
-                            className={`formattedTextBox-inner${rounded} ${this.layoutDoc.layout_centered ? 'centered' : ''}`}
+                            className={`formattedTextBox-inner${rounded} ${this.layoutDoc._layout_centered ? 'centered' : ''}`}
                             ref={this.createDropTarget}
                             style={{
                                 padding: StrCast(this.layoutDoc._textBoxPadding),
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index cd0cdaa74..dc2c06701 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -104,7 +104,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         return this._activeAlignment;
     }
     @computed get textVcenter() {
-        return BoolCast(this.layoutDoc?.layout_centered);
+        return BoolCast(this.layoutDoc?._layout_centered);
     }
     _disposer: IReactionDisposer | undefined;
     componentDidMount() {
@@ -450,7 +450,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     }
 
     vcenterToggle = (view: EditorView, dispatch: any) => {
-        this.layoutDoc && (this.layoutDoc.layout_centered = !this.layoutDoc.layout_centered);
+        this.layoutDoc && (this.layoutDoc._layout_centered = !this.layoutDoc._layout_centered);
     };
     align = (view: EditorView, dispatch: any, alignment: 'left' | 'right' | 'center') => {
         if (this.TextView?._props.rootSelected?.()) {
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 9bd41f42c..d5c91fc09 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -78,7 +78,7 @@ export class RichTextRules {
                 this.TextBox.dataDoc[paintedField] = CollectionView.LayoutString(this.TextBox.fieldKey);
                 const layoutFieldKey = StrCast(this.TextBox.layoutDoc.layout_fieldKey); // save the current layout fieldkey
                 this.TextBox.layoutDoc.layout_fieldKey = paintedField; // setup the paint layout field key
-                this.TextBox.DocumentView?.().setToggleDetail(layoutFieldKey.replace('layout_', '').replace('layout', ''), 'onPaint'); // create the script to toggle between the painted and regular view
+                this.TextBox.DocumentView?.().setToggleDetail('onPaint'); // create the script to toggle between the painted and regular view
                 this.TextBox.layoutDoc.layout_fieldKey = layoutFieldKey; // restore the layout field key to text
 
                 return state.tr.delete(start, end).setBlockType(start, start, schema.nodes.code_block);
-- 
cgit v1.2.3-70-g09d2


From 92879a2f211f5e5c3c5fe5b28bd25a6f22d274ee Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 14 Mar 2024 16:34:21 -0400
Subject: Added a GPT capability to comparison boxes ..

---
 src/client/apis/gpt/GPT.ts               |  6 +++---
 src/client/views/nodes/ComparisonBox.tsx | 27 +++++++++++++++++++++++++--
 2 files changed, 28 insertions(+), 5 deletions(-)

(limited to 'src/client/views/nodes/ComparisonBox.tsx')

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index cea862330..fb51278ae 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -14,9 +14,9 @@ type GPTCallOpts = {
 };
 
 const callTypeMap: { [type: string]: GPTCallOpts } = {
-    summary: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: 'Summarize this text in simpler terms: ' },
-    edit: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: 'Reword this: ' },
-    completion: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: '' },
+    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: '' },
 };
 
 /**
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 2b57178f4..e759030f5 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -4,7 +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 { DocCast, NumCast, StrCast } from '../../../fields/Types';
+import { DocCast, NumCast, RTFCast, StrCast } from '../../../fields/Types';
 import { DocUtils, Docs } from '../../documents/Documents';
 import { DragManager, dropActionType } from '../../util/DragManager';
 import { undoBatch } from '../../util/UndoManager';
@@ -16,6 +16,8 @@ import { FieldView, FieldViewProps } from './FieldView';
 import { PinProps, PresBox } from './trails';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { RichTextField } from '../../../fields/RichTextField';
+import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
+import { DocData } from '../../../fields/DocSymbols';
 
 @observer
 export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
@@ -174,8 +176,29 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         const displayDoc = (which: string) => {
             const whichDoc = DocCast(this.dataDoc[which]);
             const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc);
+            const subjectText = RTFCast(this.Document[this.fieldKey])?.Text;
             // 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
-            const layoutTemplateString = !targetDoc && which.endsWith('1') && this.Document[this.fieldKey] instanceof RichTextField ? FormattedTextBox.LayoutString(this.fieldKey) : undefined;
+            // 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
+                ? which.endsWith('1') && subjectText !== undefined
+                    ? FormattedTextBox.LayoutString(this.fieldKey)
+                    : which.endsWith('2') && (this.Document[which] instanceof RichTextField || typeof this.Document[which] === 'string')
+                      ? FormattedTextBox.LayoutString(which)
+                      : undefined
+                : 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 (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()));
+                }
+            }
             return targetDoc || layoutTemplateString ? (
                 <>
                     <DocumentView
-- 
cgit v1.2.3-70-g09d2


From 606088e419f0e146715244d00840349b587c80ba Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 17 Mar 2024 11:50:15 -0400
Subject: use metakey to edit computedfield result instead of expression in
 schema cell,  set default new field values on data doc. fixed stacking view
 from autoresizing when switching to a different collection view.  changed
 syntax for setting fields in text docs to use ':=' for computed fields.  
 Added call to Chat in computed functions when (( )) is used.  Added caching
 of computed function result when a function called by ComputedField uses the
 _setCacheResult_ method (currently only gptCallChat).

---
 src/client/documents/Documents.ts                  |   2 +-
 src/client/util/SnappingManager.ts                 |   5 +-
 src/client/views/GlobalKeyHandler.ts               |   2 +
 .../views/collections/CollectionStackingView.tsx   |   1 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   2 +-
 .../collectionSchema/CollectionSchemaView.tsx      |  11 ++-
 .../collectionSchema/SchemaTableCell.tsx           |  23 +++--
 src/client/views/nodes/ComparisonBox.tsx           |  35 ++++---
 src/client/views/nodes/DocumentView.tsx            |   2 +-
 src/client/views/nodes/FieldView.tsx               |   6 +-
 src/client/views/nodes/KeyValueBox.tsx             |  65 ++++++++-----
 src/client/views/nodes/KeyValuePair.tsx            |   2 +-
 .../views/nodes/formattedText/DashFieldView.tsx    |   2 +-
 .../views/nodes/formattedText/RichTextRules.ts     |  36 +++++---
 src/client/views/nodes/formattedText/nodes_rts.ts  |   1 -
 src/fields/Doc.ts                                  |  21 ++++-
 src/fields/ScriptField.ts                          | 102 +++++++++++++--------
 src/server/public/assets/documentation.png         | Bin 0 -> 4526 bytes
 18 files changed, 203 insertions(+), 115 deletions(-)
 create mode 100644 src/server/public/assets/documentation.png

(limited to 'src/client/views/nodes/ComparisonBox.tsx')

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
new file mode 100644
index 000000000..95c76b198
Binary files /dev/null and b/src/server/public/assets/documentation.png differ
-- 
cgit v1.2.3-70-g09d2


From a974aa4e6573c8becf93f78610406747fec14c1c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 19 Mar 2024 17:08:46 -0400
Subject: cleaned up user templates to not get changed on reload.  made setting
 a template add it to the template tools list and as a tools button.   fixed
 linking to parts of a template.   fixed disappearing templates caused by
 stacking view set a field with an empty key.   updated field assignment
 syntax in trees, dash field views, and key value box to all use :,:=,=,=:=
 syntax.  added text elide button.  added @(title) syntax for hyperlinking. 
 made using a template both inherit from the template to get default values
 and use the template to render.    fixed submenu placement of context menu. 
 updated RTF markdown doc.

---
 src/client/util/CurrentUserUtils.ts                | 19 +++--
 src/client/util/DropConverter.ts                   | 45 +++++-----
 src/client/util/LinkManager.ts                     | 13 +--
 src/client/util/RTFMarkup.tsx                      | 30 +++----
 src/client/views/ContextMenuItem.tsx               |  2 +-
 src/client/views/TemplateMenu.scss                 |  1 +
 src/client/views/TemplateMenu.tsx                  |  7 +-
 .../collections/CollectionMasonryViewFieldRow.tsx  | 10 +--
 .../CollectionStackingViewFieldColumn.tsx          |  6 +-
 src/client/views/collections/TreeView.tsx          | 14 +++-
 src/client/views/global/globalScripts.ts           | 33 +++-----
 src/client/views/nodes/ComparisonBox.tsx           | 71 +++++++++-------
 src/client/views/nodes/DocumentView.tsx            | 32 ++++++-
 src/client/views/nodes/FieldView.tsx               |  1 +
 src/client/views/nodes/FontIconBox/FontIconBox.tsx | 22 ++---
 src/client/views/nodes/KeyValueBox.tsx             |  2 +-
 src/client/views/nodes/KeyValuePair.tsx            |  2 +-
 .../views/nodes/formattedText/DashFieldView.tsx    | 57 +++++++++----
 .../views/nodes/formattedText/FormattedTextBox.tsx | 34 ++++----
 .../views/nodes/formattedText/RichTextMenu.tsx     | 13 +++
 .../views/nodes/formattedText/RichTextRules.ts     | 97 ++++++++++++----------
 src/client/views/nodes/formattedText/marks_rts.ts  |  5 +-
 src/client/views/nodes/formattedText/nodes_rts.ts  |  2 +
 src/fields/Doc.ts                                  | 42 +++++-----
 src/fields/util.ts                                 |  4 +
 25 files changed, 328 insertions(+), 236 deletions(-)

(limited to 'src/client/views/nodes/ComparisonBox.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 84a33500d..bbee18707 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -73,7 +73,7 @@ export class CurrentUserUtils {
         };
         const reqdScripts = { dropConverter : "convertToButtons(dragData)" };
         const reqdFuncs = { /* hidden: "IsNoviceMode()"  */ };
-        return DocUtils.AssignScripts(DocUtils.AssignOpts(userDocTemplates, reqdOpts, userTemplates) ?? Docs.Create.MasonryDocument(userTemplates, reqdOpts), reqdScripts, reqdFuncs);
+        return DocUtils.AssignScripts(userDocTemplates ?? Docs.Create.MasonryDocument(userTemplates, reqdOpts), reqdScripts, reqdFuncs);
     }
 
     /// Initializes templates for editing click funcs of a document 
@@ -133,11 +133,19 @@ export class CurrentUserUtils {
         const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, isSystem: true };
         return DocUtils.AssignOpts(tempNotes, reqdOpts, reqdNoteList) ?? (doc[field] = Docs.Create.TreeDocument(reqdNoteList, reqdOpts));
     }
+    static setupUserTemplates(doc: Doc, field="template_user") {
+        const tempUsers = DocCast(doc[field]);
+        const reqdUserList = DocListCast(tempUsers?.data);
+
+        const reqdOpts:DocumentOptions = { title: "User Layouts", _height: 75, isSystem: true };
+        return DocUtils.AssignOpts(tempUsers, reqdOpts, reqdUserList) ?? (doc[field] = Docs.Create.TreeDocument(reqdUserList, reqdOpts));
+    }
 
     /// Initializes collection of templates for notes and click functions
     static setupDocTemplates(doc: Doc, field="myTemplates") {
         const templates = [
             CurrentUserUtils.setupNoteTemplates(doc),
+            CurrentUserUtils.setupUserTemplates(doc),
             CurrentUserUtils.setupClickEditorTemplates(doc)
         ];
         CurrentUserUtils.setupChildClickEditors(doc)
@@ -375,9 +383,9 @@ pie title Minerals in my tap water
             { toolTip: "Tap or drag to create a flashcard",       title: "Flashcard", icon: "id-card",  dragFactory: doc.emptyFlashcard as Doc,  clickFactory: DocCast(doc.emptyFlashcard)},
             { toolTip: "Tap or drag to create an equation",       title: "Math", icon: "calculator",    dragFactory: doc.emptyEquation as Doc,   clickFactory: DocCast(doc.emptyEquation)},
             { toolTip: "Tap or drag to create a mermaid node",    title: "Mermaids", icon: "rocket",    dragFactory: doc.emptyMermaids as Doc,   clickFactory: DocCast(doc.emptyMermaids)}, 
-            { toolTip: "Tap or drag to create a plotly node",     title: "Plotly", icon: "rocket",      dragFactory: doc.emptyPlotly as Doc,   clickFactory: DocCast(doc.emptyMermaids)}, 
+            { toolTip: "Tap or drag to create a plotly node",     title: "Plotly", icon: "rocket",      dragFactory: doc.emptyPlotly as Doc,     clickFactory: DocCast(doc.emptyMermaids)}, 
             { toolTip: "Tap or drag to create a physics simulation",title: "Simulation", icon: "rocket",dragFactory: doc.emptySimulation as Doc, clickFactory: DocCast(doc.emptySimulation), funcs: { hidden: "IsNoviceMode()"}}, 
-            { toolTip: "Tap or drag to create a note board",      title: "Notes", icon: "book",       dragFactory: doc.emptyNoteboard as Doc,  clickFactory: DocCast(doc.emptyNoteboard)},
+            { toolTip: "Tap or drag to create a note board",      title: "Notes", icon: "book",         dragFactory: doc.emptyNoteboard as Doc,  clickFactory: DocCast(doc.emptyNoteboard)},
             { toolTip: "Tap or drag to create a collection",      title: "Col", icon: "folder",         dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab)},
             { toolTip: "Tap or drag to create a webpage",         title: "Web", icon: "globe-asia",     dragFactory: doc.emptyWebpage as Doc,    clickFactory: DocCast(doc.emptyWebpage)},
             { toolTip: "Tap or drag to create a comparison box",  title: "Compare", icon: "columns",    dragFactory: doc.emptyComparison as Doc, clickFactory: DocCast(doc.emptyComparison)},
@@ -385,10 +393,10 @@ pie title Minerals in my tap water
             { toolTip: "Tap or drag to create a map",             title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc,        clickFactory: DocCast(doc.emptyMap)},
             { toolTip: "Tap or drag to create a screen grabber",  title: "Grab", icon: "photo-video",   dragFactory: doc.emptyScreengrab as Doc, clickFactory: DocCast(doc.emptyScreengrab), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}},
             { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc,     clickFactory: DocCast(doc.emptyWebCam),     openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}},
-            { toolTip: "Tap or drag to create a button",          title: "Button", icon: "circle",        dragFactory: doc.emptyButton as Doc,     clickFactory: DocCast(doc.emptyButton)},
+            { toolTip: "Tap or drag to create a button",          title: "Button", icon: "circle",      dragFactory: doc.emptyButton as Doc,     clickFactory: DocCast(doc.emptyButton)},
             { toolTip: "Tap or drag to create a scripting box",   title: "Script", icon: "terminal",    dragFactory: doc.emptyScript as Doc,     clickFactory: DocCast(doc.emptyScript),                                             funcs: { hidden: "IsNoviceMode()"}},
             { toolTip: "Tap or drag to create a data viz node",   title: "DataViz", icon: "chart-bar",  dragFactory: doc.emptyDataViz as Doc,    clickFactory: DocCast(doc.emptyDataViz)},
-            { toolTip: "Tap or drag to create a bullet slide",    title: "PPT Slide", icon: "person-chalkboard",     dragFactory: doc.emptySlide as Doc,      clickFactory: DocCast(doc.emptySlide),      openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}},
+            { toolTip: "Tap or drag to create a bullet slide",    title: "PPT Slide", icon: "person-chalkboard", dragFactory: doc.emptySlide as Doc,      clickFactory: DocCast(doc.emptySlide),      openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}},
             { toolTip: "Tap or drag to create a view slide",      title: "View Slide", icon: "address-card", dragFactory: doc.emptyViewSlide as Doc,clickFactory: DocCast(doc.emptyViewSlide),openFactoryLocation: OpenWhere.overlay,funcs: { hidden: "IsNoviceMode()"}},
             { toolTip: "Tap or drag to create a data note",       title: "DataNote", icon: "window-maximize",dragFactory: doc.emptyHeader as Doc,clickFactory: DocCast(doc.emptyHeader),     openFactoryAsDelegate: true,            funcs: { hidden: "IsNoviceMode()"} },
             { toolTip: "Toggle a Calculator REPL",                title: "replviewer",  icon: "calculator",                                      clickFactory: '<ScriptingRepl />' as any,   openFactoryLocation: OpenWhere.overlay}, // hack: clickFactory is not a Doc but will get interpreted as a custom UI by the openDoc() onClick script
@@ -738,6 +746,7 @@ pie title Minerals in my tap water
                 { title: "Center",   toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",ignoreClick: true,  scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
                 { title: "Right",    toolTip: "Right align (Cmd-])",  btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", ignoreClick: true,  scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
             ]},
+            { title: "Elide",    toolTip: "Elide selection",      btnType: ButtonType.ToggleButton, icon: "eye",         toolType:"elide",     ignoreClick: true,  scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'}},
             { title: "Dictate",  toolTip: "Dictate",              btnType: ButtonType.ToggleButton, icon: "microphone",  toolType:"dictation", ignoreClick: true,  scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'}},
             { title: "NoLink",   toolTip: "Auto Link",            btnType: ButtonType.ToggleButton, icon: "link",        toolType:"noAutoLink", expertMode:true,   scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}},
             // { title: "Strikethrough", tooltip: "Strikethrough", btnType: ButtonType.ToggleButton, icon: "strikethrough", scripts: {onClick:: 'toggleStrikethrough()'}},
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 3df3e36c6..ed5749d06 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -28,6 +28,7 @@ export function MakeTemplate(doc: Doc) {
 }
 
 /**
+ *
  * Recursively converts 'doc' into a template that can be used to render other documents.
  *
  * For recurive Docs in the template, their target fieldKey is defined by their title,
@@ -75,32 +76,36 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
                 remProps.map(prop => (dbox[prop] = undefined));
             }
         } else if (!doc.onDragStart && !doc.isButtonBar) {
-            const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
-            if (layoutDoc.type !== DocumentType.FONTICON) {
-                !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc);
-            }
-            layoutDoc.isTemplateDoc = true;
-            dbox = Docs.Create.FontIconDocument({
-                _nativeWidth: 100,
-                _nativeHeight: 100,
-                _width: 100,
-                _height: 100,
-                backgroundColor: StrCast(doc.backgroundColor),
-                title: StrCast(layoutDoc.title),
-                btnType: ButtonType.ClickButton,
-                icon: 'bolt',
-                isSystem: false,
-            });
-            dbox.title = ComputedField.MakeFunction('this.dragFactory.title');
-            dbox.dragFactory = layoutDoc;
-            dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined;
-            dbox.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory)');
+            dbox = makeUserTemplateButton(doc);
         } else if (doc.isButtonBar) {
             dbox.ignoreClick = true;
         }
         data.droppedDocuments[i] = dbox;
     });
 }
+export function makeUserTemplateButton(doc: Doc) {
+    const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
+    if (layoutDoc.type !== DocumentType.FONTICON) {
+        !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc);
+    }
+    layoutDoc.isTemplateDoc = true;
+    const dbox = Docs.Create.FontIconDocument({
+        _nativeWidth: 100,
+        _nativeHeight: 100,
+        _width: 100,
+        _height: 100,
+        backgroundColor: StrCast(doc.backgroundColor),
+        title: StrCast(layoutDoc.title),
+        btnType: ButtonType.ClickButton,
+        icon: 'bolt',
+        isSystem: false,
+    });
+    dbox.title = ComputedField.MakeFunction('this.dragFactory.title');
+    dbox.dragFactory = layoutDoc;
+    dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined;
+    dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory)');
+    return dbox;
+}
 ScriptingGlobals.add(
     function convertToButtons(dragData: any) {
         convertDropDataToButtons(dragData as DragManager.DocumentDragData);
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 0c8d18a7a..cf16c4d6d 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -58,8 +58,8 @@ export class LinkManager {
                                 link &&
                                     action(lAnchProtoProtos => {
                                         Doc.AddDocToList(Doc.UserDoc(), 'links', link);
-                                        lAnchs[0] && lAnchs[0][DocData][DirectLinks].add(link);
-                                        lAnchs[1] && lAnchs[1][DocData][DirectLinks].add(link);
+                                        lAnchs[0]?.[DocData][DirectLinks].add(link);
+                                        lAnchs[1]?.[DocData][DirectLinks].add(link);
                                     })
                             )
                         )
@@ -170,10 +170,11 @@ export class LinkManager {
             console.log('WAITING FOR DOC/PROTO IN LINKMANAGER');
             return [];
         }
-        const dirLinks = Doc.GetProto(anchor)[DirectLinks];
-        const annos = DocListCast(anchor[Doc.LayoutFieldKey(anchor) + '_annotations']);
-        if (!annos) debugger;
-        return annos.reduce((list, anno) => [...list, ...LinkManager.Instance.relatedLinker(anno)], Array.from(dirLinks).slice());
+
+        const dirLinks = Array.from(anchor[DocData][DirectLinks]).filter(l => Doc.GetProto(anchor) === anchor[DocData] || ['1', '2'].includes(LinkManager.anchorIndex(l, anchor) as any));
+        const anchorRoot = DocCast(anchor.rootDocument, anchor); // template Doc fields store annotations on the topmost root of a template (not on themselves since the template layout items are only for layout)
+        const annos = DocListCast(anchorRoot[Doc.LayoutFieldKey(anchor) + '_annotations']);
+        return annos.reduce((list, anno) => [...list, ...LinkManager.Instance.relatedLinker(anno)], Array.from(dirLinks));
     }, true);
 
     // returns map of group type to anchor's links in that group type
diff --git a/src/client/util/RTFMarkup.tsx b/src/client/util/RTFMarkup.tsx
index f96d8a5df..315daad42 100644
--- a/src/client/util/RTFMarkup.tsx
+++ b/src/client/util/RTFMarkup.tsx
@@ -3,18 +3,12 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { MainViewModal } from '../views/MainViewModal';
 import { SettingsManager } from './SettingsManager';
-import { Doc } from '../../fields/Doc';
-import { StrCast } from '../../fields/Types';
 
 @observer
 export class RTFMarkup extends React.Component<{}> {
     static Instance: RTFMarkup;
     @observable private isOpen = false; // whether the SharingManager modal is open or not
 
-    // private get linkVisible() {
-    //     return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false;
-    // }
-
     @action
     public open = () => (this.isOpen = true);
 
@@ -39,6 +33,10 @@ export class RTFMarkup extends React.Component<{}> {
                     <b style={{ fontSize: 'larger' }}>{`wiki:phrase`}</b>
                     {` display wikipedia page for entered text (terminate with carriage return)`}
                 </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`(( any text ))`}</b>
+                    {` submit text to Chat GPT to have results appended afterward`}
+                </p>
                 <p>
                     <b style={{ fontSize: 'larger' }}>{`#tag `}</b>
                     {`  add hashtag metadata to document. e.g, #idea`}
@@ -47,10 +45,6 @@ export class RTFMarkup extends React.Component<{}> {
                     <b style={{ fontSize: 'larger' }}>{`#, ## ... ###### `}</b>
                     {`  set heading style based on number of '#'s between 1 and 6`}
                 </p>
-                <p>
-                    <b style={{ fontSize: 'larger' }}>{`#tag `}</b>
-                    {`  add hashtag metadata to document. e.g, #idea`}
-                </p>
                 <p>
                     <b style={{ fontSize: 'larger' }}>{`>> `}</b>
                     {`  add a sidebar text document inline`}
@@ -61,7 +55,7 @@ export class RTFMarkup extends React.Component<{}> {
                 </p>
                 <p>
                     <b style={{ fontSize: 'larger' }}>{`cmd-f `}</b>
-                    {`  collapse to an inline footnote)`}
+                    {`  collapse to an inline footnote`}
                 </p>
                 <p>
                     <b style={{ fontSize: 'larger' }}>{`cmd-e `}</b>
@@ -116,20 +110,20 @@ export class RTFMarkup extends React.Component<{}> {
                     {`  start a block of text that begins with a hanging indent`}
                 </p>
                 <p>
-                    <b style={{ fontSize: 'larger' }}>{`[:doctitle]] `}</b>
+                    <b style={{ fontSize: 'larger' }}>{`@(doctitle) `}</b>
                     {`  hyperlink to document specified by it’s title`}
                 </p>
                 <p>
-                    <b style={{ fontSize: 'larger' }}>{`[[fieldname]] `}</b>
-                    {`  display value of fieldname`}
+                    <b style={{ fontSize: 'larger' }}>{`[@(doctitle.)fieldname] `}</b>
+                    {`  display value of fieldname of text document (unless (doctitle.) is used to indicate another document by it's title)`}
                 </p>
                 <p>
-                    <b style={{ fontSize: 'larger' }}>{`[[fieldname=value]] `}</b>
-                    {`  assign value to fieldname of document and display it`}
+                    <b style={{ fontSize: 'larger' }}>{`[@fieldname:value] `}</b>
+                    {`  assign value to fieldname to data document and display it (if '=' is used instead of ':' the value is set on the layout Doc.  if value is wrapped in (()) then it will be sent to ChatGPT and the response will replace the value)`}
                 </p>
                 <p>
-                    <b style={{ fontSize: 'larger' }}>{`[[fieldname:doctitle]] `}</b>
-                    {`  show value of fieldname from doc specified by it’s title`}
+                    <b style={{ fontSize: 'larger' }}>{`[@fieldname:=expression] `}</b>
+                    {`  assign a computed expression to fieldname to data document and display it (if '=:=' is used instead of ':=' the expression is set on the layout Doc.  if value is wrapped in (()) then it will be sent to ChatGPT and the prompt/response will replace the value)`}
                 </p>
             </div>
         );
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index b2076e1a5..d15ab749c 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -113,7 +113,7 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
                 <div
                     className="contextMenu-subMenu-cont"
                     style={{
-                        marginLeft: window.innerHeight - this._overPosX - 50 > 0 ? '90%' : '20%',
+                        marginLeft: window.innerWidth - this._overPosX - 50 > 0 ? '90%' : '20%',
                         marginTop,
                         background: SettingsManager.userBackgroundColor,
                     }}>
diff --git a/src/client/views/TemplateMenu.scss b/src/client/views/TemplateMenu.scss
index 4d0f1bf00..36a9ce6d0 100644
--- a/src/client/views/TemplateMenu.scss
+++ b/src/client/views/TemplateMenu.scss
@@ -39,6 +39,7 @@
     display: inline-block;
     height: 100%;
     width: 100%;
+    max-height: 250px;
 
     .templateToggle,
     .chromeToggle {
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index eed197b0b..e7269df98 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -1,7 +1,8 @@
-import { action, computed, observable, ObservableSet, runInAction } from 'mobx';
+import { computed, observable, ObservableSet, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast } from '../../fields/Doc';
+import { DocData } from '../../fields/DocSymbols';
 import { ScriptField } from '../../fields/ScriptField';
 import { Cast, DocCast, StrCast } from '../../fields/Types';
 import { TraceMobx } from '../../fields/util';
@@ -9,12 +10,10 @@ import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, retu
 import { Docs, DocUtils } from '../documents/Documents';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
 import { Transform } from '../util/Transform';
-import { undoBatch } from '../util/UndoManager';
 import { CollectionTreeView } from './collections/CollectionTreeView';
 import { DocumentView, returnEmptyDocViewList } from './nodes/DocumentView';
 import { DefaultStyleProvider } from './StyleProvider';
 import './TemplateMenu.scss';
-import { DocData } from '../../fields/DocSymbols';
 
 @observer
 class TemplateToggle extends React.Component<{ template: string; checked: boolean; toggle: (event: React.ChangeEvent<HTMLInputElement>, template: string) => void }> {
@@ -99,7 +98,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
                 .filter(key => !noteTypes.some(nt => nt.title === key))
                 .forEach(template => templateMenu.push(<OtherToggle key={template} name={template} checked={templateName === template} toggle={e => this.toggleLayout(e, template)} />));
         return (
-            <ul className="template-list" style={{ display: 'block' }}>
+            <ul className="template-list">
                 {Doc.noviceMode ? null : <input placeholder="+ layout" ref={this._customRef} onKeyPress={this.onCustomKeypress} />}
                 {templateMenu}
                 <CollectionTreeView
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index 763d2e3a6..7dcfd32bd 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -100,9 +100,8 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
         if (de.complete.docDragData) {
             const key = this._props.pivotField;
             const castedValue = this.getValue(this.heading);
-            const onLayoutDoc = this.onLayoutDoc(key);
             if (this._props.parent.onInternalDrop(e, de)) {
-                de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, !onLayoutDoc));
+                key && de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, !this.onLayoutDoc(key)));
             }
             return true;
         }
@@ -128,7 +127,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
                     return false;
                 }
             }
-            this._props.docList.forEach(d => Doc.SetInPlace(d, key, castedValue, true));
+            key && this._props.docList.forEach(d => Doc.SetInPlace(d, key, castedValue, true));
             this._heading = castedValue.toString();
             return true;
         }
@@ -155,10 +154,9 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
         this._createEmbeddingSelected = false;
         const key = this._props.pivotField;
         const newDoc = Docs.Create.TextDocument('', { _layout_autoHeight: true, _width: 200, _layout_fitWidth: true, title: value });
-        const onLayoutDoc = this.onLayoutDoc(key);
         FormattedTextBox.SetSelectOnLoad(newDoc);
         FormattedTextBox.SelectOnLoadChar = value;
-        (onLayoutDoc ? newDoc : newDoc[DocData])[key] = this.getValue(this._props.heading);
+        key && ((this.onLayoutDoc(key) ? newDoc : newDoc[DocData])[key] = this.getValue(this._props.heading));
         const docs = this._props.parent.childDocList;
         return docs ? (docs.splice(0, 0, newDoc) ? true : false) : this._props.parent._props.addDocument?.(newDoc) || false; // should really extend addDocument to specify insertion point (at beginning of list)
     };
@@ -167,7 +165,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
         action(() => {
             this._createEmbeddingSelected = false;
             const key = this._props.pivotField;
-            this._props.docList.forEach(d => Doc.SetInPlace(d, key, undefined, true));
+            key && this._props.docList.forEach(d => Doc.SetInPlace(d, key, undefined, true));
             if (this._props.parent.colHeaderData && this._props.headingObject) {
                 const index = this._props.parent.colHeaderData.indexOf(this._props.headingObject);
                 this._props.parent.colHeaderData.splice(index, 1);
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 6a3cb759e..641e01b81 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -112,7 +112,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
             if (this._props.colHeaderData?.map(i => i.heading).indexOf(castedValue.toString()) !== -1) {
                 return false;
             }
-            this._props.docList.forEach(d => (d[this._props.pivotField] = castedValue));
+            this._props.pivotField && this._props.docList.forEach(d => (d[this._props.pivotField] = castedValue));
             if (this._props.headingObject) {
                 this._props.headingObject.setHeading(castedValue.toString());
                 this._heading = this._props.headingObject.heading;
@@ -137,7 +137,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
         if (!value && !forceEmptyNote) return false;
         const key = this._props.pivotField;
         const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _layout_fitWidth: true, title: value, _layout_autoHeight: true });
-        newDoc[key] = this.getValue(this._props.heading);
+        key && (newDoc[key] = this.getValue(this._props.heading));
         const maxHeading = this._props.docList.reduce((maxHeading, doc) => (NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading), 0);
         const heading = maxHeading === 0 || this._props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3;
         newDoc.heading = heading;
@@ -148,7 +148,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
 
     @action
     deleteColumn = () => {
-        this._props.docList.forEach(d => (d[this._props.pivotField] = undefined));
+        this._props.pivotField && this._props.docList.forEach(d => (d[this._props.pivotField] = undefined));
         if (this._props.colHeaderData && this._props.headingObject) {
             const index = this._props.colHeaderData.indexOf(this._props.headingObject);
             this._props.colHeaderData.splice(index, 1);
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 08c00d696..1b4349f44 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -574,11 +574,21 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
             <div style={{ display: 'flex', overflow: 'auto' }} key={'newKeyValue'}>
                 <EditableView
                     key="editableView"
-                    contents={'+key:value'}
+                    contents={'+key=value'}
                     height={13}
                     fontSize={12}
                     GetValue={returnEmptyString}
-                    SetValue={value => value.indexOf(':') !== -1 && KeyValueBox.SetField(doc, value.substring(0, value.indexOf(':')), value.substring(value.indexOf(':') + 1, value.length), true)}
+                    SetValue={input => {
+                        const match = input.match(/([a-zA-Z0-9_-]+)(=|=:=)([a-zA-Z,_@\?\+\-\*\/\ 0-9\(\)]+)/);
+                        if (match) {
+                            const key = match[1];
+                            const assign = match[2];
+                            const val = match[3];
+                            KeyValueBox.SetField(doc, key, assign + val, false);
+                            return true;
+                        }
+                        return false;
+                    }}
                 />
             </div>
         );
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 0579b07c7..3fdc9a488 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -1,29 +1,26 @@
 import { Colors } from 'browndash-components';
 import { action, runInAction } from 'mobx';
+import { aggregateBounds } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
 import { InkTool } from '../../../fields/InkField';
 import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
 import { GestureUtils } from '../../../pen-gestures/GestureUtils';
-import { aggregateBounds } from '../../../Utils';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { LinkManager } from '../../util/LinkManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SelectionManager } from '../../util/SelectionManager';
-import { undoable, UndoManager } from '../../util/UndoManager';
-import { CollectionFreeFormView } from '../collections/collectionFreeForm';
+import { UndoManager, undoable } from '../../util/UndoManager';
 import { GestureOverlay } from '../GestureOverlay';
 import { ActiveFillColor, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth, SetActiveIsInkMask } from '../InkingStroke';
+import { CollectionFreeFormView } from '../collections/collectionFreeForm';
 // import { InkTranscription } from '../InkTranscription';
+import { DocData } from '../../../fields/DocSymbols';
 import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
 import { DocumentView } from '../nodes/DocumentView';
-import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
-import { WebBox } from '../nodes/WebBox';
 import { VideoBox } from '../nodes/VideoBox';
-import { DocData } from '../../../fields/DocSymbols';
-import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
-import { PrefetchProxy } from '../../../fields/Proxy';
-import { MakeTemplate } from '../../util/DropConverter';
+import { WebBox } from '../nodes/WebBox';
+import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
 
 ScriptingGlobals.add(function IsNoneSelected() {
     return SelectionManager.Views.length <= 0;
@@ -76,19 +73,7 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b
 
 // toggle: Set overlay status of selected document
 ScriptingGlobals.add(function setDefaultTemplate(checkResult?: boolean) {
-    if (checkResult) {
-        return Doc.UserDoc().defaultTextLayout;
-    }
-    const view = SelectionManager.Views.length === 1 && SelectionManager.Views[0].ComponentView instanceof FormattedTextBox ? SelectionManager.Views[0] : undefined;
-
-    if (view) {
-        const tempDoc = view.Document;
-        if (!view.layoutDoc.isTemplateDoc) {
-            MakeTemplate(tempDoc);
-        }
-        Doc.UserDoc().defaultTextLayout = new PrefetchProxy(tempDoc);
-        tempDoc && Doc.AddDocToList(Cast(Doc.UserDoc().template_notes, Doc, null), 'data', tempDoc);
-    } else Doc.UserDoc().defaultTextLayout = undefined;
+    return DocumentView.setDefaultTemplate(checkResult);
 });
 // toggle: Set overlay status of selected document
 ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boolean) {
@@ -197,7 +182,7 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh
     map.get(attr)?.setDoc?.();
 });
 
-type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal';
+type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'elide' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal';
 type attrfuncs = [attrname, { checkResult: () => boolean; toggle: () => any }];
 
 ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: boolean) {
@@ -221,6 +206,8 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
     const attrs:attrfuncs[] = [
         ['dictation', { checkResult: () => textView?._recordingDictation ? true:false,
                         toggle: () => textView && runInAction(() => (textView._recordingDictation = !textView._recordingDictation)) }],
+        ['elide',      { checkResult: () => false,
+                        toggle: () => editorView ? RichTextMenu.Instance?.elideSelection(): 0}],
         ['noAutoLink',{ checkResult: () => (editorView ? RichTextMenu.Instance.noAutoLink : false),
                         toggle: () => editorView && RichTextMenu.Instance?.toggleNoAutoLinkAnchor()}],
         ['bold',      { checkResult: () => (editorView ? RichTextMenu.Instance.bold : (Doc.UserDoc().fontWeight === 'bold') ? true:false),
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 715b23fb6..62f630c6c 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -159,6 +159,37 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     moveDoc2 = (doc: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true);
     remDoc1 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true);
     remDoc2 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true);
+
+    /**
+     * Tests for whether a comparison box slot (ie, before or after) has renderable text content
+     * @param whichSlot field key for start or end slot
+     * @returns a JSX layout string if a text field is found, othwerise undefined
+     */
+    testForTextFields = (whichSlot: string) => {
+        const slotHasText = Doc.Get(this.dataDoc, whichSlot, true) instanceof RichTextField || typeof Doc.Get(this.dataDoc, whichSlot, true) === 'string';
+        const subjectText = RTFCast(this.Document[this.fieldKey])?.Text.trim();
+        const altText = RTFCast(this.Document[this.fieldKey + '_alternate'])?.Text.trim();
+        const layoutTemplateString =
+            slotHasText ? FormattedTextBox.LayoutString(whichSlot):
+            whichSlot.endsWith('1') ? (subjectText !== undefined ? FormattedTextBox.LayoutString(this.fieldKey) : undefined) :
+                                       altText !== undefined     ? FormattedTextBox.LayoutString(this.fieldKey + '_alternate'): undefined; // prettier-ignore
+
+        // A bit hacky to try out the concept of using GPT to fill in flashcards
+        //   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 that includes a GPT query (indicated by (( && )) ) that is parameterized (optionally) by the fieldKey text (this) or other metadata (this.<field>).
+        // 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
+        // The GPT call will put the "answer"  in the second slot of the comparison (eg., text_2)
+        if (whichSlot.endsWith('2') && !layoutTemplateString?.includes(whichSlot)) {
+            var queryText = altText.replace('(this)', subjectText); // TODO: this should be done in KeyValueBox.setField but it doesn't know about the fieldKey ...
+            if (queryText && queryText.match(/\(\(.*\)\)/)) {
+                KeyValueBox.SetField(this.Document, whichSlot, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt
+            }
+        }
+        return layoutTemplateString;
+    };
+
     _closeRef = React.createRef<HTMLDivElement>();
     render() {
         const clearButton = (which: string) => {
@@ -176,48 +207,24 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         /**
          * 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
+         * @param whichSlot
          * @returns
          */
-        const displayDoc = (which: string) => {
-            const whichDoc = DocCast(this.dataDoc[which]);
+        const displayDoc = (whichSlot: string) => {
+            const whichDoc = DocCast(this.dataDoc[whichSlot]);
             const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc);
-            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
-                ? which.endsWith('1') && subjectText !== undefined
-                    ? FormattedTextBox.LayoutString(this.fieldKey)
-                    : which.endsWith('2') && (this.Document[which] instanceof RichTextField || typeof this.Document[which] === 'string')
-                      ? FormattedTextBox.LayoutString(which)
-                      : undefined
-                : 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 (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) {
-                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
-                }
-            }
+            const layoutTemplateString = targetDoc ? '' : this.testForTextFields(whichSlot);
             return targetDoc || layoutTemplateString ? (
                 <>
                     <DocumentView
                         {...this._props}
+                        ignoreUsePath={layoutTemplateString ? true : undefined}
                         renderDepth={this.props.renderDepth + 1}
                         LayoutTemplateString={layoutTemplateString}
                         Document={layoutTemplateString ? this.Document : targetDoc}
                         containerViewPath={this.DocumentView?.().docViewPath}
-                        moveDocument={which.endsWith('1') ? this.moveDoc1 : this.moveDoc2}
-                        removeDocument={which.endsWith('1') ? this.remDoc1 : this.remDoc2}
+                        moveDocument={whichSlot.endsWith('1') ? this.moveDoc1 : this.moveDoc2}
+                        removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2}
                         NativeWidth={returnZero}
                         NativeHeight={returnZero}
                         isContentActive={emptyFunction}
@@ -227,7 +234,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         hideLinkButton={true}
                         pointerEvents={this._isAnyChildContentActive ? undefined : returnNone}
                     />
-                    {layoutTemplateString ? null : clearButton(which)}
+                    {layoutTemplateString ? null : clearButton(whichSlot)}
                 </> // placeholder image if doc is missing
             ) : (
                 <div className="placeholder">
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 9848f18e0..e9ce98583 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -10,6 +10,7 @@ import { AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fi
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
+import { PrefetchProxy } from '../../../fields/Proxy';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
@@ -19,10 +20,11 @@ import { DocServer } from '../../DocServer';
 import { Networking } from '../../Network';
 import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
-import { DocOptions, DocUtils, Docs } from '../../documents/Documents';
+import { DocUtils, Docs } from '../../documents/Documents';
 import { DictationManager } from '../../util/DictationManager';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager, dropActionType } from '../../util/DragManager';
+import { MakeTemplate, makeUserTemplateButton } from '../../util/DropConverter';
 import { FollowLinkScript } from '../../util/LinkFollower';
 import { LinkManager } from '../../util/LinkManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
@@ -36,6 +38,7 @@ import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { DocComponent, ViewBoxInterface } from '../DocComponent';
 import { EditableView } from '../EditableView';
+import { FieldsDropdown } from '../FieldsDropdown';
 import { GestureOverlay } from '../GestureOverlay';
 import { LightboxView } from '../LightboxView';
 import { AudioAnnoState, StyleProp } from '../StyleProvider';
@@ -47,7 +50,6 @@ import { KeyValueBox } from './KeyValueBox';
 import { LinkAnchorBox } from './LinkAnchorBox';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { PresEffect, PresEffectDirection } from './trails';
-import { FieldsDropdown } from '../FieldsDropdown';
 interface Window {
     MediaRecorder: MediaRecorder;
 }
@@ -1271,6 +1273,32 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         custom && DocUtils.makeCustomViewClicked(this.Document, Docs.Create.StackingDocument, layout, undefined);
     }, 'set custom view');
 
+    public static setDefaultTemplate(checkResult?: boolean) {
+        if (checkResult) {
+            return Doc.UserDoc().defaultTextLayout;
+        }
+        const view = SelectionManager.Views[0]?._props.renderDepth > 0 ? SelectionManager.Views[0] : undefined;
+        undoable(() => {
+            var tempDoc: Opt<Doc>;
+            if (view) {
+                if (!view.layoutDoc.isTemplateDoc) {
+                    tempDoc = view.Document;
+                    MakeTemplate(tempDoc);
+                    Doc.AddDocToList(Doc.UserDoc(), 'template_user', tempDoc);
+                    Doc.AddDocToList(DocListCast(Doc.MyTools.data)[1], 'data', makeUserTemplateButton(tempDoc));
+                    tempDoc && Doc.AddDocToList(Cast(Doc.UserDoc().template_user, Doc, null), 'data', tempDoc);
+                } else {
+                    tempDoc = DocCast(view.Document[StrCast(view.Document.layout_fieldKey)]);
+                    if (!tempDoc) {
+                        tempDoc = view.Document;
+                        while (tempDoc && !Doc.isTemplateDoc(tempDoc)) tempDoc = DocCast(tempDoc.proto);
+                    }
+                }
+            }
+            Doc.UserDoc().defaultTextLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined;
+        }, 'set default template')();
+    }
+
     /**
      * This switches between the current view of a Doc and a specified alternate layout view.
      * The current view of the Doc is stored in the layout_default field so that it can be restored.
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 4ecaaa283..5b47dd91d 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -55,6 +55,7 @@ export interface FieldViewSharedProps {
     ignoreAutoHeight?: boolean;
     disableBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over.
     hideClickBehaviors?: boolean; // whether to suppress menu item options for changing click behaviors
+    ignoreUsePath?: boolean; // ignore the usePath field for selecting the fieldKey (eg., on text docs)
     CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView;
     containerViewPath?: () => DocumentView[];
     fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index f02ad7300..57ae92359 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -5,8 +5,7 @@ import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
-import { ScriptField } from '../../../../fields/ScriptField';
-import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
+import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { emptyFunction, returnTrue, setupMoveUpEvents, Utils } from '../../../../Utils';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { SelectionManager } from '../../../util/SelectionManager';
@@ -61,23 +60,12 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
     }
 
     @observable noTooltip = false;
-    showTemplate = (): void => {
-        const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null);
-        dragFactory && this._props.addDocTab(dragFactory, OpenWhere.addRight);
-    };
-    dragAsTemplate = (): void => {
-        this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
-    };
-    useAsPrototype = (): void => {
-        this.layoutDoc.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)');
-    };
 
+    showTemplate = (dragFactory: Doc) => this._props.addDocTab(dragFactory, OpenWhere.addRight);
     specificContextMenu = (): void => {
-        if (!Doc.noviceMode && Cast(this.layoutDoc.dragFactory, Doc, null)) {
-            const cm = ContextMenu.Instance;
-            cm.addItem({ description: 'Show Template', event: this.showTemplate, icon: 'tag' });
-            cm.addItem({ description: 'Use as Render Template', event: this.dragAsTemplate, icon: 'tag' });
-            cm.addItem({ description: 'Use as Prototype', event: this.useAsPrototype, icon: 'tag' });
+        const dragFactory = DocCast(this.layoutDoc.dragFactory);
+        if (!Doc.noviceMode && dragFactory) {
+            ContextMenu.Instance.addItem({ description: 'Show Template', event: () => this.showTemplate(dragFactory), icon: 'tag' });
         }
     };
 
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 2bcad806f..d85432631 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -115,7 +115,7 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
                 field === undefined && (field = res.result);
             }
         }
-        if (!key) return field;
+        if (!key) return false;
         if (Field.IsField(field, true) && (key !== 'proto' || field !== target)) {
             target[key] = field;
             return true;
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index d59489a78..f9e8ce4f3 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) ? true : false)}
+                            SetValue={(value: string) => KeyValueBox.SetField(this._props.doc, this._props.keyName, value)}
                         />
                     </div>
                 </td>
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 62cb460c2..6b66d829c 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -1,6 +1,6 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
-import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import * as ReactDOM from 'react-dom/client';
@@ -8,12 +8,12 @@ import { Doc, DocListCast, Field } from '../../../../fields/Doc';
 import { List } from '../../../../fields/List';
 import { listSpec } from '../../../../fields/Schema';
 import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
-import { Cast } from '../../../../fields/Types';
+import { Cast, DocCast } from '../../../../fields/Types';
 import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
 import { Transform } from '../../../util/Transform';
-import { undoBatch } from '../../../util/UndoManager';
+import { undoable, undoBatch } from '../../../util/UndoManager';
 import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
 import { SchemaTableCell } from '../../collections/collectionSchema/SchemaTableCell';
 import { FilterPanel } from '../../FilterPanel';
@@ -21,6 +21,7 @@ import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { OpenWhere } from '../DocumentView';
 import './DashFieldView.scss';
 import { FormattedTextBox } from './FormattedTextBox';
+import { DocData } from '../../../../fields/DocSymbols';
 
 export class DashFieldView {
     dom: HTMLDivElement; // container for label and value
@@ -62,6 +63,8 @@ export class DashFieldView {
                 height={node.attrs.height}
                 hideKey={node.attrs.hideKey}
                 editable={node.attrs.editable}
+                expanded={node.attrs.expanded}
+                dataDoc={node.attrs.dataDoc}
                 tbox={tbox}
             />
         );
@@ -89,6 +92,8 @@ interface IDashFieldViewInternal {
     width: number;
     height: number;
     editable: boolean;
+    expanded: boolean;
+    dataDoc: boolean;
     node: any;
     getPos: any;
     unclickable: () => boolean;
@@ -101,18 +106,19 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     _fieldKey: string;
     _fieldRef = React.createRef<HTMLDivElement>();
     @observable _dashDoc: Doc | undefined = undefined;
-    @observable _expanded = false;
+    @observable _expanded = this._props.expanded;
 
     constructor(props: IDashFieldViewInternal) {
         super(props);
         makeObservable(this);
         this._fieldKey = this._props.fieldKey;
-        this._textBoxDoc = this._fieldKey.startsWith('_') ? this._props.tbox.Document : this._props.tbox.dataDoc;
+        this._textBoxDoc = this._props.tbox.Document;
+        const setDoc = (doc: Doc) => (this._dashDoc = this._props.dataDoc ? doc[DocData] : doc);
 
         if (this._props.docId) {
-            DocServer.GetRefField(this._props.docId).then(action(dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc)));
+            DocServer.GetRefField(this._props.docId).then(dashDoc => dashDoc instanceof Doc && setDoc(dashDoc));
         } else {
-            this._dashDoc = this._fieldKey.startsWith('_') ? this._props.tbox.Document : this._props.tbox.dataDoc;
+            setDoc(this._props.tbox.Document);
         }
     }
 
@@ -126,7 +132,9 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     componentWillUnmount() {
         this._reactionDisposer?.();
     }
-    return100 = () => 100;
+    isRowActive = () => this._expanded && this._props.editable;
+    finishEdit = action(() => (this._expanded = false));
+    selectedCell = (): [Doc, number] => [this._dashDoc!, 0];
 
     // set the display of the field's value (checkbox for booleans, span of text for strings)
     @computed get fieldValueContent() {
@@ -137,18 +145,18 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
                     col={0}
                     deselectCell={emptyFunction}
                     selectCell={emptyFunction}
-                    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]}
+                    maxWidth={this._props.hideKey || this._hideKey ? undefined : this._props.tbox._props.PanelWidth}
+                    columnWidth={returnZero}
+                    selectedCell={this.selectedCell}
                     fieldKey={this._fieldKey}
                     rowHeight={returnZero}
-                    isRowActive={() => this._expanded && this._props.editable}
+                    isRowActive={this.isRowActive}
                     padding={0}
                     getFinfo={emptyFunction}
                     setColumnValues={returnFalse}
                     allowCRs={true}
                     oneLine={!this._expanded}
-                    finishEdit={action(() => (this._expanded = false))}
+                    finishEdit={this.finishEdit}
                     transform={Transform.Identity}
                     menuTarget={null}
                 />
@@ -173,11 +181,21 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
         }
     };
 
+    toggleFieldHide = undoable(
+        action(() => this._dashDoc && (this._dashDoc[this._fieldKey + '_hideKey'] = !this._dashDoc[this._fieldKey + '_hideKey'])),
+        'hideKey'
+    );
+
+    @computed get _hideKey() {
+        return this._dashDoc && this._dashDoc[this._fieldKey + '_hideKey'];
+    }
+
     // clicking on the label creates a pivot view collection of all documents
     // in the same collection.  The pivot field is the fieldKey of this label
     onPointerDownLabelSpan = (e: any) => {
         setupMoveUpEvents(this, e, returnFalse, returnFalse, e => {
             DashFieldViewMenu.createFieldView = this.createPivotForField;
+            DashFieldViewMenu.toggleFieldHide = this.toggleFieldHide;
             DashFieldViewMenu.Instance.show(e.clientX, e.clientY + 16, this._fieldKey);
         });
     };
@@ -188,6 +206,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     };
 
     @computed get values() {
+        if (this._props.expanded) return [];
         const vals = FilterPanel.gatherFieldValues(DocListCast(Doc.ActiveDashboard?.data), this._fieldKey, []);
 
         return vals.strings.map(facet => ({ value: facet, label: facet }));
@@ -203,9 +222,9 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
                     height: this._props.height,
                     pointerEvents: this._props.tbox._props.rootSelected?.() || this._props.tbox.isAnyChildContentActive?.() ? undefined : 'none',
                 }}>
-                {this._props.hideKey ? null : (
+                {this._props.hideKey || this._hideKey ? null : (
                     <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}>
-                        {(this._textBoxDoc === this._dashDoc ? '' : this._dashDoc?.title + ':') + this._fieldKey}
+                        {(Doc.AreProtosEqual(DocCast(this._textBoxDoc.rootDocument) ?? this._textBoxDoc, DocCast(this._dashDoc?.rootDocument) ?? this._dashDoc) ? '' : this._dashDoc?.title + ':') + this._fieldKey}
                     </span>
                 )}
                 {this._props.fieldKey.startsWith('#') ? null : this.fieldValueContent}
@@ -224,6 +243,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
 export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> {
     static Instance: DashFieldViewMenu;
     static createFieldView: (e: React.MouseEvent) => void = emptyFunction;
+    static toggleFieldHide: () => void = emptyFunction;
     constructor(props: any) {
         super(props);
         DashFieldViewMenu.Instance = this;
@@ -233,6 +253,10 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> {
         DashFieldViewMenu.createFieldView(e);
         DashFieldViewMenu.Instance.fadeOut(true);
     };
+    toggleFieldHide = (e: React.MouseEvent) => {
+        DashFieldViewMenu.toggleFieldHide();
+        DashFieldViewMenu.Instance.fadeOut(true);
+    };
 
     @observable _fieldKey = '';
 
@@ -252,6 +276,9 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> {
                 <button className="antimodeMenu-button" onPointerDown={this.showFields}>
                     <FontAwesomeIcon icon="eye" size="lg" />
                 </button>
+                <button className="antimodeMenu-button" onPointerDown={this.toggleFieldHide}>
+                    <FontAwesomeIcon icon="bullseye" size="lg" />
+                </button>
             </Tooltip>
         );
     }
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index fb709818c..2b48494f2 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -451,7 +451,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             var tr = this._editorView.state.tr as any;
             const autoAnch = this._editorView.state.schema.marks.autoLinkAnchor;
             tr = tr.removeMark(0, tr.doc.content.size, autoAnch);
-            Doc.MyPublishedDocs.forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks)));
+            Doc.MyPublishedDocs.filter(term => term.title).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks)));
             tr = tr.setSelection(isNodeSel && false ? new NodeSelection(tr.doc.resolve(f)) : new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t)));
             this._editorView?.dispatch(tr);
         }
@@ -958,8 +958,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight),
                 icon: this.Document._layout_autoHeight ? 'lock' : 'unlock',
             });
-        optionItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: <BsMarkdownFill /> });
         !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
+        const help = cm.findByDescription('Help...');
+        const helpItems = help && 'subitems' in help ? help.subitems : [];
+        helpItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: <BsMarkdownFill /> });
+        !help && cm.addItem({ description: 'Help...', subitems: helpItems, icon: 'eye' });
         this._downX = this._downY = Number.NaN;
     };
 
@@ -1239,8 +1242,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                             this._editorView.updateState(EditorState.fromJSON(this.config, updatedState));
                             this.tryUpdateScrollHeight();
                         }
-                    } else if (incomingValue?.str) {
-                        selectAll(this._editorView.state, tx => this._editorView?.dispatch(tx.insertText(incomingValue.str)));
+                    } else {
+                        selectAll(this._editorView.state, tx => this._editorView?.dispatch(tx.insertText(incomingValue?.str ?? '')));
                     }
                 }
             }
@@ -1339,15 +1342,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             DocServer.GetRefField(pdfAnchorId).then(pdfAnchor => {
                 if (pdfAnchor instanceof Doc) {
                     const dashField = view.state.schema.nodes.paragraph.create({}, [
-                        view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, editable: false }, undefined, [
+                        view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, editable: false, expanded: true }, undefined, [
                             view.state.schema.marks.linkAnchor.create({
                                 allAnchors: [{ href: `/doc/${this.Document[Id]}`, title: this.Document.title, anchorId: `${this.Document[Id]}` }],
-                                title: `from: ${DocCast(pdfAnchor.embedContainer).title}`,
+                                title: StrCast(pdfAnchor.title),
                                 noPreview: true,
-                                docref: false,
+                                docref: true,
+                                fontSize: '8px',
                             }),
-                            view.state.schema.marks.pFontSize.create({ fontSize: '8px' }),
-                            view.state.schema.marks.em.create({}),
                         ]),
                     ]);
 
@@ -1926,11 +1928,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             </div>
         );
     }
-    cycleAlternateText = () => {
-        if (this.layoutDoc._layout_enableAltContentUI) {
-            const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`];
-            this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined;
-        }
+    cycleAlternateText = (skipHover?: boolean) => {
+        this.layoutDoc._layout_enableAltContentUI = true;
+        const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`];
+        this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' && !skipHover ? 'alternate:hover' : undefined;
     };
     @computed get overlayAlternateIcon() {
         const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`];
@@ -1965,7 +1966,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         );
     }
     get fieldKey() {
-        const usePath = StrCast(this.layoutDoc[`${this._props.fieldKey}_usePath`]);
+        return this._fieldKey;
+    }
+    @computed get _fieldKey() {
+        const usePath = this._props.ignoreUsePath ? '' : StrCast(this.layoutDoc[`${this._props.fieldKey}_usePath`]);
         return this._props.fieldKey + (usePath && (!usePath.includes(':hover') || this._isHovering || this._props.isContentActive()) ? `_${usePath.replace(':hover', '')}` : '');
     }
     @observable _isHovering = false;
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index dc2c06701..bee0d72e3 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -318,6 +318,19 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         });
     }
 
+    elideSelection = () => {
+        const state = this.view?.state;
+        if (!state) return;
+        if (state.selection.empty) return false;
+        const mark = state.schema.marks.summarize.create();
+        const tr = state.tr;
+        tr.addMark(state.selection.from, state.selection.to, mark);
+        const content = tr.selection.content();
+        const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() });
+        this.view?.dispatch?.(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
+        return true;
+    };
+
     toggleNoAutoLinkAnchor = () => {
         if (this.view) {
             const mark = this.view.state.schema.mark(this.view.state.schema.marks.noAutoLinkAnchor);
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index c798ae4b3..b97141e92 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -267,7 +267,7 @@ export class RichTextRules {
 
             // toggle alternate text UI  %/
             new InputRule(new RegExp(/%\//), (state, match, start, end) => {
-                setTimeout(this.TextBox.cycleAlternateText);
+                setTimeout(() => this.TextBox.cycleAlternateText(true));
                 return state.tr.deleteRange(start, end);
             }),
 
@@ -283,40 +283,26 @@ 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: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]+)?\]\]$/),
-                (state, match, start, end) => {
-                    const fieldKey = match[1];
-                    const assign = match[2] === '=' ? '' : match[2];
-                    const value = match[4];
-                    const docTitle = match[5]?.replace(':', '');
+            // create a hyperlink to a titled document
+            // @(<doctitle>)
+            new InputRule(new RegExp(/(^|\s)@\(([a-zA-Z_@:\.\? \-0-9]+)\)/), (state, match, start, end) => {
+                const docTitle = match[2];
+                const prefixLength = '@('.length;
+                if (docTitle) {
                     const linkToDoc = (target: Doc) => {
-                        const rstate = this.TextBox.EditorView?.state;
-                        const selection = rstate?.selection.$from.pos;
-                        if (rstate) {
-                            this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
+                        const editor = this.TextBox.EditorView;
+                        const selection = editor?.state?.selection.$from.pos;
+                        if (editor) {
+                            const estate = editor.state;
+                            editor.dispatch(estate.tr.setSelection(new TextSelection(estate.doc.resolve(start), estate.doc.resolve(end - prefixLength))));
                         }
 
                         DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { link_relationship: 'portal to:portal from' });
 
-                        const fstate = this.TextBox.EditorView?.state;
-                        if (fstate && selection) {
-                            this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection))));
+                        const teditor = this.TextBox.EditorView;
+                        if (teditor && selection) {
+                            const tstate = teditor.state;
+                            teditor.dispatch(tstate.tr.setSelection(new TextSelection(tstate.doc.resolve(selection))));
                         }
                     };
                     const getTitledDoc = (docTitle: string) => {
@@ -326,32 +312,57 @@ export class RichTextRules {
                         const titledDoc = DocServer.FindDocByTitle(docTitle);
                         return titledDoc ? Doc.BestEmbedding(titledDoc) : titledDoc;
                     };
-                    if (!fieldKey) {
-                        if (docTitle) {
-                            const target = getTitledDoc(docTitle);
-                            if (target) {
-                                setTimeout(() => linkToDoc(target));
-                                return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3);
-                            }
-                        }
-                        return state.tr;
+                    const target = getTitledDoc(docTitle);
+                    if (target) {
+                        setTimeout(() => linkToDoc(target));
+                        return state.tr.insertText(' ').deleteRange(start, start + prefixLength);
                     }
+                }
+                return state.tr;
+            }),
+
+            // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
+            // [@{this,doctitle,}.fieldKey{:,=,:=,=:=}value]
+            // [@{this,doctitle,}.fieldKey]
+            new InputRule(
+                new RegExp(/\[(@|@this\.|@[a-zA-Z_\? \-0-9]+\.)([a-zA-Z_\?\-0-9]+)((:|=|:=|=:=)([a-zA-Z,_@\?\+\-\*\/\ 0-9\(\)]*))?\]/),
+                (state, match, start, end) => {
+                    const docTitle = match[1].substring(1).replace(/\.$/, '');
+                    const fieldKey = match[2];
+                    const assign = match[4] === ':' ? (match[4] = '') : match[4];
+                    const value = match[5];
+                    const dataDoc = value === undefined ? !fieldKey.startsWith('_') : !assign?.startsWith('=');
+                    const getTitledDoc = (docTitle: string) => {
+                        if (!DocServer.FindDocByTitle(docTitle)) {
+                            Doc.AddToMyPublished(Docs.Create.TextDocument('', { title: docTitle, _width: 400, _layout_autoHeight: true }));
+                        }
+                        return DocServer.FindDocByTitle(docTitle);
+                    };
                     // 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) {
-                        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
+                        KeyValueBox.SetField(this.Document, fieldKey, assign + value, Doc.IsDataProto(this.Document) ? true : undefined, assign.includes(":=") ? undefined:
+                            (gptval: FieldResult) => (dataDoc ? this.Document[DocData]:this.Document)[fieldKey] = gptval as string ); // prettier-ignore
                     }
-                    const target = getTitledDoc(docTitle);
-                    const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false });
+                    const target = docTitle ? getTitledDoc(docTitle) : undefined;
+                    const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false, dataDoc });
                     return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true);
                 },
                 { inCode: true }
             ),
 
+            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
             // wiki:title
             new InputRule(new RegExp(/wiki:([a-zA-Z_@:\.\?\-0-9]+ )$/), (state, match, start, end) => {
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index a141ef041..b68acc8f8 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -74,6 +74,7 @@ export const marks: { [index: string]: MarkSpec } = {
             allAnchors: { default: [] as { href: string; title: string; anchorId: string }[] },
             title: { default: null },
             noPreview: { default: false },
+            fontSize: { default: null },
             docref: { default: false }, // flags whether the linked text comes from a document within Dash.  If so, an attribution label is appended after the text
         },
         inclusive: false,
@@ -93,14 +94,16 @@ export const marks: { [index: string]: MarkSpec } = {
             const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.anchorId : item.anchorId), '');
             return node.attrs.docref && node.attrs.title
                 ? [
-                      'div',
+                      'a',
                       ['span', 0],
                       [
                           'span',
                           {
                               ...node.attrs,
                               class: 'prosemirror-attribution',
+                              'data-targethrefs': targethrefs,
                               href: node.attrs.allAnchors[0].href,
+                              style: `font-size: ${node.attrs.fontSize}`,
                           },
                           node.attrs.title,
                       ],
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index c9115be90..905146ee2 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -265,6 +265,8 @@ export const nodes: { [index: string]: NodeSpec } = {
             docId: { default: '' },
             hideKey: { default: false },
             editable: { default: true },
+            expanded: { default: null },
+            dataDoc: { default: false },
         },
         leafText: node => Field.toString((DocServer.GetCachedRefField(node.attrs.docId as string) as Doc)?.[node.attrs.fieldKey as string] as Field),
         group: 'inline',
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index daae32e9f..4b40d11b9 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -445,6 +445,12 @@ export namespace Doc {
     export function GetT<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): FieldResult<T> {
         return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult<T>;
     }
+    export function isTemplateDoc(doc: Doc) {
+        return GetT(doc, 'isTemplateDoc', 'boolean', true);
+    }
+    export function isTemplateForField(doc: Doc) {
+        return GetT(doc, 'isTemplateForField', 'string', true);
+    }
     export function IsDataProto(doc: Doc) {
         return GetT(doc, 'isDataDoc', 'boolean', true);
     }
@@ -642,7 +648,7 @@ export namespace Doc {
         cloneLinks: boolean,
         cloneTemplates: boolean
     ): Promise<Doc> {
-        if (Doc.IsBaseProto(doc) || ((Doc.Get(doc, 'isTemplateDoc', true) || Doc.Get(doc, 'isTemplateForField', true)) && !cloneTemplates)) {
+        if (Doc.IsBaseProto(doc) || ((Doc.isTemplateDoc(doc) || Doc.isTemplateForField(doc)) && !cloneTemplates)) {
             return doc;
         }
         if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
@@ -735,7 +741,7 @@ export namespace Doc {
                 const docAtKey = DocCast(clone[key]);
                 if (docAtKey && !Doc.IsSystem(docAtKey)) {
                     if (!Array.from(cloneMap.values()).includes(docAtKey)) {
-                        clone[key] = !cloneTemplates && (Doc.Get(docAtKey, 'isTemplateDoc', true) || Doc.Get(docAtKey, 'isTemplateForField', true)) ? docAtKey : cloneMap.get(docAtKey[Id]);
+                        clone[key] = !cloneTemplates && (Doc.isTemplateDoc(docAtKey) || Doc.isTemplateForField(docAtKey)) ? docAtKey : cloneMap.get(docAtKey[Id]);
                     } else {
                         repairClone(docAtKey, cloneMap, cloneTemplates, visited);
                     }
@@ -857,7 +863,7 @@ export namespace Doc {
     // of the original layout while allowing for individual layout properties to be overridden in the expanded layout.
     export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc) {
         // nothing to do if the layout isn't a template or we don't have a target that's different than the template
-        if (!targetDoc || templateLayoutDoc === targetDoc || (!templateLayoutDoc.isTemplateForField && !templateLayoutDoc.isTemplateDoc)) {
+        if (!targetDoc || templateLayoutDoc === targetDoc || (!Doc.isTemplateForField(templateLayoutDoc) && !Doc.isTemplateDoc(templateLayoutDoc))) {
             return templateLayoutDoc;
         }
 
@@ -874,7 +880,7 @@ export namespace Doc {
             expandedTemplateLayout = undefined;
             _pendingMap.add(targetDoc[Id] + expandedLayoutFieldKey);
         } else if (expandedTemplateLayout === undefined && !_pendingMap.has(targetDoc[Id] + expandedLayoutFieldKey)) {
-            if (templateLayoutDoc.resolvedDataDoc === (targetDoc.rootDocument ?? Doc.GetProto(targetDoc))) {
+            if (templateLayoutDoc.resolvedDataDoc === targetDoc[DocData]) {
                 expandedTemplateLayout = templateLayoutDoc; // reuse an existing template layout if its for the same document with the same params
             } else {
                 templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = DocCast(templateLayoutDoc.proto, templateLayoutDoc)); // if the template has already been applied (ie, a nested template), then use the template's prototype
@@ -910,8 +916,9 @@ export namespace Doc {
             console.log('Warning: GetLayoutDataDocPair childDoc not defined');
             return { layout: childDoc, data: childDoc };
         }
-        const resolvedDataDoc = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!childDoc.isTemplateDoc && !childDoc.isTemplateForField) ? undefined : containerDataDoc;
-        return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc), data: resolvedDataDoc };
+        const resolvedDataDoc = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!Doc.isTemplateDoc(childDoc) && !Doc.isTemplateForField(childDoc)) ? undefined : containerDataDoc;
+        const templateRoot = DocCast(containerDoc?.rootDocument);
+        return { layout: Doc.expandTemplateLayout(childDoc, templateRoot), data: resolvedDataDoc };
     }
 
     export function FindReferences(infield: Doc | List<any>, references: Set<Doc>, system: boolean | undefined) {
@@ -1035,20 +1042,13 @@ export namespace Doc {
     // (ie, the 'data' doc), and then creates another delegate of that (ie, the 'layout' doc).
     // This is appropriate if you're trying to create a document that behaves like all
     // regularly created documents (e.g, text docs, pdfs, etc which all have data/layout docs)
-    export function MakeDelegateWithProto(doc: Doc, id?: string, title?: string): Doc {
-        const delegateProto = new Doc();
-        delegateProto[Initializing] = true;
-        delegateProto.proto = doc;
-        delegateProto.author = Doc.CurrentUserEmail;
-        delegateProto.isDataDoc = true;
-        title && (delegateProto.title = title);
-        const delegate = new Doc(id, true);
-        delegate[Initializing] = true;
-        delegate.proto = delegateProto;
-        delegate.author = Doc.CurrentUserEmail;
-        delegate[Initializing] = false;
-        delegateProto[Initializing] = false;
-        return delegate;
+    export function MakeDelegateWithProto(doc: Doc, id?: string, title?: string) {
+        const ndoc = Doc.ApplyTemplate(doc);
+        if (ndoc) {
+            Doc.GetProto(ndoc).isDataDoc = true;
+            ndoc && (Doc.GetProto(ndoc).proto = doc);
+        }
+        return ndoc;
     }
 
     let _applyCount: number = 0;
@@ -1671,7 +1671,7 @@ ScriptingGlobals.add(function getEmbedding(doc: any) {
     return Doc.MakeEmbedding(doc);
 });
 ScriptingGlobals.add(function getCopy(doc: any, copyProto: any) {
-    return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto);
+    return doc.isTemplateDoc ? Doc.MakeDelegateWithProto(doc) : Doc.MakeCopy(doc, copyProto);
 });
 ScriptingGlobals.add(function copyField(field: any) {
     return Field.Copy(field);
diff --git a/src/fields/util.ts b/src/fields/util.ts
index b73520999..c2ec3f13a 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -286,6 +286,10 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
 // target should be either a Doc or ListImpl.  receiver should be a Proxy<Doc> Or List.
 //
 export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
+    if (!in_prop) {
+        console.log('WARNING: trying to set an empty property.  This should be fixed. ');
+        return false;
+    }
     let prop = in_prop;
     const effectiveAcl = in_prop === 'constructor' || typeof in_prop === 'symbol' ? AclAdmin : GetPropAcl(target, prop);
     if (effectiveAcl !== AclEdit && effectiveAcl !== AclAugment && effectiveAcl !== AclAdmin) return true;
-- 
cgit v1.2.3-70-g09d2


From e0b846bf66b5d830b452ce0faa73eeb5f8a6cdcd Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 25 Mar 2024 14:25:49 -0400
Subject: fixed exception in comparison box

---
 src/client/views/nodes/ComparisonBox.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/client/views/nodes/ComparisonBox.tsx')

diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 62f630c6c..59d99b371 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -182,7 +182,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         //     where (this) is replaced by the text in the fieldKey slot abd this.excludeWords is repalced by the conetnts of the excludeWords field
         // The GPT call will put the "answer"  in the second slot of the comparison (eg., text_2)
         if (whichSlot.endsWith('2') && !layoutTemplateString?.includes(whichSlot)) {
-            var queryText = altText.replace('(this)', subjectText); // TODO: this should be done in KeyValueBox.setField but it doesn't know about the fieldKey ...
+            var queryText = altText?.replace('(this)', subjectText); // TODO: this should be done in KeyValueBox.setField but it doesn't know about the fieldKey ...
             if (queryText && queryText.match(/\(\(.*\)\)/)) {
                 KeyValueBox.SetField(this.Document, whichSlot, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt
             }
-- 
cgit v1.2.3-70-g09d2


From 1b592f74a7df8f6dd7b2881032725f26aedff403 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 28 Mar 2024 11:38:56 -0400
Subject: fixed keyvaluebox to show props document, never the doc in the
 fieldKey slot.  changed computedFIelds to do mobx caching.  changed text
 boxes to do updating from templates based on a fieldKey_autoUpdate flag
 combined with modification timestamps.  enabled comparison box to work with
 text fields in addition to docs.

---
 src/client/util/Scripting.ts                       |  9 +--
 src/client/views/InkControlPtHandles.tsx           | 85 ++++++++++++----------
 src/client/views/PropertiesButtons.tsx             |  4 +-
 src/client/views/nodes/ComparisonBox.tsx           |  6 +-
 src/client/views/nodes/KeyValueBox.tsx             |  2 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx | 31 +++++---
 src/fields/Doc.ts                                  |  2 +-
 src/fields/ScriptField.ts                          | 22 ++++--
 8 files changed, 90 insertions(+), 71 deletions(-)

(limited to 'src/client/views/nodes/ComparisonBox.tsx')

diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index f5e162d16..31222aa50 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -88,15 +88,10 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
             }
 
             const result = compiledFunction.apply(thisParam, params).apply(thisParam, argsArray);
-            if (batch) {
-                batch.end();
-            }
-
+            batch?.end();
             return { success: true, result };
         } catch (error) {
-            if (batch) {
-                batch.end();
-            }
+            batch?.end();
             onError?.(script + ' ' + error);
             return { success: false, error, result: errorVal };
         }
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index 31b13d2c8..01d52135a 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react';
-import { action, observable } from 'mobx';
+import { action, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc } from '../../fields/Doc';
 import { ControlPoint, InkData, PointData } from '../../fields/InkField';
@@ -13,6 +13,7 @@ import { Colors } from './global/globalEnums';
 import { InkingStroke } from './InkingStroke';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { SnappingManager } from '../util/SnappingManager';
+import { ObservableReactComponent } from './ObservableReactComponent';
 
 export interface InkControlProps {
     inkDoc: Doc;
@@ -24,10 +25,15 @@ export interface InkControlProps {
 }
 
 @observer
-export class InkControlPtHandles extends React.Component<InkControlProps> {
+export class InkControlPtHandles extends ObservableReactComponent<InkControlProps> {
     @observable private _overControl = -1;
     get docView() {
-        return this.props.inkView.DocumentView?.();
+        return this._props.inkView.DocumentView?.();
+    }
+
+    constructor(props: InkControlProps) {
+        super(props);
+        makeObservable(this);
     }
 
     componentDidMount() {
@@ -42,51 +48,51 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
      */
     @action
     onControlDown = (e: React.PointerEvent, controlIndex: number): void => {
-        const ptFromScreen = this.props.inkView.ptFromScreen;
+        const ptFromScreen = this._props.inkView.ptFromScreen;
         if (ptFromScreen) {
             const order = controlIndex % 4;
-            const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this.props.inkCtrlPoints.length) % this.props.inkCtrlPoints.length;
-            const handleIndexB = (order === 3 ? controlIndex + 2 : controlIndex + 1) % this.props.inkCtrlPoints.length;
-            const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec('number'));
+            const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this._props.inkCtrlPoints.length) % this._props.inkCtrlPoints.length;
+            const handleIndexB = (order === 3 ? controlIndex + 2 : controlIndex + 1) % this._props.inkCtrlPoints.length;
+            const brokenIndices = Cast(this._props.inkDoc.brokenInkIndices, listSpec('number'));
             const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex;
             if (!wasSelected) InkStrokeProperties.Instance._currentPoint = -1;
-            const origInk = this.props.inkCtrlPoints.slice();
+            const origInk = this._props.inkCtrlPoints.slice();
             setupMoveUpEvents(
                 this,
                 e,
                 action((e: PointerEvent, down: number[], delta: number[]) => {
-                    if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('drag ink ctrl pt');
+                    if (!this._props.inkView.controlUndo) this._props.inkView.controlUndo = UndoManager.StartBatch('drag ink ctrl pt');
                     const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] });
                     const inkMoveStart = ptFromScreen({ X: 0, Y: 0 });
                     this.docView && InkStrokeProperties.Instance.moveControlPtHandle(this.docView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex, origInk);
                     return false;
                 }),
                 action(() => {
-                    if (this.props.inkView.controlUndo && this.docView) {
+                    if (this._props.inkView.controlUndo && this.docView) {
                         InkStrokeProperties.Instance.snapControl(this.docView, controlIndex);
                     }
-                    this.props.inkView.controlUndo?.end();
-                    this.props.inkView.controlUndo = undefined;
+                    this._props.inkView.controlUndo?.end();
+                    this._props.inkView.controlUndo = undefined;
                     UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']);
                 }),
                 action((e: PointerEvent, doubleTap: boolean | undefined) => {
-                    const equivIndex = controlIndex === 0 ? this.props.inkCtrlPoints.length - 1 : controlIndex === this.props.inkCtrlPoints.length - 1 ? 0 : controlIndex;
+                    const equivIndex = controlIndex === 0 ? this._props.inkCtrlPoints.length - 1 : controlIndex === this._props.inkCtrlPoints.length - 1 ? 0 : controlIndex;
                     if (doubleTap || e.button === 2) {
                         if (!brokenIndices?.includes(equivIndex) && !brokenIndices?.includes(controlIndex)) {
                             if (brokenIndices) brokenIndices.push(controlIndex);
-                            else this.props.inkDoc.brokenInkIndices = new List<number>([controlIndex]);
+                            else this._props.inkDoc.brokenInkIndices = new List<number>([controlIndex]);
                         } else {
                             if (brokenIndices?.includes(equivIndex)) {
-                                if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('make smooth');
+                                if (!this._props.inkView.controlUndo) this._props.inkView.controlUndo = UndoManager.StartBatch('make smooth');
                                 this.docView && InkStrokeProperties.Instance.snapHandleTangent(this.docView, equivIndex, handleIndexA, handleIndexB);
                             }
                             if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) {
-                                if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('make smooth');
+                                if (!this._props.inkView.controlUndo) this._props.inkView.controlUndo = UndoManager.StartBatch('make smooth');
                                 this.docView && InkStrokeProperties.Instance.snapHandleTangent(this.docView, controlIndex, handleIndexA, handleIndexB);
                             }
                         }
-                        this.props.inkView.controlUndo?.end();
-                        this.props.inkView.controlUndo = undefined;
+                        this._props.inkView.controlUndo?.end();
+                        this._props.inkView.controlUndo = undefined;
                     }
                     this.changeCurrPoint(controlIndex);
                 }),
@@ -126,14 +132,14 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
 
     render() {
         // Accessing the current ink's data and extracting all control points.
-        const scrData = this.props.screenCtrlPoints;
+        const scrData = this._props.screenCtrlPoints;
         const sreenCtrlPoints: ControlPoint[] = [];
         for (let i = 0; i <= scrData.length - 4; i += 4) {
             sreenCtrlPoints.push({ ...scrData[i], I: i });
             sreenCtrlPoints.push({ ...scrData[i + 3], I: i + 3 });
         }
 
-        const inkData = this.props.inkCtrlPoints;
+        const inkData = this._props.inkCtrlPoints;
         const inkCtrlPts: ControlPoint[] = [];
         for (let i = 0; i <= inkData.length - 4; i += 4) {
             inkCtrlPts.push({ ...inkData[i], I: i });
@@ -141,23 +147,23 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
         }
 
         const closed = InkingStroke.IsClosed(inkData);
-        const nearestScreenPt = this.props.nearestScreenPt();
+        const nearestScreenPt = this._props.nearestScreenPt();
         const TagType = (broken?: boolean) => (broken ? 'rect' : 'circle');
         const hdl = (control: { X: number; Y: number; I: number }, scale: number, color: string) => {
-            const broken = Cast(this.props.inkDoc.brokenInkIndices, listSpec('number'))?.includes(control.I);
+            const broken = Cast(this._props.inkDoc.brokenInkIndices, listSpec('number'))?.includes(control.I);
             const Tag = TagType((control.I === 0 || control.I === inkData.length - 1) && !closed) as keyof JSX.IntrinsicElements;
             return (
                 <Tag
                     key={control.I.toString() + scale}
-                    x={control.X - this.props.screenSpaceLineWidth * 2 * scale}
-                    y={control.Y - this.props.screenSpaceLineWidth * 2 * scale}
+                    x={control.X - this._props.screenSpaceLineWidth * 2 * scale}
+                    y={control.Y - this._props.screenSpaceLineWidth * 2 * scale}
                     cx={control.X}
                     cy={control.Y}
-                    r={this.props.screenSpaceLineWidth * 2 * scale}
-                    opacity={this.props.inkView.controlUndo ? 0.35 : 1}
-                    height={this.props.screenSpaceLineWidth * 4 * scale}
-                    width={this.props.screenSpaceLineWidth * 4 * scale}
-                    strokeWidth={this.props.screenSpaceLineWidth / 2}
+                    r={this._props.screenSpaceLineWidth * 2 * scale}
+                    opacity={this._props.inkView.controlUndo ? 0.35 : 1}
+                    height={this._props.screenSpaceLineWidth * 4 * scale}
+                    width={this._props.screenSpaceLineWidth * 4 * scale}
+                    strokeWidth={this._props.screenSpaceLineWidth / 2}
                     stroke={Colors.MEDIUM_BLUE}
                     fill={broken ? Colors.MEDIUM_BLUE : color}
                     onPointerDown={(e: React.PointerEvent) => this.onControlDown(e, control.I)}
@@ -170,7 +176,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
         };
         return (
             <svg>
-                {!nearestScreenPt ? null : <circle key={'npt'} cx={nearestScreenPt.X} cy={nearestScreenPt.Y} r={this.props.screenSpaceLineWidth * 2} fill={'#00007777'} stroke={'#00007777'} strokeWidth={0} pointerEvents="none" />}
+                {!nearestScreenPt ? null : <circle key={'npt'} cx={nearestScreenPt.X} cy={nearestScreenPt.Y} r={this._props.screenSpaceLineWidth * 2} fill={'#00007777'} stroke={'#00007777'} strokeWidth={0} pointerEvents="none" />}
                 {sreenCtrlPoints.map(control => hdl(control, this._overControl !== control.I ? 1 : 3 / 2, Colors.WHITE))}
             </svg>
         );
@@ -185,10 +191,15 @@ export interface InkEndProps {
     endPt: () => PointData;
 }
 @observer
-export class InkEndPtHandles extends React.Component<InkEndProps> {
+export class InkEndPtHandles extends ObservableReactComponent<InkEndProps> {
     @observable _overStart: boolean = false;
     @observable _overEnd: boolean = false;
 
+    constructor(props: InkEndProps) {
+        super(props);
+        makeObservable(this);
+    }
+
     _throttle = 0; // need to throttle dragging since the position may change when the control points change.  this allows the stroke to settle so that we don't get increasingly bad jitter
     @action
     dragRotate = (e: React.PointerEvent, pt1: () => { X: number; Y: number }, pt2: () => { X: number; Y: number }) => {
@@ -198,7 +209,7 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
             e,
             action(e => {
                 if (this._throttle++ % 2 !== 0) return false;
-                if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('stretch ink');
+                if (!this._props.inkView.controlUndo) this._props.inkView.controlUndo = UndoManager.StartBatch('stretch ink');
                 // compute stretch factor by finding scaling along axis between start and end points
                 const p1 = pt1();
                 const p2 = pt2();
@@ -216,8 +227,8 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
             }),
             action(() => {
                 SnappingManager.SetIsDragging(false);
-                this.props.inkView.controlUndo?.end();
-                this.props.inkView.controlUndo = undefined;
+                this._props.inkView.controlUndo?.end();
+                this._props.inkView.controlUndo = undefined;
                 UndoManager.FilterBatches(['stroke', 'x', 'y', 'width', 'height']);
             }),
             returnFalse
@@ -230,7 +241,7 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
                 key={key}
                 cx={pt.X}
                 cy={pt.Y}
-                r={this.props.screenSpaceLineWidth * 2}
+                r={this._props.screenSpaceLineWidth * 2}
                 fill={this._overStart ? '#aaaaaa' : '#99999977'}
                 stroke={'#00007777'}
                 strokeWidth={0}
@@ -242,8 +253,8 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
         );
         return (
             <svg>
-                {hdl('start', this.props.startPt(), e => this.dragRotate(e, this.props.startPt, this.props.endPt))}
-                {hdl('end', this.props.endPt(), e => this.dragRotate(e, this.props.endPt, this.props.startPt))}
+                {hdl('start', this._props.startPt(), e => this.dragRotate(e, this._props.startPt, this._props.endPt))}
+                {hdl('end', this._props.endPt(), e => this.dragRotate(e, this._props.endPt, this._props.startPt))}
             </svg>
         );
     }
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 02f288a68..517a80d63 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -87,7 +87,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                     containerDoc._isLightbox = !containerDoc._isLightbox;
                     //containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined;
                     const containerContents = DocListCast(dv.dataDoc[Doc.LayoutFieldKey(containerDoc)]);
-                    //dv.Docuemnt.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' });
+                    //dv.Docuemnt.onClick = ScriptField.MakeScript('{this.data = undefined; documentView.select(false)}', { documentView: 'any' });
                     containerContents.forEach(doc => LinkManager.Links(doc).forEach(link => (link.link_displayLine = false)));
                 });
             }
@@ -219,7 +219,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
     //                 containerDoc._isLightbox = !containerDoc._isLightbox;
     //                 //containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined;
     //                 const containerContents = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]);
-    //                 //dv.Document.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' });
+    //                 //dv.Document.onClick = ScriptField.MakeScript('{this.data = undefined; documentView.select(false)}', { documentView: 'any' });
     //                 containerContents.forEach(doc => LinkManager.Links(doc).forEach(link => (link.layout_linkDisplay = false)));
     //             });
     //         }
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 59d99b371..9ffdc350d 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -69,8 +69,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 action((e, doubleTap) => {
                     if (doubleTap) {
                         this._isAnyChildContentActive = true;
-                        if (!this.dataDoc[this.fieldKey + '_1']) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
-                        if (!this.dataDoc[this.fieldKey + '_2']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
+                        if (!this.dataDoc[this.fieldKey + '_1'] && !this.dataDoc[this.fieldKey]) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
+                        if (!this.dataDoc[this.fieldKey + '_2'] && !this.dataDoc[this.fieldKey + '_alternate']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
                     }
                 }),
                 false,
@@ -131,8 +131,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         return false;
     };
 
-    whenChildContentsActiveChanged = action((isActive: boolean) => (this._isAnyChildContentActive = isActive));
-
     closeDown = (e: React.PointerEvent, which: string) => {
         setupMoveUpEvents(
             this,
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 78e4435ce..31a2367fc 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -55,7 +55,7 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
     @observable _splitPercentage = 50;
 
     get fieldDocToLayout() {
-        return this._props.fieldKey ? DocCast(this._props.Document[this._props.fieldKey], DocCast(this._props.Document)) : this._props.Document;
+        return DocCast(this._props.Document);
     }
 
     @action
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index d25101844..f06e5fad0 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -21,7 +21,7 @@ import { List } from '../../../../fields/List';
 import { PrefetchProxy } from '../../../../fields/Proxy';
 import { RichTextField } from '../../../../fields/RichTextField';
 import { ComputedField } from '../../../../fields/ScriptField';
-import { BoolCast, Cast, DocCast, FieldValue, NumCast, RTFCast, ScriptCast, StrCast } from '../../../../fields/Types';
+import { BoolCast, Cast, DateCast, DocCast, FieldValue, NumCast, RTFCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util';
 import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivWidth, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
 import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT';
@@ -349,6 +349,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             const prevData = Cast(this.layoutDoc[this.fieldKey], RichTextField, null); // the actual text in the text box
             const templateData = this.Document !== this.layoutDoc ? prevData : undefined; // the default text stored in a layout template
             const protoData = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype
+            const layoutData = this.layoutDoc.isTemplateDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text inherited from a prototype
             const effectiveAcl = GetEffectiveAcl(dataDoc);
 
             const removeSelection = (json: string | undefined) => json?.replace(/"selection":.*/, '');
@@ -369,22 +370,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 if (this._applyingChange !== this.fieldKey && (force || textChange || removeSelection(newJson) !== removeSelection(prevData?.Data))) {
                     this._applyingChange = this.fieldKey;
                     textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
-                    if ((!prevData && !protoData) || newText || (!newText && !protoData)) {
+                    if ((!prevData && !protoData && !layoutData) || newText || (!newText && !protoData && !layoutData)) {
                         // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
                         if (force || ((this._finishingLink || this._props.isContentActive() || this._inDrop) && (textChange || removeSelection(newJson) !== removeSelection(prevData?.Data)))) {
                             const numstring = NumCast(dataDoc[this.fieldKey], null);
-                            dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : newText || DocCast(dataDoc.proto)?.[this.fieldKey] === undefined ? new RichTextField(newJson, newText) : undefined;
-
+                            dataDoc[this.fieldKey] =
+                                numstring !== undefined ? Number(newText) : newText || (DocCast(dataDoc.proto)?.[this.fieldKey] === undefined && this.layoutDoc[this.fieldKey] === undefined) ? new RichTextField(newJson, newText) : undefined;
                             textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.Document, text: newText });
                             this._applyingChange = ''; // turning this off here allows a Doc to retrieve data from template if noTemplate below is changed to false
-                            dataDoc[this.fieldKey + '_noTemplate'] = newText ? true : false; // mark the data field as being split from the template if it has been edited
                             unchanged = false;
                         }
                     } else {
                         // if we've deleted all the text in a note driven by a template, then restore the template data
                         dataDoc[this.fieldKey] = undefined;
-                        this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((protoData || prevData).Data)));
-                        dataDoc[this.fieldKey + '_noTemplate'] = undefined; // mark the data field as not being split from any template it might have
+                        this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((layoutData ?? protoData ?? prevData).Data)));
                         ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, text: newText });
                         unchanged = false;
                     }
@@ -958,6 +957,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
         const options = cm.findByDescription('Options...');
         const optionItems = options && 'subitems' in options ? options.subitems : [];
+        optionItems.push({ description: `Toggle auto update from template`, event: () => (this.dataDoc[this.fieldKey + '_autoUpdate'] = !this.dataDoc[this.fieldKey + '_autoUpdate']), icon: 'star' });
         optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' });
         optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' });
         this._props.renderDepth &&
@@ -1244,9 +1244,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         );
         this._disposers.editorState = reaction(
             () => {
-                const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc?.proto), this.fieldKey) ? DocCast(this.layoutDoc?.proto) : this?.dataDoc;
-                const whichDoc = !this.dataDoc || !this.layoutDoc ? undefined : dataDoc?.[this.fieldKey + '_noTemplate'] || !this.layoutDoc[this.fieldKey] ? dataDoc : this.layoutDoc;
-                return !whichDoc ? undefined : { data: Cast(whichDoc[this.fieldKey], RichTextField, null), str: Field.toString(DocCast(whichDoc[this.fieldKey]) ?? StrCast(whichDoc[this.fieldKey])) };
+                const protoData = DocCast(this.dataDoc.proto)?.[this.fieldKey];
+                const dataData = this.dataDoc[this.fieldKey];
+                const layoutData = Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? undefined : this.layoutDoc[this.fieldKey];
+                const dataTime = dataData ? DateCast(this.dataDoc[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0 : 0;
+                const layoutTime = layoutData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? DateCast(DocCast(this.layoutDoc)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0 : 0;
+                const protoTime = protoData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? DateCast(DocCast(this.dataDoc.proto)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0 : 0;
+                const recentData = dataTime >= layoutTime ? (protoTime >= dataTime ? protoData : dataData) : layoutTime >= protoTime ? layoutData : protoData;
+                const whichData = recentData ?? (this.layoutDoc.isTemplateDoc ? layoutData : protoData) ?? protoData;
+                return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) };
             },
             incomingValue => {
                 if (this._editorView && this._applyingChange !== this.fieldKey) {
@@ -1256,11 +1262,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                             this._editorView.updateState(EditorState.fromJSON(this.config, updatedState));
                             this.tryUpdateScrollHeight();
                         }
-                    } else {
+                    } else if (this._editorView.state.doc.textContent !== incomingValue?.str) {
                         selectAll(this._editorView.state, tx => this._editorView?.dispatch(tx.insertText(incomingValue?.str ?? '')));
                     }
                 }
-            }
+            },
+            { fireImmediately: true }
         );
 
         this._disposers.search = reaction(
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index bdf600475..200896e25 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -48,7 +48,7 @@ export namespace Field {
         const valFunc = (field: Field): string => {
             const res =
                 field instanceof ComputedField && showComputedValue
-                    ? field._lastComputedResult
+                    ? field.value(doc)
                     : field instanceof ComputedField
                       ? `:=${field.script.originalScript.replace(/dashCallChat\(_setCacheResult_, this, `(.*)`\)/, '(($1))')}`
                       : field instanceof ScriptField
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 9021c8896..8b51088b2 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -198,14 +198,22 @@ export class ComputedField extends ScriptField {
     }
 
     _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;
-    };
+    value = (doc:Doc) => (this._lastComputedResult = this._cachedResult ?? 
+        computedFn((doc: Doc) =>
+            this.script.compiled &&
+            this.script.run( {
+                    this: doc, 
+                    //value: '',
+                    _setCacheResult_: this.setCacheResult,
+                    _last_: this._lastComputedResult,
+                    _readOnly_: true,
+                },
+                console.log
+            ).result
+        )(doc)
+    ); // prettier-ignore
 
-    [ToValue](doc: Doc)   { if (ComputedField.useComputed) return { value: this._valueOutsideReaction(doc) };  } // prettier-ignore
+    [ToValue](doc: Doc)   { if (ComputedField.useComputed) return { value: this.value(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) {
-- 
cgit v1.2.3-70-g09d2

