JavaScript/jQuery Chat - Integrate with AI Service

You can integrate DevExtreme Chat with various AI services:

OpenAI

This help topic describes how to integrate Chat with OpenAI. You can find the full example code in the following GitHub repository:

View on GitHub

Prerequisites and Installation

First, obtain an API key. Next, install OpenAI SDK:

jQuery
index.html
<head>
    <!-- ... -->
    <script type="module">
        import OpenAI from "https://esm.sh/openai@4.73.1";
    </script>
</hea>
NOTE
Ensure DevExtreme is installed in your project.
Angular
Code
npm install openai 
NOTE
Ensure DevExtreme is installed in your project.
Vue
Code
npm install openai
NOTE
Ensure DevExtreme is installed in your project.
React
Code
npm install openai
NOTE
Ensure DevExtreme is installed in your project.

AI Configuration

Create an instance of OpenAI:

jQuery
Code
const chatService = new OpenAI({ 
    dangerouslyAllowBrowser: true, 
    apiKey: "OPENAI_API_KEY", // insert your OpenAI API key
}); 
Angular
TypeScript
import { OpenAI } from "openai";

export class AppService {
    chatService: OpenAI;
    OpenAIConfig = {
        dangerouslyAllowBrowser: true,
        apiKey: "OPENAI_API_KEY", // insert your OpenAI API key
    };
    constructor() {
        this.chatService = new OpenAI(this.OpenAIConfig);
    }
}
Vue
TypeScript
import { OpenAI } from 'openai';

const OpenAIConfig = {
    dangerouslyAllowBrowser: true,
    apiKey: 'OPEN_AI_KEY', // insert your OpenAI API key
};

const chatService = new OpenAI(OpenAIConfig);
React
TypeScript
import { OpenAI } from "openai";

class AppService {
    chatService: OpenAI;
    OpenAIConfig = {
        dangerouslyAllowBrowser: true,
        apiKey: "OPENAI_API_KEY", // insert your OpenAI API key
    };
    constructor() {
        this.chatService = new OpenAI(this.OpenAIConfig);
    }
}
IMPORTANT
dangerouslyAllowBrowser: true enables browser-side requests. This exposes the API key. For production, route requests through your backend.

Implement a getAIResponse(messages) function to call the OpenAI API for responses. Here, the incoming argument messages is an array of { role: "user" | "assistant" | "system"; content: string } objects.

jQuery
Code
async function getAIResponse(messages) {
    const params = {
        messages,
        model: 'gpt-4o-mini',
    };
    const response = await chatService.chat.completions.create(params);
    return response.choices[0].message?.content;
}
Angular
TypeScript
async getAIResponse(messages: Array<{ role: "user" | "assistant" | "system"; content: string }>) {
    const params = {
    messages: messages.map(msg => ({
        role: msg.role,
        content: msg.content
    })),
    model: 'gpt-4o-mini',
    };
    const response = await this.chatService.chat.completions.create(params);
    const data = { choices: response.choices };
    return data.choices[0].message?.content;
}
Vue
TypeScript
const getAIResponse = async(messages: Array<{ role: 'user' | 'assistant' | 'system'; content: string }>) => {
    const params = {
    messages: messages.map(msg => ({
        role: msg.role,
        content: msg.content
    })),
    model: 'gpt-4o-mini'
    };

    const response = await chatService.chat.completions.create(params);
    return response.choices[0].message?.content;
};
React
TypeScript
async getAIResponse(messages: { role: 'user' | 'assistant' | 'system'; content: string }[]): Promise<any> {
    const params = {
    messages: messages.map((msg) => ({
        role: msg.role,
        content: msg.content,
    })),
    model: 'gpt-4o-mini',
    };

    const response = await this.chatService.chat.completions.create(params);
    const data = { choices: response.choices };
    return data.choices[0].message?.content;
}

Chat Configuration

To synchronize Chat and OpenAI, declare the processMessageSending() function. This function configures typingUsers, pushes the assistant message to the store, and renders the message.

