This project demonstrates how to create an OpenAI chat application using Angular, ASP.NET API, and Azure, emphasizing Azure setup, ASP.NET API configuration for OpenAI interaction, and building a responsive Angular front-end for a chat interface with features like message order and differentiation between user and OpenAI responses.
In this project, I will show you how to build an OpenAI chat application using Angular, ASP.NET API, and Azure.
For the latest updates on this project, please go to the source article at DotNetLead.
We will go over:
- Setup needed on Azure
- Code needed on the ASP.NET API
- Code needed on the Angular front end
Azure OpenAI Setup
The first thing is to create the Azure OpenAI resource shown below. It’s a resource not available by default in Azure and you have to apply for access. Once Microsoft reviews your request, which takes a few days, you will be able to create the Azure OpenAI resource. You can apply for access HERE.
Once the Azure OpenAI resource is created, go to the Model Deployment section to choose the OpenAI model that you want to use as shown below, Azure will take you to the Azure AI Studio where you can also test the chat functionality on their website.
Create the deployment by choosing the ChatGpt version you like as shown below. At the time of publication, I chose the gpt-35-turbo.
Once the model is created, note down the name of the deployment as shown below. You will need the name of the deployment for building the API.
Go back to the Azure OpenAI resource and note down the EndPoint and Key needed for building the API as shown below. You only need 1 key in the API.
ASP.NET API Setup
The code for getting the chat response from Azure OpenAI is shown below. First, create the OpenAIClient
, followed by creating the ChatCompletionsOptions
for preparing the query, which is the user chat input, then call the GetChatCompletionsAsync
method to get the response from Azure OpenAI. In the controller method, we just return the content of the response from OpenAI.
[HttpGet]
public async Task<ChatMsg> GetResponse(string query)
{
OpenAIClient client = new OpenAIClient(
new Uri(config["AzureEndPoint"]),
new AzureKeyCredential(config["AzureKey"])
);
ChatCompletionsOptions option = new ChatCompletionsOptions { };
option.Messages.Add(new ChatMessage(ChatRole.User, query));
Response<ChatCompletions> response = await client.GetChatCompletionsAsync(
config["DeploymentName"],
option
);
return new ChatMsg { Message = response.Value.Choices[0].Message.Content };
}
As noted in the ReadMe.txt file, you will need three configurations below using the .NET Secret Manager so that the configurations are not stored in plain text files.
AzureEndPoint
AzureKey
DeploymentName
For instructions on how to use the .NET Secret Manager, check out Securing ASP.NET Core configurations using the Secret Manager and Azure Key Vault.
Building Front End Chat UI using Angular
There are several properties needed for building the chat front end:
- The Chat window should stay static. In another words, it should not expand or contract due the content inside the chat window and should stay at the same location. When the text exceeds the window space, it should allow scrolling.
- The order in which the chat messages are being displayed should go from bottom to top, with the latest user input or response starting from the very bottom.
For the Chat window to stay static and allow scrolling as the contents expand, declare a fixed position flexbox container (shown as the chat-container
class in the code below), and the first flexbox item would just be another flexbox (shown as the messages class in the code below), which has the flex-direction as column-reverse
for showing the items from bottom to top, the overflow-y
as scroll
for vertical scrolling, and overflow-x
as hidden
to hide the horizontal scrolling.
The user input and the OpenAI response need to be shown differently, which can be done by declaring two different classes, which are the bot-message class for showing the OpenAI response and the user-message class for showing the user input. Notice the user-message class has align-self
as flex-end
so that it’s aligned to the right.
Below is the HTML:
<div class="chat-container">
<div class="messages">
<div *ngFor="let msg of chatHistory"
[ngClass]="{'user-message': msg.type == 1,
'bot-message': msg.type == 2}" >
<p>{{msg.message}}</p>
</div>
</div>
...
Below is the scss:
.chat-container {
position: fixed;
top: 4rem;
bottom: 4rem;
display: flex;
flex-direction: column;
width: 42rem;
box-shadow: $shadow;
.messages {
flex: 1;
display: flex;
flex-direction: column-reverse;
overflow-y: scroll;
overflow-x: hidden;
padding: 1rem 1.5rem;
.bot-message {
display: flex;
align-items: center;
p {
@include message-item($bot-color);
}
}
.user-message {
display: flex;
align-items: center;
align-self: flex-end;
p {
@include message-item($user-color);
}
}
}
...
Each of the message will have the message type of either 1 for user input or 2 for OpenAI response as shown in the html above. The definition for the messages are shown below:
export enum MessageType {
Send = 1,
Receive = 2
}
export interface IChatMsg {
type: MessageType,
message: string
}
Below is the code of the component:
export class ChatComponent implements OnInit {
frmQuery: FormGroup;
chatHistory: IChatMsg[];
constructor(private chatSvc: ChatService,
private fb: FormBuilder) { }
ngOnInit(): void {
this.frmQuery = this.fb.group({
txtQuery: [null]
});
this.chatSvc.chat(null)
.subscribe(aa => this.chatHistory = aa);
}
submitQuery() {
if (!this.frmQuery.get("txtQuery").value)
return;
let queryMessage: IChatMsg = {
type: MessageType.Send,
message: this.frmQuery.get("txtQuery").value
};
this.chatSvc.chat(queryMessage);
this.frmQuery.get("txtQuery").setValue(null);
}
}
Next, we need to show the latest message at the very bottom and push previous messages upwards. Although the CSS definition will show the messages from bottom to top, the order of the messages are from top to bottom which need to be reversed. Also, each call to the API is merely a request and a response, and the history of the chat will need to be kept. These can be solved by declaring an array to store the chat history, and use the reverse
method to return the chat history as an Observable
. The code for the chat service is shown below:
export class ChatService {
readonly url = this.appConfig.get('apiChat');
private chatHistory = new BehaviorSubject<IChatMsg[] | null>(null);
chatHistory$ = this.chatHistory.asObservable();
chatHistoryRecord : IChatMsg[] = [];
constructor(private http:HttpClient,
private appConfig:AppConfigService){}
chat(queryMessage: IChatMsg): Observable<IChatMsg[]>{
if (!queryMessage)
return this.chatHistory$;
this.chatHistoryRecord.push(queryMessage);
this.chatHistory.next(this.chatHistoryRecord.slice().reverse());
let queryUrl = this.url + "?query=" + queryMessage.message;
this.http.get<IChatMsg>(queryUrl).pipe()
.subscribe(result=>{
if (result){
const receiveMsg : IChatMsg =
{ type: MessageType.Receive, message : result.message };
this.chatHistoryRecord.push(receiveMsg);
this.chatHistory.next(this.chatHistoryRecord.slice().reverse());
}
}
)
return this.chatHistory$;
}
}
And that’s all, hope you will find this project useful.