Partilhar via


Adicionar finalizações de bate-papo OpenAI ao seu aplicativo de desktop WinUI 3 / Windows App SDK

Neste tutorial, você aprenderá como integrar a API da OpenAI ao seu aplicativo de desktop WinUI 3 / Windows App SDK. Construiremos uma interface semelhante a um bate-papo que permite gerar respostas a mensagens usando a geração de texto da OpenAI e APIs de solicitação:

Uma captura de ecrã de uma aplicação de chat WinUI menos minimalista.

Prerequisites

Criar um projeto

Você cria um novo projeto WinUI no Visual Studio seguindo as etapas na seção Criar e iniciar seu primeiro aplicativo WinUI do artigo Começar a desenvolver aplicativos do Windows . Para este exemplo, digite ChatGPT_WinUI3 como o nome do projeto e ChatGPT_WinUI3 para o nome da solução ao inserir os detalhes do projeto na caixa de diálogo.

Definir a variável de ambiente

Para usar o OpenAI SDK, você precisará definir uma variável de ambiente com sua chave de API. Neste exemplo, usaremos a OPENAI_API_KEY variável de ambiente. Depois de ter sua chave de API no painel do desenvolvedor OpenAI, você pode definir a variável de ambiente a partir da linha de comando da seguinte maneira:

setx OPENAI_API_KEY <your-api-key>

Observe que esse método funciona bem para desenvolvimento, mas você desejará usar um método mais seguro para aplicativos de produção (por exemplo: você pode armazenar sua chave de API em um cofre de chaves seguro que um serviço remoto pode acessar em nome do seu aplicativo). Consulte Práticas recomendadas para segurança de chaves OpenAI.

Instalar a biblioteca OpenAI

No menu do View Visual Studio, selecione Terminal. Você deve ver uma instância de Developer Powershell aparecer. Execute o seguinte comando a partir do diretório raiz do seu projeto para instalar o pacote OpenAI .NET:

dotnet add package OpenAI

Inicializar a biblioteca

No MainWindow.xaml.cs, inicialize a biblioteca OpenAI com sua chave de API:

//...
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);
        }
    }
}

Crie a interface do usuário do bate-papo

Usaremos a StackPanel para exibir uma lista de mensagens e a TextBox para permitir que os usuários insiram novas mensagens. Atualize MainWindow.xaml da seguinte forma:

<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>

Implementar o envio, recebimento e exibição de mensagens

Adicione um SendButton_Click manipulador de eventos para lidar com o envio, recebimento e exibição de mensagens:

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"); // or another model
                var chatOptions = new ChatCompletionOptions
                {
                    MaxOutputTokenCount = 300
                };

                // Assemble the chat prompt with a system message and the user's input
                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()]);
    }
}

Executar o aplicativo

Execute o aplicativo e tente conversar! Você deve ver algo assim:

Uma captura de tela de um aplicativo de bate-papo WinUI mínimo.

Melhorar a interface de chat

Vamos fazer as seguintes melhorias na interface de chat:

  • Adicione um ScrollViewer ao StackPanel para ativar a rolagem.
  • Adicione um TextBlock para exibir a resposta GPT de uma forma visualmente mais distinta da entrada do usuário.
  • Adicione um ProgressBar para indicar quando o aplicativo está aguardando uma resposta da API GPT.
  • Centralize o StackPanel na janela, semelhante à interface web do ChatGPT.
  • Certifique-se de que as mensagens passem para a linha seguinte quando atingirem a borda da janela.
  • Torne o TextBox maior e responsivo à tecla Enter.

Começando pelo topo:

Adicionar ScrollViewer

Envolva o ListView em um ScrollViewer para permitir a rolagem vertical em conversas longas.

        <StackPanel Orientation="Vertical" HorizontalAlignment="Stretch">
            <ScrollViewer x:Name="ConversationScrollViewer" VerticalScrollBarVisibility="Auto" MaxHeight="500">
                <ListView x:Name="ConversationList" />
            </ScrollViewer>
            <!-- ... -->
        </StackPanel>

Utilize TextBlock

Modifique o AddMessageToConversation método para definir o estilo da entrada do usuário e da resposta GPT de forma diferente:

    // ...
    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()); 
    }

Adicionar ProgressBar

Para indicar quando o aplicativo está aguardando uma resposta, adicione um ProgressBar ao 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"/> <!-- new! -->
        </StackPanel>