jQuery
index.js
async function processMessageSending() {
    toggleDisabledState(true);
    instance.option({ typingUsers: [assistant] });
    try {
        const aiResponse = await getAIResponse(messages);
        setTimeout(() => {
            instance.option({ typingUsers: [] });
            messages.push({ role: 'assistant', content: aiResponse });
            renderMessage(aiResponse);
        }, 200);
    } catch {
        instance.option({ typingUsers: [] });
        alertLimitReached();
    } finally {
        toggleDisabledState(false);
    }
}
Angular
TypeScript
async processMessageSending() {
    this.toggleDisabledState(true);
    this.typingUsersSubject.next([this.assistant]);
    try {
        const aiResponse = await this.getAIResponse(this.messages);
        setTimeout(() => {
            this.typingUsersSubject.next([]);
            this.messages.push({ role: "assistant", content: aiResponse ?? "" });
            this.renderAssistantMessage(aiResponse ?? "");
        }, 200);
    } catch {
        this.typingUsersSubject.next([]);
        this.alertLimitReached();
    } finally {
        this.toggleDisabledState(false);
    }
}
Vue
TypeScript
const processMessageSending = async() => {
    toggleDisabledState(true);
    typingUsers.value = [assistant];
    try {
        const aiResponse = await getAIResponse(messages.value);
        setTimeout(() => {
            typingUsers.value = [];
            messages.value.push({ role: 'assistant', content: aiResponse ?? '' });
            renderAssistantMessage(aiResponse ?? '');
        }, 200);
    } catch {
        typingUsers.value = [];
        alertLimitReached();
    } finally {
        toggleDisabledState(false);
    }
};
React
TypeScript
 async processMessageSending(): Promise<void> {
    this.toggleDisabledState(true);
    this.typingUsersSubject.next([this.assistant]);
    try {
        const aiResponse = await this.getAIResponse(this.messages);
        setTimeout(() => {
            this.typingUsersSubject.next([]);
            this.messages.push({ role: 'assistant', content: aiResponse ?? '' });
            this.renderAssistantMessage(aiResponse ?? '');
        }, 200);
    } catch {
        this.typingUsersSubject.next([]);
        this.alertLimitReached();
    } finally {
        this.toggleDisabledState(false);
    }
}

Call processMessageSending() after a user message is sent in the onMessageEntered event handler:

jQuery
index.js
const instance = $('#dx-ai-chat').dxChat({
    // ...
    dataSource: customStore,
    reloadOnChange: false,
    onMessageEntered: (e) => {
        const { message } = e;
        customStore.push([{ type: 'insert', data: { id: Date.now(), ...message } }]);
        messages.push({ role: 'user', content: message.text });
        processMessageSending();
    }
}).dxChat('instance');
Angular
TypeScript
 async onMessageEntered({ message, event }: MessageEnteredEvent) {
    this.dataSource
    ?.store()
    .push([{ type: "insert", data: { id: Date.now(), ...message } }]);

    this.messages.push({ role: "user", content: message?.text ?? "" });
    this.processMessageSending();
}
Vue
TypeScript
const onMessageEntered = async(e: MessageEnteredEvent) => {
    let { message } = e;
    dataSource.value?.store().push([{
        type: 'insert',
        data: { id: Date.now(), ...message }
    }]);

    messages.value.push({ role: 'user', content: message?.text ?? '' });
    await processMessageSending();
};
React
TypeScript
onMessageEntered({ message }: MessageEnteredEvent): void {
    this.dataSource
    ?.store()
    .push([{ type: 'insert', data: { id: Date.now(), ...message } }]);

    this.messages.push({ role: 'user', content: message?.text ?? '' });
    void this.processMessageSending();
}

Additional Configuration

You can add UI features to improve user experience:

  • Add a Markdown converter for assistant output. For more information, refer to the Markdown Support help topic.
  • Define a messageTemplate for assistant responses and add the Copy and Regenerate Response buttons. See the example code in the GitHub repository:

View on GitHub

Azure OpenAI

Azure OpenAI allows you to run OpenAI models in Microsoft Azure cloud services.

Client Configuration

Configure a Chat to send user messages to your backend endpoint and render responses.

IMPORTANT
Azure OpenAI credentials must remain on the server. Do not call Azure OpenAI directly from client code.

Use onMessageEntered to send a message to your API and push the response to the data store:

