Skip to content

Commit 3574f08

Browse files
Chat: refactor AIAndChatbotIntegration React demo (#28663)
1 parent 67546f4 commit 3574f08

File tree

6 files changed

+381
-324
lines changed

6 files changed

+381
-324
lines changed
Lines changed: 31 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
1-
import React, { useState } from 'react';
1+
import React, { useCallback, useState } from 'react';
22
import Chat, { ChatTypes } from 'devextreme-react/chat';
3-
import { AzureOpenAI } from 'openai';
43
import { MessageEnteredEvent } from 'devextreme/ui/chat';
5-
import CustomStore from 'devextreme/data/custom_store';
6-
import DataSource from 'devextreme/data/data_source';
74
import { loadMessages } from 'devextreme/localization';
8-
import {
5+
import {
96
user,
107
assistant,
11-
AzureOpenAIConfig,
12-
REGENERATION_TEXT,
138
CHAT_DISABLED_CLASS,
14-
ALERT_TIMEOUT
159
} from './data.ts';
1610
import Message from './Message.tsx';
17-
18-
const store = [];
19-
const messages = [];
11+
import { dataSource, useApi } from './useApi.ts';
2012

2113
loadMessages({
2214
en: {
@@ -26,148 +18,49 @@ loadMessages({
2618
},
2719
});
2820

29-
const chatService = new AzureOpenAI(AzureOpenAIConfig);
30-
31-
async function getAIResponse(messages) {
32-
const params = {
33-
messages,
34-
model: AzureOpenAIConfig.deployment,
35-
max_tokens: 1000,
36-
temperature: 0.7,
37-
};
38-
39-
const response = await chatService.chat.completions.create(params);
40-
const data = { choices: response.choices };
41-
42-
return data.choices[0].message?.content;
43-
}
44-
45-
function updateLastMessage(text = REGENERATION_TEXT) {
46-
const items = dataSource.items();
47-
const lastMessage = items.at(-1);
48-
49-
dataSource.store().push([{
50-
type: 'update',
51-
key: lastMessage.id,
52-
data: { text },
53-
}]);
54-
}
55-
56-
function renderAssistantMessage(text) {
57-
const message = {
58-
id: Date.now(),
59-
timestamp: new Date(),
60-
author: assistant,
61-
text,
62-
};
63-
64-
dataSource.store().push([{ type: 'insert', data: message }]);
65-
}
21+
export default function App() {
22+
const {
23+
alerts, insertMessage, fetchAIResponse, regenerateLastAIResponse,
24+
} = useApi();
6625

67-
const customStore = new CustomStore({
68-
key: 'id',
69-
load: () => {
70-
return new Promise((resolve) => {
71-
setTimeout(() => {
72-
resolve([...store]);
73-
}, 0);
74-
});
75-
},
76-
insert: (message) => {
77-
return new Promise((resolve) => {
78-
setTimeout(() => {
79-
store.push(message);
80-
resolve(message);
81-
});
82-
});
83-
},
84-
});
26+
const [typingUsers, setTypingUsers] = useState<ChatTypes.User[]>([]);
27+
const [isProcessing, setIsProcessing] = useState(false);
8528

86-
const dataSource = new DataSource({
87-
store: customStore,
88-
paginate: false,
89-
})
29+
const processAIRequest = useCallback(async (message: ChatTypes.Message): Promise<void> => {
30+
setIsProcessing(true);
31+
setTypingUsers([assistant]);
9032

91-
export default function App() {
92-
const [alerts, setAlerts] = useState<ChatTypes.Alert[]>([]);
93-
const [typingUsers, setTypingUsers] = useState<ChatTypes.User[]>([]);
94-
const [classList, setClassList] = useState<string>('');
33+
await fetchAIResponse(message);
9534

96-
function alertLimitReached() {
97-
setAlerts([{
98-
message: 'Request limit reached, try again in a minute.'
99-
}]);
100-
101-
setTimeout(() => {
102-
setAlerts([]);
103-
}, ALERT_TIMEOUT);
104-
}
35+
setTypingUsers([]);
36+
setIsProcessing(false);
37+
}, [fetchAIResponse]);
10538

106-
function toggleDisabledState(disabled: boolean, event = undefined) {
107-
setClassList(disabled ? CHAT_DISABLED_CLASS : '');
39+
const onMessageEntered = useCallback(async ({ message, event }: MessageEnteredEvent): Promise<void> => {
40+
insertMessage({ id: Date.now(), ...message });
10841

109-
if (disabled) {
110-
event?.target.blur();
111-
} else {
112-
event?.target.focus();
113-
}
114-
};
42+
if (!alerts.length) {
43+
(event.target as HTMLElement).blur();
11544

116-
async function processMessageSending(message, event) {
117-
toggleDisabledState(true, event);
45+
await processAIRequest(message);
11846

119-
messages.push({ role: 'user', content: message.text });
120-
setTypingUsers([assistant]);
121-
122-
try {
123-
const aiResponse = await getAIResponse(messages);
124-
125-
setTimeout(() => {
126-
setTypingUsers([]);
127-
messages.push({ role: 'assistant', content: aiResponse });
128-
renderAssistantMessage(aiResponse);
129-
}, 200);
130-
} catch {
131-
setTypingUsers([]);
132-
messages.pop();
133-
alertLimitReached();
134-
} finally {
135-
toggleDisabledState(false, event);
47+
(event.target as HTMLElement).focus();
13648
}
137-
}
138-
139-
async function regenerate() {
140-
toggleDisabledState(true);
49+
}, [insertMessage, alerts.length, processAIRequest]);
14150

142-
try {
143-
const aiResponse = await getAIResponse(messages.slice(0, -1));
51+
const onRegenerateButtonClick = useCallback(async (): Promise<void> => {
52+
setIsProcessing(true);
14453

145-
updateLastMessage(aiResponse);
146-
messages.at(-1).content = aiResponse;
147-
} catch {
148-
updateLastMessage(messages.at(-1).content);
149-
alertLimitReached();
150-
} finally {
151-
toggleDisabledState(false);
152-
}
153-
}
54+
await regenerateLastAIResponse();
15455

155-
function onMessageEntered({ message, event }: MessageEnteredEvent) {
156-
dataSource.store().push([{ type: 'insert', data: { id: Date.now(), ...message } }]);
157-
158-
if (!alerts.length) {
159-
processMessageSending(message, event);
160-
}
161-
}
56+
setIsProcessing(false);
57+
}, [regenerateLastAIResponse]);
16258

163-
function onRegenerateButtonClick() {
164-
updateLastMessage();
165-
regenerate();
166-
}
59+
const messageRender = useCallback(({ message }: { message: ChatTypes.Message }) => <Message text={message.text} onRegenerateButtonClick={onRegenerateButtonClick} />, [onRegenerateButtonClick]);
16760

16861
return (
16962
<Chat
170-
className={classList}
63+
className={isProcessing ? CHAT_DISABLED_CLASS : ''}
17164
dataSource={dataSource}
17265
reloadOnChange={false}
17366
showAvatar={false}
@@ -177,7 +70,7 @@ export default function App() {
17770
onMessageEntered={onMessageEntered}
17871
alerts={alerts}
17972
typingUsers={typingUsers}
180-
messageRender={(data) => Message(data, onRegenerateButtonClick)}
73+
messageRender={messageRender}
18174
/>
18275
);
18376
}
Lines changed: 52 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,67 @@
1-
import React, { useState } from 'react';
1+
import React, { useCallback, useState, FC } from 'react';
22
import Button from 'devextreme-react/button';
33
import { unified } from 'unified';
44
import remarkParse from 'remark-parse';
55
import remarkRehype from 'remark-rehype';
66
import rehypeStringify from 'rehype-stringify';
77
import HTMLReactParser from 'html-react-parser';
88

9+
import { Properties as dxButtonProperties } from 'devextreme/ui/button';
910
import { REGENERATION_TEXT } from './data.ts';
1011

11-
function convertToHtml(value: string) {
12-
const result = unified()
13-
.use(remarkParse)
14-
.use(remarkRehype)
15-
.use(rehypeStringify)
16-
.processSync(value)
17-
.toString();
12+
function convertToHtml(value: string): string {
13+
const result = unified()
14+
.use(remarkParse)
15+
.use(remarkRehype)
16+
.use(rehypeStringify)
17+
.processSync(value)
18+
.toString();
1819

19-
return result;
20+
return result;
2021
}
2122

22-
function Message({ message }, onRegenerateButtonClick) {
23-
const [icon, setIcon] = useState('copy');
24-
25-
if (message.text === REGENERATION_TEXT) {
26-
return <span>{REGENERATION_TEXT}</span>;
27-
}
28-
29-
function onCopyButtonClick() {
30-
navigator.clipboard?.writeText(message.text);
31-
setIcon('check');
32-
33-
setTimeout(() => {
34-
setIcon('copy');
35-
}, 2500);
36-
}
37-
38-
return (
39-
<React.Fragment>
40-
<div
41-
className='dx-chat-messagebubble-text'
42-
>
43-
{HTMLReactParser(convertToHtml(message.text))}
44-
</div>
45-
<div className='dx-bubble-button-container'>
46-
<Button
47-
icon={icon}
48-
stylingMode='text'
49-
hint='Copy'
50-
onClick={onCopyButtonClick}
51-
/>
52-
<Button
53-
icon='refresh'
54-
stylingMode='text'
55-
hint='Regenerate'
56-
onClick={onRegenerateButtonClick}
57-
/>
58-
</div>
59-
</React.Fragment>
60-
)
23+
interface MessageProps {
24+
text: string;
25+
onRegenerateButtonClick: dxButtonProperties['onClick'];
6126
}
6227

28+
const Message: FC<MessageProps> = ({ text, onRegenerateButtonClick }) => {
29+
const [icon, setIcon] = useState('copy');
30+
31+
const onCopyButtonClick = useCallback(() => {
32+
navigator.clipboard?.writeText(text);
33+
setIcon('check');
34+
35+
setTimeout(() => {
36+
setIcon('copy');
37+
}, 2500);
38+
}, [text]);
39+
40+
if (text === REGENERATION_TEXT) {
41+
return <span>{REGENERATION_TEXT}</span>;
42+
}
43+
44+
return (
45+
<React.Fragment>
46+
<div className='dx-chat-messagebubble-text'>
47+
{HTMLReactParser(convertToHtml(text))}
48+
</div>
49+
<div className='dx-bubble-button-container'>
50+
<Button
51+
icon={icon}
52+
stylingMode='text'
53+
hint='Copy'
54+
onClick={onCopyButtonClick}
55+
/>
56+
<Button
57+
icon='refresh'
58+
stylingMode='text'
59+
hint='Regenerate'
60+
onClick={onRegenerateButtonClick}
61+
/>
62+
</div>
63+
</React.Fragment>
64+
);
65+
};
66+
6367
export default Message;

0 commit comments

Comments
 (0)