Feel free to share demo-related thoughts here.
If you have technical questions, please create a support ticket in the DevExpress Support Center.
Thank you for the feedback!
If you have technical questions, please create a support ticket in the DevExpress Support Center.
Backend API
@model DevExtreme.MVC.Demos.ViewModels.ChatViewModel
@section ExternalDependencies {
<script type="module">
import { AzureOpenAI } from "https://esm.sh/openai";
import { unified } from "https://esm.sh/unified@11?bundle";
import remarkParse from "https://esm.sh/remark-parse@11?bundle";
import remarkRehype from "https://esm.sh/remark-rehype@11?bundle";
import rehypeStringify from "https://esm.sh/rehype-stringify@10?bundle";
window.AzureOpenAI = AzureOpenAI;
window.unified = unified;
window.remarkParse = remarkParse;
window.remarkRehype = remarkRehype;
window.rehypeStringify = rehypeStringify;
</script>
}
@(Html.DevExtreme().Chat()
.ID("dx-ai-chat")
.Height(710)
.User(user => user
.Id(Model.CurrentUser.Id)
)
.DataSource(new JS("dataSource"))
.ReloadOnChange(false)
.ShowAvatar(false)
.ShowDayHeaders(false)
.OnMessageEntered("onMessageEntered")
.MessageTemplate(new JS("messageTemplate"))
.OnInitialized("aiChatInitialized")
)
<script>
var instance;
var chatService
var store = [];
var messages = [];
function aiChatInitialized({ component }) {
instance = component;
}
var assistant = {
id: "assistant",
name: "Virtual Assistant",
};
var REGENERATION_TEXT = "Regeneration...";
var CHAT_DISABLED_CLASS = "dx-chat-disabled";
var ALERT_TIMEOUT = 1000 * 60;
function alertLimitReached() {
instance.option({
alerts: [{
message: "Request limit reached, try again in a minute.",
}],
});
setTimeout(() => {
instance.option({ alerts: [] });
}, ALERT_TIMEOUT);
}
DevExpress.localization.loadMessages({
en: {
"dxChat-emptyListMessage": "Chat is Empty",
"dxChat-emptyListPrompt": "AI Assistant is ready to answer your questions.",
"dxChat-textareaPlaceholder": "Ask AI Assistant...",
},
});
const deployment = "gpt-4o-mini";
const apiVersion = "2024-02-01";
const endpoint = "https://public-api.devexpress.com/demo-openai";
const apiKey = "DEMO";
$(() => {
chatService = new AzureOpenAI({
dangerouslyAllowBrowser: true,
deployment,
endpoint,
apiVersion,
apiKey,
});
});
async function getAIResponse(messages) {
const params = {
messages,
model: deployment,
max_tokens: 1000,
temperature: 0.7,
};
const response = await chatService.chat.completions.create(params);
const data = { choices: response.choices };
return data.choices[0].message?.content;
}
async function regenerate() {
toggleDisabledState(true);
try {
const aiResponse = await getAIResponse(messages.slice(0, -1));
updateLastMessage(aiResponse);
messages.at(-1).content = aiResponse;
} catch {
updateLastMessage(messages.at(-1).content);
alertLimitReached();
} finally {
toggleDisabledState(false);
}
}
function renderAssistantMessage(text) {
const message = {
id: Date.now(),
timestamp: new Date(),
author: assistant,
text,
};
dataSource.store().push([{ type: "insert", data: message }]);
}
function updateLastMessage(text) {
const items = dataSource.items();
const lastMessage = items.at(-1);
const data = {
text: text ?? REGENERATION_TEXT,
};
dataSource.store().push([{
type: "update",
key: lastMessage.id,
data,
}]);
}
function toggleDisabledState(disabled, event) {
instance.element().toggleClass(CHAT_DISABLED_CLASS, disabled);
if (disabled) {
event?.target.blur();
} else {
event?.target.focus();
}
};
async function processMessageSending(message, event) {
toggleDisabledState(true, event);
messages.push({ role: "user", content: message.text });
instance.option({ typingUsers: [assistant] });
try {
const aiResponse = await getAIResponse(messages);
setTimeout(() => {
instance.option({ typingUsers: [] });
messages.push({ role: "assistant", content: aiResponse });
renderAssistantMessage(aiResponse);
}, 200);
} catch {
instance.option({ typingUsers: [] });
messages.pop();
alertLimitReached();
} finally {
toggleDisabledState(false, event);
}
}
function onMessageEntered({ message, event }) {
dataSource.store().push([{ type: "insert", data: { id: Date.now(), ...message } }]);
if (!instance.option("alerts").length) {
processMessageSending(message, event);
}
}
function convertToHtml(value) {
const result = unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeStringify)
.processSync(value)
.toString();
return result;
}
const customStore = new DevExpress.data.CustomStore({
key: "id",
load: () => {
const d = $.Deferred();
setTimeout(() => {
d.resolve([...store]);
});
return d.promise();
},
insert: (message) => {
const d = $.Deferred();
setTimeout(() => {
store.push(message);
d.resolve();
});
return d.promise();
},
});
const dataSource = new DevExpress.data.DataSource({
store: customStore,
paginate: false,
});
function onCopyButtonClick(component, text) {
navigator.clipboard?.writeText(text);
component.option({ icon: "check" });
setTimeout(() => {
component.option({ icon: "copy" });
}, 2500);
}
function onRegenerateButtonClick() {
if (instance.option("alerts").length) {
return;
}
updateLastMessage();
regenerate();
}
function renderMessageContent(message, element) {
$("<div>")
.addClass("dx-chat-messagebubble-text")
.html(convertToHtml(message.text))
.appendTo(element);
const $buttonContainer = $("<div>")
.addClass("dx-bubble-button-container");
$("<div>").dxButton({
icon: "copy",
stylingMode: "text",
hint: "Copy",
onClick: ({ component }) => {
onCopyButtonClick(component, message.text);
},
})
.appendTo($buttonContainer);
$("<div>").dxButton({
icon: "refresh",
stylingMode: "text",
hint: "Regenerate",
onClick: onRegenerateButtonClick,
})
.appendTo($buttonContainer);
$buttonContainer.appendTo(element);
}
function messageTemplate(data, element) {
const { message } = data;
if (message.text === REGENERATION_TEXT) {
element.text(REGENERATION_TEXT);
return;
}
renderMessageContent(message, element)
}
</script>
using DevExtreme.MVC.Demos.Models.SampleData;
using DevExtreme.MVC.Demos.ViewModels;
using System.Web.Mvc;
namespace DevExtreme.MVC.Demos.Controllers {
public class ChatController : Controller {
public ActionResult AIAndChatbotIntegration() {
return View(new ChatViewModel {
CurrentUser = SampleData.CurrentUser,
});
}
}
}
.demo-container {
display: flex;
justify-content: center;
}
.dx-chat {
max-width: 900px;
}
.dx-chat-messagelist-empty-image {
display: none;
}
.dx-chat-messagelist-empty-message {
font-size: var(--dx-font-size-heading-5);
}
.dx-chat-messagebubble-content,
.dx-chat-messagebubble-text {
display: flex;
flex-direction: column;
}
.dx-bubble-button-container {
display: none;
}
.dx-button {
display: inline-block;
color: var(--dx-color-icon);
}
.dx-chat-messagegroup-alignment-start:last-child .dx-chat-messagebubble:last-child .dx-bubble-button-container {
display: flex;
gap: 4px;
margin-top: 8px;
}
.dx-chat-messagebubble-content > div > p:first-child {
margin-top: 0;
}
.dx-chat-messagebubble-content > div > p:last-child {
margin-bottom: 0;
}
.dx-chat-messagebubble-content ol,
.dx-chat-messagebubble-content ul {
white-space: normal;
}
.dx-chat-messagebubble-content h1,
.dx-chat-messagebubble-content h2,
.dx-chat-messagebubble-content h3,
.dx-chat-messagebubble-content h4,
.dx-chat-messagebubble-content h5,
.dx-chat-messagebubble-content h6 {
font-size: revert;
font-weight: revert;
}
.dx-chat-disabled .dx-chat-messagebox {
opacity: 0.5;
pointer-events: none;
}
Custom Message Template
The Chat specifies a messageTemplate that displays "Copy" and "Regenerate" buttons in bot messages.
Response Format Conversion: Markdown to HTML
The AI model outputs responses in Markdown, while the Chat requires HTML output. This example uses the unified plugin library to convert response content. Review convertToHtml
function code for implementation details.
Default Caption Customization
The Chat component in this demo displays modified captions when the conversation is empty. The demo uses localization techniques to alter built-in text.