@model DevExtreme.MVC.Demos.ViewModels.ChatViewModel
@section ExternalDependencies {
<script type="module">
import { AzureOpenAI } from "https://esm.sh/openai@4.73.1";
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)
.SpeechToTextEnabled(true)
.OnMessageEntered("onMessageEntered")
.OnInitialized("aiChatInitialized")
.MessageTemplate(@<text>
<% if (message.text === "Regeneration...") {%><div class="dx-chat-messagebubble-text">Regeneration...</div><%} else { %>
<div class="dx-chat-messagebubble-text">
<%= convertToHtml(message.text) %>
</div>
<div class="dx-bubble-button-container">
@(Html.DevExtreme().Button()
.Icon("copy")
.StylingMode(ButtonStylingMode.Text)
.Hint("Copy")
.OnClick("({ component }) => { onCopyButtonClick(component, message.text); }")
)
@(Html.DevExtreme().Button()
.Icon("refresh")
.StylingMode(ButtonStylingMode.Text)
.Hint("Regenerate")
.OnClick("onRegenerateButtonClick")
)
</div>
<% } %>
</text>)
)
<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 = "demo-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_completion_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();
}
</script>
using System;
using System.Linq;
using System.Collections.Generic;
using System.Web.Mvc;
using DevExtreme.MVC.Demos.Models.Chat;
using DevExtreme.MVC.Demos.Models.SampleData;
using DevExtreme.MVC.Demos.ViewModels;
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;
}