Skip to content

Commit d0a1ad6

Browse files
r-farkhutdinovRuslan FarkhutdinovCopilot
authored
Chat: Add PromptSuggestions demos (#33286)
Signed-off-by: Ruslan Farkhutdinov <fr3ddy4@yandex.ru> Co-authored-by: Ruslan Farkhutdinov <ruslan.farkhutdinov@devexpress.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 88d90bb commit d0a1ad6

34 files changed

+2316
-0
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Injectable } from '@angular/core';
2+
import { AzureOpenAI, OpenAI } from 'openai';
3+
import { type AIResponse } from 'devextreme-angular/common/ai-integration';
4+
5+
export type AIMessage = (
6+
OpenAI.ChatCompletionUserMessageParam
7+
| OpenAI.ChatCompletionSystemMessageParam
8+
| OpenAI.ChatCompletionAssistantMessageParam) & {
9+
content: string;
10+
};
11+
12+
const AzureOpenAIConfig = {
13+
dangerouslyAllowBrowser: true,
14+
deployment: 'gpt-4o-mini',
15+
apiVersion: '2024-02-01',
16+
endpoint: 'https://public-api.devexpress.com/demo-openai',
17+
apiKey: 'DEMO',
18+
};
19+
20+
@Injectable()
21+
export class AiService {
22+
chatService: AzureOpenAI;
23+
24+
constructor() {
25+
this.chatService = new AzureOpenAI(AzureOpenAIConfig);
26+
}
27+
28+
async getAIResponse(messages: AIMessage[]): Promise<AIResponse> {
29+
const params = {
30+
messages,
31+
model: AzureOpenAIConfig.deployment,
32+
max_tokens: 1000,
33+
temperature: 0.7,
34+
};
35+
36+
const response = await this.chatService.chat.completions.create(params);
37+
const data = { choices: response.choices };
38+
39+
return data.choices[0].message?.content;
40+
}
41+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
.demo-container {
2+
display: flex;
3+
flex-direction: column;
4+
align-items: center;
5+
gap: 20px;
6+
}
7+
8+
.options-container {
9+
padding: 20px;
10+
display: flex;
11+
flex-direction: column;
12+
min-width: 280px;
13+
background-color: rgba(191, 191, 191, 0.15);
14+
gap: 16px;
15+
width: 100%;
16+
max-width: 900px;
17+
box-sizing: border-box;
18+
}
19+
20+
.options {
21+
display: flex;
22+
align-items: center;
23+
gap: 24px;
24+
}
25+
26+
.option {
27+
display: flex;
28+
align-items: center;
29+
gap: 8px;
30+
}
31+
32+
.caption {
33+
font-size: var(--dx-font-size-sm);
34+
font-weight: 500;
35+
}
36+
37+
::ng-deep .dx-chat {
38+
max-width: 900px;
39+
}
40+
41+
::ng-deep .dx-chat-messagelist-empty-image {
42+
display: none;
43+
}
44+
45+
::ng-deep .dx-chat-messagelist-empty-message {
46+
font-size: var(--dx-font-size-heading-5);
47+
}
48+
49+
::ng-deep .dx-template-wrapper > div > p:first-child {
50+
margin-top: 0;
51+
}
52+
53+
::ng-deep .dx-template-wrapper > div > p:last-child {
54+
margin-bottom: 0;
55+
}
56+
57+
::ng-deep .dx-chat-messagebubble-content h1,
58+
::ng-deep .dx-chat-messagebubble-content h2,
59+
::ng-deep .dx-chat-messagebubble-content h3,
60+
::ng-deep .dx-chat-messagebubble-content h4,
61+
::ng-deep .dx-chat-messagebubble-content h5,
62+
::ng-deep .dx-chat-messagebubble-content h6 {
63+
font-size: revert;
64+
font-weight: revert;
65+
}
66+
67+
::ng-deep .chat-disabled .dx-chat-messagebox {
68+
opacity: 0.5;
69+
pointer-events: none;
70+
}
71+
72+
::ng-deep .dx-chat-suggestions .dx-button {
73+
max-width: 255px;
74+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<div class="demo-container">
2+
<dx-chat
3+
[class.chat-disabled]="isDisabled"
4+
[dataSource]="dataSource"
5+
[reloadOnChange]="false"
6+
[showAvatar]="false"
7+
[showDayHeaders]="false"
8+
[user]="user"
9+
height="520"
10+
[typingUsers]="typingUsers$ | async"
11+
[alerts]="alerts$ | async"
12+
[speechToTextEnabled]="true"
13+
[inputFieldText]="inputFieldText"
14+
[suggestions]="suggestions"
15+
(onMessageEntered)="onMessageEntered($event)"
16+
(onInputFieldTextChanged)="onInputFieldTextChanged($event)"
17+
messageTemplate="messageTemplate"
18+
>
19+
<div *dxTemplate="let data of 'messageTemplate'">
20+
<div
21+
class="chat-messagebubble-text"
22+
[innerHTML]="convertToHtml(data.message)"
23+
></div>
24+
</div>
25+
</dx-chat>
26+
27+
<div class="options-container">
28+
<div class="caption">Suggestion Options</div>
29+
<div class="options">
30+
<div class="option">
31+
<dx-switch
32+
[value]="sendImmediately"
33+
(onValueChanged)="onSendImmediatelyChanged($event)"
34+
></dx-switch>
35+
<span>Send Immediately</span>
36+
</div>
37+
<div class="option">
38+
<dx-switch
39+
[value]="hideAfterUse"
40+
(onValueChanged)="onHideAfterUseChanged($event)"
41+
></dx-switch>
42+
<span>Hide After Use</span>
43+
</div>
44+
</div>
45+
</div>
46+
</div>
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { bootstrapApplication } from '@angular/platform-browser';
2+
import { Component, enableProdMode, provideZoneChangeDetection } from '@angular/core';
3+
import { AsyncPipe } from '@angular/common';
4+
import { DxChatModule, DxSwitchModule } from 'devextreme-angular';
5+
import type { DxChatTypes } from 'devextreme-angular/ui/chat';
6+
import type { DxSwitchTypes } from 'devextreme-angular/ui/switch';
7+
import { Observable } from 'rxjs';
8+
import { loadMessages } from 'devextreme-angular/common/core/localization';
9+
import { DataSource } from 'devextreme-angular/common/data';
10+
import { AppService } from './app.service';
11+
import { AiService } from './ai/ai.service';
12+
13+
if (!/localhost/.test(document.location.host)) {
14+
enableProdMode();
15+
}
16+
17+
let modulePrefix = '';
18+
// @ts-ignore
19+
if (window && window.config?.packageConfigPaths) {
20+
modulePrefix = '/app';
21+
}
22+
23+
@Component({
24+
selector: 'demo-app',
25+
templateUrl: `.${modulePrefix}/app.component.html`,
26+
styleUrls: [`.${modulePrefix}/app.component.css`],
27+
imports: [
28+
DxChatModule,
29+
DxSwitchModule,
30+
AsyncPipe,
31+
],
32+
})
33+
export class AppComponent {
34+
dataSource: DataSource;
35+
36+
user: DxChatTypes.User;
37+
38+
typingUsers$: Observable<DxChatTypes.User[]>;
39+
40+
alerts$: Observable<DxChatTypes.Alert[]>;
41+
42+
isDisabled = false;
43+
44+
inputFieldText = '';
45+
46+
sendImmediately = false;
47+
48+
hideAfterUse = false;
49+
50+
suggestions: DxChatTypes.Properties['suggestions'];
51+
52+
constructor(private readonly appService: AppService) {
53+
loadMessages(this.appService.getDictionary());
54+
55+
this.dataSource = this.appService.dataSource;
56+
this.user = this.appService.user;
57+
this.alerts$ = this.appService.alerts$;
58+
this.typingUsers$ = this.appService.typingUsers$;
59+
this.suggestions = {
60+
items: this.appService.suggestionItems,
61+
onItemClick: this.onSuggestionItemClick.bind(this),
62+
};
63+
}
64+
65+
convertToHtml(message: DxChatTypes.Message): string {
66+
return this.appService.convertToHtml(message.text);
67+
}
68+
69+
toggleDisabledState(disabled: boolean, event = undefined) {
70+
this.isDisabled = disabled;
71+
72+
if (disabled) {
73+
event?.target.blur();
74+
} else {
75+
event?.target.focus();
76+
}
77+
}
78+
79+
async onMessageEntered(e: DxChatTypes.MessageEnteredEvent): Promise<void> {
80+
if (!this.appService.alerts.length) {
81+
this.toggleDisabledState(true, e.event);
82+
}
83+
84+
try {
85+
await this.appService.onMessageEntered(e);
86+
} finally {
87+
this.toggleDisabledState(false, e.event);
88+
}
89+
}
90+
91+
async onSuggestionItemClick(e: { itemData?: { text: string; prompt: string } }): Promise<void> {
92+
const { text = '', prompt = '' } = e.itemData ?? {};
93+
94+
if (this.hideAfterUse) {
95+
const currentItems = (this.suggestions?.items ?? []) as { text: string; prompt: string }[];
96+
this.suggestions = {
97+
items: currentItems.filter((item) => item.text !== text),
98+
onItemClick: this.onSuggestionItemClick.bind(this),
99+
};
100+
}
101+
102+
if (this.sendImmediately) {
103+
const message: DxChatTypes.Message = {
104+
id: Date.now(),
105+
timestamp: new Date(),
106+
author: this.user,
107+
text: prompt,
108+
};
109+
110+
this.appService.insertMessage(message);
111+
112+
if (!this.appService.alerts.length) {
113+
this.toggleDisabledState(true);
114+
115+
try {
116+
await this.appService.processMessageSending(message);
117+
} finally {
118+
this.toggleDisabledState(false);
119+
}
120+
}
121+
} else {
122+
this.inputFieldText = prompt;
123+
}
124+
}
125+
126+
onInputFieldTextChanged(e: DxChatTypes.InputFieldTextChangedEvent): void {
127+
this.inputFieldText = e.value ?? '';
128+
}
129+
130+
onSendImmediatelyChanged(e: DxSwitchTypes.ValueChangedEvent): void {
131+
this.sendImmediately = e.value;
132+
}
133+
134+
onHideAfterUseChanged(e: DxSwitchTypes.ValueChangedEvent): void {
135+
this.hideAfterUse = e.value;
136+
}
137+
}
138+
139+
bootstrapApplication(AppComponent, {
140+
providers: [
141+
provideZoneChangeDetection({ eventCoalescing: true }),
142+
AppService,
143+
AiService,
144+
],
145+
});

0 commit comments

Comments
 (0)