Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions apps/demos/Demos/DataGrid/AIAssistant/Angular/app/ai/ai.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { AzureOpenAI, OpenAI } from 'openai';
import { Injectable } from '@angular/core';
import notify from 'devextreme/ui/notify';
import {
AIIntegration,
type RequestParams,
type Response,
} from 'devextreme-angular/common/ai-integration';

type AIMessage = (OpenAI.ChatCompletionUserMessageParam | OpenAI.ChatCompletionSystemMessageParam) & {
content: string;
};

const AzureOpenAIConfig = {
dangerouslyAllowBrowser: true,
deployment: 'gpt-4o-mini',
apiVersion: '2024-02-01',
endpoint: 'https://public-api.devexpress.com/demo-openai',
apiKey: 'DEMO',
};

const RATE_LIMIT_RETRY_DELAY_MS = 30000;
const MAX_PROMPT_SIZE = 5000;

const service = new AzureOpenAI(AzureOpenAIConfig);

async function getAIResponse(messages: AIMessage[], signal: AbortSignal, responseSchema?: Record<string, unknown>) {
const params: OpenAI.ChatCompletionCreateParamsNonStreaming = {
messages,
model: AzureOpenAIConfig.deployment,
max_tokens: 1000,
temperature: 0.7,
response_format: {
type: 'json_schema',
json_schema: {
name: 'grid_assistant_response',
strict: false,
schema: responseSchema,
},
},
};

const response = await service.chat.completions.create(params, { signal });
const result = response.choices[0].message?.content;

if (!result) {
throw new Error('AI response returned empty content');
}

return result;
}

function getAIResponseRecursive(
messages: AIMessage[],
signal: AbortSignal,
responseSchema?: Record<string, unknown>,
): Promise<string> {
return getAIResponse(messages, signal, responseSchema)
.catch(async (error) => {
if (!error.message.includes('Connection error')) {
Comment thread
Raushen marked this conversation as resolved.
return Promise.reject(error);
}

notify({
message: 'Our demo AI service reached a temporary request limit. Retrying in 30 seconds.',
width: 'auto',
type: 'error',
displayTime: 5000,
});

await new Promise((resolve) => setTimeout(resolve, RATE_LIMIT_RETRY_DELAY_MS));

return getAIResponseRecursive(messages, signal, responseSchema);
});
Comment thread
Raushen marked this conversation as resolved.
}

const aiIntegration = new AIIntegration({
sendRequest({ prompt, data }: RequestParams): Response {
const isValidRequest = JSON.stringify(prompt.user).length < MAX_PROMPT_SIZE;

if (!isValidRequest) {
return {
promise: Promise.reject(new Error('Request is too long. Specify a shorter prompt.')),
Comment thread
Raushen marked this conversation as resolved.
abort: () => {},
};
}

const controller = new AbortController();
const signal = controller.signal;

if (!prompt.user || !prompt.system) {
throw new Error('Invalid prompt data');
}
Comment thread
Raushen marked this conversation as resolved.
Comment thread
Raushen marked this conversation as resolved.
Comment thread
Raushen marked this conversation as resolved.

const aiPrompt: AIMessage[] = [
{ role: 'system', content: prompt.system },
{ role: 'user', content: prompt.user },
];

const promise = getAIResponseRecursive(aiPrompt, signal, data?.responseSchema);

const result: Response = {
promise,
abort: () => {
controller.abort();
},
};

return result;
},
});

