Files
UE5-cyberHuman/Plugins/OpenAi/Source/OpenAIEditor/Private/UI/ChatGPTWidget.cpp

360 lines
12 KiB
C++
Raw Normal View History

2025-04-07 18:31:41 -07:00
// OpenAI, Copyright LifeEXE. All Rights Reserved.
#include "UI/ChatGPTWidget.h"
#include "UI/ServiceWidget.h"
#include "UI/SaveSettings.h"
#include "UI/AttachedFilesContainerWidget.h"
#include "Components/VerticalBox.h"
#include "Components/ScrollBox.h"
#include "Components/EditableText.h"
#include "Components/Button.h"
#include "Components/VerticalBoxSlot.h"
#include "Components/ComboBoxString.h"
#include "Components/GridPanel.h"
#include "Components/TextBlock.h"
#include "FuncLib/OpenAIFuncLib.h"
#include "FuncLib/FileSystemFuncLib.h"
#include "ChatGPT/Services/Weather/WeatherService.h"
#include "ChatGPT/Services/News/NewsService.h"
DEFINE_LOG_CATEGORY_STATIC(LogChatGPTWidget, All, All);
namespace
{
const FMargin UserWidgetMargin = FMargin{100.0f, 10.0f, 10.0f, 10.0f};
const FMargin AssistantWidgetMargin = FMargin{10.0f, 10.0f, 100.0f, 10.0f};
const FMargin SystemWidgetMargin = FMargin{10.0f, 10.0f, 100.0f, 10.0f};
const int32 ServicesAmountInRow = 3;
const int32 MaxAttachedFilesAmount = 3;
} // namespace
void UChatGPTWidget::NativeConstruct()
{
Super::NativeConstruct();
check(SendMessageButton);
check(DumpChatButton);
check(ClearChatButton);
check(MessagesVerticalBox);
check(UserInputTextBox);
check(ChatGPTModelComboBox);
check(ServiceContainer);
check(AttachImageButton);
check(AttachedFilesContainer);
SendMessageButton->OnClicked.AddDynamic(this, &ThisClass::OnSendMessage);
DumpChatButton->OnClicked.AddDynamic(this, &ThisClass::OnDumpChat);
ClearChatButton->OnClicked.AddDynamic(this, &ThisClass::OnClearChat);
UserInputTextBox->OnTextCommitted.AddDynamic(this, &ThisClass::OnTextCommitted);
UserInputTextBox->OnTextChanged.AddDynamic(this, &ThisClass::OnTextChanged);
ChatGPTModelComboBox->OnSelectionChanged.AddDynamic(this, &ThisClass::OnModelSelectionChanged);
AttachImageButton->OnClicked.AddDynamic(this, &ThisClass::OnImageSelected);
SendMessageButton->SetIsEnabled(false);
DumpChatButton->SetIsEnabled(false);
ClearChatButton->SetIsEnabled(false);
MessagesVerticalBox->ClearChildren();
StartVerticalBox->SetVisibility(ESlateVisibility::Visible);
ChatScrollBox->SetVisibility(ESlateVisibility::HitTestInvisible);
AttachedFilesContainer->OnFileRemoved().AddUObject(this, &ThisClass::OnFileRemoved);
InitChatGPT();
InitModelsComboBox();
CreateServiceWidgets();
FSlateApplication::Get().SetKeyboardFocus(UserInputTextBox->GetCachedWidget());
}
void UChatGPTWidget::InitChatGPT()
{
const auto Auth = UOpenAIFuncLib::LoadAPITokensFromFile(FPaths::ProjectDir().Append("OpenAIAuth.ini"));
ChatGPT = NewObject<UChatGPT>();
check(ChatGPT);
ChatGPT->SetAuth(Auth);
ChatGPT->SetMaxTokens(MaxTokens);
ChatGPT->OnRequestCompleted().AddUObject(this, &ThisClass::OnRequestCompleted);
ChatGPT->OnRequestUpdated().AddUObject(this, &ThisClass::OnRequestUpdated);
ServiceSecrets = UOpenAIFuncLib::LoadServiceSecretsFromFile(FPaths::ProjectDir().Append("OnlineServicesAuth.ini"));
SaveSettings = USaveSettings::Load();
check(SaveSettings);
}
void UChatGPTWidget::InitModelsComboBox()
{
auto AddModel = [&](EMainModelEnum Model) -> void //
{ //
ChatGPTModelComboBox->AddOption(UOpenAIFuncLib::OpenAIMainModelToString(Model));
};
ChatGPTModelComboBox->ClearOptions();
AddModel(EMainModelEnum::GPT_4O);
AddModel(EMainModelEnum::GPT_4);
AddModel(EMainModelEnum::GPT_4_Vision_Preview);
AddModel(EMainModelEnum::GPT_4_0314);
AddModel(EMainModelEnum::GPT_4_0613);
AddModel(EMainModelEnum::GPT_3_5_Turbo);
AddModel(EMainModelEnum::GPT_3_5_Turbo_0301);
ChatGPTModelComboBox->SetSelectedOption(UOpenAIFuncLib::OpenAIMainModelToString(EMainModelEnum::GPT_4O));
ChatGPT->SetModel(ChatGPTModelComboBox->GetSelectedOption());
UpdateVisionElements();
}
void UChatGPTWidget::CreateServiceWidgets()
{
ServiceContainer->ClearChildren();
int32 Row{0}, Column{0};
for (const auto& ServiceClass : Services)
{
auto* ServiceWidget = CreateWidget<UServiceWidget>(GetWorld(), ServiceWidgetClass);
check(ServiceWidget);
ServiceWidget->SetServiceClass(ServiceClass);
ServiceWidget->OnServiceEnabled().AddUObject(this, &ThisClass::OnServiceEnabled);
ServiceContainer->AddChildToGrid(ServiceWidget, Row, Column);
ServiceWidget->SetEnabled(SaveSettings->IsServerRegistered(ServiceClass));
ServiceWidgets.Add(ServiceWidget);
if (ServiceWidget->IsEnabled())
{
OnServiceEnabled(true, ServiceClass, ServiceWidget);
}
if (++Column >= ServicesAmountInRow)
{
Column = 0;
Row++;
}
}
}
void UChatGPTWidget::OnServiceEnabled(
bool IsEnabled, const TSubclassOf<UBaseService>& ServiceClass, const TObjectPtr<UServiceWidget>& ServiceWidget)
{
if (!IsEnabled)
{
ChatGPT->UnRegisterService(ServiceClass);
SaveSettings->UnRegisterService(ServiceClass);
return;
}
if (ChatGPT->RegisterService(ServiceClass, ServiceSecrets))
{
SaveSettings->RegisterService(ServiceClass);
}
else if (ServiceWidget)
{
ServiceWidget->SetEnabled(false);
SaveSettings->UnRegisterService(ServiceClass);
}
}
void UChatGPTWidget::OnSendMessage()
{
const auto InputText = UserInputTextBox->GetText();
UserInputTextBox->SetText({});
FMessage Message;
Message.Role = UOpenAIFuncLib::OpenAIRoleToString(ERole::User);
if (AttachedFilesContainer->HasFiles())
{
for (const auto& File : AttachedFilesContainer->FilePaths())
{
FMessageContent MessageContent;
MessageContent.Type = UOpenAIFuncLib::OpenAIMessageContentTypeToString(EMessageContentType::Image_URL);
MessageContent.Image_URL.URL = UOpenAIFuncLib::FilePathToBase64(File);
Message.ContentArray.Add(MessageContent);
}
if (!InputText.IsEmpty())
{
FMessageContent MessageContent;
MessageContent.Text = InputText.ToString();
MessageContent.Type = UOpenAIFuncLib::OpenAIMessageContentTypeToString(EMessageContentType::Text);
Message.ContentArray.Add(MessageContent);
}
}
else
{
Message.Content = InputText.ToString();
}
ChatGPT->AddMessage(Message);
const auto AssistantMessage = FMessage{UOpenAIFuncLib::OpenAIRoleToString(ERole::Assistant), {}};
ChatGPT->SetAssistantMessage(AssistantMessage);
CreateMessageWidget(Message);
ChatGPT->MakeRequest();
AttachedFilesContainer->Clear();
}
void UChatGPTWidget::CreateMessageWidget(const FMessage& Message)
{
auto* UserChatWidget = CreateWidget<UChatMessageWidget>(GetWorld(), ChatMessageWidgetClass);
check(UserChatWidget);
UserChatWidget->SetMessage(Message, false);
ChatWidgets.Add(UserChatWidget);
MessagesVerticalBox->AddChild(UserChatWidget);
if (auto* ChatSlot = Cast<UVerticalBoxSlot>(UserChatWidget->Slot))
{
ChatSlot->SetPadding(UserWidgetMargin);
}
auto* AssistantChatWidget = CreateWidget<UChatMessageWidget>(GetWorld(), ChatMessageWidgetClass);
check(AssistantChatWidget);
AssistantChatWidget->SetMessage(ChatGPT->GetAssistantMessage());
ChatWidgets.Add(AssistantChatWidget);
MessagesVerticalBox->AddChild(AssistantChatWidget);
if (auto* ChatSlot = Cast<UVerticalBoxSlot>(AssistantChatWidget->Slot))
{
ChatSlot->SetPadding(AssistantWidgetMargin);
}
EnableControls(false);
ChatScrollBox->SetConsumeMouseWheel(EConsumeMouseWheel::Never);
ChatScrollBox->SetVisibility(ESlateVisibility::Visible);
ChatScrollBox->ScrollToEnd();
StartVerticalBox->SetVisibility(ESlateVisibility::Collapsed);
}
void UChatGPTWidget::OnRequestCompleted()
{
ChatScrollBox->SetConsumeMouseWheel(EConsumeMouseWheel::WhenScrollingPossible);
EnableControls(true);
FSlateApplication::Get().SetKeyboardFocus(UserInputTextBox->GetCachedWidget());
}
void UChatGPTWidget::OnRequestUpdated(const FMessage& Message, bool WasError)
{
ChatWidgets.Last()->SetMessage(Message, WasError);
ChatScrollBox->ScrollToEnd();
}
void UChatGPTWidget::OnDumpChat()
{
const FString FilePath = GenerateFilePath();
const FString Model = ChatGPT->GetModel();
if (UFileSystemFuncLib::SaveChatHistoryToFile(ChatGPT->GetHistory(), Model, FilePath))
{
auto* SystemChatWidget = CreateWidget<UChatMessageWidget>(GetWorld(), ChatMessageWidgetClass);
check(SystemChatWidget);
ChatWidgets.Add(SystemChatWidget);
MessagesVerticalBox->AddChild(SystemChatWidget);
if (auto* ChatSlot = Cast<UVerticalBoxSlot>(SystemChatWidget->Slot))
{
ChatSlot->SetPadding(SystemWidgetMargin);
}
FMessage Message;
Message.Content = FString::Format(TEXT("Chat history was saved to: {0}"), {FPaths::ConvertRelativePathToFull(FilePath)});
Message.Role = UOpenAIFuncLib::OpenAIRoleToString(ERole::System);
SystemChatWidget->SetMessage(Message);
ChatScrollBox->ScrollToEnd();
}
}
void UChatGPTWidget::OnClearChat()
{
MessagesVerticalBox->ClearChildren();
ChatWidgets.Empty();
ChatGPT->ClearHistory();
StartVerticalBox->SetVisibility(ESlateVisibility::Visible);
ChatScrollBox->SetVisibility(ESlateVisibility::HitTestInvisible);
DumpChatButton->SetIsEnabled(false);
ClearChatButton->SetIsEnabled(false);
AttachedFilesContainer->Clear();
}
void UChatGPTWidget::OnTextChanged(const FText& Text)
{
UpdateSendMessageButton();
}
void UChatGPTWidget::OnModelSelectionChanged(FString SelectedItem, ESelectInfo::Type SelectionType)
{
ChatGPT->SetModel(ChatGPTModelComboBox->GetSelectedOption());
AttachedFilesContainer->Clear();
UpdateVisionElements();
}
void UChatGPTWidget::OnTextCommitted(const FText& Text, ETextCommit::Type CommitMethod)
{
if (CommitMethod == ETextCommit::Type::OnEnter)
{
OnSendMessage();
}
}
FString UChatGPTWidget::GenerateFilePath() const
{
const auto FileName = "ChatGptChatHistory";
FString SavedDir = FPaths::ProjectDir().Append("Saved/");
const FString Date = FDateTime::Now().ToString();
return SavedDir.Append(FileName).Append("_").Append(Date).Append(".txt");
}
void UChatGPTWidget::EnableControls(bool Enabled)
{
ChatScrollBox->SetAllowOverscroll(Enabled);
if (Enabled)
{
UpdateVisionElements();
}
else
{
SendMessageButton->SetIsEnabled(false);
AttachImageButton->SetIsEnabled(false);
}
DumpChatButton->SetIsEnabled(Enabled);
ClearChatButton->SetIsEnabled(Enabled);
UserInputTextBox->SetIsReadOnly(!Enabled);
}
void UChatGPTWidget::UpdateVisionElements()
{
const bool VisionModel = UOpenAIFuncLib::ModelSupportsVision(ChatGPTModelComboBox->GetSelectedOption());
const bool AmountIsGood = AttachedFilesContainer->FilePaths().Num() < MaxAttachedFilesAmount;
AttachImageButton->SetIsEnabled(VisionModel && AmountIsGood);
AttachImageButton->SetToolTipText(VisionModel ? FText{} : FText::FromString("Image attachment is only supported by the vision model"));
ServiceContainer->SetVisibility(VisionModel ? ESlateVisibility::Collapsed : ESlateVisibility::Visible);
ServicesWarningTextBlock->SetVisibility(!VisionModel ? ESlateVisibility::Collapsed : ESlateVisibility::Visible);
UpdateSendMessageButton();
}
void UChatGPTWidget::OnImageSelected()
{
const auto FileExtensions = UFileSystemFuncLib::GetFileExtensions(EFileType::Image);
TArray<FString> OutFilePaths;
if (UFileSystemFuncLib::OpenFile(FileExtensions, OutFilePaths))
{
if (OutFilePaths.Num())
{
const auto Amount = FMath::Min(OutFilePaths.Num(), MaxAttachedFilesAmount);
for (int32 i = 0; i < Amount; ++i)
{
AttachedFilesContainer->AddImage(OutFilePaths[i]);
}
}
UpdateVisionElements();
}
}
void UChatGPTWidget::UpdateSendMessageButton()
{
const bool IsEnabled = !UserInputTextBox->GetText().IsEmpty() || AttachedFilesContainer->HasFiles();
SendMessageButton->SetIsEnabled(IsEnabled);
}
void UChatGPTWidget::OnFileRemoved()
{
UpdateVisionElements();
}