Add OpenAI chat completions to your WinUI 3 / Windows App SDK desktop app
Makale 26.11.2024
6 katılımcı
Geri Bildirim
Bu makalede
Prerequisites
Create a project
Set your environment variable
Install the OpenAI library
Initialize the library
Build the chat UI
Implement message sending, receiving, and displaying
Run the app
Improve the chat interface
Run the improved app
Recap
Full code files
Related content
9 daha göster
In this how-to, you'll learn how to integrate OpenAI's API into your WinUI 3 / Windows App SDK desktop app. We'll build a chat-like interface that lets you generate responses to messages using OpenAI's chat completions API :
Open Visual Studio and create a new project via File
> New
> Project
.
Search for WinUI
and select the Blank App, Packaged (WinUI 3 in Desktop)
C# project template.
Specify a project name, solution name, and directory. In this example, our ChatGPT_WinUI3
project belongs to a ChatGPT_WinUI3
solution, which will be created in C:\Projects\
.
After creating your project, you should see the following default file structure in your Solution Explorer:
Set your environment variable
In order to use the OpenAI SDK, you'll need to set an environment variable with your API key. In this example, we'll use the OPENAI_API_KEY
environment variable. Once you have your API key from the OpenAI developer dashboard , you can set the environment variable from the command line as follows:
setx OPENAI_API_KEY <your-api -key>
Note that this method works well for development, but you'll want to use a more secure method for production apps (for example: you could store your API key in a secure key vault that a remote service can access on behalf of your app). See Best practices for OpenAI key safety .
Install the OpenAI library
From Visual Studio's View
menu, select Terminal
. You should see an instance of Developer Powershell
appear. Run the following command from your project's root directory to install the OpenAI .NET package:
dotnet add package OpenAI
In MainWindow.xaml.cs
, initialize the OpenAI library with your API key:
using OpenAI;
using OpenAI.Chat;
namespace ChatGPT_WinUI3
{
public sealed partial class MainWindow : Window
{
private OpenAIClient openAiService;
public MainWindow ( )
{
this .InitializeComponent();
var openAiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY" );
openAiService = new (openAiKey);
}
}
}
We'll use a StackPanel
to display a list of messages, and a TextBox
to let users enter new messages. Update MainWindow.xaml
as follows:
<Window
x:Class ="ChatGPT_WinUI3.MainWindow"
xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local ="using:ChatGPT_WinUI3"
xmlns:d ="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc ="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable ="d" >
<Grid >
<StackPanel Orientation ="Vertical" HorizontalAlignment ="Stretch" >
<ListView x:Name ="ConversationList" />
<StackPanel Orientation ="Horizontal" >
<TextBox x:Name ="InputTextBox" HorizontalAlignment ="Stretch" />
<Button x:Name ="SendButton" Content ="Send" Click ="SendButton_Click" />
</StackPanel >
</StackPanel >
</Grid >
</Window >
Implement message sending, receiving, and displaying
Add a SendButton_Click
event handler to handle the sending, receiving, and display of messages:
public sealed partial class MainWindow : Window
{
private async void SendButton_Click (object sender, RoutedEventArgs e )
{
try
{
string userInput = InputTextBox.Text;
if (!string .IsNullOrEmpty(userInput))
{
AddMessageToConversation($"User: {userInput} " );
InputTextBox.Text = string .Empty;
var chatClient = openAiService.GetChatClient("gpt-4o" );
var chatOptions = new ChatCompletionOptions
{
MaxOutputTokenCount = 300
};
var completionResult = await chatClient.CompleteChatAsync(
[
ChatMessage.CreateSystemMessage("You are a helpful assistant." ),
ChatMessage.CreateUserMessage(userInput)
],
chatOptions);
if (completionResult != null && completionResult.Value.Content.Count > 0 )
{
AddMessageToConversation($"GPT: {completionResult.Value.Content.First().Text} " );
}
else
{
AddMessageToConversation($"GPT: Sorry, something bad happened: {completionResult?.Value.Refusal ?? "Unknown error." } " );
}
}
}
catch (Exception ex)
{
AddMessageToConversation($"GPT: Sorry, something bad happened: {ex.Message} " );
}
}
private void AddMessageToConversation (string message )
{
ConversationList.Items.Add(message);
ConversationList.ScrollIntoView(ConversationList.Items[ConversationList.Items.Last()]);
}
}
Run the app and try chatting! You should see something like this:
Improve the chat interface
Let's make the following improvements to the chat interface:
Add a ScrollViewer
to the StackPanel
to enable scrolling.
Add a TextBlock
to display the GPT response in a way that's more visually distinct from the user's input.
Add a ProgressBar
to indicate when the app is waiting for a response from the GPT API.
Center the StackPanel
in the window, similar to ChatGPT's web interface .
Ensure that messages wrap to the next line when they reach the edge of the window.
Make the TextBox
larger and responsive to the Enter
key.
Starting from the top:
Wrap the ListView
in a ScrollViewer
to enable vertical scrolling on long conversations:
<StackPanel Orientation ="Vertical" HorizontalAlignment ="Stretch" >
<ScrollViewer x:Name ="ConversationScrollViewer" VerticalScrollBarVisibility ="Auto" MaxHeight ="500" >
<ListView x:Name ="ConversationList" />
</ScrollViewer >
</StackPanel >
Modify the AddMessageToConversation
method to style the user's input and the GPT response differently:
private void AddMessageToConversation (string message )
{
var messageBlock = new TextBlock
{
Text = message,
Margin = new Thickness(5 )
};
if (message.StartsWith("User:" ))
{
messageBlock.Foreground = new SolidColorBrush(Colors.LightBlue);
}
else
{
messageBlock.Foreground = new SolidColorBrush(Colors.LightGreen);
}
ConversationList.Items.Add(messageBlock);
ConversationList.ScrollIntoView(ConversationList.Items.Last());
}
To indicate when the app is waiting for a response, add a ProgressBar
to the StackPanel
:
<StackPanel Orientation ="Vertical" HorizontalAlignment ="Stretch" >
<ScrollViewer x:Name ="ConversationScrollViewer" VerticalScrollBarVisibility ="Auto" MaxHeight ="500" >
<ListView x:Name ="ConversationList" />
</ScrollViewer >
<ProgressBar x:Name ="ResponseProgressBar" Height ="5" IsIndeterminate ="True" Visibility ="Collapsed" />
</StackPanel >
Then, update the SendButton_Click
event handler to show the ProgressBar
while waiting for a response:
private async void SendButton_Click (object sender, RoutedEventArgs e )
{
ResponseProgressBar.Visibility = Visibility.Visible;
string userInput = InputTextBox.Text;
try
{
if (!string .IsNullOrEmpty(userInput))
{
AddMessageToConversation($"User: {userInput} " );
InputTextBox.Text = string .Empty;
var chatClient = openAiService.GetChatClient("gpt-4o" );
var chatOptions = new ChatCompletionOptions
{
MaxOutputTokenCount = 300
};
var completionResult = await chatClient.CompleteChatAsync(
[
ChatMessage.CreateSystemMessage("You are a helpful assistant." ),
ChatMessage.CreateUserMessage(userInput)
],
chatOptions);
if (completionResult != null && completionResult.Value.Content.Count > 0 )
{
AddMessageToConversation($"GPT: {completionResult.Value.Content.First().Text} " );
}
else
{
AddMessageToConversation($"GPT: Sorry, something bad happened: {completionResult?.Value.Refusal ?? "Unknown error." } " );
}
}
}
catch (Exception ex)
{
AddMessageToConversation($"GPT: Sorry, something bad happened: {ex.Message} " );
}
finally
{
ResponseProgressBar.Visibility = Visibility.Collapsed;
}
}
To center the StackPanel
and pull the messages down towards the TextBox
, adjust the Grid
settings in MainWindow.xaml
:
<Grid VerticalAlignment ="Bottom" HorizontalAlignment ="Center" >
</Grid >
To ensure that messages wrap to the next line when they reach the edge of the window, update MainWindow.xaml
to use an ItemsControl
.
Replace this:
<ScrollViewer x:Name ="ConversationScrollViewer" VerticalScrollBarVisibility ="Auto" MaxHeight ="500" >
<ListView x:Name ="ConversationList" />
</ScrollViewer >
With this:
<ScrollViewer x:Name ="ConversationScrollViewer" VerticalScrollBarVisibility ="Auto" MaxHeight ="500" >
<ItemsControl x:Name ="ConversationList" Width ="300" >
<ItemsControl.ItemTemplate >
<DataTemplate >
<TextBlock Text ="{Binding Text}" TextWrapping ="Wrap" Margin ="5" Foreground ="{Binding Color}" />
</DataTemplate >
</ItemsControl.ItemTemplate >
</ItemsControl >
</ScrollViewer >
We'll then introduce a MessageItem
class to facilitate binding and coloring:
public class MessageItem
{
public string Text { get ; set ; }
public SolidColorBrush Color { get ; set ; }
}
Finally, update the AddMessageToConversation
method to use the new MessageItem
class:
private void AddMessageToConversation (string message )
{
var messageItem = new MessageItem
{
Text = message,
Color = message.StartsWith("User:" ) ? new SolidColorBrush(Colors.LightBlue)
: new SolidColorBrush(Colors.LightGreen)
};
ConversationList.Items.Add(messageItem);
ConversationScrollViewer.UpdateLayout();
ConversationScrollViewer.ChangeView(null , ConversationScrollViewer.ScrollableHeight, null );
}
To make the TextBox
larger and responsive to the Enter
key, update MainWindow.xaml
as follows:
<StackPanel Orientation ="Vertical" Width ="300" >
<TextBox x:Name ="InputTextBox" HorizontalAlignment ="Stretch" VerticalAlignment ="Stretch" KeyDown ="InputTextBox_KeyDown" TextWrapping ="Wrap" MinHeight ="100" MaxWidth ="300" />
<Button x:Name ="SendButton" Content ="Send" Click ="SendButton_Click" HorizontalAlignment ="Right" />
</StackPanel >
Then, add the InputTextBox_KeyDown
event handler to handle the Enter
key:
private void InputTextBox_KeyDown (object sender, KeyRoutedEventArgs e )
{
if (e.Key == Windows.System.VirtualKey.Enter && !string .IsNullOrWhiteSpace(InputTextBox.Text))
{
SendButton_Click(this , new RoutedEventArgs());
}
}
Your new-and-improved chat interface should look something like this:
Here's what you accomplished in this how-to:
You added OpenAI's API capabilities to your WinUI 3 / Windows App SDK desktop app by installing the official OpenAI library and initializing it with your API key.
You built a chat-like interface that lets you generate responses to messages using OpenAI's chat completions API .
You improved the chat interface by:
adding a ScrollViewer
,
using a TextBlock
to display the GPT response,
adding a ProgressBar
to indicate when the app is waiting for a response from the GPT API,
centering the StackPanel
in the window,
ensuring that messages wrap to the next line when they reach the edge of the window, and
making the TextBox
larger, resizable, and responsive to the Enter
key.
The following code is a full example of the chat app with OpenAI chat completions integrated:
<?xml version="1.0" encoding="utf-8"?>
<Window
x:Class ="ChatGPT_WinUI3.MainWindow"
xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local ="using:ChatGPT_WinUI3"
xmlns:d ="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc ="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable ="d" >
<Grid VerticalAlignment ="Bottom" HorizontalAlignment ="Center" >
<StackPanel Orientation ="Vertical" HorizontalAlignment ="Center" >
<ScrollViewer x:Name ="ConversationScrollViewer" VerticalScrollBarVisibility ="Auto" MaxHeight ="500" >
<ItemsControl x:Name ="ConversationList" Width ="300" >
<ItemsControl.ItemTemplate >
<DataTemplate >
<TextBlock Text ="{Binding Text}" TextWrapping ="Wrap" Margin ="5" Foreground ="{Binding Color}" />
</DataTemplate >
</ItemsControl.ItemTemplate >
</ItemsControl >
</ScrollViewer >
<ProgressBar x:Name ="ResponseProgressBar" Height ="5" IsIndeterminate ="True" Visibility ="Collapsed" />
<StackPanel Orientation ="Vertical" Width ="300" >
<TextBox x:Name ="InputTextBox" HorizontalAlignment ="Stretch" VerticalAlignment ="Stretch" KeyDown ="InputTextBox_KeyDown" TextWrapping ="Wrap" MinHeight ="100" MaxWidth ="300" />
<Button x:Name ="SendButton" Content ="Send" Click ="SendButton_Click" HorizontalAlignment ="Right" />
</StackPanel >
</StackPanel >
</Grid >
</Window >
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using System;
using System.Collections.Generic;
using System.Linq;
using OpenAI;
using OpenAI.Chat;
namespace ChatGPT_WinUI3
{
public class MessageItem
{
public string Text { get ; set ; }
public SolidColorBrush Color { get ; set ; }
}
public sealed partial class MainWindow : Window
{
private OpenAIService openAiService;
public MainWindow ( )
{
this .InitializeComponent();
var openAiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY" );
openAiService = new (openAiKey);
}
private async void SendButton_Click (object sender, RoutedEventArgs e )
{
ResponseProgressBar.Visibility = Visibility.Visible;
string userInput = InputTextBox.Text;
try
{
if (!string .IsNullOrEmpty(userInput))
{
AddMessageToConversation($"User: {userInput} " );
InputTextBox.Text = string .Empty;
var chatClient = openAiService.GetChatClient("gpt-4o" );
var chatOptions = new ChatCompletionOptions
{
MaxOutputTokenCount = 300
};
var completionResult = await chatClient.CompleteChatAsync(
[
ChatMessage.CreateSystemMessage("You are a helpful assistant." ),
ChatMessage.CreateUserMessage(userInput)
],
chatOptions);
if (completionResult != null && completionResult.Value.Content.Count > 0 )
{
AddMessageToConversation($"GPT: {completionResult.Value.Content.First().Text} " );
}
else
{
AddMessageToConversation($"GPT: Sorry, something bad happened: {completionResult?.Value.Refusal ?? "Unknown error." } " );
}
}
}
catch (Exception ex)
{
AddMessageToConversation($"GPT: Sorry, something bad happened: {ex.Message} " );
}
finally
{
ResponseProgressBar.Visibility = Visibility.Collapsed;
}
}
private void AddMessageToConversation (string message )
{
var messageItem = new MessageItem
{
Text = message,
Color = message.StartsWith("User:" ) ? new SolidColorBrush(Colors.LightBlue)
: new SolidColorBrush(Colors.LightGreen)
};
ConversationList.Items.Add(messageItem);
ConversationScrollViewer.UpdateLayout();
ConversationScrollViewer.ChangeView(null , ConversationScrollViewer.ScrollableHeight, null );
}
private void InputTextBox_KeyDown (object sender, KeyRoutedEventArgs e )
{
if (e.Key == Windows.System.VirtualKey.Enter && !string .IsNullOrWhiteSpace(InputTextBox.Text))
{
SendButton_Click(this , new RoutedEventArgs());
}
}
}
}