Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Injectable } from '@angular/core';
import { AzureOpenAI, OpenAI } from 'openai';

export type AIMessage = (
OpenAI.ChatCompletionUserMessageParam
| OpenAI.ChatCompletionSystemMessageParam
| OpenAI.ChatCompletionAssistantMessageParam) & {
content: string;
};

export interface GetAIResponseStreamOptions {
onAborted: () => void;
onDelta: (delta: string) => void;
onError?: (error: unknown) => void;
signal: AbortSignal;
}

const AzureOpenAIConfig = {
dangerouslyAllowBrowser: true,
deployment: 'gpt-4o-mini',
apiVersion: '2024-02-01',
endpoint: 'https://public-api.devexpress.com/demo-openai',
apiKey: 'DEMO',
};

@Injectable()
export class AiService {
chatService: AzureOpenAI;

constructor() {
this.chatService = new AzureOpenAI(AzureOpenAIConfig);
}

async getAIResponseStream(
messages: AIMessage[],
{ onAborted, onDelta, onError, signal }: GetAIResponseStreamOptions,
): Promise<void> {
const params = {
messages,
model: AzureOpenAIConfig.deployment,
max_tokens: 1000,
temperature: 0.7,
stream: true as const,
};

try {
const stream = await this.chatService.chat.completions.create(params, { signal });

for await (const event of stream) {
const delta = event.choices?.[0]?.delta?.content;
if (delta) {
onDelta(delta);
}
}

if (signal.aborted) {
onAborted();
}
} catch (e) {
if ((e as Error)?.name === 'AbortError') {
onAborted();
}
onError?.(e);
throw e;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
.demo-container {
display: flex;
justify-content: center;
}

::ng-deep .dx-chat {
max-width: 900px;
}

::ng-deep .dx-chat-messagelist-empty-image {
display: none;
}

::ng-deep .dx-chat-messagelist-empty-message {
font-size: var(--dx-font-size-heading-5);
}

::ng-deep .dx-chat-messagebubble-content,
::ng-deep .chat-messagebubble-text {
display: flex;
flex-direction: column;
}

::ng-deep .dx-template-wrapper > div > p:first-child {
margin-top: 0;
}

::ng-deep .dx-template-wrapper > div > p:last-child {
margin-bottom: 0;
}

::ng-deep .dx-chat-messagebubble-content h1,
::ng-deep .dx-chat-messagebubble-content h2,
::ng-deep .dx-chat-messagebubble-content h3,
::ng-deep .dx-chat-messagebubble-content h4,
::ng-deep .dx-chat-messagebubble-content h5,
::ng-deep .dx-chat-messagebubble-content h6 {
font-size: revert;
font-weight: revert;
}

::ng-deep .dx-chat-suggestion-cards {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 16px;
margin-top: 32px;
width: 100%;
}

::ng-deep .dx-chat-suggestion-card {
border-radius: 12px;
padding: 16px;
border: 1px solid #EBEBEB;
background: #FAFAFA;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
flex: 0 1 230px;
max-width: 230px;
text-align: left;
cursor: pointer;
transition: 0.2s ease;
width: 230px;
}

::ng-deep .dx-chat-suggestion-card:hover {
border: 1px solid #E0E0E0;
background: #F5F5F5;
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.04), 0 4px 24px 0 rgba(0, 0, 0, 0.02);
}

::ng-deep .dx-chat-suggestion-card-title {
color: #242424;
font-size: 12px;
font-weight: 600;
line-height: 16px;
}

::ng-deep .dx-chat-suggestion-card-prompt {
color: #616161;
font-size: 12px;
font-weight: 400;
line-height: 16px;
}

::ng-deep .dx-chat-messagelist-empty-prompt {
margin-top: 4px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<div class="demo-container">
<dx-chat
[dataSource]="dataSource"
[reloadOnChange]="false"
[showAvatar]="false"
[showDayHeaders]="false"
[user]="user"
height="710"
[typingUsers]="typingUsers$ | async"
[alerts]="alerts$ | async"
[sendButtonOptions]="sendButtonOptions$ | async"
[speechToTextEnabled]="true"
(onMessageEntered)="onMessageEntered($event)"
messageTemplate="messageTemplate"
emptyViewTemplate="emptyViewTemplate"
>
<div *dxTemplate="let data of 'messageTemplate'">
<div
class="chat-messagebubble-text"
[innerHTML]="convertToHtml(data.message)"
></div>
</div>

<div *dxTemplate="let data of 'emptyViewTemplate'">
<div class="dx-chat-messagelist-empty-message">{{ data.texts.message }}</div>
<div class="dx-chat-messagelist-empty-prompt">{{ data.texts.prompt }}</div>
<div class="dx-chat-suggestion-cards">
@for (card of suggestionCards; track card.title) {
<button
type="button"
class="dx-chat-suggestion-card"
(click)="onSuggestionClick(card.prompt)"
>
<div class="dx-chat-suggestion-card-title">{{ card.title }}</div>
<div class="dx-chat-suggestion-card-prompt">{{ card.description }}</div>
</button>
}
</div>
</div>
</dx-chat>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { Component, enableProdMode, provideZoneChangeDetection } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { DxChatModule } from 'devextreme-angular';
import type { DxChatTypes } from 'devextreme-angular/ui/chat';
import { Observable, map } from 'rxjs';
import { loadMessages } from 'devextreme-angular/common/core/localization';
import { DataSource } from 'devextreme-angular/common/data';
import { AppService, suggestionCards } from './app.service';
import { AiService } from './ai/ai.service';

if (!/localhost/.test(document.location.host)) {
enableProdMode();
}

let modulePrefix = '';
// @ts-ignore
if (window && window.config?.packageConfigPaths) {
modulePrefix = '/app';
}

@Component({
selector: 'demo-app',
templateUrl: `.${modulePrefix}/app.component.html`,
styleUrls: [`.${modulePrefix}/app.component.css`],
imports: [
DxChatModule,
AsyncPipe,
],
})
export class AppComponent {
dataSource: DataSource;

user: DxChatTypes.User;

typingUsers$: Observable<DxChatTypes.User[]>;

alerts$: Observable<DxChatTypes.Alert[]>;

sendButtonOptions$: Observable<DxChatTypes.SendButtonProperties>;

readonly suggestionCards = suggestionCards;

constructor(private readonly appService: AppService) {
loadMessages(this.appService.getDictionary());

this.dataSource = this.appService.dataSource;
this.user = this.appService.user;
this.alerts$ = this.appService.alerts$;
this.typingUsers$ = this.appService.typingUsers$;

this.sendButtonOptions$ = this.appService.isStreaming$.pipe(
map((isStreaming) => (isStreaming ? {
action: 'custom' as const,
icon: 'stopfilled',
onClick: () => this.appService.stopStreaming(),
} : {
action: 'send' as const,
icon: 'arrowright',
onClick: () => {},
})),
);
}

convertToHtml(message: DxChatTypes.Message): string {
return this.appService.convertToHtml(message.text);
}

onMessageEntered(e: DxChatTypes.MessageEnteredEvent): void {
this.appService.onMessageEntered(e);
}

onSuggestionClick(prompt: string): void {
this.appService.sendSuggestion(prompt);
}
}

bootstrapApplication(AppComponent, {
providers: [
provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }),
AppService,
AiService,
],
});
Loading
Loading