-
Notifications
You must be signed in to change notification settings - Fork 672
AI Assistant: DataGrid - write JS frameworks demos (Angular) #33665
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 26_1
Are you sure you want to change the base?
Changes from all commits
24df2a9
96237cc
8431509
fc60a49
ef9e1ae
59c5fc1
4e8a02e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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')) { | ||
| 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); | ||
| }); | ||
|
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.')), | ||
|
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'); | ||
| } | ||
|
Raushen marked this conversation as resolved.
Raushen marked this conversation as resolved.
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> |
| 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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ItemClickEvent is generic, DxButtonGroupTypes.ItemClickEvent should work without necessity of assserion
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'; | ||
|
|
||
|
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, | ||
| }]); | ||
|
Raushen marked this conversation as resolved.
|
||
| } | ||
|
Raushen marked this conversation as resolved.
|
||
| } | ||
|
|
||
| bootstrapApplication(AppComponent, { | ||
| providers: [ | ||
| provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }), | ||
| ], | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.