Backend API
import React, { useCallback, useState, useRef } from 'react';
import Chat from 'devextreme-react/chat';
import type { ChatTypes } from 'devextreme-react/chat';
import { Switch, type SwitchTypes } from 'devextreme-react/switch';
import { loadMessages } from 'devextreme-react/common/core/localization';
import {
user,
assistant,
CHAT_DISABLED_CLASS,
suggestionItems,
} from './data.ts';
import Message from './Message.tsx';
import { dataSource, useApi } from './useApi.ts';
loadMessages({
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 default function App() {
const {
alerts,
insertMessage,
fetchAIResponse,
} = useApi();
const [typingUsers, setTypingUsers] = useState<ChatTypes.User[]>([]);
const [isDisabled, setIsDisabled] = useState<boolean>(false);
const [inputFieldText, setInputFieldText] = useState<string>('');
const [suggestionList, setSuggestionList] = useState(suggestionItems);
const sendImmediately = useRef<boolean>(false);
const hideAfterUse = useRef<boolean>(false);
const processAIRequest = useCallback(async (message: ChatTypes.Message): Promise<void> => {
setIsDisabled(true);
setTypingUsers([assistant]);
await fetchAIResponse(message);
setTypingUsers([]);
setIsDisabled(false);
}, [fetchAIResponse]);
const onSuggestionClick = useCallback((e: { itemData?: { text: string; prompt: string } }) => {
const { prompt = '', text = '' } = e.itemData ?? {};
if (hideAfterUse.current) {
setSuggestionList((items) => items.filter((item) => item.text !== text));
}
if (sendImmediately.current) {
const message = { id: Date.now(), timestamp: new Date(), author: user, text: prompt };
insertMessage(message);
if (!alerts.length) {
processAIRequest(message);
}
} else {
setInputFieldText(prompt);
}
}, [alerts.length, insertMessage, processAIRequest]);
const suggestions = { items: suggestionList, onItemClick: onSuggestionClick, disabled: isDisabled };
const onMessageEntered = useCallback(async ({ message, event }: ChatTypes.MessageEnteredEvent): Promise<void> => {
if (isDisabled) return;
insertMessage({ id: Date.now(), ...message });
if (!alerts.length) {
(event?.target as HTMLElement).blur();
await processAIRequest(message);
(event?.target as HTMLElement).focus();
}
}, [isDisabled, insertMessage, alerts.length, processAIRequest]);
const onInputFieldTextChanged = useCallback((e: ChatTypes.InputFieldTextChangedEvent) => {
setInputFieldText(e?.value ?? '');
}, []);
const messageRender = useCallback(({ message }: { message: ChatTypes.Message }) => <Message text={message.text} />, []);
return (
<>
<Chat
className={isDisabled ? CHAT_DISABLED_CLASS : ''}
dataSource={dataSource}
reloadOnChange={false}
showAvatar={false}
showDayHeaders={false}
user={user}
height={520}
alerts={alerts}
typingUsers={typingUsers}
speechToTextEnabled={true}
inputFieldText={inputFieldText}
suggestions={suggestions}
messageRender={messageRender}
onMessageEntered={onMessageEntered}
onInputFieldTextChanged={onInputFieldTextChanged}
/>
<div className='options'>
<div className='caption'>Suggestion Options</div>
<div className='suggestions-options'>
<div className='option'>
<Switch
defaultValue={false}
onValueChanged={(e: SwitchTypes.ValueChangedEvent) => { sendImmediately.current = e.value; }}
/>
<span>Send Immediately</span>
</div>
<div className='option'>
<Switch
defaultValue={false}
onValueChanged={(e: SwitchTypes.ValueChangedEvent) => { hideAfterUse.current = e.value; }}
/>
<span>Hide After Use</span>
</div>
</div>
</div>
</>
);
}
import React, { useCallback, useState, useRef } from 'react';
import Chat from 'devextreme-react/chat';
import { Switch } from 'devextreme-react/switch';
import { loadMessages } from 'devextreme-react/common/core/localization';
import {
user, assistant, CHAT_DISABLED_CLASS, suggestionItems,
} from './data.js';
import Message from './Message.js';
import { dataSource, useApi } from './useApi.js';
loadMessages({
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 default function App() {
const { alerts, insertMessage, fetchAIResponse } = useApi();
const [typingUsers, setTypingUsers] = useState([]);
const [isDisabled, setIsDisabled] = useState(false);
const [inputFieldText, setInputFieldText] = useState('');
const [suggestionList, setSuggestionList] = useState(suggestionItems);
const sendImmediately = useRef(false);
const hideAfterUse = useRef(false);
const processAIRequest = useCallback(
async (message) => {
setIsDisabled(true);
setTypingUsers([assistant]);
await fetchAIResponse(message);
setTypingUsers([]);
setIsDisabled(false);
},
[fetchAIResponse],
);
const onSuggestionClick = useCallback(
(e) => {
const { prompt = '', text = '' } = e.itemData ?? {};
if (hideAfterUse.current) {
setSuggestionList((items) => items.filter((item) => item.text !== text));
}
if (sendImmediately.current) {
const message = {
id: Date.now(), timestamp: new Date(), author: user, text: prompt,
};
insertMessage(message);
if (!alerts.length) {
processAIRequest(message);
}
} else {
setInputFieldText(prompt);
}
},
[alerts.length, insertMessage, processAIRequest],
);
const suggestions = {
items: suggestionList,
onItemClick: onSuggestionClick,
disabled: isDisabled,
};
const onMessageEntered = useCallback(
async ({ message, event }) => {
if (isDisabled) return;
insertMessage({ id: Date.now(), ...message });
if (!alerts.length) {
(event?.target).blur();
await processAIRequest(message);
(event?.target).focus();
}
},
[isDisabled, insertMessage, alerts.length, processAIRequest],
);
const onInputFieldTextChanged = useCallback((e) => {
setInputFieldText(e?.value ?? '');
}, []);
const messageRender = useCallback(({ message }) => <Message text={message.text} />, []);
return (
<>
<Chat
className={isDisabled ? CHAT_DISABLED_CLASS : ''}
dataSource={dataSource}
reloadOnChange={false}
showAvatar={false}
showDayHeaders={false}
user={user}
height={520}
alerts={alerts}
typingUsers={typingUsers}
speechToTextEnabled={true}
inputFieldText={inputFieldText}
suggestions={suggestions}
messageRender={messageRender}
onMessageEntered={onMessageEntered}
onInputFieldTextChanged={onInputFieldTextChanged}
/>
<div className="options">
<div className="caption">Suggestion Options</div>
<div className="suggestions-options">
<div className="option">
<Switch
defaultValue={false}
onValueChanged={(e) => {
sendImmediately.current = e.value;
}}
/>
<span>Send Immediately</span>
</div>
<div className="option">
<Switch
defaultValue={false}
onValueChanged={(e) => {
hideAfterUse.current = e.value;
}}
/>
<span>Hide After Use</span>
</div>
</div>
</div>
</>
);
}
import React from 'react';
import type { FC } from 'react';
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeMinifyWhitespace from 'rehype-minify-whitespace';
import rehypeStringify from 'rehype-stringify';
import HTMLReactParser from 'html-react-parser';
function convertToHtml(value: string): string {
return unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeMinifyWhitespace)
.use(rehypeStringify)
.processSync(value)
.toString();
}
interface MessageProps {
text: string;
}
const Message: FC<MessageProps> = ({ text }: MessageProps) => (
<div className='chat-messagebubble-text'>
{HTMLReactParser(convertToHtml(text))}
</div>
);
export default Message;
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App.tsx';
ReactDOM.render(
<App />,
document.getElementById('app'),
);
import type { ChatTypes } from 'devextreme-react/chat';
export const CHAT_DISABLED_CLASS = 'chat-disabled';
export const ALERT_TIMEOUT = 1000 * 60;
export const user: ChatTypes.User = {
id: 'user',
};
export const assistant: ChatTypes.User = {
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.
`;
import { AzureOpenAI, OpenAI } from 'openai';
import { type AIResponse } from 'devextreme-react/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);
const wait = (delay: number): Promise<void> =>
new Promise((resolve): void => {
setTimeout(resolve, delay);
});
export async function getAIResponse(messages: AIMessage[], delay?: number): Promise<AIResponse> {
const params = {
messages,
model: AzureOpenAIConfig.deployment,
max_completion_tokens: 1000,
temperature: 0.7,
};
const response = await chatService.chat.completions.create(params);
const data = { choices: response.choices };
if (delay) {
await wait(delay);
}
return data.choices[0].message?.content ?? '';
}
import { useCallback, useState } from 'react';
import type { ChatTypes } from 'devextreme-react/chat';
import { CustomStore, DataSource } from 'devextreme-react/common/data';
import {
ALERT_TIMEOUT,
assistant,
SYSTEM_PROMPT,
} from './data.ts';
import { getAIResponse } from './service.ts';
import type { AIMessage } from './service.ts';
const store: ChatTypes.Message[] = [];
const customStore = new CustomStore({
key: 'id',
load: (): Promise<ChatTypes.Message[]> => new Promise((resolve) => {
setTimeout(() => {
resolve([...store]);
}, 0);
}),
insert: (message: ChatTypes.Message): Promise<ChatTypes.Message> => new Promise((resolve) => {
setTimeout(() => {
store.push(message);
resolve(message);
});
}),
});
export const dataSource = new DataSource({
store: customStore,
paginate: false,
});
const dataItemToMessage = (item: ChatTypes.Message): AIMessage => ({
role: item.author?.id as AIMessage['role'],
content: item.text,
});
const getMessageHistory = (): AIMessage[] => [...dataSource.items()].map(dataItemToMessage);
export const useApi = () => {
const [alerts, setAlerts] = useState<ChatTypes.Alert[]>([]);
const insertMessage = useCallback((data: ChatTypes.Message): void => {
dataSource.store().push([{ type: 'insert', data }]);
}, []);
const alertLimitReached = useCallback((): void => {
setAlerts([{
message: 'Request limit reached, try again in a minute.',
}]);
setTimeout(() => {
setAlerts([]);
}, ALERT_TIMEOUT);
}, []);
const fetchAIResponse = useCallback(async (message: ChatTypes.Message): Promise<void> => {
const messages = [
{ role: 'system' as const, content: SYSTEM_PROMPT },
...getMessageHistory(),
dataItemToMessage(message),
];
try {
const aiResponse = await getAIResponse(messages, 200);
insertMessage({
id: Date.now(),
timestamp: new Date(),
author: assistant,
text: aiResponse,
});
} catch {
alertLimitReached();
}
}, [alertLimitReached, insertMessage]);
return {
alerts,
insertMessage,
fetchAIResponse,
};
};
window.exports = window.exports || {};
window.config = {
transpiler: 'ts',
typescriptOptions: {
module: 'system',
emitDecoratorMetadata: true,
experimentalDecorators: true,
jsx: 'react',
},
meta: {
'react': {
'esModule': true,
},
'typescript': {
'exports': 'ts',
},
'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: {
'npm:': 'https://cdn.jsdelivr.net/npm/',
'bundles:': '../../../../bundles/',
'externals:': '../../../../bundles/externals/',
'anti-forgery:': '../../../../shared/anti-forgery/',
},
defaultExtension: 'js',
map: {
'anti-forgery': 'anti-forgery:fetch-override.js',
'ts': 'npm:plugin-typescript@8.0.0/lib/plugin.js',
'typescript': 'npm:typescript@4.2.4/lib/typescript.js',
'jszip': 'npm:jszip@3.10.1/dist/jszip.min.js',
'react': 'npm:react@17.0.2/umd/react.development.js',
'react-dom': 'npm:react-dom@17.0.2/umd/react-dom.development.js',
'prop-types': 'npm:prop-types/prop-types.js',
'html-react-parser': 'npm:html-react-parser@1.4.14/dist/html-react-parser.min.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-react': 'npm:devextreme-react@link:../../packages/devextreme-react/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',
'devextreme-cldr-data': 'npm:devextreme-cldr-data@1.0.3',
// SystemJS plugins
'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': {
defaultExtension: 'js',
},
'devextreme-react': {
main: 'index.js',
},
'devextreme-react/common': {
main: 'index.js',
},
'devextreme/events/utils': {
main: 'index',
},
'devextreme/common/core/events/utils': {
main: 'index',
},
'devextreme/localization/messages': {
format: 'json',
defaultExtension: 'json',
},
'devextreme/events': {
main: 'index',
},
'es6-object-assign': {
main: './index.js',
defaultExtension: 'js',
},
},
packageConfigPaths: [
'npm:@devextreme/*/package.json',
],
babelOptions: {
sourceMaps: false,
stage0: true,
react: 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 React from 'react';
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeMinifyWhitespace from 'rehype-minify-whitespace';
import rehypeStringify from 'rehype-stringify';
import HTMLReactParser from 'html-react-parser';
function convertToHtml(value) {
return unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeMinifyWhitespace)
.use(rehypeStringify)
.processSync(value)
.toString();
}
const Message = ({ text }) => (
<div className="chat-messagebubble-text">{HTMLReactParser(convertToHtml(text))}</div>
);
export default Message;
import { AzureOpenAI } from 'openai';
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);
const wait = (delay) =>
new Promise((resolve) => {
setTimeout(resolve, delay);
});
export async function getAIResponse(messages, delay) {
const params = {
messages,
model: AzureOpenAIConfig.deployment,
max_completion_tokens: 1000,
temperature: 0.7,
};
const response = await chatService.chat.completions.create(params);
const data = { choices: response.choices };
if (delay) {
await wait(delay);
}
return data.choices[0].message?.content ?? '';
}
import { useCallback, useState } from 'react';
import { CustomStore, DataSource } from 'devextreme-react/common/data';
import { ALERT_TIMEOUT, assistant, SYSTEM_PROMPT } from './data.js';
import { getAIResponse } from './service.js';
const store = [];
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,
});
const dataItemToMessage = (item) => ({
role: item.author?.id,
content: item.text,
});
const getMessageHistory = () => [...dataSource.items()].map(dataItemToMessage);
export const useApi = () => {
const [alerts, setAlerts] = useState([]);
const insertMessage = useCallback((data) => {
dataSource.store().push([{ type: 'insert', data }]);
}, []);
const alertLimitReached = useCallback(() => {
setAlerts([
{
message: 'Request limit reached, try again in a minute.',
},
]);
setTimeout(() => {
setAlerts([]);
}, ALERT_TIMEOUT);
}, []);
const fetchAIResponse = useCallback(
async (message) => {
const messages = [
{ role: 'system', content: SYSTEM_PROMPT },
...getMessageHistory(),
dataItemToMessage(message),
];
try {
const aiResponse = await getAIResponse(messages, 200);
insertMessage({
id: Date.now(),
timestamp: new Date(),
author: assistant,
text: aiResponse,
});
} catch {
alertLimitReached();
}
},
[alertLimitReached, insertMessage],
);
return {
alerts,
insertMessage,
fetchAIResponse,
};
};
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App.js';
ReactDOM.render(<App />, document.getElementById('app'));
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.
`;
<!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" />
<link rel="stylesheet" type="text/css" href="styles.css" />
<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.tsx");
</script>
</head>
<body class="dx-viewport">
<div class="demo-container">
<div id="app"></div>
</div>
</body>
</html>
#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;
}
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.