DevExtreme v26.1 is now available.

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

Your search did not match any results.

Vue Chat - Prompt Suggestions

DevExtreme Chat supports suggestions - hints that guide users throughout conversations. The component displays suggestion buttons above the message input field and supports both persistent and temporary hints.

Backend API
<template> <DxChat :class="{ 'chat-disabled': isDisabled }" :height="520" :data-source="dataSource" :reload-on-change="false" :show-avatar="false" :show-day-headers="false" :user="user" :speech-to-text-enabled="true" :input-field-text="inputFieldText" :suggestions="suggestions" message-template="message" v-model:typing-users="typingUsers" v-model:alerts="alerts" @message-entered="onMessageEntered($event)" @input-field-text-changed="onInputFieldTextChanged($event)" > <template #message="{ data }"> <div v-html="convertToHtml(data.message.text)" class="chat-messagebubble-text" /> </template> </DxChat> <div class="options"> <div class="caption">Suggestion Options</div> <div class="suggestions-options"> <div class="option"> <DxSwitch :value="sendImmediately" @value-changed="sendImmediately = $event.value" /> <span>Send Immediately</span> </div> <div class="option"> <DxSwitch :value="hideAfterUse" @value-changed="hideAfterUse = $event.value" /> <span>Hide After Use</span> </div> </div> </div> </template> <script setup lang="ts"> import { ref, onBeforeMount } from 'vue'; import DxChat, { type DxChatTypes } from 'devextreme-vue/chat'; import DxSwitch from 'devextreme-vue/switch'; import { loadMessages } from 'devextreme-vue/common/core/localization'; import { type Events } from 'devextreme-vue/common/core'; import { dictionary, messages, user, assistant, dataSource, convertToHtml, suggestionItems, ALERT_TIMEOUT, } from './data.ts'; import { getAIResponse } from './service.ts'; const typingUsers = ref<DxChatTypes.User[]>([]); const alerts = ref<DxChatTypes.Alert[]>([]); const isDisabled = ref(false); const inputFieldText = ref(''); const sendImmediately = ref(false); const hideAfterUse = ref(false); const suggestions = ref<DxChatTypes.Properties['suggestions']>({ items: suggestionItems, onItemClick: onSuggestionItemClick, }); onBeforeMount(() => { loadMessages(dictionary); }); function toggleDisabledState(disabled: boolean, event?: Events.EventObject): void { isDisabled.value = disabled; suggestions.value = { ...suggestions.value, disabled }; if (disabled) { (event?.target as HTMLElement)?.blur(); } else { (event?.target as HTMLElement)?.focus(); } } function renderAssistantMessage(text: string): void { dataSource.store().push([{ type: 'insert', data: { id: Date.now(), timestamp: new Date(), author: assistant, text, }, }]); } async function processMessageSending( message: DxChatTypes.TextMessage, event?: Events.EventObject, ): Promise<void> { toggleDisabledState(true, event); messages.push({ role: 'user', content: message.text ?? '' }); typingUsers.value = [assistant]; try { const aiResponse = await getAIResponse(messages) as string; setTimeout(() => { typingUsers.value = []; messages.push({ role: 'assistant', content: aiResponse }); renderAssistantMessage(aiResponse); }, 200); } catch { typingUsers.value = []; messages.pop(); alertLimitReached(); } finally { toggleDisabledState(false, event); } } function alertLimitReached(): void { alerts.value = [{ message: 'Request limit reached, try again in a minute.' }]; setTimeout(() => { alerts.value = []; }, ALERT_TIMEOUT); } function onMessageEntered({ message, event }: DxChatTypes.MessageEnteredEvent): void { if (isDisabled.value) return; dataSource.store().push([{ type: 'insert', data: { id: Date.now(), ...message } }]); if (!alerts.value.length) { processMessageSending(message, event); } } function onInputFieldTextChanged(e: DxChatTypes.InputFieldTextChangedEvent): void { inputFieldText.value = e.value ?? ''; } function onSuggestionItemClick(e: { itemData?: { text: string; prompt: string } }): void { const { text = '', prompt = '' } = e.itemData ?? {}; if (hideAfterUse.value) { const currentItems = (suggestions.value?.items ?? []) as { text: string; prompt: string }[]; suggestions.value = { items: currentItems.filter((item) => item.text !== text), onItemClick: onSuggestionItemClick, }; } if (sendImmediately.value) { const message: DxChatTypes.Message = { id: Date.now(), timestamp: new Date(), author: user, text: prompt, }; dataSource.store().push([{ type: 'insert', data: message }]); if (!alerts.value.length) { processMessageSending(message); } } else { inputFieldText.value = prompt; } } </script> <style scoped> #app { 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 > 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; } </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, }, 'zod': { 'esModule': true, }, 'zod-to-json-schema': { 'esModule': true, }, }, paths: { 'project:': '../../../../', 'npm:': 'https://cdn.jsdelivr.net/npm/', 'bundles:': '../../../../bundles/', 'externals:': '../../../../bundles/externals/', 'anti-forgery:': '../../../../shared/anti-forgery/', }, map: { 'anti-forgery': 'anti-forgery:fetch-override.js', 'vue': 'npm:vue@3.5.32/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@link:../../packages/devextreme/artifacts/npm/devextreme/cjs', 'devextreme-vue': 'npm:devextreme-vue@link:../../packages/devextreme-vue/npm/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', 'zod': 'externals:zod.bundle.js', 'zod-to-json-schema': 'externals:zod-to-json-schema.bundle.js', 'devextreme-quill': 'npm:devextreme-quill@1.7.9/dist/dx-quill.min.js', 'devexpress-diagram': 'npm:devexpress-diagram@2.2.29/dist/dx-diagram.js', 'devexpress-gantt': 'npm:devexpress-gantt@4.1.69/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.14.1/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, }, }; window.process = { env: { NODE_ENV: 'production', }, }; System.config(window.config); // eslint-disable-next-line const useTgzInCSB = ['openai']; let packagesInfo = { "openai": { "version": "4.73.1" }, "rehype-minify-whitespace": { "version": "5.0.1" }, "rehype-parse": { "version": "8.0.5" }, "rehype-remark": { "version": "9.0.0" }, "rehype-stringify": { "version": "9.0.4" }, "remark-parse": { "version": "10.0.2" }, "remark-rehype": { "version": "10.1.0" }, "remark-stringify": { "version": "10.0.3" }, "unified": { "version": "10.1.2" }, "zod": { "version": "3.24.4" }, "zod-to-json-schema": { "version": "3.24.6" } };
import { CustomStore, DataSource } from 'devextreme-vue/common/data'; import { type DxChatTypes } from 'devextreme-vue/chat'; 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'; import { type AIMessage } from './service'; export const dictionary = { 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...', }, }; export const CHAT_DISABLED_CLASS = 'chat-disabled'; export const ALERT_TIMEOUT = 1000 * 60; export const user = { id: 'user', }; export const assistant = { id: 'assistant', name: 'AI Assistant', }; export const 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' }, ]; export const 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. `; export const messages: AIMessage[] = [ { role: 'system', content: SYSTEM_PROMPT }, ]; const store: DxChatTypes.Message[] = []; const customStore = new CustomStore<DxChatTypes.Message>({ key: 'id', load: () => new Promise((resolve) => { setTimeout(() => { resolve([...store]); }, 0); }), insert: (message: DxChatTypes.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');
import { AzureOpenAI, OpenAI } from 'openai'; import { type AIResponse } from 'devextreme-vue/common/ai-integration'; export type AIMessage = ( OpenAI.ChatCompletionUserMessageParam | OpenAI.ChatCompletionSystemMessageParam | OpenAI.ChatCompletionAssistantMessageParam) & { content: string; }; const AzureOpenAIConfig = { dangerouslyAllowBrowser: true, deployment: 'demo-mini', apiVersion: '2024-02-01', endpoint: 'https://public-api.devexpress.com/demo-openai', apiKey: 'DEMO', }; const chatService = new AzureOpenAI(AzureOpenAIConfig); export async function getAIResponse(messages: AIMessage[]): Promise<AIResponse> { const params: Record<string, any> = { messages, model: AzureOpenAIConfig.deployment, max_completion_tokens: 1000, temperature: 0.7, }; const response = await chatService.chat.completions.create(params as any); const data = { choices: response.choices }; return data.choices[0].message?.content || ''; }
<!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/26.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.9.3/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>

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.