Skip to content

Commit

Permalink
Chat: Add Vue AI And Chatbot Integration Demo
Browse files Browse the repository at this point in the history
  • Loading branch information
Zedwag authored Dec 10, 2024
1 parent 9ca0343 commit 733a755
Show file tree
Hide file tree
Showing 5 changed files with 376 additions and 2 deletions.
267 changes: 267 additions & 0 deletions apps/demos/Demos/Chat/AIAndChatbotIntegration/Vue/App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
<template>
<div
class="chat-container"
:class="{'dx-chat-disabled' : isDisabled == true }"
>
<DxChat
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="dx-chat-messagebubble-text"
>
</div>
<div class="dx-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 from 'devextreme-vue/chat';
import DxButton from 'devextreme-vue/button';
import { loadMessages } from 'devextreme/localization';
import { AzureOpenAI } from 'openai';
import {
dictionary,
store,
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,
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) {
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 }) {
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,
.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;
}
.dx-chat-disabled .dx-chat-messagebox {
opacity: 0.5;
pointer-events: none;
}
</style>
74 changes: 74 additions & 0 deletions apps/demos/Demos/Chat/AIAndChatbotIntegration/Vue/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import CustomStore from "devextreme/data/custom_store";
import DataSource from "devextreme/data/data_source";
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';

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 = 'dx-chat-disabled';
export const ALERT_TIMEOUT = 1000 * 60;

export const user = {
id: 'c94c0e76-fb49-4b9b-8f07-9f93ed93b4f3',
name: 'John Doe',
};

export const assistant = {
id: 'assistant',
name: 'Virtual Assistant',
};

export const store = [];
export const messages = [];

const customStore = new CustomStore({
key: 'id',
load: () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([...store]);
}, 0);
});
},
insert: (message) => {
return 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(rehypeStringify)
.processSync(value)
.toString();

return result;
}
29 changes: 29 additions & 0 deletions apps/demos/Demos/Chat/AIAndChatbotIntegration/Vue/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!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="../../../../node_modules/devextreme-dist/css/dx.light.css" />

<script type="module">
import * as vueCompilerSFC from "../../../../node_modules/@vue/compiler-sfc/dist/compiler-sfc.esm-browser.js";

window.vueCompilerSFC = vueCompilerSFC;
</script>
<script src="../../../../node_modules/typescript/lib/typescript.js"></script>
<script src="../../../../node_modules/core-js/client/shim.min.js"></script>
<script src="../../../../node_modules/systemjs/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>
4 changes: 4 additions & 0 deletions apps/demos/Demos/Chat/AIAndChatbotIntegration/Vue/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createApp } from 'vue';
import App from './App.vue';

createApp(App).mount('#app');
4 changes: 2 additions & 2 deletions apps/demos/testing/widgets/tagbox/GroupedItems.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ runManualTest('TagBox', 'GroupedItems', ['jQuery', 'React', 'Vue', 'Angular'], (
await testScreenshot(t, takeScreenshot, 'tagbox_groupeditems_first_opened.png');

await t
.pressKey('esc')
.pressKey('esc');

await t
.pressKey('tab')
Expand All @@ -28,7 +28,7 @@ runManualTest('TagBox', 'GroupedItems', ['jQuery', 'React', 'Vue', 'Angular'], (
await testScreenshot(t, takeScreenshot, 'tagbox_groupeditems_second_opened.png');

await t
.pressKey('esc')
.pressKey('esc');

await t
.pressKey('tab')
Expand Down

0 comments on commit 733a755

Please sign in to comment.