Skip to content

Commit c8d2f52

Browse files
Merge remote-tracking branch 'my/26_1_global_format' into 26_1_global_format
# Conflicts: # packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts
2 parents 11aa7e1 + 5c5771c commit c8d2f52

200 files changed

Lines changed: 23887 additions & 20987 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
This demo implements the DevExtreme Chat component with [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service) to stream AI-generated responses in real time. As the AI model produces output token by token, the Chat component renders the response incrementally inside a message bubble. A typing indicator appears while the response is being streamed, and the send button transforms into a stop button so that users can cancel an in-progress stream at any time.
2+
3+
The empty Chat displays custom suggestion cards. Clicking a card sends the corresponding prompt directly to the AI without extra user input.
4+
5+
<!--split-->
6+
7+
## Streaming AI Responses
8+
9+
The demo calls the Azure OpenAI Chat Completions API with `stream: true`. Incoming delta chunks are passed through a `createDelayedRenderer` queue that introduces a short display delay between chunks to produce a smooth typing effect. The demo appends each chunk to a growing buffer and updates the assistant message in the data store with every render cycle via a `dataSource.store().push(...)` call.
10+
11+
## Stopping a Stream
12+
13+
The demo creates an `AbortController` before each request and passes its `signal` (`AbortSignal`) to the Azure OpenAI SDK in the request options. When the user clicks the stop button, the demo calls `abortController.abort()` to cancel the in-progress HTTP request.
14+
15+
The [sendButtonOptions](/Documentation/ApiReference/UI_Components/dxChat/Configuration/#sendButtonOptions) property switches the button's `action` property to `'custom'` and the icon to `'stopfilled'` while streaming is active, then reverts to the default (send) configuration once streaming ends.
16+
17+
## Custom Empty View
18+
19+
The Chat component specifies an [emptyViewTemplate](/Documentation/ApiReference/UI_Components/dxChat/Configuration/#emptyViewTemplate) that replaces the default empty state with custom suggestion cards. Clicking a card creates a message and triggers the demo message sending flow directly, bypassing the text input.

apps/demos/Demos/Chat/PromptSuggestions/Angular/app/app.component.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export class AppComponent {
6868

6969
toggleDisabledState(disabled: boolean, event = undefined) {
7070
this.isDisabled = disabled;
71+
this.suggestions = { ...this.suggestions, disabled };
7172

7273
if (disabled) {
7374
event?.target.blur();
@@ -77,6 +78,8 @@ export class AppComponent {
7778
}
7879

7980
async onMessageEntered(e: DxChatTypes.MessageEnteredEvent): Promise<void> {
81+
if (this.isDisabled) return;
82+
8083
if (!this.appService.alerts.length) {
8184
this.toggleDisabledState(true, e.event);
8285
}

apps/demos/Demos/Chat/PromptSuggestions/React/App.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,20 @@ export default function App() {
2828
} = useApi();
2929

3030
const [typingUsers, setTypingUsers] = useState<ChatTypes.User[]>([]);
31-
const [isProcessing, setIsProcessing] = useState<boolean>(false);
31+
const [isDisabled, setIsDisabled] = useState<boolean>(false);
3232
const [inputFieldText, setInputFieldText] = useState<string>('');
3333
const [suggestionList, setSuggestionList] = useState(suggestionItems);
3434
const sendImmediately = useRef<boolean>(false);
3535
const hideAfterUse = useRef<boolean>(false);
3636

3737
const processAIRequest = useCallback(async (message: ChatTypes.Message): Promise<void> => {
38-
setIsProcessing(true);
38+
setIsDisabled(true);
3939
setTypingUsers([assistant]);
4040

4141
await fetchAIResponse(message);
4242

4343
setTypingUsers([]);
44-
setIsProcessing(false);
44+
setIsDisabled(false);
4545
}, [fetchAIResponse]);
4646

4747
const onSuggestionClick = useCallback((e: { itemData?: { text: string; prompt: string } }) => {
@@ -64,9 +64,10 @@ export default function App() {
6464
}
6565
}, [alerts.length, insertMessage, processAIRequest]);
6666

67-
const suggestions = { items: suggestionList, onItemClick: onSuggestionClick };
67+
const suggestions = { items: suggestionList, onItemClick: onSuggestionClick, disabled: isDisabled };
6868

6969
const onMessageEntered = useCallback(async ({ message, event }: ChatTypes.MessageEnteredEvent): Promise<void> => {
70+
if (isDisabled) return;
7071
insertMessage({ id: Date.now(), ...message });
7172

7273
if (!alerts.length) {
@@ -76,7 +77,7 @@ export default function App() {
7677

7778
(event?.target as HTMLElement).focus();
7879
}
79-
}, [insertMessage, alerts.length, processAIRequest]);
80+
}, [isDisabled, insertMessage, alerts.length, processAIRequest]);
8081

8182
const onInputFieldTextChanged = useCallback((e: ChatTypes.InputFieldTextChangedEvent) => {
8283
setInputFieldText(e?.value ?? '');
@@ -87,7 +88,7 @@ export default function App() {
8788
return (
8889
<>
8990
<Chat
90-
className={isProcessing ? CHAT_DISABLED_CLASS : ''}
91+
className={isDisabled ? CHAT_DISABLED_CLASS : ''}
9192
dataSource={dataSource}
9293
reloadOnChange={false}
9394
showAvatar={false}

apps/demos/Demos/Chat/PromptSuggestions/ReactJs/App.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,18 @@ loadMessages({
1919
export default function App() {
2020
const { alerts, insertMessage, fetchAIResponse } = useApi();
2121
const [typingUsers, setTypingUsers] = useState([]);
22-
const [isProcessing, setIsProcessing] = useState(false);
22+
const [isDisabled, setIsDisabled] = useState(false);
2323
const [inputFieldText, setInputFieldText] = useState('');
2424
const [suggestionList, setSuggestionList] = useState(suggestionItems);
2525
const sendImmediately = useRef(false);
2626
const hideAfterUse = useRef(false);
2727
const processAIRequest = useCallback(
2828
async (message) => {
29-
setIsProcessing(true);
29+
setIsDisabled(true);
3030
setTypingUsers([assistant]);
3131
await fetchAIResponse(message);
3232
setTypingUsers([]);
33-
setIsProcessing(false);
33+
setIsDisabled(false);
3434
},
3535
[fetchAIResponse],
3636
);
@@ -54,17 +54,22 @@ export default function App() {
5454
},
5555
[alerts.length, insertMessage, processAIRequest],
5656
);
57-
const suggestions = { items: suggestionList, onItemClick: onSuggestionClick };
57+
const suggestions = {
58+
items: suggestionList,
59+
onItemClick: onSuggestionClick,
60+
disabled: isDisabled,
61+
};
5862
const onMessageEntered = useCallback(
5963
async ({ message, event }) => {
64+
if (isDisabled) return;
6065
insertMessage({ id: Date.now(), ...message });
6166
if (!alerts.length) {
6267
(event?.target).blur();
6368
await processAIRequest(message);
6469
(event?.target).focus();
6570
}
6671
},
67-
[insertMessage, alerts.length, processAIRequest],
72+
[isDisabled, insertMessage, alerts.length, processAIRequest],
6873
);
6974
const onInputFieldTextChanged = useCallback((e) => {
7075
setInputFieldText(e?.value ?? '');
@@ -73,7 +78,7 @@ export default function App() {
7378
return (
7479
<>
7580
<Chat
76-
className={isProcessing ? CHAT_DISABLED_CLASS : ''}
81+
className={isDisabled ? CHAT_DISABLED_CLASS : ''}
7782
dataSource={dataSource}
7883
reloadOnChange={false}
7984
showAvatar={false}

apps/demos/Demos/Chat/PromptSuggestions/Vue/App.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ onBeforeMount(() => {
8181
8282
function toggleDisabledState(disabled: boolean, event?: Events.EventObject): void {
8383
isDisabled.value = disabled;
84+
suggestions.value = { ...suggestions.value, disabled };
8485
8586
if (disabled) {
8687
(event?.target as HTMLElement)?.blur();
@@ -136,6 +137,8 @@ function alertLimitReached(): void {
136137
}
137138
138139
function onMessageEntered({ message, event }: DxChatTypes.MessageEnteredEvent): void {
140+
if (isDisabled.value) return;
141+
139142
dataSource.store().push([{ type: 'insert', data: { id: Date.now(), ...message } }]);
140143
141144
if (!alerts.value.length) {

apps/demos/Demos/Chat/PromptSuggestions/jQuery/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ $(() => {
4747
}
4848

4949
function toggleDisabledState(disabled, event) {
50+
isDisabled = disabled;
51+
instance.option({ suggestions: { disabled } });
5052
instance.element().toggleClass(CHAT_DISABLED_CLASS, disabled);
5153

5254
if (disabled) {
@@ -139,6 +141,7 @@ $(() => {
139141
paginate: false,
140142
});
141143

144+
let isDisabled = false;
142145
let sendImmediately = false;
143146
let hideAfterUse = false;
144147

@@ -176,6 +179,8 @@ $(() => {
176179
speechToTextEnabled: true,
177180
suggestions,
178181
onMessageEntered: (e) => {
182+
if (isDisabled) return;
183+
179184
const { message, event } = e;
180185

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

0 commit comments

Comments
 (0)