import { action, makeObservable, observable } from 'mobx'; import { computedFn } from 'mobx-utils'; import { PropSchema, SKIP, createSimpleSchema, custom, map, object, primitive, serializable } from 'serializr'; import { emptyFunction, numberRange } from '../Utils'; import { GPTCallType, gptAPICall } from '../client/apis/gpt/GPT'; import { CompileScript, CompiledScript, ScriptOptions, Transformer } from '../client/util/Scripting'; import { ScriptingGlobals, scriptingGlobal } from '../client/util/ScriptingGlobals'; import { Deserializable, autoObject } from '../client/util/SerializationHelper'; import { Doc, Field, FieldType, FieldResult, ObjGetRefField, 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'; function optional(propSchema: PropSchema) { return custom( value => { if (value !== undefined) { return propSchema.serializer(value, '', undefined); // this function only takes one parameter, but I think its typescript typings are messed up to take 3 } return SKIP; }, (jsonValue, context, oldValue, callback) => { if (jsonValue !== undefined) { return propSchema.deserializer(jsonValue, callback, context, oldValue); } return SKIP; } ); } const optionsSchema = createSimpleSchema({ requiredType: true, addReturn: true, typecheck: true, editable: true, readonly: true, params: optional(map(primitive())), }); const scriptSchema = createSimpleSchema({ options: object(optionsSchema), originalScript: true, }); // eslint-disable-next-line no-use-before-define function finalizeScript(scriptIn: ScriptField) { const script = scriptIn; const comp = CompileScript(script.script.originalScript, script.script.options); if (!comp.compiled) { throw new Error("Couldn't compile loaded script"); } if (script.setterscript) { const compset = CompileScript(script.setterscript?.originalScript, script.setterscript.options); if (!compset.compiled) { throw new Error("Couldn't compile setter script"); } script.setterscript = compset; } return comp; } // eslint-disable-next-line no-use-before-define async function deserializeScript(scriptIn: ScriptField) { const script = scriptIn; if (script.captures) { const captured: { [key: string]: undefined | string | number | boolean | Doc } = {}; (script.script.options as ScriptOptions).capturedVariables = captured; Promise.all( script.captures.map(async capture => { const key = capture.split(':')[0]; const val = capture.split(':')[1]; if (val === 'true') captured[key] = true; else if (val === 'false') captured[key] = false; else if (val.startsWith('ID->')) captured[key] = await ObjGetRefField(val.replace('ID->', '')); else if (!isNaN(Number(val))) captured[key] = Number(val); else captured[key] = val; }) ).then(() => { script.script = finalizeScript(script); }); } else { // eslint-disable-next-line no-use-before-define script.script = ScriptField.GetScriptFieldCache(script.script.originalScript) ?? finalizeScript(script); } } @scriptingGlobal @Deserializable('script', (obj: unknown) => deserializeScript(obj as ScriptField)) export class ScriptField extends ObjectField { @serializable readonly rawscript: string | undefined; @serializable(object(scriptSchema)) script: CompiledScript; @serializable(object(scriptSchema)) setterscript: CompiledScript | undefined; @serializable @observable _cachedResult: FieldResult = undefined; setCacheResult = action((value: FieldResult) => { this._cachedResult = value; this[FieldChanged]?.(); }); @serializable(autoObject()) captures?: List; public static _scriptFieldCache: Map> = new Map(); public static GetScriptFieldCache(field: string) { return this._scriptFieldCache.get(field); } constructor(script: CompiledScript | undefined, setterscript?: CompiledScript, rawscript?: string) { super(); const captured = script?.options?.capturedVariables; if (captured) { this.captures = new List(Object.keys(captured).map(key => key + ':' + (captured[key] instanceof Doc ? 'ID->' + (captured[key] as Doc)[Id] : captured[key]?.toString()))); } this.rawscript = rawscript; this.setterscript = setterscript; this.script = script ?? ScriptField.GetScriptFieldCache('false:') ?? (CompileScript('false', { addReturn: true }) as CompiledScript); } [Copy](): ObjectField { return new ScriptField(this.script, this.setterscript, this.rawscript); } toString() { return `${this.script.originalScript} + ${this.setterscript?.originalScript}`; } [ToJavascriptString]() { return this.script.originalScript; } [ToScriptString]() { return this.script.originalScript; } [ToString]() { return this.script.originalScript; } // eslint-disable-next-line default-param-last 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 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, }); } // eslint-disable-next-line default-param-last 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; } // eslint-disable-next-line default-param-last public static MakeScript(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { const compiled = ScriptField.CompileScript(script, params, false, capturedVariables); return compiled.compiled ? new ScriptField(compiled) : undefined; } public static CallGpt(queryTextIn: string, setVal: (val: FieldResult) => void, target: Doc) { let queryText = queryTextIn; 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 FieldType)); } 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', (obj: unknown) => deserializeScript(obj as ComputedField)) export class ComputedField extends ScriptField { static undefined = '__undefined'; static useComputed = true; static DisableComputedFields() { this.useComputed = false; } // prettier-ignore static EnableComputedFields() { this.useComputed = true; } // prettier-ignore static WithoutComputed(fn: () => T) { this.DisableComputedFields(); try { return fn(); } finally { this.EnableComputedFields(); } } constructor(script: CompiledScript | undefined, setterscript?: CompiledScript, rawscript?: string) { super(script, setterscript, rawscript); makeObservable(this); } _lastComputedResult: FieldResult; value = (doc: Doc) => { this._lastComputedResult = this._cachedResult ?? computedFn(() => this.script.compiled && this.script.run( { this: doc, // value: '', _setCacheResult_: this.setCacheResult, _last_: this._lastComputedResult, _readOnly_: true, }, console.log ).result as FieldResult )(); // prettier-ignore return this._lastComputedResult; }; [ToValue](doc: Doc) { return ComputedField.useComputed ? { value: this.value(doc) } : undefined; } // prettier-ignore [Copy](): ObjectField { return new ComputedField(this.script, this.setterscript, this.rawscript); } // prettier-ignore // eslint-disable-next-line default-param-last 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) { if (!doc[`${fieldKey}_indexed`]) { const flist = new List(numberRange(curTimecode + 1).map(emptyFunction) as unknown as number[]); flist[curTimecode] = Cast(doc[fieldKey], 'number', null); doc[`${fieldKey}_indexed`] = flist; } const getField = ScriptField.CompileScript(`getIndexVal(this['${fieldKey}_indexed'], this.${interpolatorKey}, ${defaultVal})`, {}, true, {}); const setField = ScriptField.CompileScript(`setIndexVal(this['${fieldKey}_indexed'], this.${interpolatorKey}, value)`, { value: 'any' }, true, {}); return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; } public static MakeInterpolatedString(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) { if (!doc[`${fieldKey}_`]) { const flist = new List(numberRange(curTimecode + 1).map(emptyFunction) as unknown as string[]); flist[curTimecode] = StrCast(doc[fieldKey]); doc[`${fieldKey}_indexed`] = flist; } const getField = ScriptField.CompileScript(`getIndexVal(this['${fieldKey}_indexed'], this.${interpolatorKey})`, {}, true, {}); const setField = ScriptField.CompileScript(`setIndexVal(this['${fieldKey}_indexed'], this.${interpolatorKey}, value)`, { value: 'any' }, true, {}); return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; } public static MakeInterpolatedDataField(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) { if (doc[`${fieldKey}`] instanceof List) return undefined; if (!doc[`${fieldKey}_indexed`]) { const flist = new List(numberRange(curTimecode + 1).map(emptyFunction) as unknown as FieldType[]); flist[curTimecode] = Field.Copy(doc[fieldKey]); doc[`${fieldKey}_indexed`] = flist; } const getField = ScriptField.CompileScript(`getIndexVal(this['${fieldKey}_indexed'], this.${interpolatorKey})`, {}, true, {}); const setField = ScriptField.CompileScript( `{setIndexVal (this['${fieldKey}_indexed'], this.${interpolatorKey}, value); console.log(this["${fieldKey}_indexed"][this.${interpolatorKey}],this.data,this["${fieldKey}_indexed"]))}`, { value: 'any' }, false, {} ); doc[fieldKey] = getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; return doc[fieldKey]; } } ScriptingGlobals.add( // eslint-disable-next-line prefer-arrow-callback function setIndexVal(list: FieldResult[], index: number, value: FieldType) { while (list.length <= index) list.push(undefined); list[index] = value; }, 'sets the value at a given index of a list', '(list: any[], index: number, value: any)' ); ScriptingGlobals.add( // eslint-disable-next-line prefer-arrow-callback function getIndexVal(list: unknown[], index: number, defaultVal: Opt = undefined) { return list?.reduce((p, x, i) => ((i <= index && x !== undefined) || p === undefined ? x : p), defaultVal); }, 'returns the value at a given index of a list', '(list: any[], index: number)' ); ScriptingGlobals.add( // eslint-disable-next-line prefer-arrow-callback function makeScript(script: string) { return ScriptField.MakeScript(script); }, 'returns the value at a given index of a list', '(list: any[], index: number)' ); // eslint-disable-next-line prefer-arrow-callback 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');