Skip to content

Commit

Permalink
Chat - add ai chat bot demo for ReactJs (#28521)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zedwag authored Dec 9, 2024
1 parent c2f2b0b commit 234ec88
Show file tree
Hide file tree
Showing 8 changed files with 383 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useRef } from 'react';
import React, { useState } from 'react';
import Button from 'devextreme-react/button';
import { unified } from 'unified';
import remarkParse from 'remark-parse';
Expand Down
3 changes: 0 additions & 3 deletions apps/demos/Demos/Chat/AIAndChatbotIntegration/React/data.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { ChatTypes } from 'devextreme-react/chat';

const date = new Date();
date.setHours(0, 0, 0, 0);

export const AzureOpenAIConfig = {
dangerouslyAllowBrowser: true,
deployment: 'gpt-4o-mini',
Expand Down
181 changes: 181 additions & 0 deletions apps/demos/Demos/Chat/AIAndChatbotIntegration/ReactJs/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import React, { useState } from 'react';
import Chat from 'devextreme-react/chat';
import { AzureOpenAI } from 'openai';
import CustomStore from 'devextreme/data/custom_store';
import DataSource from 'devextreme/data/data_source';
import { loadMessages } from 'devextreme/localization';
import {
user,
assistant,
AzureOpenAIConfig,
REGENERATION_TEXT,
CHAT_DISABLED_CLASS,
ALERT_TIMEOUT
} from './data.js';
import Message from './Message.js';

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

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(AzureOpenAIConfig);

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 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 }]);
}

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);
});
});
},
});

const dataSource = new DataSource({
store: customStore,
paginate: false,
})

export default function App() {
const [alerts, setAlerts] = useState([]);
const [typingUsers, setTypingUsers] = useState([]);
const [classList, setClassList] = useState('');

function alertLimitReached() {
setAlerts([{
message: 'Request limit reached, try again in a minute.'
}]);

setTimeout(() => {
setAlerts([]);
}, ALERT_TIMEOUT);
}

function toggleDisabledState(disabled, event = undefined) {
setClassList(disabled ? CHAT_DISABLED_CLASS : '');

if (disabled) {
event?.target.blur();
} else {
event?.target.focus();
}
};

async function processMessageSending(message, event) {
toggleDisabledState(true, event);

messages.push({ role: 'user', content: message.text });
setTypingUsers([assistant]);

try {
const aiResponse = await getAIResponse(messages);

setTimeout(() => {
setTypingUsers([]);
messages.push({ role: 'assistant', content: aiResponse });
renderAssistantMessage(aiResponse);
}, 200);
} catch {
setTypingUsers([]);
messages.pop();
alertLimitReached();
} finally {
toggleDisabledState(false, event);
}
}

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.length) {
processMessageSending(message, event);
}
}

function onRegenerateButtonClick() {
updateLastMessage();
regenerate();
}

return (
<Chat
className={classList}
dataSource={dataSource}
reloadOnChange={false}
showAvatar={false}
showDayHeaders={false}
user={user}
height={710}
onMessageEntered={onMessageEntered}
alerts={alerts}
typingUsers={typingUsers}
messageRender={(data) => Message(data, onRegenerateButtonClick)}
/>
);
}
63 changes: 63 additions & 0 deletions apps/demos/Demos/Chat/AIAndChatbotIntegration/ReactJs/Message.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useState } from 'react';
import Button from 'devextreme-react/button';
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import HTMLReactParser from 'html-react-parser';

import { REGENERATION_TEXT } from './data.js';

function convertToHtml(value) {
const result = unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeStringify)
.processSync(value)
.toString();

return result;
}

function Message({ message }, onRegenerateButtonClick) {
const [icon, setIcon] = useState('copy');

if (message.text === REGENERATION_TEXT) {
return <span>{REGENERATION_TEXT}</span>;
}

function onCopyButtonClick() {
navigator.clipboard?.writeText(message.text);
setIcon('check');

setTimeout(() => {
setIcon('copy');
}, 2500);
}

return (
<React.Fragment>
<div
className='dx-chat-messagebubble-text'
>
{HTMLReactParser(convertToHtml(message.text))}
</div>
<div className='dx-bubble-button-container'>
<Button
icon={icon}
stylingMode='text'
hint='Copy'
onClick={onCopyButtonClick}
/>
<Button
icon='refresh'
stylingMode='text'
hint='Regenerate'
onClick={onRegenerateButtonClick}
/>
</div>
</React.Fragment>
)
}

export default Message;
20 changes: 20 additions & 0 deletions apps/demos/Demos/Chat/AIAndChatbotIntegration/ReactJs/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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: 'user',
};

export const assistant = {
id: 'assistant',
name: 'Virtual Assistant',
};
44 changes: 44 additions & 0 deletions apps/demos/Demos/Chat/AIAndChatbotIntegration/ReactJs/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<!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"
/>
<link
rel="stylesheet"
type="text/css"
href="styles.css"
/>

<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.js");
</script>
</head>

<body class="dx-viewport">
<div class="demo-container">
<div id="app"></div>
</div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';

import App from './App.js';

ReactDOM.render(
<App />,
document.getElementById('app'),
);
Loading

0 comments on commit 234ec88

Please sign in to comment.