2020-10-02 15:10:00 +00:00
|
|
|
import {KoArray} from 'app/client/lib/koArray';
|
2023-03-23 18:22:28 +00:00
|
|
|
import {localStorageJsonObs} from 'app/client/lib/localStorageObs';
|
2022-10-17 09:47:16 +00:00
|
|
|
import {CellRec, DocModel, IRowModel, recordSet,
|
|
|
|
refRecord, TableRec, ViewFieldRec} from 'app/client/models/DocModel';
|
2023-03-23 18:22:28 +00:00
|
|
|
import {urlState} from 'app/client/models/gristUrlState';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {jsonObservable, ObjObservable} from 'app/client/models/modelUtil';
|
2023-05-08 18:15:22 +00:00
|
|
|
import {AssistanceState} from 'app/common/AssistancePrompts';
|
2020-10-02 15:10:00 +00:00
|
|
|
import * as gristTypes from 'app/common/gristTypes';
|
2022-02-04 11:13:03 +00:00
|
|
|
import {getReferencedTableId} from 'app/common/gristTypes';
|
|
|
|
import {
|
|
|
|
BaseFormatter,
|
|
|
|
createFullFormatterRaw,
|
|
|
|
createVisibleColFormatterRaw,
|
|
|
|
FullFormatterArgs
|
|
|
|
} from 'app/common/ValueFormatter';
|
2022-12-20 12:56:43 +00:00
|
|
|
import {createParser} from 'app/common/ValueParser';
|
2023-03-23 18:22:28 +00:00
|
|
|
import {Observable} from 'grainjs';
|
2020-10-02 15:10:00 +00:00
|
|
|
import * as ko from 'knockout';
|
2023-07-20 14:25:26 +00:00
|
|
|
import {v4 as uuidv4} from 'uuid';
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2022-10-14 10:07:19 +00:00
|
|
|
// Column behavior type, used primarily in the UI.
|
|
|
|
export type BEHAVIOR = "empty"|"formula"|"data";
|
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
// Represents a column in a user-defined table.
|
|
|
|
export interface ColumnRec extends IRowModel<"_grist_Tables_column"> {
|
|
|
|
table: ko.Computed<TableRec>;
|
|
|
|
widgetOptionsJson: ObjObservable<any>;
|
2023-10-20 11:05:29 +00:00
|
|
|
/** Widget options that are save to copy over (for now, without rules) */
|
|
|
|
cleanWidgetOptionsJson: ko.Computed<string>;
|
2020-10-02 15:10:00 +00:00
|
|
|
viewFields: ko.Computed<KoArray<ViewFieldRec>>;
|
|
|
|
summarySource: ko.Computed<ColumnRec>;
|
|
|
|
|
|
|
|
// Is an empty column (undecided if formula or data); denoted by an empty formula.
|
|
|
|
isEmpty: ko.Computed<boolean>;
|
|
|
|
|
|
|
|
// Is a real formula column (not an empty column; i.e. contains a non-empty formula).
|
|
|
|
isRealFormula: ko.Computed<boolean>;
|
|
|
|
|
2021-09-25 19:14:19 +00:00
|
|
|
// Is a trigger formula column (not formula, but contains non-empty formula)
|
|
|
|
hasTriggerFormula: ko.Computed<boolean>;
|
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
// Used for transforming a column.
|
|
|
|
// Reference to the original column for a transform column, or to itself for a non-transforming column.
|
|
|
|
origColRef: ko.Observable<number>;
|
|
|
|
origCol: ko.Computed<ColumnRec>;
|
|
|
|
// Indicates whether a column is transforming. Manually set, but should be true in both the original
|
|
|
|
// column being transformed and that column's transform column.
|
|
|
|
isTransforming: ko.Observable<boolean>;
|
|
|
|
|
|
|
|
// Convenience observable to obtain and set the type with no suffix
|
|
|
|
pureType: ko.Computed<string>;
|
|
|
|
|
2022-10-14 10:07:19 +00:00
|
|
|
// Column behavior as seen by the user.
|
|
|
|
behavior: ko.Computed<BEHAVIOR>;
|
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
// The column's display column
|
|
|
|
_displayColModel: ko.Computed<ColumnRec>;
|
|
|
|
|
2021-11-19 20:30:11 +00:00
|
|
|
// Display col ref to use for the column, defaulting to the plain column itself.
|
|
|
|
displayColRef: ko.Computed<number>;
|
|
|
|
|
|
|
|
// The display column to use for the column, or the column itself when no displayCol is set.
|
|
|
|
displayColModel: ko.Computed<ColumnRec>;
|
|
|
|
visibleColModel: ko.Computed<ColumnRec>;
|
|
|
|
|
2021-03-17 03:45:44 +00:00
|
|
|
disableModifyBase: ko.Computed<boolean>; // True if column config can't be modified (name, type, etc.)
|
2022-10-17 09:47:16 +00:00
|
|
|
disableModify: ko.Computed<boolean>; // True if column can't be modified (is summary) or is being transformed.
|
2021-03-17 03:45:44 +00:00
|
|
|
disableEditData: ko.Computed<boolean>; // True to disable editing of the data in this column.
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
isHiddenCol: ko.Computed<boolean>;
|
|
|
|
|
|
|
|
// Returns the rowModel for the referenced table, or null, if is not a reference column.
|
|
|
|
refTable: ko.Computed<TableRec|null>;
|
|
|
|
|
2021-12-15 22:31:53 +00:00
|
|
|
// Helper for Reference/ReferenceList columns, which returns a formatter according
|
|
|
|
// to the visibleCol associated with column.
|
|
|
|
visibleColFormatter: ko.Computed<BaseFormatter>;
|
|
|
|
|
2022-01-13 10:04:56 +00:00
|
|
|
// A formatter for values of this column.
|
|
|
|
// The difference between visibleColFormatter and formatter is especially important for ReferenceLists:
|
|
|
|
// `visibleColFormatter` is for individual elements of a list, sometimes hypothetical
|
|
|
|
// (i.e. they aren't actually referenced but they exist in the visible column and are relevant to e.g. autocomplete)
|
|
|
|
// `formatter` formats actual cell values, e.g. a whole list from the display column.
|
|
|
|
formatter: ko.Computed<BaseFormatter>;
|
2022-10-17 09:47:16 +00:00
|
|
|
cells: ko.Computed<KoArray<CellRec>>;
|
2022-01-13 10:04:56 +00:00
|
|
|
|
2023-03-23 18:22:28 +00:00
|
|
|
/**
|
|
|
|
* Current history of chat. This is a temporary array used only in the ui.
|
|
|
|
*/
|
2023-05-08 18:15:22 +00:00
|
|
|
chatHistory: ko.PureComputed<Observable<ChatHistory>>;
|
2023-03-23 18:22:28 +00:00
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
// Helper which adds/removes/updates column's displayCol to match the formula.
|
|
|
|
saveDisplayFormula(formula: string): Promise<void>|undefined;
|
2022-12-20 12:56:43 +00:00
|
|
|
|
|
|
|
createValueParser(): (value: string) => any;
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export function createColumnRec(this: ColumnRec, docModel: DocModel): void {
|
|
|
|
this.table = refRecord(docModel.tables, this.parentId);
|
|
|
|
this.widgetOptionsJson = jsonObservable(this.widgetOptions);
|
|
|
|
this.viewFields = recordSet(this, docModel.viewFields, 'colRef');
|
|
|
|
this.summarySource = refRecord(docModel.columns, this.summarySourceCol);
|
2022-10-17 09:47:16 +00:00
|
|
|
this.cells = recordSet(this, docModel.cells, 'colRef');
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
// Is this an empty column (undecided if formula or data); denoted by an empty formula.
|
|
|
|
this.isEmpty = ko.pureComputed(() => this.isFormula() && this.formula() === '');
|
|
|
|
|
|
|
|
// Is this a real formula column (not an empty column; i.e. contains a non-empty formula).
|
|
|
|
this.isRealFormula = ko.pureComputed(() => this.isFormula() && this.formula() !== '');
|
2021-09-25 19:14:19 +00:00
|
|
|
// If this column has a trigger formula defined
|
|
|
|
this.hasTriggerFormula = ko.pureComputed(() => !this.isFormula() && this.formula() !== '');
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
// Used for transforming a column.
|
|
|
|
// Reference to the original column for a transform column, or to itself for a non-transforming column.
|
|
|
|
this.origColRef = ko.observable(this.getRowId());
|
|
|
|
this.origCol = refRecord(docModel.columns, this.origColRef);
|
|
|
|
// Indicates whether a column is transforming. Manually set, but should be true in both the original
|
|
|
|
// column being transformed and that column's transform column.
|
|
|
|
this.isTransforming = ko.observable(false);
|
|
|
|
|
|
|
|
// Convenience observable to obtain and set the type with no suffix
|
|
|
|
this.pureType = ko.pureComputed(() => gristTypes.extractTypeFromColType(this.type()));
|
|
|
|
|
|
|
|
// The column's display column
|
|
|
|
this._displayColModel = refRecord(docModel.columns, this.displayCol);
|
|
|
|
|
|
|
|
// Helper which adds/removes/updates this column's displayCol to match the formula.
|
|
|
|
this.saveDisplayFormula = function(formula) {
|
|
|
|
if (formula !== (this._displayColModel().formula() || '')) {
|
|
|
|
return docModel.docData.sendAction(["SetDisplayFormula", this.table().tableId(),
|
|
|
|
null, this.getRowId(), formula]);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-11-19 20:30:11 +00:00
|
|
|
// Display col ref to use for the column, defaulting to the plain column itself.
|
|
|
|
this.displayColRef = ko.pureComputed(() => this.displayCol() || this.origColRef());
|
|
|
|
|
|
|
|
// The display column to use for the column, or the column itself when no displayCol is set.
|
|
|
|
this.displayColModel = refRecord(docModel.columns, this.displayColRef);
|
|
|
|
this.visibleColModel = refRecord(docModel.columns, this.visibleCol);
|
|
|
|
|
2021-03-17 03:45:44 +00:00
|
|
|
this.disableModifyBase = ko.pureComputed(() => Boolean(this.summarySourceCol()));
|
|
|
|
this.disableModify = ko.pureComputed(() => this.disableModifyBase() || this.isTransforming());
|
2020-10-02 15:10:00 +00:00
|
|
|
this.disableEditData = ko.pureComputed(() => Boolean(this.summarySourceCol()));
|
|
|
|
|
2020-12-21 22:14:40 +00:00
|
|
|
this.isHiddenCol = ko.pureComputed(() => gristTypes.isHiddenCol(this.colId()));
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
// Returns the rowModel for the referenced table, or null, if this is not a reference column.
|
|
|
|
this.refTable = ko.pureComputed(() => {
|
2021-07-23 15:29:35 +00:00
|
|
|
const refTableId = getReferencedTableId(this.type() || "");
|
2022-07-06 07:41:09 +00:00
|
|
|
return refTableId ? docModel.visibleTables.all().find(t => t.tableId() === refTableId) || null : null;
|
2020-10-02 15:10:00 +00:00
|
|
|
});
|
2021-11-19 20:30:11 +00:00
|
|
|
|
|
|
|
// Helper for Reference/ReferenceList columns, which returns a formatter according to the visibleCol
|
|
|
|
// associated with this column. If no visible column available, return formatting for the column itself.
|
2022-02-04 11:13:03 +00:00
|
|
|
this.visibleColFormatter = ko.pureComputed(() => formatterForRec(this, this, docModel, 'vcol'));
|
2022-01-13 10:04:56 +00:00
|
|
|
|
2022-02-04 11:13:03 +00:00
|
|
|
this.formatter = ko.pureComputed(() => formatterForRec(this, this, docModel, 'full'));
|
2022-10-14 10:07:19 +00:00
|
|
|
|
2022-12-20 12:56:43 +00:00
|
|
|
this.createValueParser = function() {
|
|
|
|
const parser = createParser(docModel.docData, this.id.peek());
|
|
|
|
return parser.cleanParse.bind(parser);
|
|
|
|
};
|
|
|
|
|
2022-10-14 10:07:19 +00:00
|
|
|
this.behavior = ko.pureComputed(() => this.isEmpty() ? 'empty' : this.isFormula() ? 'formula' : 'data');
|
2023-03-23 18:22:28 +00:00
|
|
|
|
|
|
|
this.chatHistory = this.autoDispose(ko.computed(() => {
|
|
|
|
const docId = urlState().state.get().doc ?? '';
|
2023-05-08 18:15:22 +00:00
|
|
|
// Changed key name from history to history-v2 when ChatHistory changed in incompatible way.
|
|
|
|
const key = `formula-assistant-history-v2-${docId}-${this.table().tableId()}-${this.colId()}`;
|
2023-07-20 14:25:26 +00:00
|
|
|
return localStorageJsonObs(key, {messages: [], conversationId: uuidv4()} as ChatHistory);
|
2023-03-23 18:22:28 +00:00
|
|
|
}));
|
2023-10-20 11:05:29 +00:00
|
|
|
|
|
|
|
this.cleanWidgetOptionsJson = ko.pureComputed(() => {
|
|
|
|
const options = this.widgetOptionsJson();
|
|
|
|
if (options && options.rules) {
|
|
|
|
delete options.rules;
|
|
|
|
}
|
|
|
|
return JSON.stringify(options);
|
|
|
|
});
|
2022-01-13 10:04:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export function formatterForRec(
|
2022-02-04 11:13:03 +00:00
|
|
|
rec: ColumnRec | ViewFieldRec, colRec: ColumnRec, docModel: DocModel, kind: 'full' | 'vcol'
|
2022-01-13 10:04:56 +00:00
|
|
|
): BaseFormatter {
|
2022-02-04 11:13:03 +00:00
|
|
|
const vcol = rec.visibleColModel();
|
|
|
|
const func = kind === 'full' ? createFullFormatterRaw : createVisibleColFormatterRaw;
|
|
|
|
const args: FullFormatterArgs = {
|
|
|
|
docData: docModel.docData,
|
|
|
|
type: colRec.type(),
|
|
|
|
widgetOpts: rec.widgetOptionsJson(),
|
|
|
|
visibleColType: vcol?.type(),
|
|
|
|
visibleColWidgetOpts: vcol?.widgetOptionsJson(),
|
|
|
|
docSettings: docModel.docInfoRow.documentSettingsJson(),
|
|
|
|
};
|
|
|
|
return func(args);
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
2023-03-23 18:22:28 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A chat message. Either send by the user or by the AI.
|
|
|
|
*/
|
|
|
|
export interface ChatMessage {
|
|
|
|
/**
|
|
|
|
* The message to display. It is a prompt typed by the user or a formula returned from the AI.
|
|
|
|
*/
|
|
|
|
message: string;
|
|
|
|
/**
|
|
|
|
* The sender of the message. Either the user or the AI.
|
|
|
|
*/
|
|
|
|
sender: 'user' | 'ai';
|
|
|
|
/**
|
2023-05-08 18:15:22 +00:00
|
|
|
* The formula returned from the AI. It is only set when the sender is the AI.
|
2023-03-23 18:22:28 +00:00
|
|
|
*/
|
2023-06-02 11:25:14 +00:00
|
|
|
formula?: string|null;
|
|
|
|
/**
|
|
|
|
* Suggested actions returned from the AI.
|
|
|
|
*/
|
|
|
|
action?: any;
|
2023-03-23 18:22:28 +00:00
|
|
|
}
|
2023-05-08 18:15:22 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The state of assistance for a particular column.
|
|
|
|
* ChatMessages are what are shown in the UI, whereas state is
|
|
|
|
* how the back-end represents the conversation. The two are
|
|
|
|
* similar but not the same because of post-processing.
|
|
|
|
* It may be possible to reconcile them when things settle down
|
|
|
|
* a bit?
|
|
|
|
*/
|
|
|
|
export interface ChatHistory {
|
|
|
|
messages: ChatMessage[];
|
2023-07-20 14:25:26 +00:00
|
|
|
conversationId?: string;
|
2023-05-08 18:15:22 +00:00
|
|
|
state?: AssistanceState;
|
|
|
|
}
|