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
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { AzureOpenAI, type OpenAI } from 'openai';
import { Injectable } from '@angular/core';
import notify from 'devextreme/ui/notify';
import { AIIntegration } from 'devextreme-angular/common/ai-integration';
import type { RequestParams, AIResponse } from 'devextreme-angular/common/ai-integration';
import { AI_SERVICE_CONFIG, ChatCommandError } from '../data';

async function getAIResponse(
aiService: AzureOpenAI,
messages: OpenAI.ChatCompletionMessageParam[],
signal: AbortSignal,
): Promise<AIResponse> {
const params = {
messages,
model: AI_SERVICE_CONFIG.deployment,
max_completion_tokens: 1000,
temperature: 0,
};

const response = await aiService.chat.completions.create(params, { signal });

return response.choices[0].message?.content ?? '';
}

function getAIResponseRecursive(
aiService: AzureOpenAI,
messages: OpenAI.ChatCompletionMessageParam[],
signal: AbortSignal,
): Promise<AIResponse> {
return getAIResponse(aiService, messages, signal).catch(async (error: Error) => {
if (!error.message.includes('Connection error')) {
throw 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, 30000); });

return getAIResponseRecursive(aiService, messages, signal);
});
}

const aiService = new AzureOpenAI({
dangerouslyAllowBrowser: true,
deployment: AI_SERVICE_CONFIG.deployment,
endpoint: AI_SERVICE_CONFIG.endpoint,
apiVersion: AI_SERVICE_CONFIG.apiVersion,
apiKey: AI_SERVICE_CONFIG.apiKey,
});

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

if (!isValidRequest) {
return {
promise: Promise.reject(
new ChatCommandError('❌ This message is too long for me to process. Please shorten it and try again.'),
),
abort: () => {},
};
}

const controller = new AbortController();
const { signal } = controller;

const isSmartPasteRequest = Array.isArray((data as { fields?: unknown[] } | undefined)?.fields);
const system = isSmartPasteRequest
? `${prompt.system ?? ''} IMPORTANT: reply on a SINGLE line with no line breaks of any kind - use ';;;' as the only separator between fields.`
: prompt.system ?? '';

const aiPrompt: OpenAI.ChatCompletionMessageParam[] = [
{ role: 'system', content: system },
{ role: 'user', content: prompt.user ?? '' },
];
const promise = getAIResponseRecursive(aiService, aiPrompt, signal);

return {
promise,
abort: () => {
controller.abort();
},
};
},
});

@Injectable()
export class AiService {
getAiIntegration(): AIIntegration {
return aiIntegration;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.demo-container {
margin: 20px;
height: 556px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div class="demo-container">
Comment thread
16adianay marked this conversation as resolved.
<app-employee-form [aiIntegration]="aiIntegration"></app-employee-form>
<app-task-grid></app-task-grid>
<app-ai-assistant
(messageSubmitted)="onMessageSubmitted($event)"
></app-ai-assistant>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { bootstrapApplication } from '@angular/platform-browser';
import {
Component, ViewChild, enableProdMode, provideZoneChangeDetection,
} from '@angular/core';
import config from 'devextreme/core/config';
import { loadMessages } from 'devextreme-angular/common/core/localization';
import type { AIIntegration } from 'devextreme-angular/common/ai-integration';
import type { DxChatTypes } from 'devextreme-angular/ui/chat';
import { AiAssistantComponent } from './components/ai-assistant/ai-assistant.component';
import { EmployeeFormComponent } from './components/employee-form/employee-form.component';
import { TaskGridComponent } from './components/task-grid/task-grid.component';
import { AiService } from './ai/ai.service';
import { routeMessage } from './app.service';

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

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

config({
editorStylingMode: 'filled',
});

config({
floatingActionButtonConfig: {
position: {
my: 'right bottom',
at: 'right bottom',
of: '#grid-container',
offset: '-16 -16',
},
},
});

loadMessages({
en: {
'dxChat-textareaPlaceholder': 'Enter a prompt...',
},
});

@Component({
selector: 'demo-app',
templateUrl: `.${modulePrefix}/app.component.html`,
styleUrls: [`.${modulePrefix}/app.component.css`],
providers: [AiService],
imports: [
AiAssistantComponent,
EmployeeFormComponent,
TaskGridComponent,
],
})
export class AppComponent {
@ViewChild(EmployeeFormComponent) private employeeForm!: EmployeeFormComponent;

@ViewChild(TaskGridComponent) private taskGrid!: TaskGridComponent;

@ViewChild(AiAssistantComponent) private aiAssistant!: AiAssistantComponent;

readonly aiIntegration: AIIntegration;

constructor(aiService: AiService) {
this.aiIntegration = aiService.getAiIntegration();
}

async onMessageSubmitted(message: DxChatTypes.TextMessage): Promise<void> {
this.aiAssistant.setDisabled(true);

try {
await routeMessage(message.text ?? '', {
form: this.employeeForm.formComponent,
gridInstance: this.taskGrid.gridComponent,
aiIntegration: this.aiIntegration,
pushMessage: (message) => this.aiAssistant.pushMessage(message),
});
} finally {
this.aiAssistant.setDisabled(false);
}
}
}

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