DevExtreme v26.1 is now available.

Explore our newest features/capabilities and share your thoughts with us.

Your search did not match any results.

JavaScript/jQuery Chat - Message Streaming

This sample uses the DevExtreme JavaScript Chat component alongside Azure OpenAI to stream AI-generated responses in real time. Since the AI model generates output token by token, our JavaScript 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 JavaScript Chat displays custom suggestion cards. Clicking a card sends the corresponding prompt directly to the AI service without extra user input.

Backend API
$(() => { const store = []; const messages = []; let 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...', }, }); const 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, }); // eslint-disable-next-line no-restricted-syntax 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; } function renderMessageContent(message, element) { $('<div>') .addClass('chat-messagebubble-text') .html(convertToHtml(message.text)) .appendTo(element); } 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', (e) => { sendSuggestion(card.prompt, e); }); } const instance = $('#dx-ai-chat') .dxChat({ user, height: 710, dataSource, reloadOnChange: false, showAvatar: false, showDayHeaders: false, speechToTextEnabled: true, sendButtonOptions: { action: 'send', }, onMessageEntered: (e) => { const { message } = e; dataSource .store() .push([{ type: 'insert', data: { id: Date.now(), ...message } }]); if (!instance.option('alerts').length) { processMessageSending(message); } }, messageTemplate: (data, element) => { const { message } = data; renderMessageContent(message, element); }, emptyViewTemplate(data) { const $suggestionCards = $('<div>').addClass('chat-suggestion-cards'); suggestionCards.forEach((card) => { $suggestionCards.append(createSuggestionCard(card)); }); return $('<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, ); }, }) .dxChat('instance'); });
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" lang="en"> <head> <title>DevExtreme Demo</title> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" /> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script> <script>window.jQuery || document.write(decodeURIComponent('%3Cscript src="js/jquery.min.js"%3E%3C/script%3E'))</script> <link rel="stylesheet" type="text/css" href="https://cdn3.devexpress.com/jslib/26.1.3/css/dx.light.css" /> <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> <script src="js/dx.all.js?v=26.1.3"></script> <script src="data.js"></script> <link rel="stylesheet" type="text/css" href="styles.css" /> <script src="index.js"></script> </head> <body class="dx-viewport"> <div class="demo-container"> <div id="dx-ai-chat"></div> </div> </body> </html>
.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; }
const deployment = 'demo-mini'; const apiVersion = '2024-02-01'; const endpoint = 'https://public-api.devexpress.com/demo-openai'; const apiKey = 'DEMO'; const ALERT_TIMEOUT = 1000 * 60; const user = { id: 'user', }; const assistant = { id: 'assistant', name: 'AI Assistant', }; const 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?', }, ];

Streaming AI Responses

The demo calls the Azure OpenAI JavaScript 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 JavaScript 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.