Smart Pivot Table in React
The Syncfusion React Pivot Table can be integrated with the React AI AssistView to let users configure reports with natural-language commands. Users can filter, sort, summarize, reorganize, format, and visualize a report while the field list, grouping bar, toolbar, and chart controls remain available.
The assistant does not apply arbitrary component properties. Each prompt is converted into exactly one validated Pivot action, and only the corresponding report setting is changed.
The AI integration described here is an application pattern. It requires a server-side LLM integration. Never expose an OpenAI API key in React code or a
VITE_*environment variable.
How it works
- The user submits a prompt through
AIAssistViewComponent. - The client sends the prompt and compact report metadata to
/api/pivot-action. - The server authenticates and rate-limits the request.
- If
OPENAI_API_KEYis configured, the OpenAI Responses API returns one structured action. Otherwise, the sample uses deterministic mock actions. - The server validates the action and checks all referenced fields and members.
- The client validates the response again and immutably updates only the Pivot setting on the allow list.
- AI AssistView displays the applied change or a recovery message.
The raw dataset, event handlers, templates, credentials, and data-source configuration are never sent to the model.
Supported AI operations
| Operation | Example prompt | Pivot Table setting |
|---|---|---|
| Member filter | “Show only France and Germany” |
filterSettings with Include
|
| Label filter | “Show countries containing land” |
filterSettings with Label
|
| Value filter | “Show countries with Amount greater than 100000” |
filterSettings with Value
|
| Clear filter | “Clear the Country filter” | Removes one or all filter settings |
| Member sort | “Sort Country descending” | sortSettings |
| Clear sort | “Clear Country sorting” | Removes one or all sort settings |
| Aggregation | “Average units sold” | values[].type |
| Move field | “Move Quarter to rows” |
rows, columns, values, or filters
|
| Expand or collapse | “Expand Country” |
expandAll or field expandAll
|
| Primary view | “Switch to chart view” | displayOption.primary |
| Chart type | “Use a line chart” | chartSettings.chartSeries.type |
| Totals | “Hide row grand totals” | subtotal and grand-total visibility |
| Number format | “Format Amount as currency with two decimals” | formatSettings |
Calculated fields, conditional formatting, drill-through, export, report persistence, raw-record editing, and data-source replacement are intentionally not automated. Keep these operations in the standard Pivot Table UI or introduce a separate reviewed contract.
Prerequisites
- Node.js 18 or later and npm 8 or later.
- React 18 or later with TypeScript and Vite. The sample is verified with React 19.2.
- Syncfusion Essential JS 2 packages from the same release family.
- A Syncfusion license key or active trial.
- An OpenAI API key with available API billing quota for live mode.
- For production, an OIDC provider and Redis.
Register the Syncfusion license as described in the React licensing guide.
The reference sample was verified with Node.js 24, React 19.2.3, Syncfusion 34.1.x, OpenAI JavaScript SDK 5.23.2, Express 4.22.2, and Zod 3.25.76. Use the repository lock files to reproduce the exact dependency graph.
Use the complete sample
For the fastest path, clone and run the complete sample:
git clone https://github.com/SyncfusionExamples/react-assistive-pivot.git
cd react-assistive-pivotThe sample contains the complete runnable client, server, environment templates, schemas, reducers, tests, and CI configuration. Use the manual steps below when you want to recreate the integration in an existing application.
Create the projects manually
Create a React TypeScript project and a sibling server project:
npm create vite@latest assistive-pivot -- --template react-ts
mkdir server
cd server
npm init -y
cd ..Use the following structure:
assistive-pivot/
src/
ai/ Action types, reducer, and API client
frontend/ PivotView and AI AssistView UI
data/ Trusted sample data
.env.example
server/
auth.js OIDC authentication boundary
rate-limit.js Redis and development rate-limit stores
schema.js Request and action validation
ai-model.js Live and deterministic mock adapters
server.js Express routes and error mapping
.env.exampleWhen building manually, create each listed file before adding the corresponding snippets. Use the complete sample when you need the full file contents instead of focused excerpts.
Install and configure the packages
Install the client packages:
cd assistive-pivot
npm install @syncfusion/ej2-base @syncfusion/ej2-react-pivotview @syncfusion/ej2-react-interactive-chat @syncfusion/ej2-react-popups @syncfusion/ej2-tailwind3-theme
npm install --save-dev vitestInstall the server packages:
cd ../server
npm install cors dotenv express express-rate-limit jose openai rate-limit-redis redis zodUse the current Tailwind 3 theme-package styles in the global stylesheet. The PivotView and AI AssistView getting-started guides use component index.css files from @syncfusion/ej2-tailwind3-theme; the Dialog import is included because the assistant is rendered inside a modal dialog:
@import "@syncfusion/ej2-tailwind3-theme/styles/pivotview/index.css";
@import "@syncfusion/ej2-tailwind3-theme/styles/ai-assistview/index.css";
@import "@syncfusion/ej2-tailwind3-theme/styles/dialog/index.css";Register the Syncfusion license before rendering the application:
import { registerLicense } from '@syncfusion/ej2-base';
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const syncfusionLicenseKey = import.meta.env.VITE_SYNCFUSION_LICENSE_KEY;
if (syncfusionLicenseKey) {
registerLicense(syncfusionLicenseKey);
}
createRoot(document.getElementById('root')!).render(<App />);Create assistive-pivot/.env.example:
VITE_API_BASE_URL=http://localhost:3000
VITE_SYNCFUSION_LICENSE_KEY=
VITE_ACCESS_TOKEN=VITE_SYNCFUSION_LICENSE_KEY is intended for client registration. Never place OPENAI_API_KEY in a VITE_* variable.
VITE_ACCESS_TOKEN is only for local integration testing. Production applications should obtain a fresh token from the deployed application’s OIDC client at runtime.
Define a typed action contract
Do not accept arbitrary properties from the model. Define a discriminated union for the supported operations:
const dimensions = [
'Country', 'Year', 'Quarter', 'Product_Categories', 'Products', 'Order_Source'
] as const;
const measures = ['Sold', 'Amount'] as const;
type Dimension = typeof dimensions[number];
type Measure = typeof measures[number];
type FilterCondition =
| 'Equals' | 'DoesNotEquals' | 'Contains' | 'DoesNotContains'
| 'BeginWith' | 'EndsWith' | 'GreaterThan' | 'GreaterThanOrEqualTo'
| 'LessThan' | 'LessThanOrEqualTo' | 'Between' | 'NotBetween';
type Aggregate = 'Sum' | 'Avg' | 'Count' | 'DistinctCount' | 'Min' | 'Max' | 'Product';
type Axis = 'rows' | 'columns' | 'values' | 'filters';
type ChartType = 'Column' | 'Bar' | 'Line' | 'Spline' | 'Area' | 'StackingColumn' | 'StackingBar';
type TotalsTarget = 'rowSubtotals' | 'columnSubtotals' | 'rowGrandTotals' | 'columnGrandTotals';
type NumberFormat = 'N0' | 'N2' | 'C0' | 'C2' | 'P0' | 'P2';
type PivotAction =
| { action: 'memberFilter'; field: Dimension; mode: 'Include' | 'Exclude'; items: string[]; message: string }
| { action: 'labelFilter'; field: Dimension; condition: FilterCondition; value1: string; value2?: string; message: string }
| { action: 'valueFilter'; field: Dimension; measure: Measure; condition: FilterCondition; value1: number; value2?: number; message: string }
| { action: 'clearFilter'; field?: Dimension; message: string }
| { action: 'sort'; field: Dimension; order: 'Ascending' | 'Descending' | 'None'; message: string }
| { action: 'clearSort'; field?: Dimension; message: string }
| { action: 'aggregate'; measure: Measure; aggregation: Aggregate; message: string }
| { action: 'moveField'; field: Dimension | Measure; axis: Axis | 'remove'; message: string }
| { action: 'expand'; field?: Dimension; expanded: boolean; message: string }
| { action: 'view'; primary: 'Table' | 'Chart'; message: string }
| { action: 'chartType'; chartType: ChartType; message: string }
| { action: 'totals'; target: TotalsTarget; visible: boolean; message: string }
| { action: 'numberFormat'; measure: Measure; format: NumberFormat; message: string };Validate the action on the server and client. Check action-specific properties, enum values, field names, measure names, member values, collection sizes, message length, and unknown properties. The snippets below focus on the integration points; use the complete sample project on GitHub for the runnable client reducer and server schema.
The server must use a discriminated schema, reject unknown properties, and verify trusted references after parsing:
const action = actionSchema.parse(candidate);
validateTrustedReferences(action, request.context);The client must reject a response before applying it unless its action name, required properties, and enum values match the same contract.
Send compact report context
Send metadata required to interpret the prompt, not business records:
interface ReportContext {
rows: string[];
columns: string[];
values: Array<{ name: string; type: string }>;
filters: string[];
availableFields: readonly string[];
availableMembers: Record<string, string[]>;
primaryView: 'Table' | 'Chart';
}
interface AssistantRequest {
prompt: string;
reportId: string;
context: ReportContext;
}Build this object from the current trusted configuration:
function createReportContext(
settings: IDataOptions,
primaryView: 'Table' | 'Chart',
trustedMembers: Record<string, string[]>
): ReportContext {
return {
rows: (settings.rows ?? []).map(item => item.name ?? ''),
columns: (settings.columns ?? []).map(item => item.name ?? ''),
values: (settings.values ?? []).map(item => ({
name: item.name ?? '',
type: String(item.type ?? 'Sum')
})),
filters: (settings.filters ?? []).map(item => item.name ?? ''),
availableFields: [...dimensions, ...measures],
availableMembers: trustedMembers,
primaryView
};
}Derive trustedMembers from an application-controlled dataset or authorized metadata endpoint. Do not copy member values from a prompt or model response.
The client request is:
{
"prompt": "Show only France and Germany",
"reportId": "sales-pivot",
"context": {
"rows": ["Country"],
"columns": ["Year"],
"values": [{ "name": "Amount", "type": "Sum" }],
"filters": [],
"availableFields": ["Country", "Year", "Amount"],
"availableMembers": { "Country": ["France", "Germany"] },
"primaryView": "Table"
}
}Send the request with an AbortSignal so the AI AssistView stop-response action can cancel it. Check the HTTP status and validate the response before returning it:
interface AssistantResponse {
action: PivotAction;
mode: 'live' | 'mock';
}
async function requestPivotAction(
prompt: string,
reportId: string,
context: ReportContext,
signal: AbortSignal,
accessToken?: string
): Promise<AssistantResponse> {
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
const response = await fetch(`${apiBaseUrl}/api/pivot-action`, {
method: 'POST',
signal,
headers: {
'Content-Type': 'application/json',
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {})
},
body: JSON.stringify({ prompt, reportId, context })
});
const text = await response.text();
let body: unknown = {};
if (text) {
try {
body = JSON.parse(text);
} catch {
throw new Error('The assistant service returned an unreadable response.');
}
}
if (!response.ok) {
const error = body as { error?: string };
throw new Error(error.error ?? 'The assistant request failed.');
}
return validateAssistantResponse(body);
}Configure AI AssistView
Use AIAssistViewComponent for prompt entry, built-in suggestions, conversation history, response actions, keyboard support, and stop-response behavior. The following excerpt assumes that assistRef, requestController, suggestions, stable toolbarSettings, templates, and onPromptRequest are defined in the containing component:
import {
AIAssistViewComponent,
ViewDirective,
ViewsDirective,
type PromptRequestEventArgs,
type ToolbarSettingsModel
} from '@syncfusion/ej2-react-interactive-chat';
<AIAssistViewComponent
ref={assistRef}
id="pivot-ai-assist"
width="100%"
height="100%"
showHeader={true}
toolbarSettings={toolbarSettings}
promptSuggestions={suggestions}
promptSuggestionsHeader="Try a supported Pivot action"
promptSuggestionItemTemplate={suggestionTemplate}
promptPlaceholder="Ask to filter, sort, summarize, reorganize, or visualize the report"
showClearButton={true}
enableScrollToBottom={true}
enableAttachments={false}
speechToTextSettings={{ enable: false }}
bannerTemplate={bannerTemplate}
responseItemTemplate={responseTemplate}
promptRequest={onPromptRequest}
stopRespondingClick={() => requestController.current?.abort()}
>
<ViewsDirective>
<ViewDirective type="Assist" name="Pivot Assistant" iconCss="e-icons e-assistview-icon" />
</ViewsDirective>
</AIAssistViewComponent>Keep toolbarSettings referentially stable with useMemo. Recreating it for every render can cause third-party toolbar state to be rebuilt unnecessarily. The sample toolbar provides Start new chat, Undo, Reset, Developer details, and Close actions. Use promptToolbarSettings to customize prompt actions and responseToolbarSettings to customize response actions. Leave the default prompt toolbar actions (Edit and Copy) and response actions (Copy, Like, and Dislike) enabled unless the application has a reason to replace them.
In onPromptRequest, reject an empty prompt, prevent concurrent submissions, create an AbortController, call requestPivotAction, apply the validated action, and finish with assistRef.current?.addPromptResponse(...). The complete AI AssistView and PivotView component is available in the sample project on GitHub.
Attachments and speech input are disabled because this integration accepts text commands only and must not upload report data.
Render the AI AssistView inside a modal DialogComponent targeted inside the React root. Enable escape-to-close behavior, restore focus to the opener after closing, and size the dialog relative to the viewport.
Configure the Pivot Table
Keep the standard Pivot features available so AI remains optional. Enable filtering in dataSourceSettings:
const dataSourceSettings: IDataOptions = {
dataSource: trustedData,
enableSorting: true,
allowMemberFilter: true,
allowLabelFilter: true,
allowValueFilter: true,
rows: [{ name: 'Country' }],
columns: [{ name: 'Year' }],
values: [{ name: 'Sold', type: 'Sum' }, { name: 'Amount', type: 'Sum' }]
};The following component excerpt assumes the imported services and state shown by the complete sample:
<PivotViewComponent
ref={pivotRef}
id="ai-pivot"
dataSourceSettings={dataSourceSettings}
width="100%"
height={measuredHeight}
gridSettings={{ columnWidth: 140, allowResizing: true }}
displayOption={{ view: 'Both', primary: primaryView }}
chartSettings={{ chartSeries: { type: chartType } }}
enableValueSorting={true}
allowCalculatedField={true}
allowConditionalFormatting={true}
allowNumberFormatting={true}
showGroupingBar={true}
showFieldList={true}
showToolbar={true}
toolbar={toolbar}
>
<Inject services={[GroupingBar, FieldList, CalculatedField, Toolbar, ConditionalFormatting, NumberFormatting, PivotChart]} />
</PivotViewComponent>If the Pivot Table must fill a responsive parent, give the parent an explicit height and pass a measured pixel height to the component. Create and clean up a ResizeObserver in an effect:
const pivotContainerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const element = pivotContainerRef.current;
if (!element) return;
const updateHeight = () => {
const style = getComputedStyle(element);
const padding =
Number.parseFloat(style.paddingTop) +
Number.parseFloat(style.paddingBottom);
setPivotHeight(Math.max(460, Math.floor(element.clientHeight - padding)));
};
updateHeight();
const observer = new ResizeObserver(updateHeight);
observer.observe(element);
return () => observer.disconnect();
}, []);Apply validated actions
Clone the current IDataOptions, change only the target setting, and preserve unrelated arrays and dataSource.
For example, a member filter replaces only the filter for the selected field:
next.filterSettings = [
...(next.filterSettings ?? []).filter(item => item.name !== action.field),
{ name: action.field, type: action.mode, items: action.items }
];For view changes, preserve view: 'Both' and change only primary. For chart-type actions, also select 'Chart' as the primary view. Store a compact snapshot without raw data to implement undo.
Apply the remaining actions as follows:
| Action | Minimal update |
|---|---|
labelFilter / valueFilter
|
Replace only the matching field entry in filterSettings
|
clearFilter / clearSort
|
Remove the requested field entry, or clear the collection when the field is omitted |
sort |
Replace only the matching field entry in sortSettings
|
aggregate |
Change type on the matching value field |
moveField |
Remove the field from all axes and append it to the requested axis |
expand |
Change global expandAll or the selected row/column field |
totals |
Change only the requested subtotal or grand-total flag |
numberFormat |
Replace only the matching measure entry in formatSettings
|
Value filters use type: 'Value' and store the numeric values as strings in filterSettings:
next.filterSettings = [
...(next.filterSettings ?? []).filter(item => item.name !== action.field),
{
name: action.field,
type: 'Value',
measure: action.measure,
condition: action.condition,
value1: String(action.value1),
value2: action.value2 === undefined ? undefined : String(action.value2)
}
];Field movement must remove the field from every axis before appending it to the requested axis:
if (action.action === 'moveField') {
for (const axis of ['rows', 'columns', 'values', 'filters'] as const) {
next[axis] = (next[axis] ?? []).filter(item => item.name !== action.field);
}
if (action.axis !== 'remove') {
next[action.axis] = [...(next[action.axis] ?? []), { name: action.field }];
}
}Totals and number-format actions should touch only their matching setting:
if (action.action === 'totals' && action.target === 'rowGrandTotals') {
next.showGrandTotals = true;
next.showRowGrandTotals = action.visible;
}
if (action.action === 'numberFormat') {
next.formatSettings = [
...(next.formatSettings ?? []).filter(item => item.name !== action.measure),
{ name: action.measure, format: action.format }
];
}The reducer must reject missing measures or fields and must never accept dataSource, URLs, callbacks, templates, or arbitrary component properties.
Configure the server
Create server/.env.example, then copy it to server/.env for local development:
PORT=3000
NODE_ENV=development
CORS_ORIGIN=http://localhost:5173,http://127.0.0.1:5173
OPENAI_API_KEY=
OPENAI_MODEL=gpt-5-mini
# Required in production
OIDC_ISSUER=https://identity.example.com/
OIDC_AUDIENCE=assistive-pivot-api
OIDC_JWKS_URI=https://identity.example.com/.well-known/jwks.json
ALLOWED_REPORT_IDS=sales-pivot
REDIS_URL=redis://localhost:6379
# Development defaults
DEV_USER_ID=local-developer
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX=30When OPENAI_API_KEY is empty, the server starts in mock mode and returns deterministic validated actions for the built-in suggestions. When a key is available, it uses the OpenAI Responses API with strict JSON-schema output.
Environment variables:
| Variable | Purpose |
|---|---|
PORT |
HTTP listening port |
NODE_ENV |
Selects development or production behavior |
CORS_ORIGIN |
Comma-separated exact frontend origins |
OPENAI_API_KEY |
Server-only provider credential; empty enables mock mode |
OPENAI_MODEL |
Responses API model that supports strict JSON-schema output |
OIDC_ISSUER |
Expected token issuer URL |
OIDC_AUDIENCE |
Expected API audience |
OIDC_JWKS_URI |
HTTPS endpoint containing signing keys |
ALLOWED_REPORT_IDS |
Sample production report allowlist; replace with the deploying application’s report-permission check |
REDIS_URL |
Redis connection URL for the production rate-limit store |
DEV_USER_ID |
Development-only local identity |
RATE_LIMIT_WINDOW_MS |
Rate-limit window in milliseconds |
RATE_LIMIT_MAX |
Maximum requests per authenticated subject in one window |
Implement the server in this order:
- Load and validate the environment.
- Fail production startup if OIDC, report authorization, or Redis configuration is missing.
- Configure the exact CORS origins and a
32kbJSON body limit. - Add development identity or production OIDC bearer-token validation.
- Add the in-memory development limiter or Redis production limiter.
- Validate
{ prompt, reportId, context }. - Authorize the authenticated subject for
reportId. - Generate a deterministic mock action or call
openai.responses.create. - Validate the returned action and its trusted field/member references.
- Return
{ action, mode }.
The route should follow this shape:
app.post('/api/pivot-action', authenticate, limiter, async (req, res) => {
const parsed = requestSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: 'The assistant request is invalid.' });
}
if (!canAccessReport(req.auth, parsed.data.reportId)) {
return res.status(403).json({ error: 'Access to this report is denied.' });
}
try {
const candidate = mode === 'live'
? await createLiveAction(parsed.data.prompt, parsed.data.context)
: createMockAction(parsed.data.prompt);
const action = actionSchema.parse(candidate);
validateTrustedReferences(action, parsed.data.context);
return res.json({ action, mode });
} catch (error) {
const mapped = mapAssistantError(error);
return res.status(mapped.status).json({ error: mapped.message });
}
});Use the complete Express route, OIDC middleware, Redis limiter, and OpenAI/mock adapter in the sample project on GitHub as the runnable server implementation.
Mock mode supports the built-in examples for filtering France and Germany, averaging Sold, switching to Chart, sorting Country, and clearing filters. Other mock prompts return HTTP 422 without changing the report.
Expose a health endpoint:
{ "status": "ok", "mode": "mock" }In production:
- Validate OIDC bearer tokens using the configured issuer, audience, signature, expiry, and JWKS endpoint.
- Obtain the access token through the deploying application’s OIDC client and send it as
Authorization: Bearer <token>. - Authorize the authenticated subject for
reportIdbefore generating an action. Replace the sampleALLOWED_REPORT_IDSallowlist with the deploying application’s report-permission store. - Key rate limits by the authenticated token subject.
- Store rate limits in Redis so they are shared across instances and survive restarts.
- Fail startup when OIDC, report authorization, or Redis configuration is unavailable.
- Restrict CORS to the deployed frontend origin.
Handle errors
Return predictable responses and leave the report unchanged after a failure:
| Status | Meaning | Response |
|---|---|---|
| 200 | Valid action | { "action": PivotAction, "mode": "live" \| "mock" } |
| 400 | Invalid client payload | { "error": "The assistant request is invalid." } |
| 401 | Missing or invalid bearer token | { "error": "Authentication is required." } |
| 403 | Authenticated user cannot access the report | { "error": "Access to this report is denied." } |
| 422 | Unsupported or invalid action | { "error": "Try one of the supported Pivot actions." } |
| 429 | Application request limit reached | { "error": "Too many requests. Try again shortly." } |
| 429 | OpenAI billing quota exhausted | { "error": "The OpenAI API project has no available quota. Check API billing." } |
| 502 | Invalid provider output | { "error": "The AI service returned an invalid action." } |
| 503 | Provider, Redis, or JWKS unavailable | { "error": "The assistant service is temporarily unavailable." } |
Do not classify provider failures by HTTP status alone. Inspect the provider error code or type to distinguish temporary request-rate limits from billing-quota exhaustion. Retry temporary rate limits with bounded exponential retry delays; quota errors require available API credits or a project budget. Map provider details to safe application messages rather than returning raw provider errors to the browser.
For provider behavior, refer to the OpenAI API error codes, rate limits, and API billing setup guidance.
Abort and timeout errors should report cancellation or timeout without applying an action. Invalid JSON, schema failures, Redis connection failures, and JWKS retrieval failures must also leave the report unchanged.
Run the sample
From the cloned sample root, or from the parent folder that contains both server and assistive-pivot, start the backend:
cd server
cp .env.example .env
npm install
npm startOn Windows PowerShell, use Copy-Item .env.example .env.
Start the frontend in another terminal from the same parent folder:
cd assistive-pivot
cp .env.example .env
npm install
npm run devOn Windows PowerShell, use Copy-Item .env.example .env.
Open http://localhost:5173. The sample works in mock mode without an OpenAI key. Add the key only to server/.env, stop and restart the server, and verify that http://localhost:3000/health reports live.
The live health value confirms that a key is configured; it does not confirm available billing quota, model access, or a successful provider request. Execute one supported prompt to verify the complete live path.
Verify the integration
Verify the following behavior:
- The AI Assistant toolbar item opens the modal.
- Built-in AI AssistView suggestions execute through
promptRequest. - Filter, sort, aggregation, field movement, expand/collapse, view, chart, totals, and format actions preserve unrelated settings.
- Stop responding cancels the active request.
- Undo restores the previous compact report state and reset restores the initial report.
- Unsupported prompts and invalid members do not change the Pivot Table.
- Mock and live modes are visibly identified.
- The dialog remains keyboard accessible and usable at 320 CSS pixels.
- The Pivot Table fills its measured parent height.
Run the automated checks:
npm run lint
npm test
npm run build
cd ../server
npm testThe repository package manifests define lint, test, and build. When creating the project manually, add this entry to the client scripts:
"test": "vitest run"Add this entry to the server scripts:
"test": "node --test --test-isolation=none"Security considerations
- Keep provider credentials on the server.
- Never put
OPENAI_API_KEYin aVITE_*variable. - Authenticate and authorize every production request.
- Validate at both trust boundaries.
- Never accept arbitrary component properties, URLs, templates, callbacks, or data-source changes.
- Do not send raw records, hidden fields, credentials, or personal information to the model.
- Apply prompt, payload, output, execution-time, and per-user request limits.
- Log operational metadata without recording secrets or sensitive prompt contents.
Troubleshooting
Local sample and UI:
| Problem | Cause | Resolution |
|---|---|---|
| AI Assistant button does not open | Custom toolbar click is not bound | Add a direct click callback to the custom toolbar item |
| Suggestions do not respond | Origin is not allowed or promptRequest is missing |
Allow both configured local origins and bind promptRequest
|
| Toolbar icons appear repeatedly |
toolbarSettings is recreated on every render |
Memoize the AI AssistView toolbar configuration |
| Suggestions occupy half the dialog | Both suggestion wrapper and list use a grid | Keep the wrapper block-level and apply the grid only to its <ul>
|
| Pivot Table does not fill its parent | The parent has no definite height | Set an explicit parent height and pass a measured component height |
| Filtering has no effect | Field or member is unavailable | Validate against trusted metadata |
| Average does not apply |
Average was returned instead of Avg
|
Normalize to the Pivot aggregation identifier |
| A license message is shown | No Syncfusion license key is registered | Add VITE_SYNCFUSION_LICENSE_KEY to assistive-pivot/.env and restart the frontend |
Deployment and API:
| Problem | Cause | Resolution |
|---|---|---|
| API reports mock mode |
OPENAI_API_KEY is absent |
Configure the key in server/.env and restart |
| API returns quota error | The API project has no credits or budget | Configure OpenAI API billing; ChatGPT subscriptions do not provide API quota |
| Browser blocks the API | CORS origin differs between localhost and 127.0.0.1
|
Add the exact frontend origin to CORS_ORIGIN
|
| A previous request applies late | An older request was not canceled | Abort or version in-flight requests |
| Live mode fails after health succeeds | Key exists but lacks quota or model access | Check API billing, project budget, and model permissions |
| Production API will not start | OIDC, report authorization, or Redis configuration is missing or unavailable | Verify issuer, audience, JWKS URL, report authorization, Redis URL, and service connectivity |
| Request times out | Provider or network response exceeded the application timeout | Abort the request, keep the report unchanged, and allow a bounded retry |
GitHub reference
To see the complete implementation, check the sample project on GitHub.
This guide explains the integration and shows focused excerpts. The repository contains the complete runnable client, server, environment templates, schemas, reducers, tests, and CI configuration.
See Also
- React Pivot Table getting started
- React AI AssistView getting started
- AI AssistView toolbar items
- AI AssistView templates
- Filtering in the React Pivot Table
- Sorting in the React Pivot Table
- Aggregation in the React Pivot Table
- Pivot Chart
- Number formatting
- Show or hide totals
- OpenAI API error codes
- OpenAI API rate limits
- OpenAI API billing setup