Backend API
@model DevExtreme.NETCore.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";
import rehypeMinifyWhitespace from "https://esm.sh/rehype-minify-whitespace@6?bundle";
window.AzureOpenAI = AzureOpenAI;
window.unified = unified;
window.remarkParse = remarkParse;
window.remarkRehype = remarkRehype;
window.rehypeMinifyWhitespace = rehypeMinifyWhitespace;
window.rehypeStringify = rehypeStringify;
</script>
}
@(Html.DevExtreme().Chat()
.ID("dx-ai-chat")
.Height(520)
.User(user => user.Id(Model.CurrentUser.Id))
.DataSource(new JS("dataSource"))
.ReloadOnChange(false)
.ShowAvatar(false)
.ShowDayHeaders(false)
.SpeechToTextEnabled(true)
.Option("suggestions", new JS("suggestions"))
.OnMessageEntered("onMessageEntered")
.OnInitialized("chatInitialized")
.MessageTemplate(@<text>
<div class="chat-messagebubble-text">
<%= convertToHtml(message.text) %>
</div>
</text>)
)
<div class="options">
<div class="caption">Suggestion Options</div>
<div class="suggestions-options">
<div class="option">
@(Html.DevExtreme().Switch()
.ID("send-immediately")
.Value(false)
.OnValueChanged("sendImmediatelyValueChanged")
)
<span>Send Immediately</span>
</div>
<div class="option">
@(Html.DevExtreme().Switch()
.ID("hide-after-use")
.Value(false)
.OnValueChanged("hideAfterUseValueChanged")
)
<span>Hide After Use</span>
</div>
</div>
</div>
<script>
var SYSTEM_PROMPT = `You are a logistics support assistant for an online marketplace.
The user is logged into their account.
If asked about orders, generate realistic and consistent mock data.
Use plausible order IDs, dates within the last 30 days, and the following order status values: Processing, Shipped, In Transit, Out for Delivery, Delivered.
Never add details outside of given parameters. Do NOT create links.
Keep responses structured and professional.
When appropriate, use bullet points.
The user has exactly 3 recent orders:
- #A48291 (In Transit)
- #A47903 (Delivered Feb 8)
- #A47188 (Processing)
Favorite items (example, do NOT use real brand names):
VoltEdge 65W Fast Wall Charger (Black)
✅ Back in stock — Ships in 1-2 business days
Nimbus Pro Wireless Mouse (Graphite)
❌ Still out of stock — Restock expected Feb 26
PaperLite E-Reader (16 GB, Midnight)
✅ Back in stock — Estimated delivery Feb 20-21
LumaArc Minimal Desk Lamp (Matte Black)
⚠️ Limited stock — Only 3 left
WaveTune Over-Ear Wireless Headphones (Ocean Blue)
❌ Out of stock — No restock date available
AquaCarry Stainless Steel Bottle (20 oz, Sage)
✅ In stock — Available for same-day pick-up
Marketplace Return Policy (Mock)
1. Return Window
a. Items can be returned within 30 days of delivery.
b. Returns cannot be started before an item is delivered.
2. Condition Requirements
a. Items must be unused and in original packaging.
b. Opened electronics are eligible if returned within 14 days.
c. Digital products are non-refundable.
3. Refund Method
a. Refunds are issued to the original payment method.
b. Processing time: 3-5 business days after inspection.
4. Shipping Fees
a. Returns due to defective or incorrect items: free.
b. Returns for change of mind: $4.99 return fee deducted.
5. Exchanges
a. Exchanges are available for size or color variants only.
b. If replacement is unavailable, refund is issued instead.`;
var instance;
var store = [];
var messages = [
{ role: "system", content: SYSTEM_PROMPT },
];
var isDisabled = false;
var sendImmediately = false;
var hideAfterUse = false;
DevExpress.localization.loadMessages({
en: {
"dxChat-emptyListMessage": "Chat is Empty",
"dxChat-emptyListPrompt": "Your Shopping AI Assistant is ready to help. Ask a question or choose one of the suggested prompts to get started.",
"dxChat-textareaPlaceholder": "Ask AI Assistant...",
},
});
var user = { id: "@Model.CurrentUser.Id" };
var assistant = { id: "assistant", name: "AI Assistant" };
var deployment = "demo-mini";
var apiVersion = "2024-02-01";
var endpoint = "https://public-api.devexpress.com/demo-openai";
var apiKey = "DEMO";
var CHAT_DISABLED_CLASS = "chat-disabled";
var ALERT_TIMEOUT = 1000 * 60;
var suggestionItems = [
{ text: "📦 Track my orders", prompt: "Track my orders" },
{ text: "⭐ Check in-stock favorites", prompt: "Check in-stock favorites" },
{ text: "🔄 Start a return", prompt: "Start a return" },
];
var suggestions = {
items: suggestionItems,
onItemClick(e) {
const { prompt, text } = e.itemData;
if (hideAfterUse) {
const currentSuggestions = instance.option("suggestions");
instance.option("suggestions", { items: currentSuggestions.items.filter((item) => item.text !== text) });
}
if (sendImmediately) {
const message = { id: Date.now(), timestamp: new Date(), author: user, text: prompt };
dataSource.store().push([{ type: "insert", data: message }]);
if (!instance.option("alerts").length) {
processMessageSending(message);
}
} else {
instance.option("inputFieldText", prompt);
}
},
};
var chatService;
function chatInitialized({ component }) {
instance = component;
}
$(() => {
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;
}
function alertLimitReached() {
instance.option({
alerts: [{ message: "Request limit reached, try again in a minute." }],
});
setTimeout(() => {
instance.option({ alerts: [] });
}, ALERT_TIMEOUT);
}
function toggleDisabledState(disabled, event) {
isDisabled = disabled;
instance.option({ suggestions: { disabled } });
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 renderAssistantMessage(text) {
const message = {
id: Date.now(),
timestamp: new Date(),
author: assistant,
text,
};
dataSource.store().push([{ type: "insert", data: message }]);
}
function convertToHtml(value) {
const result = unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeMinifyWhitespace)
.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 onMessageEntered({ message, event }) {
if (isDisabled) return;
dataSource.store().push([{ type: "insert", data: { id: Date.now(), ...message } }]);
if (!instance.option("alerts").length) {
processMessageSending(message, event);
}
}
function sendImmediatelyValueChanged(e) {
sendImmediately = e.value;
}
function hideAfterUseValueChanged(e) {
hideAfterUse = e.value;
}
</script>
using System;
using System.Linq;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using DevExtreme.NETCore.Demos.Models.Chat;
using DevExtreme.NETCore.Demos.Models.SampleData;
using DevExtreme.NETCore.Demos.ViewModels;
namespace DevExtreme.NETCore.Demos.Controllers {
public class ChatController : Controller {
public ActionResult PromptSuggestions() {
return View(new ChatViewModel {
CurrentUser = SampleData.CurrentUser,
});
}
}
}
.demo-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.options {
padding: 20px;
display: flex;
flex-direction: column;
min-width: 280px;
background-color: rgba(191, 191, 191, 0.15);
gap: 16px;
width: 100%;
max-width: 900px;
box-sizing: border-box;
}
.suggestions-options {
display: flex;
align-items: center;
gap: 24px;
}
.option {
display: flex;
align-items: center;
gap: 8px;
}
.caption {
font-size: var(--dx-font-size-sm);
font-weight: 500;
}
.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-messagelist-empty-prompt {
max-width: 462px;
line-height: 20px;
}
.dx-chat-messagebubble-content,
.chat-messagebubble-text {
display: flex;
flex-direction: column;
}
.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 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;
}
.chat-disabled .dx-chat-messagebox {
opacity: 0.5;
pointer-events: none;
}
.dx-chat-suggestions .dx-button {
max-width: 255px;
}
This demo allows you to configure suggestions as follows:
- Specify whether the Chat displays a predefined prompt in the input field or immediately sends a message with that prompt.
- Enable single-use suggestions.