TypeScript
async function onMessageEntered(e) {
    const userMessage = e.message;
    dataSource.store().push([{ type: "insert", data: userMessage }]);

    const response = await fetch("http://localhost:5005/api/Chat/GetAIResponse", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(userMessage)
    });

    const assistantMessage = await response.json();
    dataSource.store().push([{ type: "insert", data: assistantMessage }]);
}

Review this demo for Chat UI integration. The demo uses a DevExpress-hosted API:

View Demo

ASP.NET Web API Backend

This section describes the server layer for Azure OpenAI integration.

The ASP.NET Web API backend performs the following actions:

  • Stores Azure credentials in server configuration.
  • Sends chat completion requests to Azure OpenAI.
  • Exposes chat endpoints for the DevExtreme Chat client.

Core endpoints:

  • POST /api/Chat/GetAIResponse
  • GET /api/Chat/GetUserMessages

The following implementations are available:

IChatClient

This implementation uses Microsoft.Extensions.AI and a single IChatClient instance.

View on GitHub

Prerequisites:

  • .NET 9 SDK
  • Azure OpenAI endpoint, API key, and deployment name

Register validated options and IChatClient in Program.cs:

Program.cs
builder.Services
    .AddOptions<AzureOpenAIOptions>()
    .Bind(builder.Configuration.GetSection(AzureOpenAIOptions.SectionName))
    .Validate(o => !string.IsNullOrWhiteSpace(o.Endpoint), "Endpoint missing")
    .Validate(o => !string.IsNullOrWhiteSpace(o.ApiKey), "ApiKey missing")
    .Validate(o => !string.IsNullOrWhiteSpace(o.ModelName), "ModelName missing")
    .ValidateOnStart();

builder.Services.AddSingleton<IChatClient>(sp => {
    var opts = sp.GetRequiredService<IOptions<AzureOpenAIOptions>>().Value;
    var client = new AzureOpenAIClient(
        new Uri(opts.Endpoint),
        new System.ClientModel.ApiKeyCredential(opts.ApiKey));
    return client.GetChatClient(opts.ModelName).AsIChatClient();
});

Call IChatClient in the controller:

ChatController.cs
[HttpPost("GetAIResponse")]
public async Task<ClientChatMessage> GetAIResponse([FromBody] ClientChatMessage userMessage) {
    var chatMessages = BuildChatHistory(userMessage);
    var completion = await _chatClient.CompleteAsync(chatMessages);
    return ToAssistantMessage(completion);
}

Run the server with dotnet run. Default URLs are http://localhost:5005 and https://localhost:5006.

IChatClient with Microsoft Agent Framework

This topic uses Microsoft Agent Framework with an Azure OpenAI-backed IChatClient.

View on GitHub

Compared to the base IChatClient setup, this implementation adds:

  • Multiple specialized agents (VisionAgent, SupportAgent, Editor).
  • A sequential workflow (VirtualAssistant) that orchestrates agent execution.
  • MCP-based documentation tools for the support agent.
  • Multipart request handling for text and file attachments.

The application uses one shared IChatClient instance, then composes agents and workflow on top of it:

Program.cs
using Azure.AI.OpenAI;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Options;
using System.ClientModel;

builder.Services.AddSingleton<IChatClient>(sp => {
    var opts = sp.GetRequiredService<IOptions<AzureOpenAIOptions>>().Value;
    var client = new AzureOpenAIClient(
        new Uri(opts.Endpoint),
        new ApiKeyCredential(opts.ApiKey));
    return client.GetChatClient(opts.ModelName).AsIChatClient();
});

builder.AddAIAgent("VisionAgent", (sp, key) => {
    var chatClient = sp.GetRequiredService<IChatClient>();
    return new ChatClientAgent(chatClient, name: key, instructions: "Vision analysis instructions");
});

builder.AddAIAgent("SupportAgent", (sp, key) => {
    var chatClient = sp.GetRequiredService<IChatClient>();
    return new ChatClientAgent(chatClient, name: key, instructions: "Support instructions", tools: [.. mcpTools.Cast<AITool>()]);
});

builder.AddAIAgent("Editor", (sp, key) => {
    var chatClient = sp.GetRequiredService<IChatClient>();
    return new ChatClientAgent(chatClient, name: key, instructions: "Editing instructions");
});

