diff --git a/apps/demos/Demos/Chat/AIAndChatbotIntegration/description.md b/apps/demos/Demos/Chat/AIAndChatbotIntegration/description.md new file mode 100644 index 00000000000..67c1bba41e2 --- /dev/null +++ b/apps/demos/Demos/Chat/AIAndChatbotIntegration/description.md @@ -0,0 +1 @@ +Chat is an interactive interface that allows users to send and receive messages in real time. diff --git a/apps/demos/Demos/Chat/AIAndChatbotIntegration/jQuery/data.js b/apps/demos/Demos/Chat/AIAndChatbotIntegration/jQuery/data.js new file mode 100644 index 00000000000..ca66557ff1e --- /dev/null +++ b/apps/demos/Demos/Chat/AIAndChatbotIntegration/jQuery/data.js @@ -0,0 +1,12 @@ +const deployment = 'gpt-4o-mini'; +const apiVersion = '2024-02-01'; +const endpoint = 'https://public-api.devexpress.com/demo-openai'; +const apiKey = 'DEMO'; +const REGENERATION_TEXT = 'Regeneration...'; +const user = { + id: 'user', +}; +const assistant = { + id: 'assistant', + name: 'Virtual Assistant', +}; diff --git a/apps/demos/Demos/Chat/AIAndChatbotIntegration/jQuery/index.html b/apps/demos/Demos/Chat/AIAndChatbotIntegration/jQuery/index.html new file mode 100644 index 00000000000..ad7bbbc745a --- /dev/null +++ b/apps/demos/Demos/Chat/AIAndChatbotIntegration/jQuery/index.html @@ -0,0 +1,20 @@ + + + + DevExtreme Demo + + + + + + + + + + + +
+
+
+ + diff --git a/apps/demos/Demos/Chat/AIAndChatbotIntegration/jQuery/index.js b/apps/demos/Demos/Chat/AIAndChatbotIntegration/jQuery/index.js new file mode 100644 index 00000000000..19e7c0d4019 --- /dev/null +++ b/apps/demos/Demos/Chat/AIAndChatbotIntegration/jQuery/index.js @@ -0,0 +1,203 @@ +import { AzureOpenAI } from 'https://esm.sh/openai'; +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'; + +$(() => { + const store = []; + let messages = []; + + 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, + }); + + async function getAIResponse(messages) { + const params = { + messages: messages, + max_tokens: 1000, + temperature: 0.7, + }; + + const responseAzure = await chatService.chat.completions.create(params); + const data = { choices: responseAzure.choices }; + + return data.choices[0].message?.content; + }; + + function alertLimitReached() { + instance.option({ + alerts: [{ + message: 'Request limit reached, try again in a minute.', + }], + }); + + setTimeout(() => { + instance.option({ alerts: [] }); + }, 10000); + }; + + async function processMessageSending() { + instance.option({ typingUsers: [assistant] }); + + try { + const aiResponse = await getAIResponse(messages); + + setTimeout(() => { + instance.option({ typingUsers: [] }); + + messages.push({ role: 'assistant', content: aiResponse }); + + renderMessage(aiResponse); + }, 200); + } catch { + instance.option({ typingUsers: [] }); + alertLimitReached(); + } + }; + + async function regenerate() { + try { + const aiResponse = await getAIResponse(messages.slice(0, -1)); + + updateLastMessage(aiResponse); + messages.at(-1).content = aiResponse; + } catch { + updateLastMessage(messages.at(-1).content); + alertLimitReached(); + } + }; + + function renderMessage(text) { + const message = { + id: Date.now(), + timestamp: new Date(), + author: assistant, + text, + }; + + customStore.push([{ type: 'insert', data: message }]); + }; + + function updateLastMessage(text) { + const { items } = instance.option(); + const lastMessage = items.at(-1); + const data = { + text: text ?? REGENERATION_TEXT, + }; + + customStore.push([{ + type: 'update', + key: lastMessage.id, + data, + }]); + }; + + function convertToHtml(value) { + const result = unified() + .use(remarkParse) + .use(remarkRehype) + .use(rehypeStringify) + .processSync(value) + .toString(); + + return result; + }; + + const customStore = new DevExpress.data.CustomStore({ + key: 'id', + load: () => { + const d = $.Deferred(); + + setTimeout(function () { + d.resolve([...store]); + }); + + return d.promise(); + }, + insert: (message) => { + const d = $.Deferred(); + + setTimeout(function () { + store.push(message); + d.resolve(); + }); + + return d.promise(); + }, + }); + + const instance = $("#dx-ai-chat").dxChat({ + dataSource: customStore, + reloadOnChange: false, + showAvatar: false, + showDayHeaders: false, + user, + height: 710, + onMessageEntered: (e) => { + const { message } = e; + + customStore.push([{ type: 'insert', data: { id: Date.now(), ...message } }]); + messages.push({ role: 'user', content: message.text }); + + processMessageSending(); + }, + messageTemplate: (data, element) => { + const { message } = data; + + if (message.text === REGENERATION_TEXT) { + element.text(REGENERATION_TEXT); + return; + } + + const $textElement = $('
') + .addClass('dx-chat-messagebubble-text') + .html(convertToHtml(message.text)) + .appendTo(element); + + const $buttonContainer = $('
') + .addClass('dx-bubble-button-container'); + + $('
') + .dxButton({ + icon: 'copy', + stylingMode: 'text', + hint: 'Copy', + onClick: ({ component }) => { + navigator.clipboard.writeText($textElement.text()); + component.option({ icon: 'check' }); + setTimeout(() => { + component.option({ icon: 'copy' }); + }, 5000); + }, + }) + .appendTo($buttonContainer); + + $('
') + .dxButton({ + icon: 'refresh', + stylingMode: 'text', + hint: 'Regenerate', + onClick: () => { + updateLastMessage(); + regenerate(); + }, + }) + .appendTo($buttonContainer); + + $buttonContainer.appendTo(element); + }, + }).dxChat('instance'); +}); diff --git a/apps/demos/Demos/Chat/AIAndChatbotIntegration/jQuery/styles.css b/apps/demos/Demos/Chat/AIAndChatbotIntegration/jQuery/styles.css new file mode 100644 index 00000000000..4d6ace3854f --- /dev/null +++ b/apps/demos/Demos/Chat/AIAndChatbotIntegration/jQuery/styles.css @@ -0,0 +1,60 @@ +.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, +.dx-chat-messagebubble-text { + display: flex; + flex-direction: column; +} + +.dx-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 .dx-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 ol, +.dx-chat-messagebubble-content ul { + white-space: normal; +} + +.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; +} diff --git a/apps/demos/menuMeta.json b/apps/demos/menuMeta.json index d5bcf0617f8..5586b5df605 100644 --- a/apps/demos/menuMeta.json +++ b/apps/demos/menuMeta.json @@ -2076,6 +2076,19 @@ ] } ] + }, + { + "Name": "AI and Chatbot Integration", + "Equivalents": "AI, Chatbot, Assistant", + "Demos": [ + { + "Title": "AI and Chatbot Integration", + "Name": "AIAndChatbotIntegration", + "Widget": "Chat", + "DemoType": "Web", + "Badge": "New" + } + ] } ] }, diff --git a/apps/demos/testing/etalons/Chat-AIAndChatbotIntegration (fluent.blue.light).png b/apps/demos/testing/etalons/Chat-AIAndChatbotIntegration (fluent.blue.light).png new file mode 100644 index 00000000000..beca57f943a Binary files /dev/null and b/apps/demos/testing/etalons/Chat-AIAndChatbotIntegration (fluent.blue.light).png differ diff --git a/apps/demos/testing/etalons/Chat-AIAndChatbotIntegration (material.blue.light).png b/apps/demos/testing/etalons/Chat-AIAndChatbotIntegration (material.blue.light).png new file mode 100644 index 00000000000..0dbaf25b2d3 Binary files /dev/null and b/apps/demos/testing/etalons/Chat-AIAndChatbotIntegration (material.blue.light).png differ diff --git a/apps/demos/testing/etalons/Chat-AIAndChatbotIntegration.png b/apps/demos/testing/etalons/Chat-AIAndChatbotIntegration.png new file mode 100644 index 00000000000..1d9dcd6a8c2 Binary files /dev/null and b/apps/demos/testing/etalons/Chat-AIAndChatbotIntegration.png differ