@opencorestack/opengridx 3.4.0 → 3.5.1

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +10 -3
  3. package/dist/ai.cjs +2 -1
  4. package/dist/ai.cjs.map +1 -1
  5. package/dist/ai.d.ts +203 -3
  6. package/dist/ai.es.js +2 -1
  7. package/dist/ai.es.js.map +1 -1
  8. package/dist/index.d.ts +248 -4
  9. package/dist/opengridx.css +1 -1
  10. package/dist/opengridx.es.js +7334 -5906
  11. package/dist/opengridx.es.js.map +1 -1
  12. package/dist/opengridx.umd.js +16 -16
  13. package/dist/opengridx.umd.js.map +1 -1
  14. package/docs/API_REFERENCE.md +88 -7
  15. package/docs/README.md +3 -1
  16. package/docs/architecture/datagrid-orchestration.md +11 -5
  17. package/docs/components/filter-panel.md +1 -0
  18. package/docs/components/header.md +4 -1
  19. package/docs/components/toolbar.md +7 -0
  20. package/docs/customization/theming.md +3 -1
  21. package/docs/features/ai-toolkit.md +99 -5
  22. package/docs/features/cell-selection.md +8 -2
  23. package/docs/features/fill-handle.md +125 -0
  24. package/docs/features/filtering.md +3 -0
  25. package/docs/features/header-filters.md +138 -0
  26. package/docs/features/keyboard-navigation.md +20 -2
  27. package/docs/features/virtualization.md +5 -5
  28. package/docs/performance.md +212 -1
  29. package/docs/roadmap.md +3 -4
  30. package/lib/ai/agentTools.ts +160 -0
  31. package/lib/ai/index.ts +19 -2
  32. package/lib/ai/promptHandler.ts +109 -0
  33. package/lib/ai/types.ts +89 -25
  34. package/lib/ai/validate.ts +10 -0
  35. package/lib/components/AiAssistant/GridAiAssistantArea.tsx +26 -0
  36. package/lib/components/AiAssistant/GridAiAssistantPanel.css +171 -0
  37. package/lib/components/AiAssistant/GridAiAssistantPanel.tsx +145 -0
  38. package/lib/components/AiAssistant/useSpeechInput.ts +74 -0
  39. package/lib/components/Cell/Cell.css +28 -0
  40. package/lib/components/Cell/Cell.tsx +6 -2
  41. package/lib/components/DataGrid/DataGrid.tsx +84 -4
  42. package/lib/components/DataGrid/GridLiveRegion.tsx +5 -2
  43. package/lib/components/DataGrid/GridPinnedRows.tsx +5 -1
  44. package/lib/components/DataGrid/GridVirtualRows.tsx +5 -1
  45. package/lib/components/FilterPanel/FilterPanel.tsx +2 -9
  46. package/lib/components/Header/Header.tsx +44 -23
  47. package/lib/components/Header/headerColumnStyle.ts +28 -0
  48. package/lib/components/HeaderFilter/HeaderFilter.css +160 -0
  49. package/lib/components/HeaderFilter/HeaderFilterCell.tsx +370 -0
  50. package/lib/components/HeaderFilter/HeaderFilterOperatorMenu.tsx +124 -0
  51. package/lib/components/HeaderFilter/HeaderFilterRow.tsx +157 -0
  52. package/lib/components/Toolbar/GridToolbar.tsx +51 -0
  53. package/lib/hooks/core/gridApiPlaceholder.ts +8 -0
  54. package/lib/hooks/core/gridToolbarHostContext.ts +5 -0
  55. package/lib/hooks/core/useGridApiMethods.ts +39 -0
  56. package/lib/hooks/core/useGridAriaRows.ts +5 -3
  57. package/lib/hooks/core/useGridCellSelection.ts +77 -11
  58. package/lib/hooks/core/useGridColumnsPanel.ts +24 -1
  59. package/lib/hooks/core/useGridControlledState.ts +27 -0
  60. package/lib/hooks/core/useGridDevWarnings.ts +21 -6
  61. package/lib/hooks/core/useGridFillHandle.ts +323 -0
  62. package/lib/hooks/core/useGridKeyboardNavigation.ts +29 -10
  63. package/lib/hooks/core/useGridMaxContentHeight.ts +16 -0
  64. package/lib/hooks/core/useGridToolbarProps.ts +19 -0
  65. package/lib/hooks/core/useGridVirtualization.ts +19 -0
  66. package/lib/hooks/features/useGridAiAssistant.ts +284 -0
  67. package/lib/hooks/features/useGridBatchEdit.ts +2 -2
  68. package/lib/index.ts +3 -0
  69. package/lib/styles/opengridx.css +2 -0
  70. package/lib/theme/DataGridThemeProvider.tsx +2 -0
  71. package/lib/theme/types.ts +2 -0
  72. package/lib/types/index.ts +219 -1
  73. package/lib/utils/aiAssistant.ts +205 -0
  74. package/lib/utils/aiBrand.ts +17 -0
  75. package/lib/utils/cellSelection.ts +25 -5
  76. package/lib/utils/editing/fill.ts +238 -0
  77. package/lib/utils/filtering/index.ts +14 -0
  78. package/lib/utils/focus/index.ts +9 -3
  79. package/lib/utils/headerFilters.ts +189 -0
  80. package/lib/utils/maxElementHeight.ts +53 -0
  81. package/llms.txt +44 -4
  82. package/package.json +3 -2
