DevExtreme v25.1 is now available.

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

Your search did not match any results.

Vue Chat - AI and Chatbot Integration

This demo leverages the Azure OpenAI service alongside the DevExtreme Chat component. You can integrate Chat with multiple AI services, including OpenAI, Google Dialogflow, and Microsoft Bot Framework.

Handling dataSource (reloadOnChange: false)

The Chat component's dataSource is a CustomStore that implements its own load and insert functions. The DevExtreme Chat component deactivates reloadOnChange to push updates directly into the store and update the conversation manually. See theonMessageEntered event handler and the processMessageSending function to review the code that manages data transfer between our Chat component and its data store.

Backend API
<template> <div class="chat-container" > <DxChat :class="{'chat-disabled' : isDisabled == true }" ref="chatElement" :height="710" :data-source="dataSource" :reload-on-change="false" :show-avatar="false" :show-day-headers="false" :user="user" message-template="message" v-model:typing-users="typingUsers" v-model:alerts="alerts" @message-entered="onMessageEntered($event)" > <template #message="{ data }"> <span v-if="data.message.text === REGENERATION_TEXT" > {{ REGENERATION_TEXT }} </span> <template v-else> <div v-html="convertToHtml(data.message.text)" class="chat-messagebubble-text" /> <div class="bubble-button-container"> <DxButton :icon="copyButtonIcon" styling-mode="text" hint="Copy" @click="onCopyButtonClick(data.message)" /> <DxButton icon="refresh" styling-mode="text" hint="Regenerate" @click="onRegenerateButtonClick()" /> </div> </template> </template> </DxChat> </div> </template> <script setup lang="ts"> import { ref, onBeforeMount } from 'vue'; import DxChat, { type DxChatTypes } from 'devextreme-vue/chat'; import DxButton from 'devextreme-vue/button'; import { loadMessages } from 'devextreme-vue/common/core/localization'; import { AzureOpenAI } from 'openai'; import { dictionary, messages, user, assistant, dataSource, convertToHtml, AzureOpenAIConfig, REGENERATION_TEXT, ALERT_TIMEOUT, } from './data.ts'; const chatService = new AzureOpenAI(AzureOpenAIConfig); const typingUsers = ref([]); const alerts = ref([]); const isDisabled = ref(false); const copyButtonIcon = ref('copy'); onBeforeMount(() => { loadMessages(dictionary); }); async function getAIResponse(messages) { const params = { messages, model: AzureOpenAIConfig.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; } function toggleDisabledState(disabled, event = undefined) { isDisabled.value = disabled; if (disabled) { event?.target.blur(); } else { event?.target.focus(); } } function updateLastMessage(text = REGENERATION_TEXT) { const items = dataSource.items(); const lastMessage = items.at(-1); dataSource.store().push([{ type: 'update', key: lastMessage.id, data: { text }, }]); } function renderAssistantMessage(text) { const message = { id: Date.now(), timestamp: new Date(), author: assistant, text, }; dataSource.store().push([{ type: 'insert', data: message }]); } async function processMessageSending(message, event) { toggleDisabledState(true, event); messages.push({ role: 'user', content: message.text }); typingUsers.value = [assistant]; try { const aiResponse = await getAIResponse(messages); setTimeout(() => { typingUsers.value = []; messages.push({ role: 'assistant', content: aiResponse }); renderAssistantMessage(aiResponse); }, 200); } catch { typingUsers.value = []; messages.pop(); alertLimitReached(); } finally { toggleDisabledState(false, event); } } function alertLimitReached() { alerts.value = [{ message: 'Request limit reached, try again in a minute.', }]; setTimeout(() => { alerts.value = []; }, ALERT_TIMEOUT); } 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 onMessageEntered({ message, event }: DxChatTypes.MessageEnteredEvent) { dataSource.store().push([{ type: 'insert', data: { id: Date.now(), ...message } }]); if (!alerts.value.length) { processMessageSending(message, event); } } function onCopyButtonClick(message) { navigator.clipboard?.writeText(message.text); copyButtonIcon.value = 'check'; setTimeout(() => { copyButtonIcon.value = 'copy'; }, 2500); } function onRegenerateButtonClick() { updateLastMessage(); regenerate(); } </script> <style scoped> .chat-container { width: 100%; display: flex; align-items: center; 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; } .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 .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 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; } </style>
window.exports = window.exports || {}; window.config = { transpiler: 'plugin-babel', meta: { '*.vue': { loader: 'vue-loader', }, '*.ts': { loader: 'demo-ts-loader', }, '*.svg': { loader: 'svg-loader', }, 'devextreme/time_zone_utils.js': { 'esModule': true, }, 'devextreme/localization.js': { 'esModule': true, }, 'devextreme/viz/palette.js': { 'esModule': true, }, 'openai': { 'esModule': true, }, }, paths: { 'project:': '../../../../', 'npm:': 'https://cdn.jsdelivr.net/npm/', 'bundles:': '../../../../bundles/', 'externals:': '../../../../bundles/externals/', }, map: { 'vue': 'npm:vue@3.4.27/dist/vue.esm-browser.js', '@vue/shared': 'npm:@vue/shared@3.4.27/dist/shared.cjs.prod.js', 'vue-loader': 'npm:dx-systemjs-vue-browser@1.1.2/index.js', 'demo-ts-loader': 'project:utils/demo-ts-loader.js', 'jszip': 'npm:jszip@3.10.1/dist/jszip.min.js', 'svg-loader': 'project:utils/svg-loader.js', 'mitt': 'npm:mitt/dist/mitt.umd.js', 'rrule': 'npm:rrule@2.6.4/dist/es5/rrule.js', 'luxon': 'npm:luxon@3.4.4/build/global/luxon.min.js', 'es6-object-assign': 'npm:es6-object-assign', 'devextreme': 'npm:devextreme@25.1.3/cjs', 'devextreme-vue': 'npm:devextreme-vue@25.1.3/cjs', 'unified': 'externals:unified/unified.bundle.js', 'remark-parse': 'externals:unified/remark-parse.bundle.js', 'remark-rehype': 'externals:unified/remark-rehype.bundle.js', 'remark-stringify': 'externals:unified/remark-stringify.bundle.js', 'rehype-parse': 'externals:unified/rehype-parse.bundle.js', 'rehype-remark': 'externals:unified/rehype-remark.bundle.js', 'rehype-stringify': 'externals:unified/rehype-stringify.bundle.js', 'rehype-minify-whitespace': 'externals:unified/rehype-minify-whitespace.bundle.js', 'openai': 'externals:openai.bundle.js', 'devextreme-quill': 'npm:devextreme-quill@1.7.3/dist/dx-quill.min.js', 'devexpress-diagram': 'npm:devexpress-diagram@2.2.19/dist/dx-diagram.js', 'devexpress-gantt': 'npm:devexpress-gantt@4.1.62/dist/dx-gantt.js', 'inferno': 'npm:inferno@8.2.3/dist/inferno.min.js', 'inferno-compat': 'npm:inferno-compat/dist/inferno-compat.min.js', 'inferno-create-element': 'npm:inferno-create-element@8.2.3/dist/inferno-create-element.min.js', 'inferno-dom': 'npm:inferno-dom/dist/inferno-dom.min.js', 'inferno-hydrate': 'npm:inferno-hydrate/dist/inferno-hydrate.min.js', 'inferno-clone-vnode': 'npm:inferno-clone-vnode/dist/inferno-clone-vnode.min.js', 'inferno-create-class': 'npm:inferno-create-class/dist/inferno-create-class.min.js', 'inferno-extras': 'npm:inferno-extras/dist/inferno-extras.min.js', '@preact/signals-core': 'npm:@preact/signals-core@1.8.0/dist/signals-core.min.js', 'plugin-babel': 'npm:systemjs-plugin-babel@0.0.25/plugin-babel.js', 'systemjs-babel-build': 'npm:systemjs-plugin-babel@0.0.25/systemjs-babel-browser.js', // Prettier 'prettier/standalone': 'npm:prettier@2.8.8/standalone.js', 'prettier/parser-html': 'npm:prettier@2.8.8/parser-html.js', }, packages: { 'devextreme-vue': { main: 'index.js', }, 'devextreme-vue/common': { main: 'index.js', }, 'devextreme': { defaultExtension: 'js', }, 'devextreme/events/utils': { main: 'index', }, 'devextreme/common/core/events/utils': { main: 'index', }, 'devextreme/events': { main: 'index', }, 'es6-object-assign': { main: './index.js', defaultExtension: 'js', }, }, packageConfigPaths: [ 'npm:@devextreme/*/package.json', ], babelOptions: { sourceMaps: false, stage0: true, }, }; System.config(window.config);
import { CustomStore, DataSource } from 'devextreme-vue/common/data'; import { unified } from 'unified'; import remarkParse from 'remark-parse'; import remarkRehype from 'remark-rehype'; import rehypeStringify from 'rehype-stringify'; import rehypeMinifyWhitespace from 'rehype-minify-whitespace'; export const dictionary = { en: { 'dxChat-emptyListMessage': 'Chat is Empty', 'dxChat-emptyListPrompt': 'AI Assistant is ready to answer your questions.', 'dxChat-textareaPlaceholder': 'Ask AI Assistant...', }, }; export const AzureOpenAIConfig = { dangerouslyAllowBrowser: true, deployment: 'gpt-4o-mini', apiVersion: '2024-02-01', endpoint: 'https://public-api.devexpress.com/demo-openai', apiKey: 'DEMO', }; export const REGENERATION_TEXT = 'Regeneration...'; export const CHAT_DISABLED_CLASS = 'chat-disabled'; export const ALERT_TIMEOUT = 1000 * 60; export const user = { id: 'user', }; export const assistant = { id: 'assistant', name: 'Virtual Assistant', }; export const store = []; export const messages = []; const customStore = new CustomStore({ key: 'id', load: () => new Promise((resolve) => { setTimeout(() => { resolve([...store]); }, 0); }), insert: (message) => new Promise((resolve) => { setTimeout(() => { store.push(message); resolve(message); }); }), }); export const dataSource = new DataSource({ store: customStore, paginate: false, }); export function convertToHtml(value: string) { const result = unified() .use(remarkParse) .use(remarkRehype) .use(rehypeMinifyWhitespace) .use(rehypeStringify) .processSync(value) .toString(); return result; }
import { createApp } from 'vue'; import App from './App.vue'; createApp(App).mount('#app');
<!DOCTYPE html> <html 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" /> <link rel="stylesheet" type="text/css" href="https://cdn3.devexpress.com/jslib/25.1.3/css/dx.light.css" /> <script type="module"> import * as vueCompilerSFC from "https://cdn.jsdelivr.net/npm/@vue/compiler-sfc@3.4.27/dist/compiler-sfc.esm-browser.js"; window.vueCompilerSFC = vueCompilerSFC; </script> <script src="https://cdn.jsdelivr.net/npm/typescript@5.4.5/lib/typescript.js"></script> <script src="https://cdn.jsdelivr.net/npm/core-js@2.6.12/client/shim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/systemjs@0.21.3/dist/system.js"></script> <script type="text/javascript" src="config.js"></script> <script type="text/javascript"> System.import("./index.ts"); </script> </head> <body class="dx-viewport"> <div class="demo-container"> <div id="app"></div> </div> </body> </html>

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.