builder.AddWorkflow("VirtualAssistant", (sp, key) => {
    var visionAgent = sp.GetRequiredKeyedService<AIAgent>("VisionAgent");
    var supportAgent = sp.GetRequiredKeyedService<AIAgent>("SupportAgent");
    var editorAgent = sp.GetRequiredKeyedService<AIAgent>("Editor");

    return AgentWorkflowBuilder.BuildSequential(
        workflowName: key,
        agents: [visionAgent, supportAgent, editorAgent]
    );
}).AddAsAIAgent();

The support agent can call DevExpress documentation tools through MCP:

Program.cs
using ModelContextProtocol.Client;

await using var mcpClient = await McpClient.CreateAsync(
    new HttpClientTransport(new HttpClientTransportOptions
    {
        Endpoint = new Uri("https://api.devexpress.com/mcp/docs")
    })
);
var mcpTools = await mcpClient.ListToolsAsync().ConfigureAwait(false);

Assign mcpTools to the support agent in the tools parameter.

This server accepts multipart/form-data for POST /api/Chat/GetAIResponse:

  • text: user message text
  • id: client message identifier
  • timestamp: message timestamp in ISO 8601 format
  • attachments: optional attachment metadata
  • files: optional files for image analysis

Use this format when your Chat UI sends files with user messages.

For baseline IChatClient registration and Azure options, see IChatClient.

Google Dialogflow

This section describes Google Dialogflow integration into DevExtreme Chat. The Dialogflow platform includes two virtual agent services:

  • CX - suitable for large and complex agents.
  • ES - suitable for small and simple agents.

This tutorial uses Dialogflow ES. You can find the full example code in the following GitHub repository:

View on GitHub

NOTE
Read the official Google Dialogflow documentation to learn how to set up and configure your agent.

Prerequisites

Create a Dialogflow Agent and configure it as your needs dictate.

Then, obtain a Google Cloud key:

  1. Navigate to Google Cloud Console → IAM & Admin → Service Accounts.
  2. Create a new or use an existing service account.
  3. Create a key (JSON) for this account.
  4. Save the resulting JSON file as key.json.

In the Google Cloud Console, find your Project ID.

Server Configuration

Dialogflow requires a server layer. You cannot use this library on the client only because Google’s CORS policy blocks required requests.

To create a server layer, perform the following steps:

  1. Create a server folder. It should include your key.json, server.js, and dialogflow.js files (see the configuration in the GitHub example repository).

  2. Create package.json and add the following script:

    package.json
    {
        // ...
        "scripts": {
            "start:server": "node server.js"
        }
    }
  3. Install the following dependencies:

    Code
    npm i @google-cloud/dialogflow
    npm i path browser-sync body-parser express
  4. Use the following command to deploy the server:

    Code
    npm run start:server
NOTE

Ensure that the key path matches the passed parameter in dialogflow.js:

dialogflow.js
const keyFilePath = path.join(__dirname, 'key.json');

AI Configuration

jQuery

Create a jQuery application and install DevExtreme.

Angular

Create an Angular application and install DevExtreme.

Vue

Create a Vue application and install DevExtreme.

React

Create a React application and install DevExtreme.

Implement a getAIResponse(text) function to query the Dialogflow server for responses.

jQuery
Code
async function getAIResponse(text) {
    const response = await fetch('http://localhost:3000/webhook', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({ message: text, sessionId }),
    });
    const data = await response.json();
    return data.response;
}
Angular
TypeScript
async getAIResponse(text: string | undefined): Promise<string> {
    let id = this.sessionId;
    const response = await fetch('http://localhost:3000/webhook', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({ message: text, sessionId: id }),
    });
    const data: { response: string } = await response.json();
    return data.response;
}
Vue
TypeScript
const getAIResponse = async(text: string | undefined) => {
    let id = sessionId;
    const response = await fetch('http://localhost:3000/webhook', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({ message: text, sessionId: id }),
    });
    const data: { response: string } = await response.json();
    return data.response;
};
React
TypeScript
 async getAIResponse(text: string | undefined): Promise<string> {
    let id = sessionId;
    const response = await fetch('http://localhost:3000/webhook', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({ message: text, sessionId: id }),
    });
    const data: { response: string } = await response.json();
    return data.response;
}

Chat Configuration

To process DevExtreme Chat messages with Google Dialogflow, declare the processMessageSending() function. This function configures typingUsers, pushes the assistant message to the store, and renders the message.