@@ -0,0 +1,160 @@
1
+ // createGridAgentTools: plain tool objects ({ name, description, inputSchema, execute }) that let an AI
2
+ // agent read and change the grid through its apiRef. No SDK: they fit the Vercel AI SDK, CopilotKit,
3
+ // OpenAI or Anthropic tool calling and WebMCP. Every input is validated; execute never throws.
4
+ import type { GridAiState, GridRowModel } from '../types';
5
+ import { getCellValue } from '../utils/values';
6
+
7
+ type CellColumn = Parameters<typeof getCellValue>[2];
8
+ import { hasOwn, isPlainObject, PART_KEYS, usableColumns } from './columns';
9
+ import { getGridAiSchema } from './schema';
10
+ import type {
11
+ GridAgentApi,
12
+ GridAgentTool,
13
+ GridAgentToolResult,
14
+ GridAgentToolsOptions,
15
+ GridAiColumn,
16
+ GridAiJsonSchema,
17
+ GridAiPart,
18
+ } from './types';
19
+ import { validateGridAiState } from './validate';
20
+
21
+ const DEFAULT_MAX_ROWS = 20;
22
+ const MAX_ROWS_LIMIT = 1000;
23
+
24
+ const SET_TOOLS: { name: string; part: GridAiPart; description: string }[] = [
25
+ { name: 'set_filter', part: 'filter', description: 'Replace the filter model of the grid (filters and quick-filter words).' },
26
+ { name: 'set_sort', part: 'sort', description: 'Replace the sort model of the grid, most significant field first. An empty list clears sorting.' },
27
+ { name: 'set_grouping', part: 'grouping', description: 'Group the rows by these fields, outermost first. An empty list removes grouping.' },
28
+ { name: 'set_aggregation', part: 'aggregation', description: 'Replace the summaries: field to aggregation function. An empty object removes them.' },
29
+ { name: 'set_column_visibility', part: 'columnVisibility', description: 'Show (true) or hide (false) columns. Columns not listed keep their visibility.' },
30
+ ];
31
+
32
+ type ApiRef = { readonly current: GridAgentApi | null | undefined };
33
+
34
+ function failure(message: string, path = ''): GridAgentToolResult {
35
+ return { ok: false, errors: [{ path, message }] };
36
+ }
37
+
38
+ /** Runs a tool body; a throw becomes `{ ok: false }`. */
39
+ async function run(body: () => GridAgentToolResult): Promise<GridAgentToolResult> {
40
+ try {
41
+ return body();
42
+ } catch (error) {
43
+ return failure(error instanceof Error && error.message ? error.message : 'The tool failed');
44
+ }
45
+ }
46
+
47
+ function apiOf(apiRef: ApiRef): GridAgentApi | null {
48
+ const api = apiRef?.current;
49
+ return api && typeof api.getGridAiState === 'function' ? api : null;
50
+ }
51
+
52
+ /** A JSON-safe cell value: primitives as they are, a Date as an ISO string, anything else null. */
53
+ function jsonValue(value: unknown): unknown {
54
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.toISOString();
55
+ if (typeof value === 'number') return Number.isFinite(value) ? value : null;
56
+ return typeof value === 'string' || typeof value === 'boolean' ? value : null;
57
+ }
58
+
59
+ function setTool(apiRef: ApiRef, columns: readonly GridAiColumn[], name: string, part: GridAiPart, description: string): GridAgentTool | null {
60
+ const key = PART_KEYS[part] as keyof GridAiState;
61
+ const schema = getGridAiSchema(columns, { include: [part] });
62
+ if (!hasOwn(schema.properties, key)) return null;
63
+ const { $schema: _version, schemaVersion: _schemaVersion, title: _title, description: _description, ...inputSchema } = schema;
64
+ return {
65
+ name,
66
+ description,
67
+ inputSchema: { ...inputSchema, required: [key] },
68
+ execute: (input) => run(() => {
69
+ const api = apiOf(apiRef);
70
+ if (!api) return failure('The grid is not ready');
71
+ if (!isPlainObject(input) || !hasOwn(input, key)) return failure(`Expected an object with "${key}"`, key);
72
+ const { state, errors } = validateGridAiState({ [key]: input[key] }, columns, { include: [part] });
73
+ // All or nothing: the agent gets the errors back and can correct its call.
74
+ if (errors.length > 0) return { ok: false, errors };
75
+ const applied: GridAiState = {};
76
+ if (state.filterModel) api.setFilterModel(applied.filterModel = state.filterModel);
77
+ if (state.sortModel) api.setSortModel(applied.sortModel = state.sortModel);
78
+ if (state.rowGroupingModel) api.setRowGroupingModel(applied.rowGroupingModel = state.rowGroupingModel);
79
+ if (state.aggregationModel) api.setAggregationModel(applied.aggregationModel = state.aggregationModel);
80
+ if (state.columnVisibilityModel) {
81
+ applied.columnVisibilityModel = { ...api.getGridAiState().columnVisibilityModel, ...state.columnVisibilityModel };
82
+ api.setColumnVisibilityModel(applied.columnVisibilityModel);
83
+ }
84
+ return { ok: true, applied };
85
+ }),
86
+ };
87
+ }
88
+
89
+ const NO_INPUT: GridAiJsonSchema = { type: 'object', properties: {}, additionalProperties: false };
90
+
91
+ /**
92
+ * Tools for an AI agent: `get_grid_state`, `set_filter`, `set_sort`, `set_grouping`, `set_aggregation`,
93
+ * `set_column_visibility`, `clear_filters`, and `get_row_summary` only with `allowRowAccess: true` (the
94
+ * one tool that sends row values). Each tool is `{ name, description, inputSchema, execute }`; inputs are
95
+ * checked with `validateGridAiState` and applied through `apiRef` only when valid. `execute` never throws.
96
+ * @since v3.5
97
+ */
98
+ export function createGridAgentTools<C extends GridAiColumn>(
99
+ apiRef: ApiRef,
100
+ columns: readonly C[],
101
+ options: GridAgentToolsOptions = {},
102
+ ): GridAgentTool[] {
103
+ const cols = usableColumns(columns);
104
+ const tools: GridAgentTool[] = [{
105
+ name: 'get_grid_state',
106
+ description: 'Read the current filter, sort, grouping, aggregation, pivot and column visibility of the grid, and its columns.',
107
+ inputSchema: NO_INPUT,
108
+ execute: () => run(() => {
109
+ const api = apiOf(apiRef);
110
+ if (!api) return failure('The grid is not ready');
111
+ return {
112
+ ok: true,
113
+ state: api.getGridAiState(),
114
+ columns: cols.map((c) => ({ field: c.field, headerName: typeof c.headerName === 'string' ? c.headerName : c.field, type: typeof c.type === 'string' ? c.type : 'string' })),
115
+ };
116
+ }),
117
+ }];
118
+ for (const { name, part, description } of SET_TOOLS) {
119
+ const tool = setTool(apiRef, cols, name, part, description);
120
+ if (tool) tools.push(tool);
121
+ }
122
+ tools.push({
123
+ name: 'clear_filters',
124
+ description: 'Remove every filter and the quick-filter words.',
125
+ inputSchema: NO_INPUT,
126
+ execute: () => run(() => {
127
+ const api = apiOf(apiRef);
128
+ if (!api) return failure('The grid is not ready');
129
+ const filterModel = { items: [] };
130
+ api.setFilterModel(filterModel);
131
+ return { ok: true, applied: { filterModel } };
132
+ }),
133
+ });
134
+ if (options.allowRowAccess === true) {
135
+ const maxRows = typeof options.maxRows === 'number' && options.maxRows >= 1 ? Math.min(Math.floor(options.maxRows), MAX_ROWS_LIMIT) : DEFAULT_MAX_ROWS;
136
+ const allowed = Array.isArray(options.fields) ? cols.filter((c) => options.fields?.includes(c.field)) : cols;
137
+ tools.push({
138
+ name: 'get_row_summary',
139
+ description: `Count the rows that pass the current filter and read the first ones (at most ${maxRows}, fields: ${allowed.map((c) => c.field).join(', ')}).`,
140
+ inputSchema: { type: 'object', properties: { limit: { type: 'integer', minimum: 1, maximum: maxRows } }, additionalProperties: false },
141
+ execute: (input) => run(() => {
142
+ const api = apiOf(apiRef);
143
+ if (!api) return failure('The grid is not ready');
144
+ const raw = isPlainObject(input) && hasOwn(input, 'limit') ? input.limit : maxRows;
145
+ const limit = typeof raw === 'number' && raw >= 1 ? Math.min(Math.floor(raw), maxRows) : maxRows;
146
+ const all = api.getAllFilteredRows();
147
+ const list = Array.isArray(all) ? all : [];
148
+ const rows = list.slice(0, limit).map((row) => {
149
+ const out: Record<string, unknown> = {};
150
+ if (!isPlainObject(row)) return out;
151
+ // Through the column's valueGetter, as the grid shows the cell.
152
+ for (const col of allowed) out[col.field] = jsonValue(getCellValue(row as GridRowModel, col.field, col as CellColumn));
153
+ return out;
154
+ });
155
+ return { ok: true, rowCount: list.length, rows };
156
+ }),
157
+ });
158
+ }
159
+ return tools;
160
+ }
package/lib/ai/index.ts CHANGED
@@ -1,12 +1,23 @@
1
- // @opencorestack/opengridx/ai: schema and validator for driving the grid from a language model.
2
- // No React, no dependencies and no AI SDK; the app sends the schema to its own model.
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
3
  export { getGridAiSchema } from './schema';
