Skip to content

Commit

Permalink
Merge branch '24_2' into 24_2_remove_widget_hack
Browse files Browse the repository at this point in the history
  • Loading branch information
EugeniyKiyashko committed Jan 7, 2025
2 parents 8c32023 + 3574f08 commit d5d83c5
Show file tree
Hide file tree
Showing 45 changed files with 968 additions and 451 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/testcafe_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ jobs:
[ "${{ matrix.ARGS.platform }}" != "" ] && PLATFORM="--platform ${{ matrix.ARGS.platform }}"
all_args="--browsers=chrome:devextreme-shr2 --componentFolder ${{ matrix.ARGS.componentFolder }} $CONCURRENCY $INDICES $PLATFORM $THEME"
echo "$all_args"
pnpx nx test $all_args
pnpm run test $all_args
- name: Copy compared screenshot artifacts
if: ${{ failure() }}
Expand Down
169 changes: 31 additions & 138 deletions apps/demos/Demos/Chat/AIAndChatbotIntegration/React/App.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,14 @@
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import Chat, { ChatTypes } from 'devextreme-react/chat';
import { AzureOpenAI } from 'openai';
import { MessageEnteredEvent } from 'devextreme/ui/chat';
import CustomStore from 'devextreme/data/custom_store';
import DataSource from 'devextreme/data/data_source';
import { loadMessages } from 'devextreme/localization';
import {
import {
user,
assistant,
AzureOpenAIConfig,
REGENERATION_TEXT,
CHAT_DISABLED_CLASS,
ALERT_TIMEOUT
} from './data.ts';
import Message from './Message.tsx';

const store = [];
const messages = [];
import { dataSource, useApi } from './useApi.ts';

loadMessages({
en: {
Expand All @@ -26,148 +18,49 @@ loadMessages({
},
});

const chatService = new AzureOpenAI(AzureOpenAIConfig);

async function getAIResponse(messages) {
const params = {
messages,
model: AzureOpenAIConfig.deployment,
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 }]);
}
export default function App() {
const {
alerts, insertMessage, fetchAIResponse, regenerateLastAIResponse,
} = useApi();

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 [typingUsers, setTypingUsers] = useState<ChatTypes.User[]>([]);
const [isProcessing, setIsProcessing] = useState(false);

const dataSource = new DataSource({
store: customStore,
paginate: false,
})
const processAIRequest = useCallback(async (message: ChatTypes.Message): Promise<void> => {
setIsProcessing(true);
setTypingUsers([assistant]);

export default function App() {
const [alerts, setAlerts] = useState<ChatTypes.Alert[]>([]);
const [typingUsers, setTypingUsers] = useState<ChatTypes.User[]>([]);
const [classList, setClassList] = useState<string>('');
await fetchAIResponse(message);

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

setTimeout(() => {
setAlerts([]);
}, ALERT_TIMEOUT);
}
setTypingUsers([]);
setIsProcessing(false);
}, [fetchAIResponse]);

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

if (disabled) {
event?.target.blur();
} else {
event?.target.focus();
}
};
if (!alerts.length) {
(event.target as HTMLElement).blur();

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

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);
(event.target as HTMLElement).focus();
}
}

async function regenerate() {
toggleDisabledState(true);
}, [insertMessage, alerts.length, processAIRequest]);

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

updateLastMessage(aiResponse);
messages.at(-1).content = aiResponse;
} catch {
updateLastMessage(messages.at(-1).content);
alertLimitReached();
} finally {
toggleDisabledState(false);
}
}
await regenerateLastAIResponse();

function onMessageEntered({ message, event }: MessageEnteredEvent) {
dataSource.store().push([{ type: 'insert', data: { id: Date.now(), ...message } }]);

if (!alerts.length) {
processMessageSending(message, event);
}
}
setIsProcessing(false);
}, [regenerateLastAIResponse]);

function onRegenerateButtonClick() {
updateLastMessage();
regenerate();
}
const messageRender = useCallback(({ message }: { message: ChatTypes.Message }) => <Message text={message.text} onRegenerateButtonClick={onRegenerateButtonClick} />, [onRegenerateButtonClick]);

return (
<Chat
className={classList}
className={isProcessing ? CHAT_DISABLED_CLASS : ''}
dataSource={dataSource}
reloadOnChange={false}
showAvatar={false}
Expand All @@ -177,7 +70,7 @@ export default function App() {
onMessageEntered={onMessageEntered}
alerts={alerts}
typingUsers={typingUsers}
messageRender={(data) => Message(data, onRegenerateButtonClick)}
messageRender={messageRender}
/>
);
}
100 changes: 52 additions & 48 deletions apps/demos/Demos/Chat/AIAndChatbotIntegration/React/Message.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,67 @@
import React, { useState } from 'react';
import React, { useCallback, useState, FC } 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 { Properties as dxButtonProperties } from 'devextreme/ui/button';
import { REGENERATION_TEXT } from './data.ts';

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

return result;
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>
)
interface MessageProps {
text: string;
onRegenerateButtonClick: dxButtonProperties['onClick'];
}

const Message: FC<MessageProps> = ({ text, onRegenerateButtonClick }) => {
const [icon, setIcon] = useState('copy');

const onCopyButtonClick = useCallback(() => {
navigator.clipboard?.writeText(text);
setIcon('check');

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

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

return (
<React.Fragment>
<div className='dx-chat-messagebubble-text'>
{HTMLReactParser(convertToHtml(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;
Loading

0 comments on commit d5d83c5

Please sign in to comment.