@Injectable()
export class AiService {
getAiIntegration() {
return aiIntegration;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
::ng-deep #gridContainer {
max-height: 800px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<dx-data-grid
id="gridContainer"
keyExpr="Id"
[dataSource]="sales"
[showBorders]="true"
[filterSyncEnabled]="true"
>
<dxo-data-grid-search-panel
[visible]="true"
[width]="240"
placeholder="Search..."
/>
<dxo-data-grid-group-panel [visible]="true" />
<dxo-data-grid-header-filter [visible]="true" />
<dxo-data-grid-filter-row [visible]="true" />
<dxo-data-grid-paging [pageSize]="10" />
<dxo-data-grid-pager
[visible]="true"
[allowedPageSizes]="[10, 25, 50, 100]"
[showPageSizeSelector]="true"
/>

<dxo-data-grid-ai-assistant
[enabled]="true"
[aiIntegration]="aiIntegration"
[chat]="chatConfig"
/>

<dxi-data-grid-column dataField="Product" />
<dxi-data-grid-column
dataField="Amount"
caption="Sale Amount"
dataType="number"
format="currency"
/>
<dxi-data-grid-column dataField="Region" dataType="string" />
<dxi-data-grid-column dataField="Sector" dataType="string" />
<dxi-data-grid-column dataField="SaleDate" dataType="date" />
<dxi-data-grid-column dataField="Customer" dataType="string" />
</dx-data-grid>
107 changes: 107 additions & 0 deletions apps/demos/Demos/DataGrid/AIAssistant/Angular/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { Component, enableProdMode, provideZoneChangeDetection } from '@angular/core';
import { DxDataGridModule } from 'devextreme-angular';
import type { AIIntegration } from 'devextreme-angular/common/ai-integration';
import type { DxChatTypes } from 'devextreme-angular/ui/chat';
import type { DxButtonGroupTypes } from 'devextreme-angular/ui/button-group';
import { Service, type Sale } from './app.service';
import { AiService } from './ai/ai.service';

interface SuggestionItem extends DxButtonGroupTypes.Item {
prompt: string;
}

if (!/localhost/.test(document.location.host)) {
enableProdMode();
}

let modulePrefix = '';
// @ts-ignore
if (window && window.config?.packageConfigPaths) {
modulePrefix = '/app';
}

@Component({
selector: 'demo-app',
templateUrl: `.${modulePrefix}/app.component.html`,
styleUrls: [`.${modulePrefix}/app.component.css`],
providers: [Service, AiService],
imports: [DxDataGridModule],
})
export class AppComponent {
sales: Sale[];

aiIntegration: AIIntegration;

chatConfig: DxChatTypes.Properties;

private chatInstance: DxChatTypes.InitializedEvent['component'] | null = null;

constructor(service: Service, aiService: AiService) {
this.sales = service.getSales();
this.aiIntegration = aiService.getAiIntegration();

this.chatConfig = {
onInitialized: (e: DxChatTypes.InitializedEvent) => {
this.chatInstance = e.component;
},
user: { id: 'user' },
suggestions: {
items: [
{
text: '💡 Help',
prompt: `💡 The DataGrid AI Assistant allows you to control the component using natural language. You can execute commands such as the following:
• Sort records
• Apply a filter
• Search for a specific value
• Group records by a field
• Focus and select rows
• Modify paging settings
• Pin, resize, and reorder columns
• Configure data summaries
• Pick a suggestion or enter a custom request to get started.`,
},
{
text: '🔍 Filter Sector by Health',
prompt: 'Filter Sector by Health',
},
{
text: '↕️ Sort by Region',
prompt: 'Sort by Region',
},
{
text: '🧩 Group by Product',
prompt: 'Group by Product',
width: 170,
},
] as SuggestionItem[],
onItemClick: (e: DxButtonGroupTypes.ItemClickEvent) => {
this.onSuggestionItemClick(e);
},
},
};
}

onSuggestionItemClick(e: DxButtonGroupTypes.ItemClickEvent) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ItemClickEvent is generic, DxButtonGroupTypes.ItemClickEvent should work without necessity of assserion

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's leave it as is as it writes the ItemClickEvent is not generic

const { prompt, text } = e.itemData as SuggestionItem;
const userId = text === '💡 Help' ? 'help' : 'user';

Comment thread
Raushen marked this conversation as resolved.
const message = {
id: Date.now(),
timestamp: new Date(),
author: { id: userId },
text: prompt,
};

this.chatInstance.getDataSource().store().push([{
type: 'insert',
data: message,
}]);
Comment thread
Raushen marked this conversation as resolved.
}
Comment thread
Raushen marked this conversation as resolved.
}

bootstrapApplication(AppComponent, {
providers: [
provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }),
],
});
Loading
Loading