Skip to content

Commit e77d683

Browse files
author
Ruslan Farkhutdinov
committed
Chat: Message Streaming demo - add demos for React, Vue & Angular
1 parent d12a590 commit e77d683

31 files changed

+2114
-0
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Injectable } from '@angular/core';
2+
import { AzureOpenAI, OpenAI } from 'openai';
3+
4+
export type AIMessage = (
5+
OpenAI.ChatCompletionUserMessageParam
6+
| OpenAI.ChatCompletionSystemMessageParam
7+
| OpenAI.ChatCompletionAssistantMessageParam) & {
8+
content: string;
9+
};
10+
11+
export interface GetAIResponseStreamOptions {
12+
onAborted: () => void;
13+
onDelta: (delta: string) => void;
14+
onError?: (error: unknown) => void;
15+
signal: AbortSignal;
16+
}
17+
18+
const AzureOpenAIConfig = {
19+
dangerouslyAllowBrowser: true,
20+
deployment: 'gpt-4o-mini',
21+
apiVersion: '2024-02-01',
22+
endpoint: 'https://public-api.devexpress.com/demo-openai',
23+
apiKey: 'DEMO',
24+
};
25+
26+
@Injectable()
27+
export class AiService {
28+
chatService: AzureOpenAI;
29+
30+
constructor() {
31+
this.chatService = new AzureOpenAI(AzureOpenAIConfig);
32+
}
33+
34+
async getAIResponseStream(
35+
messages: AIMessage[],
36+
{ onAborted, onDelta, onError, signal }: GetAIResponseStreamOptions,
37+
): Promise<void> {
38+
const params = {
39+
messages,
40+
model: AzureOpenAIConfig.deployment,
41+
max_tokens: 1000,
42+
temperature: 0.7,
43+
stream: true as const,
44+
};
45+
46+
try {
47+
const stream = await this.chatService.chat.completions.create(params, { signal });
48+
49+
for await (const event of stream) {
50+
const delta = event.choices?.[0]?.delta?.content;
51+
if (delta) {
52+
onDelta(delta);
53+
}
54+
}
55+
56+
if (signal.aborted) {
57+
onAborted();
58+
}
59+
} catch (e) {
60+
if ((e as Error)?.name === 'AbortError') {
61+
onAborted();
62+
}
63+
onError?.(e);
64+
throw e;
65+
}
66+
}
67+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
.demo-container {
2+
display: flex;
3+
justify-content: center;
4+
}
5+
6+
::ng-deep .dx-chat {
7+
max-width: 900px;
8+
}
9+
10+
::ng-deep .dx-chat-messagelist-empty-image {
11+
display: none;
12+
}
13+
14+
::ng-deep .dx-chat-messagelist-empty-message {
15+
font-size: var(--dx-font-size-heading-5);
16+
}
17+
18+
::ng-deep .dx-chat-messagebubble-content,
19+
::ng-deep .chat-messagebubble-text {
20+
display: flex;
21+
flex-direction: column;
22+
}
23+
24+
::ng-deep .dx-template-wrapper > div > p:first-child {
25+
margin-top: 0;
26+
}
27+
28+
::ng-deep .dx-template-wrapper > div > p:last-child {
29+
margin-bottom: 0;
30+
}
31+
32+
::ng-deep .dx-chat-messagebubble-content h1,
33+
::ng-deep .dx-chat-messagebubble-content h2,
34+
::ng-deep .dx-chat-messagebubble-content h3,
35+
::ng-deep .dx-chat-messagebubble-content h4,
36+
::ng-deep .dx-chat-messagebubble-content h5,
37+
::ng-deep .dx-chat-messagebubble-content h6 {
38+
font-size: revert;
39+
font-weight: revert;
40+
}
41+
42+
::ng-deep .dx-chat-suggestion-cards {
43+
display: flex;
44+
flex-wrap: wrap;
45+
justify-content: center;
46+
gap: 16px;
47+
margin-top: 32px;
48+
width: 100%;
49+
}
50+
51+
::ng-deep .dx-chat-suggestion-card {
52+
border-radius: 12px;
53+
padding: 16px;
54+
border: 1px solid #EBEBEB;
55+
background: #FAFAFA;
56+
display: flex;
57+
flex-direction: column;
58+
align-items: flex-start;
59+
gap: 8px;
60+
flex: 0 1 230px;
61+
max-width: 230px;
62+
text-align: left;
63+
cursor: pointer;
64+
transition: 0.2s ease;
65+
width: 230px;
66+
}
67+
68+
::ng-deep .dx-chat-suggestion-card:hover {
69+
border: 1px solid #E0E0E0;
70+
background: #F5F5F5;
71+
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.04), 0 4px 24px 0 rgba(0, 0, 0, 0.02);
72+
}
73+
74+
::ng-deep .dx-chat-suggestion-card-title {
75+
color: #242424;
76+
font-size: 12px;
77+
font-weight: 600;
78+
line-height: 16px;
79+
}
80+
81+
::ng-deep .dx-chat-suggestion-card-prompt {
82+
color: #616161;
83+
font-size: 12px;
84+
font-weight: 400;
85+
line-height: 16px;
86+
}
87+
88+
::ng-deep .dx-chat-messagelist-empty-prompt {
89+
margin-top: 4px;
90+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<div class="demo-container">
2+
<dx-chat
3+
[dataSource]="dataSource"
4+
[reloadOnChange]="false"
5+
[showAvatar]="false"
6+
[showDayHeaders]="false"
7+
[user]="user"
8+
height="710"
9+
[typingUsers]="typingUsers$ | async"
10+
[alerts]="alerts$ | async"
11+
[sendButtonOptions]="sendButtonOptions$ | async"
12+
[speechToTextEnabled]="true"
13+
(onMessageEntered)="onMessageEntered($event)"
14+
messageTemplate="messageTemplate"
15+
emptyViewTemplate="emptyViewTemplate"
16+
>
17+
<div *dxTemplate="let data of 'messageTemplate'">
18+
<div
19+
class="chat-messagebubble-text"
20+
[innerHTML]="convertToHtml(data.message)"
21+
></div>
22+
</div>
23+
24+
<div *dxTemplate="let data of 'emptyViewTemplate'">
25+
<div class="dx-chat-messagelist-empty-message">{{ data.texts.message }}</div>
26+
<div class="dx-chat-messagelist-empty-prompt">{{ data.texts.prompt }}</div>
27+
<div class="dx-chat-suggestion-cards">
28+
@for (card of suggestionCards; track card.title) {
29+
<button
30+
type="button"
31+
class="dx-chat-suggestion-card"
32+
(click)="onSuggestionClick(card.prompt)"
33+
>
34+
<div class="dx-chat-suggestion-card-title">{{ card.title }}</div>
35+
<div class="dx-chat-suggestion-card-prompt">{{ card.description }}</div>
36+
</button>
37+
}
38+
</div>
39+
</div>
40+
</dx-chat>
41+
</div>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { bootstrapApplication } from '@angular/platform-browser';
2+
import { Component, enableProdMode, provideZoneChangeDetection } from '@angular/core';
3+
import { AsyncPipe } from '@angular/common';
4+
import { DxChatModule } from 'devextreme-angular';
5+
import type { DxChatTypes } from 'devextreme-angular/ui/chat';
6+
import { Observable, map } from 'rxjs';
7+
import { loadMessages } from 'devextreme-angular/common/core/localization';
8+
import { DataSource } from 'devextreme-angular/common/data';
9+
import { AppService, suggestionCards } from './app.service';
10+
import { AiService } from './ai/ai.service';
11+
12+
if (!/localhost/.test(document.location.host)) {
13+
enableProdMode();
14+
}
15+
16+
let modulePrefix = '';
17+
// @ts-ignore
18+
if (window && window.config?.packageConfigPaths) {
19+
modulePrefix = '/app';
20+
}
21+
22+
@Component({
23+
selector: 'demo-app',
24+
templateUrl: `.${modulePrefix}/app.component.html`,
25+
styleUrls: [`.${modulePrefix}/app.component.css`],
26+
imports: [
27+
DxChatModule,
28+
AsyncPipe,
29+
],
30+
})
31+
export class AppComponent {
32+
dataSource: DataSource;
33+
34+
user: DxChatTypes.User;
35+
36+
typingUsers$: Observable<DxChatTypes.User[]>;
37+
38+
alerts$: Observable<DxChatTypes.Alert[]>;
39+
40+
sendButtonOptions$: Observable<DxChatTypes.SendButtonProperties>;
41+
42+
readonly suggestionCards = suggestionCards;
43+
44+
constructor(private readonly appService: AppService) {
45+
loadMessages(this.appService.getDictionary());
46+
47+
this.dataSource = this.appService.dataSource;
48+
this.user = this.appService.user;
49+
this.alerts$ = this.appService.alerts$;
50+
this.typingUsers$ = this.appService.typingUsers$;
51+
52+
this.sendButtonOptions$ = this.appService.isStreaming$.pipe(
53+
map((isStreaming) => (isStreaming ? {
54+
action: 'custom' as const,
55+
icon: 'stopfilled',
56+
onClick: () => this.appService.stopStreaming(),
57+
} : {
58+
action: 'send' as const,
59+
icon: 'arrowright',
60+
onClick: () => {},
61+
})),
62+
);
63+
}
64+
65+
convertToHtml(message: DxChatTypes.Message): string {
66+
return this.appService.convertToHtml(message.text);
67+
}
68+
69+
onMessageEntered(e: DxChatTypes.MessageEnteredEvent): void {
70+
this.appService.onMessageEntered(e);
71+
}
72+
73+
onSuggestionClick(prompt: string): void {
74+
this.appService.sendSuggestion(prompt);
75+
}
76+
}
77+
78+
bootstrapApplication(AppComponent, {
79+
providers: [
80+
provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }),
81+
AppService,
82+
AiService,
83+
],
84+
});

0 commit comments

Comments
 (0)