DevExtreme v25.2 is now available.

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

Your search did not match any results.

Vue Tree List - AI Columns

DevExtreme TreeList allows you to add multiple AI columns to the TreeList. These columns auto generate meaningful cell values based on component data and a custom prompt, transforming DevExtreme TreeList into an AI-powered data analysis tool. In this demo, an AI column is fixed to the right side of the component.

Backend API
<template> <DxTreeList id="employees" keyExpr="ID" parentIdExpr="Head_ID" :dataSource="employees" :autoExpandAll="true" :showBorders="true" :aiIntegration="aiIntegration" :onAIColumnRequestCreating="onAIColumnRequestCreating" class="ai__grid" > <DxScrolling :mode="'standard'"/> <DxPaging :enabled="true" :pageSize="10" /> <DxColumn caption="Employee" cssClass="employee__cell" :width="260" cellTemplate="employee-cell" /> <template #employee-cell="{ data: { data: employee} }"> <Employee :firstName="employee.First_Name" :lastName="employee.Last_Name" /> </template> <DxColumn dataField="Title" caption="Position" :width="140" /> <DxColumn dataField="Status" :minWidth="180" cellTemplate="status-cell" /> <template #status-cell="{ data: { data: employee} }"> <Status :status="employee.Status"/> </template> <DxColumn dataField="City" :width="180" /> <DxColumn dataField="State" :width="140" /> <DxColumn dataField="Email" :minWidth="200" /> <DxColumn name="AI Column" caption="AI Column" type="ai" cssClass="ai__cell" :fixed="true" fixedPosition="right" :width="180" > <DxAI mode="auto" noDataText="No data" prompt="Identify the department where the employee works. Select from the following department list: 'Management', 'Human Resources', 'IT', 'Shipping', 'Support', 'Sales', 'Engineering'. Use 'Engineering' if you cannot find a better match." /> </DxColumn> </DxTreeList> </template> <script setup lang="ts"> import { DxTreeList, DxColumn, DxScrolling, DxPaging, DxAI } from 'devextreme-vue/tree-list'; import Employee from './Employee.vue'; import Status from './Status.vue'; import { type IEmployee, employees } from './data.ts'; import { aiIntegration } from './service.ts'; const onAIColumnRequestCreating = (e: { data: Partial<IEmployee>[] }) => { e.data = e.data.map((item) => ({ ID: item.ID, First_Name: item.First_Name, Last_Name: item.Last_Name, Title: item.Title, })); }; </script> <style scoped> #app .ai__cell { background-color: var(--dx-datagrid-row-alternation-bg); } .ai__grid { min-height: 560px; } #app .employee__cell > div { align-items: flex-end; } </style>
<template> <div class="employee__wrapper"> <div class="employee__img-wrapper"> <img :src="`../../../../images/employees/new/${firstName} ${lastName}.jpg`" :alt="`${firstName} ${lastName}`" > </div> <div class="employee__text-wrapper"> <div class="employee__text">{{ firstName }}</div> <div class="employee__text">{{ lastName }}</div> </div> </div> </template> <script setup lang="ts"> defineProps<{ firstName: string; lastName: string; }>(); </script> <style scoped> .employee__wrapper { display: flex; align-items: center; gap: 8px; } .employee__img-wrapper { width: 40px; height: 40px; border: var(--dx-border-width) solid var(--dx-color-border); border-radius: 50%; cursor: pointer; } .employee__img-wrapper img { width: 100%; height: 100%; object-fit: cover; object-position: center; border-radius: 50%; } .employee__text-wrapper { width: calc(100% - 48px); } .employee__text { margin: 0; padding: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } </style>
<template> <div :class="`status status--${status.toLowerCase()}`"> <div class="status__indicator"/> <div>{{ status }}</div> </div> </template> <script setup lang="ts"> defineProps<{ status: string, }>(); </script> <style scoped> .status { display: flex; align-items: center; } .status--salaried { color: var(--dx-color-success); } .status--commission { color: #f7630c; } .status--terminated { color: var(--dx-color-danger); } .status__indicator { background-color: currentcolor; margin-right: 8px; border-radius: 50%; height: 12px; width: 12px; } </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@link:../../packages/devextreme/artifacts/npm/devextreme/cjs', 'devextreme-vue': 'npm:devextreme-vue@link:../../packages/devextreme-vue/npm/cjs', 'openai': 'externals:openai.bundle.js', 'devextreme-quill': 'npm:devextreme-quill@1.7.7/dist/dx-quill.min.js', 'devexpress-diagram': 'npm:devexpress-diagram@2.2.24/dist/dx-diagram.js', 'devexpress-gantt': 'npm:devexpress-gantt@4.1.64/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, }, }; window.process = { env: { NODE_ENV: 'production', }, }; System.config(window.config); // eslint-disable-next-line const useTgzInCSB = ['openai']; let packagesInfo = { "openai": { "version": "4.73.1" } };
export interface IEmployee { ID: number; Head_ID: number; First_Name: string; Last_Name: string; Prefix: string; Title: string; City: string; State: string; Email: string; Skype?: string; Mobile_Phone: string; Birth_Date: string; Hire_Date: string; Status: string; } export const employees: IEmployee[] = [{ ID: 1, Head_ID: 0, First_Name: 'John', Last_Name: 'Heart', Prefix: 'Mr.', Title: 'CEO', City: 'Los Angeles', State: 'California', Email: 'jheart@dx-email.com', Skype: 'jheart_DX_skype', Mobile_Phone: '(213) 555-9392', Birth_Date: '1964-03-16', Hire_Date: '1995-01-15', Status: 'Salaried', }, { ID: 3, Head_ID: 1, First_Name: 'Arthur', Last_Name: 'Miller', Prefix: 'Mr.', Title: 'CTO', City: 'Los Angeles', State: 'California', Email: 'arthurm@dx-email.com', Mobile_Phone: '+1 (310) 555-8583', Birth_Date: '1972-07-11', Hire_Date: '2007-12-18', Status: 'Salaried', }, { ID: 4, Head_ID: 1, First_Name: 'Robert', Last_Name: 'Reagan', Prefix: 'Mr.', Title: 'CMO', City: 'Pasadena', State: 'California', Email: 'robertr@dx-email.com', Mobile_Phone: '+1 (818) 555-2387', Birth_Date: '1974-09-07', Hire_Date: '2002-11-08', Status: 'Salaried', }, { ID: 5, Head_ID: 1, First_Name: 'Greta', Last_Name: 'Sims', Prefix: 'Ms.', Title: 'HR Manager', City: 'Alhambra', State: 'California', Email: 'gretas@dx-email.com', Mobile_Phone: '+1 (818) 555-6546', Birth_Date: '1977-11-22', Hire_Date: '1998-04-23', Status: 'Salaried', }, { ID: 6, Head_ID: 3, First_Name: 'Brett', Last_Name: 'Wade', Prefix: 'Mr.', Title: 'IT Manager', City: 'San Marino', State: 'California', Email: 'brettw@dx-email.com', Mobile_Phone: '+1 (626) 555-0358', Birth_Date: '1968-12-01', Hire_Date: '2009-03-06', Status: 'Salaried', }, { ID: 7, Head_ID: 5, First_Name: 'Sandra', Last_Name: 'Johnson', Prefix: 'Mrs.', Title: 'Controller', City: 'Long Beach', State: 'California', Email: 'sandraj@dx-email.com', Mobile_Phone: '+1 (562) 555-2082', Birth_Date: '1974-11-15', Hire_Date: '2005-05-11', Status: 'Salaried', }, { ID: 21, Head_ID: 6, First_Name: 'Taylor', Last_Name: 'Riley', Prefix: 'Mr.', Title: 'Network Admin', City: 'West Hollywood', State: 'California', Email: 'taylorr@dx-email.com', Mobile_Phone: '+1 (310) 555-7276', Birth_Date: '1982-08-14', Hire_Date: '2012-04-14', Status: 'Salaried', }, { ID: 22, Head_ID: 6, First_Name: 'Amelia', Last_Name: 'Harper', Prefix: 'Mrs.', Title: 'Network Admin', City: 'Los Angeles', State: 'California', Email: 'ameliah@dx-email.com', Mobile_Phone: '+1 (213) 555-4276', Birth_Date: '1983-11-19', Hire_Date: '2011-02-10', Status: 'Salaried', }, { ID: 25, Head_ID: 6, First_Name: 'Karen', Last_Name: 'Goodson', Prefix: 'Miss', Title: 'Programmer', City: 'South Pasadena', State: 'California', Email: 'kareng@dx-email.com', Mobile_Phone: '+1 (626) 555-0908', Birth_Date: '1987-04-26', Hire_Date: '2011-03-14', Status: 'Salaried', }, { ID: 28, Head_ID: 6, First_Name: 'Morgan', Last_Name: 'Kennedy', Prefix: 'Mrs.', Title: 'Graphic Designer', City: 'San Fernando Valley', State: 'California', Email: 'morgank@dx-email.com', Mobile_Phone: '+1 (818) 555-8238', Birth_Date: '1984-07-17', Hire_Date: '2012-01-11', Status: 'Salaried', }, { ID: 29, Head_ID: 28, First_Name: 'Violet', Last_Name: 'Bailey', Prefix: 'Ms.', Title: 'Jr Graphic Designer', City: 'La Canada', State: 'California', Email: 'violetb@dx-email.com', Mobile_Phone: '+1 (818) 555-2478', Birth_Date: '1985-06-10', Hire_Date: '2012-01-19', Status: 'Salaried', }, { ID: 32, Head_ID: 3, First_Name: 'Bart', Last_Name: 'Arnaz', Prefix: 'Mr.', Title: 'Director of Engineering', City: 'Irvine', State: 'California', Email: 'barta@dx-email.com', Mobile_Phone: '+1 (714) 555-2000', Birth_Date: '1979-11-01', Hire_Date: '2008-06-29', Status: 'Salaried', }, { ID: 36, Head_ID: 32, First_Name: 'Samantha', Last_Name: 'Piper', Prefix: 'Ms.', Title: 'Engineer', City: 'Los Angeles', State: 'California', Email: 'samanthap@dx-email.com', Mobile_Phone: '+1 (323) 555-4512', Birth_Date: '1984-12-01', Hire_Date: '2008-01-22', Status: 'Salaried', }, { ID: 38, Head_ID: 32, First_Name: 'Terry', Last_Name: 'Bradley', Prefix: 'Mr.', Title: 'QA Engineer', City: 'Simi Valley', State: 'California', Email: 'terryb@dx-email.com', Mobile_Phone: '+1 (805) 555-2788', Birth_Date: '1984-01-09', Hire_Date: '2007-10-18', Status: 'Salaried', }, { ID: 44, Head_ID: 4, First_Name: 'Clark', Last_Name: 'Morgan', Prefix: 'Mr.', Title: 'Retail Sales Manager', City: 'Martinez', State: 'California', Email: 'clarkm@dx-email.com', Mobile_Phone: '+1 (925) 555-2525', Birth_Date: '1988-04-07', Hire_Date: '2012-04-11', Status: 'Commission', }, { ID: 45, Head_ID: 4, First_Name: 'Todd', Last_Name: 'Hoffman', Prefix: 'Mr.', Title: 'Retail Sales Manager', City: 'Livermore', State: 'California', Email: 'toddh@dx-email.com', Mobile_Phone: '+1 (925) 555-3579', Birth_Date: '1987-03-25', Hire_Date: '2012-04-19', Status: 'Commission', }, { ID: 48, Head_ID: 32, First_Name: 'Brad', Last_Name: 'Farkus', Prefix: 'Mr.', Title: 'Engineer', City: 'Los Angeles', State: 'California', Email: 'bradf@dx-email.com', Mobile_Phone: '+1 (213) 555-3626', Birth_Date: '1991-03-17', Hire_Date: '2010-04-15', Status: 'Terminated', }, { ID: 51, Head_ID: 32, First_Name: 'Stu', Last_Name: 'Pizaro', Prefix: 'Mr.', Title: 'Engineer', City: 'Los Angeles', State: 'California', Email: 'stu@dx-email.com', Mobile_Phone: '+1 (213) 555-2552', Birth_Date: '1985-09-11', Hire_Date: '2011-07-19', Status: 'Terminated', }];
import { createApp } from 'vue'; import App from './App.vue'; createApp(App).mount('#app');
import { AzureOpenAI, OpenAI } from 'openai'; import { AIIntegration, type RequestParams, type Response, } from 'devextreme-vue/common/ai-integration'; import notify from 'devextreme/ui/notify'; type AIMessage = (OpenAI.ChatCompletionUserMessageParam | OpenAI.ChatCompletionSystemMessageParam) & { content: string; }; const AzureOpenAIConfig = { dangerouslyAllowBrowser: true, deployment: 'gpt-4o-mini', apiVersion: '2024-02-01', endpoint: 'https://public-api.devexpress.com/demo-openai', apiKey: 'DEMO', }; const RATE_LIMIT_RETRY_DELAY_MS = 30000; const MAX_PROMPT_SIZE = 5000; const service = new AzureOpenAI(AzureOpenAIConfig); async function getAIResponse(messages: AIMessage[], signal: AbortSignal) { const params = { messages, model: AzureOpenAIConfig.deployment, max_tokens: 1000, temperature: 0.7, }; const response = await service.chat.completions.create(params, { signal }); const result = response.choices[0].message?.content; if (!result) { throw new Error('AI response returned empty content'); } return result; } async function getAIResponseRecursive(messages: AIMessage[], signal: AbortSignal): Promise<string> { return getAIResponse(messages, signal) .catch(async (error) => { if (!error.message.includes('Connection error')) { return Promise.reject(error); } notify({ message: 'Our demo AI service reached a temporary request limit. Retrying in 30 seconds.', width: 'auto', type: 'error', displayTime: 5000, }); await new Promise((resolve) => setTimeout(resolve, RATE_LIMIT_RETRY_DELAY_MS)); return getAIResponseRecursive(messages, signal); }); } export const aiIntegration = new AIIntegration({ sendRequest({ prompt }: RequestParams): Response { const isValidRequest = JSON.stringify(prompt.user).length < MAX_PROMPT_SIZE; if (!isValidRequest) { return { promise: Promise.reject(new Error('Request is too long. Specify a shorter prompt.')), abort: () => {}, }; } const controller = new AbortController(); const signal = controller.signal; if (!prompt.user || !prompt.system) { throw new Error('Invalid prompt data'); } const aiPrompt: AIMessage[] = [ { role: 'system', content: prompt.system }, { role: 'user', content: prompt.user }, ]; const promise = getAIResponseRecursive(aiPrompt, signal); const result: Response = { promise, abort: () => { controller.abort(); }, }; return result; }, });
<!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.2.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>

AI services used for this demo have been rate and data limited. As such, you may experience performance-related delays when exploring the capabilities of TreeList AI Columns.

When connected to your own AI model/service without rate and data limits, TreeList AI Columns will perform seamlessly, without artificial delays. Note that DevExtreme does not offer an AI REST API and does not ship any built-in LLMs/SLMs.

This demo instructs the AI service to identify the department name associated with each employee. You can modify the default prompt or enter a custom prompt in the AI column header menu.

To integrate an AI column into the DevExtreme TreeList, you must:

  • Configure the aiIntegration property at the component or column level (aiIntegration or columns[].ai.aiIntegration).
  • Set the column type to "ai".
  • Specify the column name.
  • Configure columns[].ai options, such as generation mode, predefined prompt, and no data text (displayed when the AI service returns no data for a row).

Our DevExtreme TreeList component uses all visible row data in AI requests, including fields not bound to a column and hidden column fields. This data gives LLMs broader context, but increases the use of AI resources. To limit data included in AI requests, modify the AIColumnRequestCreatingEvent.data parameter in the onAIColumnRequestCreating event handler.