@opencorestack/opengridx 3.3.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/README.md +10 -1
- package/dist/ai.cjs +2 -0
- package/dist/ai.cjs.map +1 -0
- package/dist/ai.d.ts +366 -0
- package/dist/ai.es.js +2 -0
- package/dist/ai.es.js.map +1 -0
- package/dist/index.d.ts +396 -6
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +8029 -6090
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +16 -14
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +182 -4
- package/docs/README.md +5 -1
- package/docs/architecture/datagrid-orchestration.md +19 -5
- package/docs/components/filter-panel.md +1 -0
- package/docs/components/header.md +4 -1
- package/docs/components/toolbar.md +7 -0
- package/docs/customization/theming.md +12 -1
- package/docs/features/ai-toolkit.md +213 -0
- package/docs/features/cell-selection.md +8 -2
- package/docs/features/clipboard.md +88 -0
- package/docs/features/editing-reordering.md +50 -0
- package/docs/features/fill-handle.md +125 -0
- package/docs/features/filtering.md +3 -0
- package/docs/features/header-filters.md +138 -0
- package/docs/features/keyboard-navigation.md +31 -2
- package/docs/features/undo-redo.md +79 -0
- package/docs/roadmap.md +3 -4
- package/lib/ai/agentTools.ts +160 -0
- package/lib/ai/coerce.ts +90 -0
- package/lib/ai/columns.ts +90 -0
- package/lib/ai/index.ts +33 -0
- package/lib/ai/promptHandler.ts +109 -0
- package/lib/ai/schema.ts +214 -0
- package/lib/ai/types.ts +152 -0
- package/lib/ai/validate.ts +296 -0
- package/lib/components/AiAssistant/GridAiAssistantArea.tsx +26 -0
- package/lib/components/AiAssistant/GridAiAssistantPanel.css +171 -0
- package/lib/components/AiAssistant/GridAiAssistantPanel.tsx +145 -0
- package/lib/components/AiAssistant/useSpeechInput.ts +74 -0
- package/lib/components/Cell/Cell.css +33 -0
- package/lib/components/Cell/Cell.tsx +6 -2
- package/lib/components/Cell/GridEditInputCell.css +12 -4
- package/lib/components/Cell/GridEditInputCell.tsx +19 -12
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +14 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +3 -2
- package/lib/components/DataGrid/DataGrid.tsx +138 -5
- package/lib/components/DataGrid/GridLiveRegion.tsx +5 -2
- package/lib/components/DataGrid/GridPinnedRows.tsx +5 -1
- package/lib/components/DataGrid/GridVirtualRows.tsx +5 -1
- package/lib/components/FilterPanel/FilterPanel.css +14 -0
- package/lib/components/FilterPanel/FilterPanel.tsx +5 -11
- package/lib/components/Header/Header.tsx +44 -23
- package/lib/components/Header/headerColumnStyle.ts +28 -0
- package/lib/components/HeaderFilter/HeaderFilter.css +160 -0
- package/lib/components/HeaderFilter/HeaderFilterCell.tsx +370 -0
- package/lib/components/HeaderFilter/HeaderFilterOperatorMenu.tsx +124 -0
- package/lib/components/HeaderFilter/HeaderFilterRow.tsx +157 -0
- package/lib/components/ListView/ListView.css +2 -4
- package/lib/components/ListView/ListViewRow.tsx +2 -2
- package/lib/components/Toolbar/GlobalSearch.css +16 -0
- package/lib/components/Toolbar/GlobalSearch.tsx +3 -2
- package/lib/components/Toolbar/GridToolbar.tsx +51 -0
- package/lib/components/ui/Checkbox.tsx +16 -5
- package/lib/components/ui/Input.css +43 -0
- package/lib/components/ui/Input.tsx +24 -5
- package/lib/hooks/core/gridApiPlaceholder.ts +16 -1
- package/lib/hooks/core/gridToolbarHostContext.ts +5 -0
- package/lib/hooks/core/useDataGrid.ts +27 -0
- package/lib/hooks/core/useGridApiMethods.ts +39 -0
- package/lib/hooks/core/useGridAriaRows.ts +5 -3
- package/lib/hooks/core/useGridCellSelection.ts +107 -13
- package/lib/hooks/core/useGridClipboardPaste.ts +283 -0
- package/lib/hooks/core/useGridColumnsPanel.ts +24 -1
- package/lib/hooks/core/useGridControlledState.ts +27 -0
- package/lib/hooks/core/useGridFillHandle.ts +323 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +29 -10
- package/lib/hooks/core/useGridToolbarProps.ts +19 -0
- package/lib/hooks/core/useGridUndoRedo.ts +220 -0
- package/lib/hooks/features/useGridAiAssistant.ts +284 -0
- package/lib/hooks/features/useGridBatchEdit.ts +217 -0
- package/lib/hooks/features/useGridEditing.ts +24 -45
- package/lib/index.ts +10 -0
- package/lib/styles/opengridx.css +4 -1
- package/lib/theme/DataGridThemeProvider.tsx +2 -0
- package/lib/theme/types.ts +2 -0
- package/lib/types/index.ts +343 -1
- package/lib/utils/aiAssistant.ts +205 -0
- package/lib/utils/aiBrand.ts +17 -0
- package/lib/utils/cellSelection.ts +25 -5
- package/lib/utils/editing/commit.ts +69 -0
- package/lib/utils/editing/fill.ts +238 -0
- package/lib/utils/editing/targets.ts +109 -0
- package/lib/utils/filtering/index.ts +14 -0
- package/lib/utils/focus/index.ts +9 -3
- package/lib/utils/headerFilters.ts +189 -0
- package/lib/utils/parsing.ts +252 -0
- package/llms.txt +97 -3
- package/package.json +8 -2
package/lib/ai/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @opencorestack/opengridx/ai: schema, validator, prompt handler and agent tools for driving the grid
|
|
2
|
+
// from a language model. No React, no dependencies and no AI SDK; the app calls its own model.
|
|
3
|
+
export { getGridAiSchema } from './schema';
|
|
4
|
+
export { validateGridAiState } from './validate';
|
|
5
|
+
export { createGridAiPromptHandler } from './promptHandler';
|
|
6
|
+
export { createGridAgentTools } from './agentTools';
|
|
7
|
+
export type {
|
|
8
|
+
GridAgentApi,
|
|
9
|
+
GridAgentTool,
|
|
10
|
+
GridAgentToolResult,
|
|
11
|
+
GridAgentToolsOptions,
|
|
12
|
+
GridAiColumn,
|
|
13
|
+
GridAiHistoryEntry,
|
|
14
|
+
GridAiJsonSchema,
|
|
15
|
+
GridAiModelRequest,
|
|
16
|
+
GridAiPart,
|
|
17
|
+
GridAiPartOptions,
|
|
18
|
+
GridAiPromptContext,
|
|
19
|
+
GridAiPromptHandlerOptions,
|
|
20
|
+
GridAiPromptResult,
|
|
21
|
+
GridAiSchema,
|
|
22
|
+
GridAiSchemaOptions,
|
|
23
|
+
GridAiState,
|
|
24
|
+
GridAiValidateOptions,
|
|
25
|
+
GridAiValidationError,
|
|
26
|
+
GridAiValidationResult,
|
|
27
|
+
} from './types';
|
|
28
|
+
export type {
|
|
29
|
+
GridAiAssistantOptions,
|
|
30
|
+
GridAiAssistantPanelProps,
|
|
31
|
+
GridAiAssistantStatus,
|
|
32
|
+
GridAiChip,
|
|
33
|
+
} from '../types';
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// createGridAiPromptHandler: the `aiAssistant.onPrompt` function for an app's own model. It builds the
|
|
2
|
+
// schema once, calls the app's `callModel`, reads the reply (an object, JSON text, or text with a fenced
|
|
3
|
+
// JSON block) and returns the validated, branded result. The grid never sees the raw reply.
|
|
4
|
+
import type { GridAiPromptContext, GridAiPromptResult } from '../types';
|
|
5
|
+
import { brandGridAiValidated } from '../utils/aiBrand';
|
|
6
|
+
import { hasOwn, isPlainObject } from './columns';
|
|
7
|
+
import { getGridAiSchema } from './schema';
|
|
8
|
+
import type { GridAiColumn, GridAiPartOptions, GridAiPromptHandlerOptions, GridAiValidationError } from './types';
|
|
9
|
+
import { validateGridAiState } from './validate';
|
|
10
|
+
|
|
11
|
+
const FENCE = '```';
|
|
12
|
+
|
|
13
|
+
/** A DOMException named `AbortError`, as `fetch` rejects with. */
|
|
14
|
+
export function abortError(): Error {
|
|
15
|
+
return typeof DOMException === 'function'
|
|
16
|
+
? new DOMException('The operation was aborted.', 'AbortError')
|
|
17
|
+
: Object.assign(new Error('The operation was aborted.'), { name: 'AbortError' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Splits a text reply into the JSON it holds and the text around it: the whole text when it starts
|
|
22
|
+
* with `{`, else the first fenced block (with no language or `json`). A plain loop, no regex.
|
|
23
|
+
*/
|
|
24
|
+
export function readModelText(text: string): { json: string | null; message: string } {
|
|
25
|
+
const trimmed = text.trim();
|
|
26
|
+
if (trimmed.startsWith('{')) return { json: trimmed, message: '' };
|
|
27
|
+
const open = text.indexOf(FENCE);
|
|
28
|
+
const lineEnd = open < 0 ? -1 : text.indexOf('\n', open + FENCE.length);
|
|
29
|
+
const close = lineEnd < 0 ? -1 : text.indexOf(FENCE, lineEnd + 1);
|
|
30
|
+
const lang = close < 0 ? '' : text.slice(open + FENCE.length, lineEnd).trim().toLowerCase();
|
|
31
|
+
if (close < 0 || (lang !== '' && lang !== 'json')) return { json: null, message: trimmed };
|
|
32
|
+
return {
|
|
33
|
+
json: text.slice(lineEnd + 1, close),
|
|
34
|
+
message: `${text.slice(0, open)}${text.slice(close + FENCE.length)}`.trim(),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function errorText(error: unknown): string {
|
|
39
|
+
if (error instanceof Error && error.message) return error.message;
|
|
40
|
+
return typeof error === 'string' && error ? error : 'Unknown error';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A branded result with an empty state: nothing to apply, the errors explain why. */
|
|
44
|
+
function emptyResult(errors: GridAiValidationError[], message?: string): GridAiPromptResult {
|
|
45
|
+
const result: GridAiPromptResult = { state: brandGridAiValidated({}), errors };
|
|
46
|
+
if (message) result.message = message;
|
|
47
|
+
return brandGridAiValidated(result);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Validates a model reply into a branded result. Never throws. */
|
|
51
|
+
export function readModelReply(reply: unknown, columns: readonly GridAiColumn[], parts: GridAiPartOptions): GridAiPromptResult {
|
|
52
|
+
let json: unknown = reply;
|
|
53
|
+
let message = '';
|
|
54
|
+
if (typeof reply === 'string') {
|
|
55
|
+
const text = readModelText(reply);
|
|
56
|
+
if (text.json === null) return emptyResult([], text.message || undefined);
|
|
57
|
+
json = text.json;
|
|
58
|
+
message = text.message;
|
|
59
|
+
} else if (isPlainObject(reply) && hasOwn(reply, 'message') && typeof reply.message === 'string') {
|
|
60
|
+
// A reply such as { filterModel, message: 'Showing paid orders' }: the message is not state.
|
|
61
|
+
const { message: text, ...rest } = reply;
|
|
62
|
+
json = rest;
|
|
63
|
+
message = text as string;
|
|
64
|
+
}
|
|
65
|
+
const result: GridAiPromptResult = validateGridAiState(json, columns, parts);
|
|
66
|
+
if (message) result.message = message;
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Builds `aiAssistant.onPrompt` for your own model. On each prompt it calls
|
|
72
|
+
* `callModel({ prompt, schema, currentState, history }, { signal })`, reads the reply (a state object,
|
|
73
|
+
* a JSON string, or a fenced JSON block inside text), validates it with `validateGridAiState` and
|
|
74
|
+
* returns the branded result. A `callModel` throw or rejection becomes
|
|
75
|
+
* `{ state: {}, errors: [{ path: '', message }] }`; an abort rejects with an `AbortError`.
|
|
76
|
+
* @since v3.5
|
|
77
|
+
*/
|
|
78
|
+
export function createGridAiPromptHandler<C extends GridAiColumn>(
|
|
79
|
+
options: GridAiPromptHandlerOptions<C>,
|
|
80
|
+
): (prompt: string, context: GridAiPromptContext) => Promise<GridAiPromptResult> {
|
|
81
|
+
const { columns, callModel, parts, schemaOptions = {} } = options;
|
|
82
|
+
const partOptions: GridAiPartOptions = { include: parts ?? schemaOptions.include, exclude: schemaOptions.exclude };
|
|
83
|
+
const schema = getGridAiSchema(columns, { ...schemaOptions, ...partOptions });
|
|
84
|
+
|
|
85
|
+
return async (prompt, context) => {
|
|
86
|
+
const signal = context?.signal ?? new AbortController().signal;
|
|
87
|
+
if (signal.aborted) throw abortError();
|
|
88
|
+
// Stop resolves the prompt at once, even when callModel ignores the signal.
|
|
89
|
+
const aborted = new Promise<never>((_, reject) => {
|
|
90
|
+
signal.addEventListener('abort', () => reject(abortError()), { once: true });
|
|
91
|
+
});
|
|
92
|
+
aborted.catch(() => undefined);
|
|
93
|
+
let reply: unknown;
|
|
94
|
+
try {
|
|
95
|
+
const request = {
|
|
96
|
+
prompt: String(prompt),
|
|
97
|
+
schema,
|
|
98
|
+
currentState: context?.currentState ?? {},
|
|
99
|
+
history: Array.isArray(context?.history) ? context.history : [],
|
|
100
|
+
};
|
|
101
|
+
reply = await Promise.race([callModel(request, { signal }), aborted]);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (signal.aborted) throw abortError();
|
|
104
|
+
return emptyResult([{ path: '', message: `The model call failed: ${errorText(error)}` }]);
|
|
105
|
+
}
|
|
106
|
+
if (signal.aborted) throw abortError();
|
|
107
|
+
return readModelReply(reply, columns, partOptions);
|
|
108
|
+
};
|
|
109
|
+
}
|
package/lib/ai/schema.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// getGridAiSchema: a JSON Schema (draft 2020-12) of the grid state a model may produce. Built from
|
|
2
|
+
// the column definitions only; no row data is read. Same columns and options, same schema.
|
|
3
|
+
import {
|
|
4
|
+
MAX_FILTER_DEPTH,
|
|
5
|
+
NO_VALUE_OPS,
|
|
6
|
+
PART_KEYS,
|
|
7
|
+
PIVOT_FNS,
|
|
8
|
+
aggregationFnsOf,
|
|
9
|
+
isFilterable,
|
|
10
|
+
isGroupable,
|
|
11
|
+
isHideable,
|
|
12
|
+
isSortable,
|
|
13
|
+
operatorsOf,
|
|
14
|
+
optionValues,
|
|
15
|
+
selectedParts,
|
|
16
|
+
usableColumns,
|
|
17
|
+
} from './columns';
|
|
18
|
+
import type { GridAiColumn, GridAiJsonSchema, GridAiPart, GridAiSchema, GridAiSchemaOptions } from './types';
|
|
19
|
+
|
|
20
|
+
/** Title of every schema. Also the marker `check-bundle` uses to keep this code out of the core bundle. */
|
|
21
|
+
export const GRID_AI_SCHEMA_TITLE = 'OpenGridX grid state';
|
|
22
|
+
|
|
23
|
+
const MAX_EXAMPLES = 10;
|
|
24
|
+
|
|
25
|
+
interface Ctx {
|
|
26
|
+
descriptions: boolean;
|
|
27
|
+
examples: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function label(col: GridAiColumn): string {
|
|
31
|
+
const header = typeof col.headerName === 'string' && col.headerName !== '' ? col.headerName : col.field;
|
|
32
|
+
const desc = typeof col.description === 'string' && col.description !== '' ? ` (${col.description})` : '';
|
|
33
|
+
return `${header}${desc}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function describe(node: GridAiJsonSchema, text: string, ctx: Ctx): GridAiJsonSchema {
|
|
37
|
+
return ctx.descriptions ? { ...node, description: text } : node;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function examplesOf(col: GridAiColumn, ctx: Ctx): unknown[] {
|
|
41
|
+
if (!ctx.examples || !Array.isArray(col.aiExamples)) return [];
|
|
42
|
+
return col.aiExamples
|
|
43
|
+
.filter((v) => typeof v === 'string' || typeof v === 'boolean' || (typeof v === 'number' && Number.isFinite(v)))
|
|
44
|
+
.slice(0, MAX_EXAMPLES);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Schema of one filter value for the column's type. */
|
|
48
|
+
function valueSchema(col: GridAiColumn, ctx: Ctx): GridAiJsonSchema {
|
|
49
|
+
let node: GridAiJsonSchema;
|
|
50
|
+
switch (col.type) {
|
|
51
|
+
case 'number':
|
|
52
|
+
node = { type: 'number' };
|
|
53
|
+
break;
|
|
54
|
+
case 'date':
|
|
55
|
+
node = { type: 'string', format: 'date' };
|
|
56
|
+
break;
|
|
57
|
+
case 'boolean':
|
|
58
|
+
node = { type: 'boolean' };
|
|
59
|
+
break;
|
|
60
|
+
case 'singleSelect': {
|
|
61
|
+
const values = optionValues(col);
|
|
62
|
+
const one: GridAiJsonSchema = values.length > 0 ? { enum: values } : { type: 'string' };
|
|
63
|
+
node = { anyOf: [one, { type: 'array', items: one }] };
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
default:
|
|
67
|
+
node = { type: 'string' };
|
|
68
|
+
}
|
|
69
|
+
const examples = examplesOf(col, ctx);
|
|
70
|
+
return examples.length > 0 ? { ...node, examples } : node;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function filterItemSchema(col: GridAiColumn, ctx: Ctx): GridAiJsonSchema {
|
|
74
|
+
const operators = operatorsOf(col);
|
|
75
|
+
const noValue = operators.filter((op) => NO_VALUE_OPS.includes(op));
|
|
76
|
+
const hint = col.type === 'singleSelect' ? '; isAnyOf takes an array' : '';
|
|
77
|
+
const ops = noValue.length > 0 ? `${noValue.join(' and ')} take no value${hint}` : hint.slice(2);
|
|
78
|
+
return describe(
|
|
79
|
+
{
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
field: { const: col.field },
|
|
83
|
+
operator: { enum: operators },
|
|
84
|
+
value: valueSchema(col, ctx),
|
|
85
|
+
},
|
|
86
|
+
required: ['field', 'operator'],
|
|
87
|
+
additionalProperties: false,
|
|
88
|
+
},
|
|
89
|
+
ops ? `${label(col)}: ${ops}` : label(col),
|
|
90
|
+
ctx,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function filterSchema(cols: GridAiColumn[], ctx: Ctx, defs: Record<string, GridAiJsonSchema>): GridAiJsonSchema | null {
|
|
95
|
+
const filterable = cols.filter(isFilterable);
|
|
96
|
+
const properties: Record<string, GridAiJsonSchema> = {};
|
|
97
|
+
const logic = { enum: ['and', 'or'] };
|
|
98
|
+
if (filterable.length > 0) {
|
|
99
|
+
defs.filterItem = { oneOf: filterable.map((col) => filterItemSchema(col, ctx)) };
|
|
100
|
+
// Groups are unrolled to MAX_FILTER_DEPTH levels: the deepest one holds items only.
|
|
101
|
+
for (let depth = MAX_FILTER_DEPTH; depth >= 1; depth--) {
|
|
102
|
+
const child = depth === MAX_FILTER_DEPTH ? { $ref: '#/$defs/filterItem' } : { anyOf: [{ $ref: '#/$defs/filterItem' }, { $ref: `#/$defs/filterGroup${depth + 1}` }] };
|
|
103
|
+
defs[`filterGroup${depth}`] = {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: { logicOperator: logic, items: { type: 'array', items: child } },
|
|
106
|
+
required: ['logicOperator', 'items'],
|
|
107
|
+
additionalProperties: false,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
properties.items = { type: 'array', items: { anyOf: [{ $ref: '#/$defs/filterItem' }, { $ref: '#/$defs/filterGroup1' }] } };
|
|
111
|
+
properties.logicOperator = logic;
|
|
112
|
+
}
|
|
113
|
+
properties.quickFilterValues = describe({ type: 'array', items: { type: 'string' } }, 'Words a row must contain, in any column', ctx);
|
|
114
|
+
return { type: 'object', properties, additionalProperties: false };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function fieldEnum(cols: GridAiColumn[]): GridAiJsonSchema {
|
|
118
|
+
return { enum: cols.map((c) => c.field) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function partSchema(part: GridAiPart, cols: GridAiColumn[], ctx: Ctx, defs: Record<string, GridAiJsonSchema>): GridAiJsonSchema | null {
|
|
122
|
+
switch (part) {
|
|
123
|
+
case 'filter':
|
|
124
|
+
return filterSchema(cols, ctx, defs);
|
|
125
|
+
case 'sort': {
|
|
126
|
+
const sortable = cols.filter(isSortable);
|
|
127
|
+
if (sortable.length === 0) return null;
|
|
128
|
+
return describe(
|
|
129
|
+
{
|
|
130
|
+
type: 'array',
|
|
131
|
+
items: {
|
|
132
|
+
type: 'object',
|
|
133
|
+
properties: { field: fieldEnum(sortable), sort: { enum: ['asc', 'desc'] } },
|
|
134
|
+
required: ['field', 'sort'],
|
|
135
|
+
additionalProperties: false,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
'Most significant first',
|
|
139
|
+
ctx,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
case 'grouping': {
|
|
143
|
+
const groupable = cols.filter(isGroupable);
|
|
144
|
+
if (groupable.length === 0) return null;
|
|
145
|
+
return describe({ type: 'array', items: fieldEnum(groupable), uniqueItems: true }, 'Outermost first', ctx);
|
|
146
|
+
}
|
|
147
|
+
case 'aggregation': {
|
|
148
|
+
const properties: Record<string, GridAiJsonSchema> = {};
|
|
149
|
+
for (const col of cols) {
|
|
150
|
+
const fns = aggregationFnsOf(col);
|
|
151
|
+
if (fns.length > 0) properties[col.field] = describe({ enum: fns }, label(col), ctx);
|
|
152
|
+
}
|
|
153
|
+
if (Object.keys(properties).length === 0) return null;
|
|
154
|
+
return { type: 'object', properties, additionalProperties: false };
|
|
155
|
+
}
|
|
156
|
+
case 'pivot': {
|
|
157
|
+
const dims = cols.filter(isGroupable);
|
|
158
|
+
const values = cols
|
|
159
|
+
.map((col) => ({ col, fns: aggregationFnsOf(col, PIVOT_FNS) }))
|
|
160
|
+
.filter((v) => v.fns.length > 0)
|
|
161
|
+
.map(({ col, fns }) => describe({
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: { field: { const: col.field }, aggFn: { enum: fns } },
|
|
164
|
+
required: ['field', 'aggFn'],
|
|
165
|
+
additionalProperties: false,
|
|
166
|
+
}, label(col), ctx));
|
|
167
|
+
if (dims.length === 0 || values.length === 0) return null;
|
|
168
|
+
const dimArray = { type: 'array', items: fieldEnum(dims), uniqueItems: true };
|
|
169
|
+
return {
|
|
170
|
+
type: 'object',
|
|
171
|
+
properties: { rowFields: dimArray, columnFields: dimArray, valueFields: { type: 'array', items: { oneOf: values } } },
|
|
172
|
+
required: ['rowFields', 'columnFields', 'valueFields'],
|
|
173
|
+
additionalProperties: false,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
case 'columnVisibility': {
|
|
177
|
+
const properties: Record<string, GridAiJsonSchema> = {};
|
|
178
|
+
for (const col of cols.filter(isHideable)) properties[col.field] = describe({ type: 'boolean' }, label(col), ctx);
|
|
179
|
+
if (Object.keys(properties).length === 0) return null;
|
|
180
|
+
return describe({ type: 'object', properties, additionalProperties: false }, 'false hides a column', ctx);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* A JSON Schema (draft 2020-12) of the grid state a model may return for these columns: filter,
|
|
187
|
+
* sort, grouping, aggregation, pivot and column visibility, each limited to the fields, operators
|
|
188
|
+
* and functions the columns allow. Send it with the user's prompt to a model with structured
|
|
189
|
+
* output, then pass the reply to `validateGridAiState`. Reads column definitions only, never rows.
|
|
190
|
+
* @since v3.4
|
|
191
|
+
*/
|
|
192
|
+
export function getGridAiSchema<C extends GridAiColumn>(columns: readonly C[], options: GridAiSchemaOptions = {}): GridAiSchema {
|
|
193
|
+
const ctx: Ctx = { descriptions: options.descriptions !== false, examples: options.examples !== false };
|
|
194
|
+
const cols = usableColumns(columns);
|
|
195
|
+
const defs: Record<string, GridAiJsonSchema> = {};
|
|
196
|
+
const properties: Record<string, GridAiJsonSchema> = {};
|
|
197
|
+
for (const part of selectedParts(options)) {
|
|
198
|
+
const node = partSchema(part, cols, ctx, defs);
|
|
199
|
+
if (node) properties[PART_KEYS[part]] = node;
|
|
200
|
+
}
|
|
201
|
+
const schema: GridAiSchema = {
|
|
202
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
203
|
+
schemaVersion: 1,
|
|
204
|
+
title: GRID_AI_SCHEMA_TITLE,
|
|
205
|
+
type: 'object',
|
|
206
|
+
properties,
|
|
207
|
+
additionalProperties: false,
|
|
208
|
+
};
|
|
209
|
+
if (ctx.descriptions) {
|
|
210
|
+
schema.description = 'Grid state to apply. Include only the parts the user asked to change.';
|
|
211
|
+
}
|
|
212
|
+
if (Object.keys(defs).length > 0) schema.$defs = defs;
|
|
213
|
+
return schema;
|
|
214
|
+
}
|
package/lib/ai/types.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { GridAiPart, GridAiState, GridAiValidationError } from '../types';
|
|
2
|
+
|
|
3
|
+
// The state types live with the grid's own types (lib/types), so the grid's `aiAssistant` prop can
|
|
4
|
+
// use them without importing this entry point. They are type-only and re-exported from here.
|
|
5
|
+
export type {
|
|
6
|
+
GridAiHistoryEntry,
|
|
7
|
+
GridAiPart,
|
|
8
|
+
GridAiPromptContext,
|
|
9
|
+
GridAiPromptResult,
|
|
10
|
+
GridAiState,
|
|
11
|
+
GridAiValidationError,
|
|
12
|
+
} from '../types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The column properties the AI toolkit reads. A `GridColDef` (typed or untyped) is accepted as is;
|
|
16
|
+
* renderers, getters and formatters are never called, and no row data is read.
|
|
17
|
+
* @since v3.4
|
|
18
|
+
*/
|
|
19
|
+
export interface GridAiColumn {
|
|
20
|
+
field: string;
|
|
21
|
+
headerName?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
type?: string;
|
|
24
|
+
filterable?: boolean;
|
|
25
|
+
sortable?: boolean;
|
|
26
|
+
groupable?: boolean;
|
|
27
|
+
hideable?: boolean;
|
|
28
|
+
aggregable?: boolean;
|
|
29
|
+
availableAggregationFunctions?: readonly string[];
|
|
30
|
+
valueOptions?: readonly (string | number | { value: unknown; label: string })[];
|
|
31
|
+
/** Example values sent to the model. Real data: see `GridColDef.aiExamples`. */
|
|
32
|
+
aiExamples?: readonly unknown[];
|
|
33
|
+
/** Internal spacer columns are skipped. */
|
|
34
|
+
isSpacer?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Chooses the parts of the state; `exclude` wins over `include`. Default: every part. */
|
|
38
|
+
export interface GridAiPartOptions {
|
|
39
|
+
include?: readonly GridAiPart[];
|
|
40
|
+
exclude?: readonly GridAiPart[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface GridAiSchemaOptions extends GridAiPartOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Put each column's `headerName` and `description` into the schema, so the model can map the
|
|
46
|
+
* user's words ("revenue") to a field (`amt_usd`).
|
|
47
|
+
* @default true
|
|
48
|
+
*/
|
|
49
|
+
descriptions?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Include the columns' `aiExamples`. Columns without `aiExamples` never send example values.
|
|
52
|
+
* @default true
|
|
53
|
+
*/
|
|
54
|
+
examples?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type GridAiValidateOptions = GridAiPartOptions;
|
|
58
|
+
|
|
59
|
+
/** A JSON Schema (draft 2020-12) node. */
|
|
60
|
+
export type GridAiJsonSchema = { [keyword: string]: unknown };
|
|
61
|
+
|
|
62
|
+
/** The schema returned by `getGridAiSchema`. `schemaVersion` changes when its shape changes. */
|
|
63
|
+
export interface GridAiSchema extends GridAiJsonSchema {
|
|
64
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema';
|
|
65
|
+
schemaVersion: 1;
|
|
66
|
+
type: 'object';
|
|
67
|
+
properties: Record<string, GridAiJsonSchema>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The validated state and what was dropped. Since v3.5 the result and its `state` carry the brand
|
|
72
|
+
* `Symbol.for('opengridx.ai.validated')` (not enumerable), which the grid's `aiAssistant` checks.
|
|
73
|
+
*/
|
|
74
|
+
export interface GridAiValidationResult {
|
|
75
|
+
state: GridAiState;
|
|
76
|
+
errors: GridAiValidationError[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** What `callModel` receives: send it to your model. @since v3.5 */
|
|
80
|
+
export interface GridAiModelRequest {
|
|
81
|
+
prompt: string;
|
|
82
|
+
/** `getGridAiSchema(columns, …)`: use it as the structured-output format. */
|
|
83
|
+
schema: GridAiSchema;
|
|
84
|
+
currentState: GridAiState;
|
|
85
|
+
history: { prompt: string; applied: GridAiState }[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Options of `createGridAiPromptHandler`. @since v3.5 */
|
|
89
|
+
export interface GridAiPromptHandlerOptions<C extends GridAiColumn = GridAiColumn> {
|
|
90
|
+
columns: readonly C[];
|
|
91
|
+
/**
|
|
92
|
+
* Calls your model. Resolve with its reply: a state object, a JSON string, or text with a fenced JSON
|
|
93
|
+
* block. A string `message` key next to the state, or the text around the JSON, is shown as the reply.
|
|
94
|
+
*/
|
|
95
|
+
callModel: (request: GridAiModelRequest, options: { signal: AbortSignal }) => Promise<unknown>;
|
|
96
|
+
/** The parts the model may change (schema and validation). Default: every part. */
|
|
97
|
+
parts?: readonly GridAiPart[];
|
|
98
|
+
/** Options for the schema; its `include` / `exclude` also limit validation. */
|
|
99
|
+
schemaOptions?: GridAiSchemaOptions;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The part of `GridApi` the agent tools use. A grid's `apiRef.current` fits it.
|
|
104
|
+
* @since v3.5
|
|
105
|
+
*/
|
|
106
|
+
export interface GridAgentApi {
|
|
107
|
+
getGridAiState: () => GridAiState;
|
|
108
|
+
setFilterModel: (model: NonNullable<GridAiState['filterModel']>) => void;
|
|
109
|
+
setSortModel: (model: NonNullable<GridAiState['sortModel']>) => void;
|
|
110
|
+
setRowGroupingModel: (model: NonNullable<GridAiState['rowGroupingModel']>) => void;
|
|
111
|
+
setAggregationModel: (model: NonNullable<GridAiState['aggregationModel']>) => void;
|
|
112
|
+
setColumnVisibilityModel: (model: NonNullable<GridAiState['columnVisibilityModel']>) => void;
|
|
113
|
+
getAllFilteredRows: () => object[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Options of `createGridAgentTools`. @since v3.5 */
|
|
117
|
+
export interface GridAgentToolsOptions {
|
|
118
|
+
/** Adds `get_row_summary`, the one tool that sends row values to the model. Default: false. */
|
|
119
|
+
allowRowAccess?: boolean;
|
|
120
|
+
/** Fields `get_row_summary` returns. Default: every column the tools were given. */
|
|
121
|
+
fields?: readonly string[];
|
|
122
|
+
/** Most rows `get_row_summary` returns. Default: 20. */
|
|
123
|
+
maxRows?: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** What every tool's `execute` resolves with. It never throws or rejects. @since v3.5 */
|
|
127
|
+
export interface GridAgentToolResult {
|
|
128
|
+
ok: boolean;
|
|
129
|
+
/** The state the tool applied. */
|
|
130
|
+
applied?: GridAiState;
|
|
131
|
+
/** Why nothing was applied. */
|
|
132
|
+
errors?: GridAiValidationError[];
|
|
133
|
+
/** `get_grid_state`: the current models. */
|
|
134
|
+
state?: GridAiState;
|
|
135
|
+
/** `get_grid_state`: the columns the tools know. */
|
|
136
|
+
columns?: { field: string; headerName: string; type: string }[];
|
|
137
|
+
/** `get_row_summary`: the number of rows that pass the filter. */
|
|
138
|
+
rowCount?: number;
|
|
139
|
+
/** `get_row_summary`: the first rows, limited to the allowed fields. */
|
|
140
|
+
rows?: Record<string, unknown>[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A tool in the shape most tool-calling APIs take: `name`, `description`, a JSON Schema for the input
|
|
145
|
+
* and `execute`. No SDK is involved. @since v3.5
|
|
146
|
+
*/
|
|
147
|
+
export interface GridAgentTool {
|
|
148
|
+
name: string;
|
|
149
|
+
description: string;
|
|
150
|
+
inputSchema: GridAiJsonSchema;
|
|
151
|
+
execute: (input: unknown) => Promise<GridAgentToolResult>;
|
|
152
|
+
}
|