From a5d81be52ec685aede35791a706e5e1cb7f40613 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 14 Mar 2024 08:45:16 -0400
Subject: fixed disabling info ui to keep it off -- use settings manager to
 turn it back on.

---
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx   | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 9500b918a..7bfbbf3f9 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1417,8 +1417,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return anchor;
     };
 
-    @action closeInfo = () => (this.Document._hideInfo = true);
-    infoUI = () => (this.Document._hideInfo || this.Document.annotationOn || this._props.renderDepth ? null : <CollectionFreeFormInfoUI Document={this.Document} Freeform={this} close={this.closeInfo} />);
+    @action closeInfo = () => (Doc.IsInfoUIDisabled = true);
+    infoUI = () => (Doc.IsInfoUIDisabled || this.Document.annotationOn || this._props.renderDepth ? null : <CollectionFreeFormInfoUI Document={this.Document} Freeform={this} close={this.closeInfo} />);
 
     componentDidMount() {
         this._props.setContentViewBox?.(this);
-- 
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/collections/collectionFreeForm/CollectionFreeFormView.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