4
4
  export { validateGridAiState } from './validate';
5
+ export { createGridAiPromptHandler } from './promptHandler';
6
+ export { createGridAgentTools } from './agentTools';
5
7
  export type {
8
+ GridAgentApi,
9
+ GridAgentTool,
10
+ GridAgentToolResult,
11
+ GridAgentToolsOptions,
6
12
  GridAiColumn,
13
+ GridAiHistoryEntry,
7
14
  GridAiJsonSchema,
15
+ GridAiModelRequest,
8
16
  GridAiPart,
9
17
  GridAiPartOptions,
18
+ GridAiPromptContext,
19
+ GridAiPromptHandlerOptions,
20
+ GridAiPromptResult,
10
21
  GridAiSchema,
11
22
  GridAiSchemaOptions,
12
23
  GridAiState,
@@ -14,3 +25,9 @@ export type {
14
25
  GridAiValidationError,
15
26
  GridAiValidationResult,
16
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/types.ts CHANGED
@@ -1,10 +1,14 @@
1
- import type {
2
- GridAggregationModel,
3
- GridColumnVisibilityModel,
4
- GridFilterModel,
5
- GridPivotModel,
6
- GridRowGroupingModel,
7
- GridSortItem,
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,
8
12
  } from '../types';
9
13
 
10
14
  /**
@@ -30,9 +34,6 @@ export interface GridAiColumn {
30
34
  isSpacer?: boolean;
31
35
  }
32
36
 
33
- /** A part of the grid state the model may change. */
34
- export type GridAiPart = 'filter' | 'sort' | 'grouping' | 'aggregation' | 'pivot' | 'columnVisibility';
35
-
36
37
  /** Chooses the parts of the state; `exclude` wins over `include`. Default: every part. */
37
38
  export interface GridAiPartOptions {
38
39
  include?: readonly GridAiPart[];
@@ -66,23 +67,86 @@ export interface GridAiSchema extends GridAiJsonSchema {
66
67
  properties: Record<string, GridAiJsonSchema>;
67
68
  }
68
69
 
69
- /** The validated state: only parts, fields, operators and values the columns allow. */
70
- export interface GridAiState {
71
- filterModel?: GridFilterModel;
72
- sortModel?: GridSortItem[];
73
- rowGroupingModel?: GridRowGroupingModel;
74
- aggregationModel?: GridAggregationModel;
75
- pivotModel?: GridPivotModel;
76
- columnVisibilityModel?: GridColumnVisibilityModel;
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
77
  }
78
78
 
79
- /** One thing the validator dropped or could not use. `path` points into the model output. */
80
- export interface GridAiValidationError {
81
- path: string;
82
- message: string;
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 }[];
83
86
  }
84
87
 
85
- export interface GridAiValidationResult {
86
- state: GridAiState;
87
- errors: GridAiValidationError[];
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>;
88
152
  }
@@ -14,6 +14,7 @@ import type {
14
14
  GridSortItem,
15
15
  } from '../types';
16
16
  import { toLocalDateString } from '../utils/values';
17
+ import { brandGridAiValidated } from '../utils/aiBrand';
17
18
  import { coerceBoolean, coerceValue } from './coerce';
18
19
  import {
19
20
  ALL_PARTS,
@@ -244,6 +245,9 @@ function readPart(ctx: Ctx, part: GridAiPart, raw: unknown, path: string, state:
244
245
  * parts, fields, operators and enum values are dropped, each with an error; values are coerced to
245
246
  * the column type (`"1,200"` → 1200, a date → `"YYYY-MM-DD"`); filter groups left empty are
246
247
  * removed. Never throws, and never returns a field the columns do not allow.
248
+ *
249
+ * The result and its `state` carry the brand `Symbol.for('opengridx.ai.validated')` (not enumerable):
250
+ * the grid's `aiAssistant` applies only a branded state. Spreading the result keeps `state` branded.
247
251
  * @since v3.4
248
252
  */
249
253
  export function validateGridAiState<C extends GridAiColumn>(
@@ -251,6 +255,12 @@ export function validateGridAiState<C extends GridAiColumn>(
251
255
  columns: readonly C[],
252
256
  options: GridAiValidateOptions = {},
253
257
  ): GridAiValidationResult {
258
+ const result = validate(json, columns, options);
259
+ brandGridAiValidated(result.state);
260
+ return brandGridAiValidated(result);
261
+ }
262
+
263
+ function validate(json: unknown, columns: readonly GridAiColumn[], options: GridAiValidateOptions): GridAiValidationResult {
254
264
  const ctx: Ctx = { errors: [], byField: new Map() };
255
265
  const state: GridAiState = {};
256
266
  try {
@@ -0,0 +1,26 @@
1
+ import type React from 'react';
2
+ import type { GridAiAssistantPanelProps, GridSlots } from '../../types';
3
+ import { GridAiAssistantPanel } from './GridAiAssistantPanel';
4
+
5
+ interface GridAiAssistantAreaProps {
6
+ panelProps: GridAiAssistantPanelProps;
7
+ slot: GridSlots['aiAssistantPanel'];
8
+ slotProps: Record<string, unknown> | undefined;
9
+ }
10
+
11
+ /**
12
+ * Anchors the `aiAssistant` panel (or `slots.aiAssistantPanel`) under the toolbar, over the grid.
13
+ * Escape anywhere in it closes the panel.
14
+ */
15
+ export function GridAiAssistantArea({ panelProps, slot: Slot, slotProps }: GridAiAssistantAreaProps) {
16
+ const handleKeyDown = (event: React.KeyboardEvent) => {
17
+ if (event.key !== 'Escape') return;
18
+ event.stopPropagation();
19
+ panelProps.close();
20
+ };
21
+ return (
22
+ <div className="ogx-ai-anchor" onKeyDown={handleKeyDown}>
23
+ {Slot ? <Slot {...panelProps} {...slotProps} /> : <GridAiAssistantPanel {...panelProps} />}
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,171 @@
1
+ /* aiAssistant panel (v3.5): anchored under the toolbar, over the grid body. */
2
+ .ogx-ai-anchor {
3
+ position: relative;
4
+ height: 0;
5
+ z-index: 20;
6
+ }
7
+ .ogx-ai-panel {
8
+ position: absolute;
9
+ top: 6px;
10
+ right: 8px;
11
+ width: min(480px, calc(100% - 16px));
12
+ max-height: var(--ogx-ai-panel-max-height, 420px);
13
+ overflow: auto;
14
+ box-sizing: border-box;
15
+ display: flex;
16
+ flex-direction: column;
17
+ gap: 10px;
18
+ padding: 12px;
19
+ background: var(--ogx-overlay-background, var(--ogx-grid-background));
20
+ color: var(--ogx-grid-row-text);
21
+ border: 1px solid var(--ogx-overlay-border, var(--ogx-grid-border-color));
22
+ border-radius: var(--ogx-border-radius-lg, 8px);
23
+ box-shadow: var(--ogx-shadow-lg, 0 8px 24px rgba(0, 0, 0, 0.15));
24
+ font-size: var(--ogx-font-size-sm);
25
+ }
26
+ .ogx-ai-panel__header {
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: space-between;
30
+ }
31
+ .ogx-ai-panel__title {
32
+ display: inline-flex;
33
+ align-items: center;
34
+ gap: 6px;
35
+ font-weight: 600;
36
+ color: var(--ogx-color-primary);
37
+ }
38
+ .ogx-ai-panel__close {
39
+ min-width: 28px;
40
+ font-size: 18px;
41
+ line-height: 1;
42
+ }
43
+ .ogx-ai-panel__form {
44
+ display: flex;
45
+ gap: 6px;
46
+ align-items: center;
47
+ }
48
+ .ogx-ai-panel__input {
49
+ flex: 1 1 0;
50
+ min-width: 0;
51
+ }
52
+ .ogx-ai-panel__form > .ogx-button {
53
+ flex: 0 0 auto;
54
+ }
55
+ .ogx-ai-panel__mic {
56
+ display: inline-flex;
57
+ align-items: center;
58
+ justify-content: center;
59
+ }
60
+ .ogx-ai-panel__mic--on {
61
+ color: var(--ogx-color-error);
62
+ border-color: var(--ogx-color-error);
63
+ }
64
+ .ogx-ai-panel__suggestions {
65
+ display: flex;
66
+ flex-wrap: wrap;
67
+ gap: 6px;
68
+ }
69
+ .ogx-ai-panel__suggestion {
70
+ padding: 3px 10px;
71
+ border: 1px solid var(--ogx-border-color, var(--ogx-grid-border-color));
72
+ border-radius: var(--ogx-border-radius-round, 999px);
73
+ background: var(--ogx-grid-header-background);
74
+ color: inherit;
75
+ font: inherit;
76
+ cursor: pointer;
77
+ }
78
+ .ogx-ai-panel__suggestion:hover:not(:disabled) {
79
+ background: var(--ogx-grid-row-hover-background);
80
+ }
81
+ .ogx-ai-panel__suggestion:disabled {
82
+ opacity: 0.5;
83
+ cursor: default;
84
+ }
85
+ .ogx-ai-panel__suggestion:focus-visible,
86
+ .ogx-ai-chip__remove:focus-visible,
87
+ .ogx-ai-panel__history-item:focus-visible {
88
+ outline: 2px solid var(--ogx-color-primary);
89
+ outline-offset: 1px;
90
+ }
91
+ .ogx-ai-panel__status {
92
+ color: var(--ogx-color-gray-600, #4b5563);
93
+ }
94
+ .ogx-ai-panel__status--error {
95
+ color: var(--ogx-color-error);
96
+ }
97
+ .ogx-ai-panel__message {
98
+ margin: 0;
99
+ white-space: pre-wrap;
100
+ }
101
+ .ogx-ai-panel__changes {
102
+ display: flex;
103
+ align-items: flex-start;
104
+ gap: 8px;
105
+ }
106
+ .ogx-ai-panel__chips {
107
+ display: flex;
108
+ flex-wrap: wrap;
109
+ gap: 6px;
110
+ flex: 1 1 auto;
111
+ margin: 0;
112
+ padding: 0;
113
+ list-style: none;
114
+ }
115
+ .ogx-ai-chip {
116
+ display: inline-flex;
117
+ align-items: center;
118
+ gap: 4px;
119
+ padding: 2px 4px 2px 10px;
120
+ border-radius: var(--ogx-border-radius-round, 999px);
121
+ background: var(--ogx-color-primary-light, #e0e7ff);
122
+ color: var(--ogx-color-primary-dark, #1e3a8a);
123
+ }
124
+ .ogx-ai-chip__remove {
125
+ width: 20px;
126
+ height: 20px;
127
+ padding: 0;
128
+ border: none;
129
+ border-radius: 50%;
130
+ background: transparent;
131
+ color: inherit;
132
+ font-size: 14px;
133
+ line-height: 1;
134
+ cursor: pointer;
135
+ }
136
+ .ogx-ai-chip__remove:hover {
137
+ background: rgba(0, 0, 0, 0.08);
138
+ }
139
+ .ogx-ai-panel__errors {
140
+ margin: 0;
141
+ padding-left: 18px;
142
+ color: var(--ogx-color-warning, #b45309);
143
+ }
144
+ .ogx-ai-panel__history-title {
145
+ font-weight: 600;
146
+ opacity: 0.7;
147
+ margin-bottom: 2px;
148
+ }
149
+ .ogx-ai-panel__history ul {
150
+ margin: 0;
151
+ padding: 0;
152
+ list-style: none;
153
+ }
154
+ .ogx-ai-panel__history-item {
155
+ padding: 2px 0;
156
+ border: none;
157
+ background: transparent;
158
+ color: var(--ogx-color-primary);
159
+ font: inherit;
160
+ text-align: left;
161
+ cursor: pointer;
162
+ }
163
+ .ogx-toolbar__ai-btn {
164
+ width: auto;
165
+ gap: 4px;
166
+ padding: 0 10px;
167
+ font: inherit;
168
+ font-size: var(--ogx-font-size-sm);
169
+ font-weight: 600;
170
+ color: var(--ogx-color-primary);
171
+ }