gbs-add-block 2.1.0 → 2.3.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/.gbs/skills/gbs-components/SKILL.md +190 -134
- package/.gbs/skills/gbs-components/references/install.md +25 -3
- package/.gbs/skills/gbs-components/references/styling.md +246 -207
- package/CHANGELOG.md +74 -0
- package/README.md +154 -23
- package/index.cjs +212 -3
- package/package.json +41 -10
- package/schema/passport-v1.schema.json +204 -0
- package/source/beta-components/accordion/passport.json +259 -0
- package/source/beta-components/accordion/styles.css +207 -208
- package/source/beta-components/alert/passport.json +250 -0
- package/source/beta-components/alert/styles.css +154 -155
- package/source/beta-components/avatar/passport.json +294 -0
- package/source/beta-components/avatar/styles.css +225 -226
- package/source/beta-components/badge/passport.json +332 -0
- package/source/beta-components/badge/styles.css +203 -204
- package/source/beta-components/breadcrumb/passport.json +243 -0
- package/source/beta-components/breadcrumb/styles.css +138 -140
- package/source/beta-components/button/passport.json +402 -0
- package/source/beta-components/button/passport.manual.json +31 -0
- package/source/beta-components/button/styles.css +232 -234
- package/source/beta-components/card/passport.json +337 -0
- package/source/beta-components/card/styles.css +230 -232
- package/source/beta-components/checkbox/passport.json +456 -0
- package/source/beta-components/checkbox/styles.css +211 -213
- package/source/beta-components/combobox/passport.json +456 -0
- package/source/beta-components/combobox/styles.css +419 -417
- package/source/beta-components/data-grid/agent/coerce.ts +368 -0
- package/source/beta-components/data-grid/agent/contract.ts +410 -0
- package/source/beta-components/data-grid/agent/dataset.ts +92 -0
- package/source/beta-components/data-grid/agent/engine.ts +470 -0
- package/source/beta-components/data-grid/agent/executors.ts +155 -0
- package/source/beta-components/data-grid/agent/index.ts +79 -0
- package/source/beta-components/data-grid/agent/intent.ts +324 -0
- package/source/beta-components/data-grid/agent/operations.ts +335 -0
- package/source/beta-components/data-grid/agent/validate.ts +630 -0
- package/source/beta-components/data-grid/agent/webmcp.ts +107 -0
- package/source/beta-components/data-grid/index.ts +14 -7
- package/source/beta-components/data-grid/passport.json +1051 -0
- package/source/beta-components/data-grid/passport.manual.json +255 -0
- package/source/beta-components/data-grid/react/AskGrid.tsx +164 -0
- package/source/beta-components/data-grid/react/DataGrid.tsx +39 -0
- package/source/beta-components/data-grid/styles.css +874 -716
- package/source/beta-components/date-picker/passport.json +407 -0
- package/source/beta-components/date-picker/styles.css +445 -446
- package/source/beta-components/dialog/passport.json +344 -0
- package/source/beta-components/dialog/styles.css +280 -279
- package/source/beta-components/file-uploader/passport.json +518 -0
- package/source/beta-components/file-uploader/styles.css +394 -396
- package/source/beta-components/input/passport.json +536 -0
- package/source/beta-components/input/styles.css +295 -297
- package/source/beta-components/menu/passport.json +322 -0
- package/source/beta-components/menu/styles.css +224 -223
- package/source/beta-components/modal/passport.json +289 -0
- package/source/beta-components/modal/styles.css +241 -240
- package/source/beta-components/number-input/passport.json +541 -0
- package/source/beta-components/number-input/styles.css +230 -231
- package/source/beta-components/popover/passport.json +238 -0
- package/source/beta-components/popover/styles.css +148 -147
- package/source/beta-components/progress/passport.json +270 -0
- package/source/beta-components/progress/styles.css +200 -201
- package/source/beta-components/radio-group/passport.json +477 -0
- package/source/beta-components/radio-group/styles.css +269 -270
- package/source/beta-components/shared/core/agent/adapter.ts +65 -0
- package/source/beta-components/shared/core/agent/history.ts +120 -0
- package/source/beta-components/shared/core/agent/index.ts +46 -0
- package/source/beta-components/shared/core/agent/numbers.ts +217 -0
- package/source/beta-components/shared/core/agent/schema.ts +180 -0
- package/source/beta-components/shared/core/agent/types.ts +169 -0
- package/source/beta-components/shared/core/agent/webmcp.ts +328 -0
- package/source/beta-components/shared/index.ts +9 -0
- package/source/beta-components/shared/react/GramproAIProvider.tsx +50 -0
- package/source/beta-components/shared/react/useAskAgent.ts +217 -0
- package/source/beta-components/shared/styles.css +79 -0
- package/source/beta-components/shared/version.json +4 -4
- package/source/beta-components/shared/version.ts +6 -6
- package/source/beta-components/skeleton/passport.json +251 -0
- package/source/beta-components/skeleton/styles.css +185 -187
- package/source/beta-components/spinner/passport.json +245 -0
- package/source/beta-components/spinner/styles.css +173 -174
- package/source/beta-components/switch/passport.json +421 -0
- package/source/beta-components/switch/styles.css +227 -229
- package/source/beta-components/tabs/passport.json +315 -0
- package/source/beta-components/tabs/styles.css +263 -264
- package/source/beta-components/textarea/passport.json +382 -0
- package/source/beta-components/textarea/styles.css +158 -160
- package/source/beta-components/toaster/passport.json +221 -0
- package/source/beta-components/toaster/styles.css +282 -282
- package/source/beta-components/tooltip/passport.json +170 -0
- package/source/beta-components/tooltip/styles.css +71 -73
- package/tools/env.cjs +61 -0
- package/tools/passport/cli.cjs +79 -0
- package/tools/passport/extract.cjs +493 -0
- package/tools/passport/index.cjs +185 -0
- package/tools/passport/merge.cjs +131 -0
- package/tools/passport/policy.cjs +65 -0
- package/tools/passport/validate.cjs +277 -0
- package/tools/ts-require.cjs +79 -0
- package/source/beta-components/accordion/__tests__/core.test.ts +0 -58
- package/source/beta-components/alert/__tests__/core.test.ts +0 -17
- package/source/beta-components/avatar/__tests__/core.test.ts +0 -88
- package/source/beta-components/badge/__tests__/core.test.ts +0 -46
- package/source/beta-components/breadcrumb/__tests__/core.test.ts +0 -58
- package/source/beta-components/button/__tests__/core.test.ts +0 -31
- package/source/beta-components/card/__tests__/core.test.ts +0 -57
- package/source/beta-components/checkbox/__tests__/core.test.ts +0 -40
- package/source/beta-components/combobox/__tests__/core.test.ts +0 -134
- package/source/beta-components/data-grid/__tests__/core.test.ts +0 -356
- package/source/beta-components/data-grid/__tests__/export.test.ts +0 -70
- package/source/beta-components/data-grid/__tests__/pdf.test.ts +0 -209
- package/source/beta-components/date-picker/__tests__/core.test.ts +0 -273
- package/source/beta-components/dialog/__tests__/core.test.ts +0 -86
- package/source/beta-components/file-uploader/__tests__/core.test.ts +0 -395
- package/source/beta-components/input/__tests__/core.test.ts +0 -75
- package/source/beta-components/menu/__tests__/core.test.ts +0 -120
- package/source/beta-components/modal/__tests__/core.test.ts +0 -55
- package/source/beta-components/number-input/__tests__/core.test.ts +0 -151
- package/source/beta-components/progress/__tests__/core.test.ts +0 -56
- package/source/beta-components/radio-group/__tests__/core.test.ts +0 -64
- package/source/beta-components/shared/__tests__/boundaries.test.ts +0 -95
- package/source/beta-components/shared/__tests__/core.test.ts +0 -55
- package/source/beta-components/shared/__tests__/position.test.ts +0 -143
- package/source/beta-components/skeleton/__tests__/core.test.ts +0 -41
- package/source/beta-components/spinner/__tests__/core.test.ts +0 -48
- package/source/beta-components/switch/__tests__/core.test.ts +0 -64
- package/source/beta-components/tabs/__tests__/core.test.ts +0 -51
- package/source/beta-components/textarea/__tests__/core.test.ts +0 -38
- package/source/beta-components/toaster/__tests__/core.test.ts +0 -256
- package/source/beta-components/tooltip/__tests__/core.test.ts +0 -42
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../schema/passport-v1.schema.json",
|
|
3
|
+
"component": "DataGrid",
|
|
4
|
+
"purpose": "Display and operate on tabular data. Virtualized; holds its own sort, filter, search, selection, column and pagination state in an external store exposed through GridApi.",
|
|
5
|
+
"intentDomains": ["Query", "ActionIntent", "Extraction"],
|
|
6
|
+
|
|
7
|
+
"$operationsNote": "Mirrors agent/operations.ts, which is what the runtime actually dispatches on. tools/eval/__tests__/corpus.test.ts fails if the two drift. `apiMethod` is present only where one GridApi method performs the operation.",
|
|
8
|
+
|
|
9
|
+
"operations": {
|
|
10
|
+
"search": {
|
|
11
|
+
"summary": "Free-text match across every searchable column.",
|
|
12
|
+
"apiMethod": "setGlobalFilter",
|
|
13
|
+
"input": { "type": "object", "required": ["text"], "properties": { "text": { "type": "string", "maxLength": 200 } } },
|
|
14
|
+
"effects": ["filters visible rows", "resets to page 0"],
|
|
15
|
+
"reversible": true
|
|
16
|
+
},
|
|
17
|
+
"filter": {
|
|
18
|
+
"summary": "Apply a typed filter to one column. Operators are constrained by the column's type.",
|
|
19
|
+
"apiMethod": "setFilter",
|
|
20
|
+
"input": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"required": ["column", "operator"],
|
|
23
|
+
"properties": {
|
|
24
|
+
"column": { "type": "string", "description": "Column id from the runtime contract." },
|
|
25
|
+
"operator": {
|
|
26
|
+
"enum": ["contains", "notContains", "equals", "notEquals", "startsWith", "endsWith",
|
|
27
|
+
"gt", "gte", "lt", "lte", "between", "before", "after", "in", "isEmpty", "isNotEmpty"]
|
|
28
|
+
},
|
|
29
|
+
"value": {},
|
|
30
|
+
"value2": { "description": "Upper bound; only for the `between` operator." }
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"effects": ["filters visible rows", "resets to page 0"],
|
|
34
|
+
"reversible": true
|
|
35
|
+
},
|
|
36
|
+
"clearFilters": {
|
|
37
|
+
"summary": "Remove every column filter and the search text.",
|
|
38
|
+
"apiMethod": "clearFilters",
|
|
39
|
+
"effects": ["restores every row"],
|
|
40
|
+
"reversible": true
|
|
41
|
+
},
|
|
42
|
+
"sort": {
|
|
43
|
+
"summary": "Sort by one column, replacing the current sort unless `append` is set.",
|
|
44
|
+
"apiMethod": "setSorting",
|
|
45
|
+
"input": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"required": ["column", "direction"],
|
|
48
|
+
"properties": {
|
|
49
|
+
"column": { "type": "string" },
|
|
50
|
+
"direction": { "enum": ["asc", "desc"] },
|
|
51
|
+
"append": { "type": "boolean", "default": false, "description": "Add to the existing sort instead of replacing it." }
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"effects": ["reorders visible rows"],
|
|
55
|
+
"reversible": true
|
|
56
|
+
},
|
|
57
|
+
"clearSort": {
|
|
58
|
+
"summary": "Return to the data's original order.",
|
|
59
|
+
"apiMethod": "setSorting",
|
|
60
|
+
"effects": ["reorders visible rows"],
|
|
61
|
+
"reversible": true
|
|
62
|
+
},
|
|
63
|
+
"selectRows": {
|
|
64
|
+
"summary": "Select or deselect rows by id.",
|
|
65
|
+
"apiMethod": "toggleRowSelected",
|
|
66
|
+
"input": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"required": ["rowIds"],
|
|
69
|
+
"properties": {
|
|
70
|
+
"rowIds": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
|
|
71
|
+
"value": { "type": "boolean", "default": true }
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"effects": ["changes rowSelection"],
|
|
75
|
+
"reversible": true
|
|
76
|
+
},
|
|
77
|
+
"selectAll": {
|
|
78
|
+
"summary": "Select, or clear, every row matching the current filters.",
|
|
79
|
+
"apiMethod": "toggleAllRowsSelected",
|
|
80
|
+
"input": { "type": "object", "properties": { "value": { "type": "boolean", "default": true } } },
|
|
81
|
+
"effects": ["changes rowSelection"],
|
|
82
|
+
"reversible": true
|
|
83
|
+
},
|
|
84
|
+
"clearSelection": {
|
|
85
|
+
"summary": "Deselect every row.",
|
|
86
|
+
"apiMethod": "clearSelection",
|
|
87
|
+
"effects": ["changes rowSelection"],
|
|
88
|
+
"reversible": true
|
|
89
|
+
},
|
|
90
|
+
"setColumnVisibility": {
|
|
91
|
+
"summary": "Show or hide one column.",
|
|
92
|
+
"apiMethod": "setColumnVisibility",
|
|
93
|
+
"input": {
|
|
94
|
+
"type": "object",
|
|
95
|
+
"required": ["column", "visible"],
|
|
96
|
+
"properties": { "column": { "type": "string" }, "visible": { "type": "boolean" } }
|
|
97
|
+
},
|
|
98
|
+
"effects": ["changes columnVisibility"],
|
|
99
|
+
"reversible": true
|
|
100
|
+
},
|
|
101
|
+
"pinColumn": {
|
|
102
|
+
"summary": "Pin a column to an edge, or unpin it with a null side.",
|
|
103
|
+
"apiMethod": "pinColumn",
|
|
104
|
+
"input": {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"required": ["column", "side"],
|
|
107
|
+
"properties": { "column": { "type": "string" }, "side": { "enum": ["left", "right", null] } }
|
|
108
|
+
},
|
|
109
|
+
"effects": ["changes columnPinning"],
|
|
110
|
+
"reversible": true
|
|
111
|
+
},
|
|
112
|
+
"moveColumn": {
|
|
113
|
+
"summary": "Move a column before or after another one.",
|
|
114
|
+
"apiMethod": "moveColumn",
|
|
115
|
+
"input": {
|
|
116
|
+
"type": "object",
|
|
117
|
+
"required": ["column", "target", "placement"],
|
|
118
|
+
"properties": {
|
|
119
|
+
"column": { "type": "string" },
|
|
120
|
+
"target": { "type": "string" },
|
|
121
|
+
"placement": { "enum": ["before", "after"] }
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"effects": ["changes columnOrder"],
|
|
125
|
+
"reversible": true
|
|
126
|
+
},
|
|
127
|
+
"setColumnWidth": {
|
|
128
|
+
"summary": "Set a column's width in pixels, or null to restore its default.",
|
|
129
|
+
"apiMethod": "setColumnWidth",
|
|
130
|
+
"input": {
|
|
131
|
+
"type": "object",
|
|
132
|
+
"required": ["column", "width"],
|
|
133
|
+
"properties": { "column": { "type": "string" }, "width": { "type": ["integer", "null"], "minimum": 1 } }
|
|
134
|
+
},
|
|
135
|
+
"effects": ["changes columnSizing"],
|
|
136
|
+
"reversible": true
|
|
137
|
+
},
|
|
138
|
+
"resetColumns": {
|
|
139
|
+
"summary": "Restore column order, width, visibility and pinning to their defaults.",
|
|
140
|
+
"apiMethod": "resetColumns",
|
|
141
|
+
"effects": ["changes columnOrder", "changes columnVisibility", "changes columnSizing", "changes columnPinning"],
|
|
142
|
+
"reversible": true
|
|
143
|
+
},
|
|
144
|
+
"setPage": {
|
|
145
|
+
"summary": "Jump to a page, counting from zero.",
|
|
146
|
+
"apiMethod": "setPageIndex",
|
|
147
|
+
"input": { "type": "object", "required": ["index"], "properties": { "index": { "type": "integer", "minimum": 0 } } },
|
|
148
|
+
"effects": ["changes the visible page"],
|
|
149
|
+
"reversible": true
|
|
150
|
+
},
|
|
151
|
+
"setPageSize": {
|
|
152
|
+
"summary": "Change how many rows a page holds.",
|
|
153
|
+
"apiMethod": "setPageSize",
|
|
154
|
+
"input": { "type": "object", "required": ["size"], "properties": { "size": { "type": "integer", "minimum": 1 } } },
|
|
155
|
+
"effects": ["changes the visible page"],
|
|
156
|
+
"reversible": true
|
|
157
|
+
},
|
|
158
|
+
"setDensity": {
|
|
159
|
+
"summary": "Change row height.",
|
|
160
|
+
"apiMethod": "setDensity",
|
|
161
|
+
"input": {
|
|
162
|
+
"type": "object",
|
|
163
|
+
"required": ["density"],
|
|
164
|
+
"properties": { "density": { "enum": ["compact", "standard", "comfortable"] } }
|
|
165
|
+
},
|
|
166
|
+
"effects": ["changes row height"],
|
|
167
|
+
"reversible": true
|
|
168
|
+
},
|
|
169
|
+
"export": {
|
|
170
|
+
"summary": "Download the chosen rows as a file.",
|
|
171
|
+
"input": {
|
|
172
|
+
"type": "object",
|
|
173
|
+
"required": ["format"],
|
|
174
|
+
"properties": {
|
|
175
|
+
"format": { "enum": ["csv", "excel", "pdf"] },
|
|
176
|
+
"scope": { "enum": ["filtered", "all", "selected", "page"], "default": "filtered" },
|
|
177
|
+
"fileName": { "type": "string", "maxLength": 120 }
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
"effects": ["downloads a file"],
|
|
181
|
+
"reversible": false,
|
|
182
|
+
"requiresConfirmation": true,
|
|
183
|
+
"note": "Three formats, three GridApi methods (exportCsv, exportExcel, exportPdf), so no single apiMethod applies."
|
|
184
|
+
},
|
|
185
|
+
"print": {
|
|
186
|
+
"summary": "Open the browser's print dialog with the table laid out for paper.",
|
|
187
|
+
"apiMethod": "print",
|
|
188
|
+
"input": {
|
|
189
|
+
"type": "object",
|
|
190
|
+
"properties": {
|
|
191
|
+
"scope": { "enum": ["filtered", "all", "selected", "page"], "default": "filtered" },
|
|
192
|
+
"title": { "type": "string", "maxLength": 120 }
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
"effects": ["opens the print dialog"],
|
|
196
|
+
"reversible": false,
|
|
197
|
+
"requiresConfirmation": true
|
|
198
|
+
},
|
|
199
|
+
"copy": {
|
|
200
|
+
"summary": "Copy the selected rows to the clipboard as tab-separated text.",
|
|
201
|
+
"apiMethod": "copyToClipboard",
|
|
202
|
+
"effects": ["writes to the clipboard"],
|
|
203
|
+
"reversible": false,
|
|
204
|
+
"requiresConfirmation": true
|
|
205
|
+
},
|
|
206
|
+
"undo": {
|
|
207
|
+
"summary": "Restore the state from before the last operation.",
|
|
208
|
+
"effects": ["restores a previous state"],
|
|
209
|
+
"reversible": false,
|
|
210
|
+
"note": "Performed by the agent runtime's snapshot history, not by GridApi."
|
|
211
|
+
},
|
|
212
|
+
"redo": {
|
|
213
|
+
"summary": "Re-apply the operation that was last undone.",
|
|
214
|
+
"effects": ["restores a later state"],
|
|
215
|
+
"reversible": false,
|
|
216
|
+
"note": "Performed by the agent runtime's snapshot history, not by GridApi."
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
"accessibility": {
|
|
221
|
+
"supplied": [
|
|
222
|
+
"WAI-ARIA grid pattern: role=row/rowgroup, aria-colindex, aria-rowindex",
|
|
223
|
+
"aria-sort on sortable header cells",
|
|
224
|
+
"aria-multiselectable when row selection is enabled",
|
|
225
|
+
"full cell keyboard navigation, including the header row"
|
|
226
|
+
],
|
|
227
|
+
"required": [
|
|
228
|
+
"aria-label on the grid when its purpose is not obvious from the page",
|
|
229
|
+
"a visible heading or caption nearby for screen-reader context"
|
|
230
|
+
]
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
"composition": {
|
|
234
|
+
"notes": "Self-contained. Columns are configuration, not children. Keep `data`, `columns` and `getRowId` referentially stable — the grid memoizes on their identity."
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
"safeMutations": {
|
|
238
|
+
"allowed": ["height", "autoHeight", "rowHeight", "headerHeight", "toolbar", "pageSizeOptions", "emptyState", "locale", "localeText", "aria-label"],
|
|
239
|
+
"forbidden": ["data", "columns", "getRowId", "mode", "state", "onStateChange", "onQueryChange"],
|
|
240
|
+
"rationale": "Presentation is safe to change. `data`/`columns`/`getRowId` are memoization keys — replacing them re-creates the grid model. `mode` and the state callbacks define the client/server contract."
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
"propNotes": {
|
|
244
|
+
"mode": "`client` lets the grid sort, filter and paginate. `server` means `data` is already the current page and the host answers `onQueryChange`.",
|
|
245
|
+
"state": "Makes the grid controlled. Prefer `initialState` unless the host genuinely owns the state."
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
"notImplemented": [
|
|
249
|
+
"grouping — no API, no state field, no operation",
|
|
250
|
+
"aggregation — no API, no state field, no operation",
|
|
251
|
+
"row-level mutation — no add, delete or set-cell API; editing is a UI affordance only",
|
|
252
|
+
"notIn — a column with fixed options filters by membership only; an exclusion must list the complement",
|
|
253
|
+
"multi-value filter on a plain string column — one column holds one filter"
|
|
254
|
+
]
|
|
255
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useId, useRef, useState, type FormEvent, type KeyboardEvent } from "react";
|
|
4
|
+
import { cx } from "../../shared/core/cx";
|
|
5
|
+
import { useAgentAdapter } from "../../shared/react/GramproAIProvider";
|
|
6
|
+
import { useAskAgent, type AskableAgent } from "../../shared/react/useAskAgent";
|
|
7
|
+
import type { GridAgent } from "../agent/engine";
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* The box a person types into.
|
|
11
|
+
*
|
|
12
|
+
* It owns no grid semantics. Everything it shows — what happened, what was
|
|
13
|
+
* refused and why, which question to ask back — comes from the validator, in
|
|
14
|
+
* the validator's own words. That matters more than it sounds: a message this
|
|
15
|
+
* component invented would be a second source of truth about what the grid
|
|
16
|
+
* can do, and would drift.
|
|
17
|
+
*
|
|
18
|
+
* Renders nothing at all when no adapter is in scope, so a grid in an
|
|
19
|
+
* application with no AI configured is the grid it has always been.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export interface AskGridProps<T> {
|
|
23
|
+
agent: GridAgent<T> | null;
|
|
24
|
+
placeholder?: string;
|
|
25
|
+
suggestions?: readonly string[];
|
|
26
|
+
className?: string;
|
|
27
|
+
"aria-label"?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const TONE: Record<string, string> = {
|
|
31
|
+
done: "ask-ok",
|
|
32
|
+
clarify: "ask-ask",
|
|
33
|
+
refused: "ask-no",
|
|
34
|
+
error: "ask-err",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function AskGrid<T>({
|
|
38
|
+
agent,
|
|
39
|
+
placeholder,
|
|
40
|
+
suggestions,
|
|
41
|
+
className,
|
|
42
|
+
"aria-label": ariaLabel,
|
|
43
|
+
}: AskGridProps<T>) {
|
|
44
|
+
const context = useAgentAdapter();
|
|
45
|
+
const [text, setText] = useState("");
|
|
46
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
47
|
+
const statusId = useId();
|
|
48
|
+
|
|
49
|
+
const { ask, confirm, reset, outcome, busy, pending } = useAskAgent(
|
|
50
|
+
agent as AskableAgent | null,
|
|
51
|
+
context.adapter,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const submit = useCallback(
|
|
55
|
+
(event: FormEvent) => {
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
void ask(text);
|
|
58
|
+
},
|
|
59
|
+
[ask, text],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const onKeyDown = useCallback(
|
|
63
|
+
(event: KeyboardEvent<HTMLInputElement>) => {
|
|
64
|
+
if (event.key === "Escape" && outcome.phase !== "idle") {
|
|
65
|
+
event.stopPropagation();
|
|
66
|
+
reset();
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
[outcome.phase, reset],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const useSuggestion = useCallback(
|
|
73
|
+
(value: string) => {
|
|
74
|
+
setText(value);
|
|
75
|
+
inputRef.current?.focus();
|
|
76
|
+
void ask(value);
|
|
77
|
+
},
|
|
78
|
+
[ask],
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// No adapter configured: the feature is simply not here.
|
|
82
|
+
if (!context.adapter) return null;
|
|
83
|
+
|
|
84
|
+
const hints = suggestions ?? context.suggestions ?? [];
|
|
85
|
+
const showHints = hints.length > 0 && text === "" && outcome.phase === "idle";
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className={cx("ask-root", className)}>
|
|
89
|
+
<form className="ask-form" onSubmit={submit}>
|
|
90
|
+
<input
|
|
91
|
+
ref={inputRef}
|
|
92
|
+
type="text"
|
|
93
|
+
className="ask-input"
|
|
94
|
+
value={text}
|
|
95
|
+
disabled={!agent}
|
|
96
|
+
placeholder={placeholder ?? context.placeholder ?? "Ask about this data…"}
|
|
97
|
+
aria-label={ariaLabel ?? "Ask about this data"}
|
|
98
|
+
aria-describedby={outcome.phase === "idle" ? undefined : statusId}
|
|
99
|
+
aria-busy={busy || undefined}
|
|
100
|
+
onChange={(event) => setText(event.target.value)}
|
|
101
|
+
onKeyDown={onKeyDown}
|
|
102
|
+
/>
|
|
103
|
+
<button type="submit" className="ask-send" disabled={busy || !agent || text.trim() === ""}>
|
|
104
|
+
{busy ? "…" : "Ask"}
|
|
105
|
+
</button>
|
|
106
|
+
</form>
|
|
107
|
+
|
|
108
|
+
{showHints && (
|
|
109
|
+
<div className="ask-hints">
|
|
110
|
+
{hints.map((hint) => (
|
|
111
|
+
<button
|
|
112
|
+
key={hint}
|
|
113
|
+
type="button"
|
|
114
|
+
className="ask-hint"
|
|
115
|
+
onClick={() => useSuggestion(hint)}
|
|
116
|
+
>
|
|
117
|
+
{hint}
|
|
118
|
+
</button>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
{outcome.phase !== "idle" && (
|
|
124
|
+
/*
|
|
125
|
+
* `polite`, not `assertive`: the grid itself has already changed, and
|
|
126
|
+
* a screen reader interrupting to repeat it would be noise.
|
|
127
|
+
*/
|
|
128
|
+
<div id={statusId} className={cx("ask-status", TONE[outcome.phase])} role="status" aria-live="polite">
|
|
129
|
+
{outcome.phase === "thinking" ? (
|
|
130
|
+
<span className="ask-thinking">Thinking…</span>
|
|
131
|
+
) : (
|
|
132
|
+
<>
|
|
133
|
+
<span className="ask-message">{outcome.message}</span>
|
|
134
|
+
|
|
135
|
+
{outcome.suggestion && <span className="ask-suggestion">{outcome.suggestion}</span>}
|
|
136
|
+
|
|
137
|
+
{outcome.layer && (
|
|
138
|
+
<span className="ask-layer" title={`Refused by the ${outcome.layer} layer`}>
|
|
139
|
+
{outcome.layer}
|
|
140
|
+
</span>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
{outcome.warnings?.map((warning) => (
|
|
144
|
+
<span key={warning} className="ask-warning">
|
|
145
|
+
{warning}
|
|
146
|
+
</span>
|
|
147
|
+
))}
|
|
148
|
+
|
|
149
|
+
{pending !== null && (
|
|
150
|
+
<button type="button" className="ask-confirm" onClick={() => void confirm()}>
|
|
151
|
+
Yes, do it
|
|
152
|
+
</button>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
<button type="button" className="ask-dismiss" onClick={reset} aria-label="Dismiss">
|
|
156
|
+
×
|
|
157
|
+
</button>
|
|
158
|
+
</>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -25,6 +25,9 @@ import { defaultLocaleText, type LocaleText } from "./locale";
|
|
|
25
25
|
import { Pagination } from "./Pagination";
|
|
26
26
|
import { Popover } from "./Popover";
|
|
27
27
|
import { Toolbar, type ToolbarOptions } from "./Toolbar";
|
|
28
|
+
import { AskGrid } from "./AskGrid";
|
|
29
|
+
import { createGridAgent, type GridAgent } from "../agent/engine";
|
|
30
|
+
import type { GridAgentPolicy, GridColumnSemantics } from "../agent/contract";
|
|
28
31
|
import { GridViewport } from "./Viewport";
|
|
29
32
|
|
|
30
33
|
export interface DataGridProps<T> extends GridOptions<T> {
|
|
@@ -48,6 +51,18 @@ export interface DataGridProps<T> extends GridOptions<T> {
|
|
|
48
51
|
locale?: string;
|
|
49
52
|
localeText?: Partial<LocaleText>;
|
|
50
53
|
"aria-label"?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Render a natural-language box above the grid.
|
|
56
|
+
*
|
|
57
|
+
* Needs a `<GramproAIProvider>` above it supplying an adapter; without one
|
|
58
|
+
* this renders nothing and the grid is unchanged. No model is bundled or
|
|
59
|
+
* downloaded — the application brings its own.
|
|
60
|
+
*/
|
|
61
|
+
ai?: boolean;
|
|
62
|
+
/** What a machine cannot read off a column definition: units, synonyms, PII. */
|
|
63
|
+
semantics?: Readonly<Record<string, GridColumnSemantics>>;
|
|
64
|
+
/** Columns and operations withheld from whatever is driving the grid. */
|
|
65
|
+
agentPolicy?: GridAgentPolicy;
|
|
51
66
|
}
|
|
52
67
|
|
|
53
68
|
const ROW_HEIGHTS: Record<Density, number> = { compact: 32, standard: 40, comfortable: 52 };
|
|
@@ -96,11 +111,33 @@ export function DataGrid<T>(props: DataGridProps<T>) {
|
|
|
96
111
|
style,
|
|
97
112
|
locale,
|
|
98
113
|
localeText: localeOverrides,
|
|
114
|
+
ai = false,
|
|
115
|
+
semantics,
|
|
116
|
+
agentPolicy,
|
|
99
117
|
} = props;
|
|
100
118
|
|
|
101
119
|
const [engine] = useState(() => createGridEngine<T>(props));
|
|
102
120
|
useImperativeHandle(ref, () => engine.api, [engine]);
|
|
103
121
|
|
|
122
|
+
/*
|
|
123
|
+
* The agent is built once, from the same engine the grid renders, so the
|
|
124
|
+
* contract it advertises is this instance's real state rather than a copy.
|
|
125
|
+
* Only when `ai` is on: an agent walks the data to derive column stats and
|
|
126
|
+
* enum values, which a grid nobody is going to ask questions of should not
|
|
127
|
+
* pay for.
|
|
128
|
+
*/
|
|
129
|
+
const [agent] = useState<GridAgent<T> | null>(() =>
|
|
130
|
+
ai
|
|
131
|
+
? createGridAgent<T>({
|
|
132
|
+
api: engine.api,
|
|
133
|
+
options: props,
|
|
134
|
+
semantics,
|
|
135
|
+
policy: agentPolicy,
|
|
136
|
+
locale,
|
|
137
|
+
})
|
|
138
|
+
: null,
|
|
139
|
+
);
|
|
140
|
+
|
|
104
141
|
// Selection and UI state are deliberately not selected here: changing them
|
|
105
142
|
// re-renders only the rows and cells that subscribe to them.
|
|
106
143
|
const internal = useStoreSelector(engine.store, selectModelState, shallowEqual);
|
|
@@ -243,6 +280,8 @@ export function DataGrid<T>(props: DataGridProps<T>) {
|
|
|
243
280
|
data-density={density}
|
|
244
281
|
data-stale={stale || undefined}
|
|
245
282
|
>
|
|
283
|
+
{ai && <AskGrid agent={agent} aria-label={props['aria-label']} />}
|
|
284
|
+
|
|
246
285
|
{toolbarOptions && (
|
|
247
286
|
<Toolbar
|
|
248
287
|
options={toolbarOptions}
|