Your search did not match any results.

Chat - Message Streaming

This sample uses the DevExtreme Chat component alongside Azure OpenAI to stream AI-generated responses in real time. Since the AI model generates output token by token, our Chat component renders the response incrementally inside a message bubble. A "typing" indicator appears while the response is being streamed, and the send button transforms into a stop button so that users can cancel an in-progress stream at any time.

The empty Chat displays custom suggestion cards. Clicking a card sends the corresponding prompt directly to the AI service without extra user input.

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(710) .User(user => user.Id(Model.CurrentUser.Id)) .DataSource(new JS("dataSource")) .ReloadOnChange(false) .ShowAvatar(false) .ShowDayHeaders(false) .SpeechToTextEnabled(true) .SendButtonOptions(opts => opts.Action(SendButtonAction.Send)) .OnMessageEntered("onMessageEntered") .OnInitialized("chatInitialized") .MessageTemplate(@<text> <div class="chat-messagebubble-text"> <%= convertToHtml(message.text) %> </div> </text>) .EmptyViewTemplate(new JS("emptyViewTemplate")) ) <script> var instance; var store = []; var messages = []; var abortController = null; DevExpress.localization.loadMessages({ en: { "dxChat-emptyListMessage": "Chat is Empty", "dxChat-emptyListPrompt": "AI Assistant is ready to answer your questions.", "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 ALERT_TIMEOUT = 1000 * 60; var suggestionCards = [ { title: "💡 What is DevExtreme?", description: "What is DevExtreme and how can it help me build modern web apps?", prompt: "What is DevExtreme, and which components and frameworks does it support?", }, { title: "🚀 Get Started with DevExtreme", description: "How do I get started with DevExtreme in my project?", prompt: "How can I get started with DevExtreme? Include instructions for library installation, linking required CSS/assets, applying an application theme, and coding a simple working app.", }, { title: "📄 DevExtreme Licensing", description: "What are the licensing options for DevExtreme?", prompt: "Which DevExtreme license do I need for a commercial project? What licensing options are available?", }, ]; var chatService; function chatInitialized({ component }) { instance = component; } $(() => { chatService = new AzureOpenAI({ dangerouslyAllowBrowser: true, deployment, endpoint, apiVersion, apiKey, }); }); function createDelayedRenderer({ delay = 20, onRender }) { let queue = []; let rendering = false; function processQueue() { if (!queue.length) { rendering = false; return; } rendering = true; const chunk = queue.shift(); onRender(chunk); setTimeout(processQueue, delay); } function pushChunk(chunk) { queue.push(chunk); if (!rendering) { processQueue(); } } function stop() { queue = []; rendering = false; } return { pushChunk, stop }; } async function getAIResponseStream(messages, { onAborted, onDelta, onError, signal }) { const params = { messages, model: deployment, max_completion_tokens: 1000, temperature: 0.7, stream: true, }; try { const stream = await chatService.chat.completions.create(params, { signal }); for await (const event of stream) { const delta = event.choices?.[0]?.delta?.content; if (delta) { onDelta(delta); } } if (signal.aborted) { onAborted(); } } catch (e) { onError?.(e); throw e; } } function alertLimitReached() { instance.option({ alerts: [{ message: "Request limit reached, try again in a minute." }], }); setTimeout(() => { instance.option({ alerts: [] }); }, ALERT_TIMEOUT); } function setMainButtonToDefault() { instance.option({ sendButtonOptions: { action: "send", icon: "arrowright", onClick: null, }, }); } function setMainButtonToStop() { instance.option({ sendButtonOptions: { action: "custom", icon: "stopfilled", onClick: stopStreaming, }, }); } function stopStreaming() { if (abortController) { abortController.abort(); setMainButtonToDefault(); } } async function processMessageSending(message) { abortController = new AbortController(); setTimeout(setMainButtonToStop, 0); messages.push({ role: "user", content: message.text }); instance.option({ typingUsers: [assistant] }); let assistantId; let buffer = ""; let typingCleared = false; const delayedRenderer = createDelayedRenderer({ onRender: (chunk) => { if (!typingCleared) { instance.option({ typingUsers: [] }); typingCleared = true; } if (!assistantId) { assistantId = insertAssistantPlaceholder(); } buffer += chunk; updateMessageText(assistantId, buffer); }, }); const onAborted = () => { delayedRenderer.stop(); }; try { await getAIResponseStream(messages, { onAborted, onDelta: delayedRenderer.pushChunk, signal: abortController.signal, }); instance.option({ typingUsers: [] }); messages.push({ role: "assistant", content: buffer }); } catch (e) { instance.option({ typingUsers: [] }); messages.pop(); if (e?.name !== "AbortError" && assistantId) { updateMessageText(assistantId, ""); alertLimitReached(); } } finally { abortController = null; setMainButtonToDefault(); } } function insertAssistantPlaceholder() { const id = Date.now(); dataSource.store().push([{ type: "insert", data: { id, timestamp: new Date(), author: assistant, text: "", }, }]); return id; } function updateMessageText(id, text) { dataSource.store().push([{ type: "update", key: id, data: { text }, }]); } 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 sendSuggestion(prompt) { 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); } } function createSuggestionCard(card) { return $("<button>") .attr({ type: "button", tabindex: 0 }) .addClass("chat-suggestion-card") .append( $("<div>").addClass("chat-suggestion-card-title").text(card.title), $("<div>").addClass("chat-suggestion-card-prompt").text(card.description), ) .on("click", () => { sendSuggestion(card.prompt); }); } function emptyViewTemplate(data, element) { const $suggestionCards = $("<div>").addClass("chat-suggestion-cards"); suggestionCards.forEach((card) => { $suggestionCards.append(createSuggestionCard(card)); }); $("<div>") .append( $("<div>").addClass("dx-chat-messagelist-empty-message").text(data.texts.message), $("<div>").addClass("dx-chat-messagelist-empty-prompt").text(data.texts.prompt), $suggestionCards, ) .appendTo(element); } function onMessageEntered({ message }) { dataSource.store().push([{ type: "insert", data: { id: Date.now(), ...message } }]); if (!instance.option("alerts").length) { processMessageSending(message); } } </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 MessageStreaming() { 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, .chat-messagebubble-text { display: flex; flex-direction: column; } .dx-button { display: inline-block; color: var(--dx-color-icon); } .dx-chat-messagebubble-content > div > p:first-child { margin-top: 0; } .dx-chat-messagebubble-content > div > p:last-child { margin-bottom: 0; } .chat-messagebubble-text pre { white-space: pre-wrap; overflow-wrap: break-word; } .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-suggestion-cards { display: flex; flex-wrap: wrap; justify-content: center; gap: 16px; margin-top: 32px; width: 100%; } .chat-suggestion-card { border-radius: 12px; padding: 16px; border: 1px solid #EBEBEB; background: #FAFAFA; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; flex: 0 1 230px; max-width: 230px; text-align: left; cursor: pointer; transition: 0.2s ease; width: 230px; } .chat-suggestion-card:hover { border: 1px solid #E0E0E0; background: #F5F5F5; box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.04), 0 4px 24px 0 rgba(0, 0, 0, 0.02); } .chat-suggestion-card-title { color: #242424; font-size: 12px; font-weight: 600; line-height: 16px; } .chat-suggestion-card-prompt { color: #616161; font-size: 12px; font-weight: 400; line-height: 16px; } .dx-chat-messagelist-empty-prompt { margin-top: 4px; }

Streaming AI Responses

The demo calls the Azure OpenAI Chat Completions API with stream: true. Incoming delta chunks are passed through a createDelayedRenderer queue with a short display delay between chunks to produce a smooth typing effect. The demo appends each chunk to a growing buffer and updates the assistant message in the data store with every render cycle via a dataSource.store().push(...) call.

Stopping a Stream

The demo creates an AbortController before each request and passes its signal (AbortSignal) to the Azure OpenAI SDK in the request options. When the user clicks the stop button, the demo calls abortController.abort() to cancel the in-progress HTTP request.

The sendButtonOptions property switches the button's action property to 'custom' and the icon to 'stopfilled' while streaming is active, then reverts to the default (send) configuration once streaming ends.

Custom Empty View

The Chat component specifies an emptyViewTemplate that replaces the default empty state with custom suggestion cards. Clicking a card creates a message and triggers demo message send operations directly, bypassing text input.