Em seguida, atualize o SendButton_Click manipulador de eventos para mostrar o ProgressBar enquanto aguarda uma resposta:

    private async void SendButton_Click(object sender, RoutedEventArgs e)
    {
        ResponseProgressBar.Visibility = Visibility.Visible; // new!
        string userInput = InputTextBox.Text;

        try
        {
            if (!string.IsNullOrEmpty(userInput))
            {
                AddMessageToConversation($"User: {userInput}");
                InputTextBox.Text = string.Empty;
                var chatClient = openAiService.GetChatClient("gpt-4o"); // or another model
                var chatOptions = new ChatCompletionOptions
                {
                    MaxOutputTokenCount = 300
                };

                // Assemble the chat prompt with a system message and the user's input
                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 // new!
        {
            ResponseProgressBar.Visibility = Visibility.Collapsed; // new!
        }
    }

Centralizar o StackPanel

Para centralizar o StackPanel e puxar as mensagens para baixo em direção ao TextBox, ajuste as Grid definições em MainWindow.xaml:

    <Grid VerticalAlignment="Bottom" HorizontalAlignment="Center">
        <!-- ... -->
    </Grid>

Encapsular mensagens

Para garantir que as mensagens sejam quebradas para a próxima linha quando chegarem à borda da janela, atualize MainWindow.xaml para usar um ItemsControl.

Substitui isto:

    <ScrollViewer x:Name="ConversationScrollViewer" VerticalScrollBarVisibility="Auto" MaxHeight="500">
        <ListView x:Name="ConversationList" />
    </ScrollViewer>

Com isso:

    <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>

Em seguida, apresentaremos uma classe MessageItem para facilitar a ligação e a coloração.

    // ...
    public class MessageItem
    {
        public string Text { get; set; }
        public SolidColorBrush Color { get; set; }
    }
    // ...

Finalmente, atualize o AddMessageToConversation método para usar a nova MessageItem classe:

    // ...
    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);

        // handle scrolling
        ConversationScrollViewer.UpdateLayout();
        ConversationScrollViewer.ChangeView(null, ConversationScrollViewer.ScrollableHeight, null);
    }
    // ...

Melhorar a TextBox

Para tornar o TextBox maior e responsivo à Enter chave, atualize MainWindow.xaml da seguinte maneira:

    <!-- ... -->
    <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>
    <!-- ... -->

Em seguida, adicione o InputTextBox_KeyDown manipulador de eventos para manipular a Enter chave:

    //...
    private void InputTextBox_KeyDown(object sender, KeyRoutedEventArgs e)
    {
        if (e.Key == Windows.System.VirtualKey.Enter && !string.IsNullOrWhiteSpace(InputTextBox.Text))
        {
            SendButton_Click(this, new RoutedEventArgs());
        }
    }
    //...

Execute a aplicação melhorada

Sua interface de bate-papo nova e aprimorada deve ter esta aparência:

Uma captura de ecrã de uma aplicação de chat WinUI 3 menos minimalista.

Recap

Veja o que você realizou neste tutorial:

  1. Você adicionou os recursos da API do OpenAI ao seu aplicativo de desktop WinUI 3 / Windows App SDK instalando a biblioteca oficial do OpenAI e inicializando-a com sua chave de API.
  2. Você criou uma interface semelhante a um bate-papo que permite gerar respostas a mensagens usando a geração de texto da OpenAI e APIs de solicitação.
  3. Você melhorou a interface do bate-papo ao realizar as seguintes ações:
    1. adicionando um ScrollViewer,
    2. usando a TextBlock para exibir a resposta GPT,
    3. adicionando um ProgressBar para indicar quando o aplicativo está aguardando uma resposta da API GPT,
    4. centralizando o StackPanel na janela,
    5. garantir que as mensagens sejam transferidas para a próxima linha quando chegarem à borda da janela, e
    6. tornando o TextBox maior, redimensionável e responsivo à tecla Enter.

Arquivos de código completos

O código a seguir é um exemplo completo do aplicativo de bate-papo com finalizações de bate-papo OpenAI integradas:

<?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"); // or another model
                    var chatOptions = new ChatCompletionOptions
                    {
                        MaxOutputTokenCount = 300
                    };

                    // Assemble the chat prompt with a system message and the user's input
                    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);

            // handle scrolling
            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());
            }
        }
    }
}