jQuery
JavaScript
async function processMessageSending(text, instance, customStore) {
    toggleDisabledState(true);
    instance.option({ typingUsers: [assistant] });
    try {
        const aiResponse = await getAIResponse(text);
        setTimeout(() => {
            instance.option({ typingUsers: [] });
            renderMessage(aiResponse, customStore);
        }, 200);
    } catch (error) {
        instance.option({ typingUsers: [] });
        pushAlert(error, instance);
    } finally {
        toggleDisabledState(false, instance);
    }
}
Angular
TypeScript
async processMessageSending(e: DxChatTypes.MessageEnteredEvent): Promise<void> {
    let { message } = e;
    this.toggleDisabledState(true, e.event);
    this.typingUsersSubject.next([this.assistant]);
    try {
        const aiResponse = await this.getAIResponse(message.text);
        setTimeout(() => {
            this.typingUsersSubject.next([]);
            this.renderAssistantMessage(aiResponse ?? '');
        }, 200);
    } catch (error) {
        this.typingUsersSubject.next([]);
        this.alertLimitReached(error);
    } finally {
        this.toggleDisabledState(false, e.event);
    }
}
Vue
TypeScript
const processMessageSending = async(e: DxChatTypes.MessageEnteredEvent) => {
    let { message } = e;
    toggleDisabledState(true);
    (e.event?.target as HTMLElement).blur();
    typingUsers.value = [assistant];
    try {
        const aiResponse = await getAIResponse(message.text);
        setTimeout(() => {
            typingUsers.value = [];
            renderAssistantMessage(aiResponse ?? '');
        }, 200);
    } catch {
        (e.event?.target as HTMLElement).focus();
        typingUsers.value = [];
        alertLimitReached(error);
    } finally {
        (e.event?.target as HTMLElement).focus();
        toggleDisabledState(false);
    }
};
React
TypeScript
 async processMessageSending(setDisabled: Function, e: ChatTypes.MessageEnteredEvent): Promise<void> {
    let { message } = e;
    setDisabled(true);
    (e.event?.target as HTMLElement).blur();
    this.typingUsersSubject.next([assistant]);
    try {
        const aiResponse = await this.getAIResponse(message.text);
        setTimeout(() => {
            this.typingUsersSubject.next([]);
            this.renderAssistantMessage(aiResponse ?? '');
        }, 200);
    } catch (error) {
        (e.event?.target as HTMLElement).focus();
        this.typingUsersSubject.next([]);
        this.alertLimitReached(error);
    } finally {
        (e.event?.target as HTMLElement).focus();
        setDisabled(false);
    }
}

Call processMessageSending() after a user message is sent in the onMessageEntered event handler:

jQuery
index.js
const instance = $('#dx-ai-chat').dxChat({
    // ...
    dataSource: customStore,
    reloadOnChange: false,
    onMessageEntered: (e) => {
        const { message } = e;
        customStore.push([{ type: 'insert', data: { id: Date.now(), ...message } }]);
        processMessageSending(message.text, instance, customStore);
    },
}).dxChat('instance');
Angular
TypeScript
async onMessageEntered(event: DxChatTypes.MessageEnteredEvent): Promise<void> {
    let { message } = event;
    this.dataSource
        ?.store()
        .push([{ type: 'insert', data: { id: Date.now(), ...message } }]);
    this.messages.push({ role: 'user', content: message?.text ?? '' });
    await this.processMessageSending(event);
}
Vue
TypeScript
const onMessageEntered = async(e: DxChatTypes.MessageEnteredEvent) => {
    let { message } = e;
    dataSource.value?.store().push([{
        type: 'insert',
        data: { id: Date.now(), ...message }
    }]);
    messages.value.push({ role: 'user', content: message?.text ?? '' });
    await processMessageSending(e);
};
React
TypeScript
async onMessageEntered(event: ChatTypes.MessageEnteredEvent, setDisabled: Function): Promise<void> {
    let { message } = event;
    this.dataSource
        ?.store()
        .push([{ type: 'insert', data: { id: Date.now(), ...message } }]);
    this.messages.push({ role: 'user', content: message?.text ?? '' });
    await this.processMessageSending(setDisabled, event);
}

For additional configuration options, refer to the Additional Configuration help topic.