diff --git a/HttpGPT.uplugin b/HttpGPT.uplugin index 749aba0..a295026 100644 --- a/HttpGPT.uplugin +++ b/HttpGPT.uplugin @@ -1,7 +1,7 @@ { "FileVersion": 3, - "Version": 17, - "VersionName": "1.5.4", + "Version": 18, + "VersionName": "1.5.5", "FriendlyName": "HttpGPT - GPT Integration", "Description": "HttpGPT is an Unreal Engine plugin that facilitates integration with OpenAI's GPT based services (ChatGPT and DALL-E) through asynchronous REST requests, making it easy for developers to communicate with these services. HttpGPT also includes new Editor Tools to integrate Chat GPT and DALL-E image generation directly in the Engine.", "Category": "Game Features", diff --git a/Source/HttpGPTEditorModule/Private/Chat/HttpGPTMessagingHandler.cpp b/Source/HttpGPTEditorModule/Private/Chat/HttpGPTMessagingHandler.cpp new file mode 100644 index 0000000..dc1c092 --- /dev/null +++ b/Source/HttpGPTEditorModule/Private/Chat/HttpGPTMessagingHandler.cpp @@ -0,0 +1,84 @@ +// Author: Lucas Vilas-Boas +// Year: 2023 +// Repo: https://github.com/lucoiso/UEHttpGPT + +#include "HttpGPTMessagingHandler.h" +#include +#include + +#ifdef UE_INLINE_GENERATED_CPP_BY_NAME +#include UE_INLINE_GENERATED_CPP_BY_NAME(HttpGPTMessagingHandler) +#endif + +UHttpGPTMessagingHandler::UHttpGPTMessagingHandler(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ +} + +void UHttpGPTMessagingHandler::RequestSent() +{ + OnMessageContentUpdated.ExecuteIfBound("Waiting for response..."); +} + +void UHttpGPTMessagingHandler::RequestFailed() +{ + OnMessageContentUpdated.ExecuteIfBound("Request Failed.\nPlease check the logs. (Enable internal logs in Project Settings -> Plugins -> HttpGPT)."); + Destroy(); +} + +void UHttpGPTMessagingHandler::ProcessUpdated(const FHttpGPTChatResponse& Response) +{ + ProcessResponse(Response); +} + +void UHttpGPTMessagingHandler::ProcessCompleted(const FHttpGPTChatResponse& Response) +{ + ProcessResponse(Response); + Destroy(); +} + +void UHttpGPTMessagingHandler::ProcessResponse(const FHttpGPTChatResponse& Response) +{ + bool bScrollToEnd = false; + if (ScrollBoxReference.IsValid()) + { + bScrollToEnd = FMath::Abs(ScrollBoxReference->GetScrollOffsetOfEnd() - ScrollBoxReference->GetScrollOffset()) <= 8.f; + } + + if (!Response.bSuccess) + { + const FStringFormatOrderedArguments Arguments_ErrorDetails{ + FString("Request Failed."), + FString("Please check the logs. (Enable internal logs in Project Settings -> Plugins -> HttpGPT)."), + FString("Error Details: "), + FString("\tError Code: ") + Response.Error.Code.ToString(), + FString("\tError Type: ") + Response.Error.Type.ToString(), + FString("\tError Message: ") + Response.Error.Message + }; + + OnMessageContentUpdated.ExecuteIfBound(FString::Format(TEXT("{0}\n{1}\n\n{2}\n{3}\n{4}\n{5}"), Arguments_ErrorDetails)); + } + else if (Response.bSuccess && !HttpGPT::Internal::HasEmptyParam(Response.Choices)) + { + OnMessageContentUpdated.ExecuteIfBound(Response.Choices[0].Message.Content); + } + else + { + return; + } + + if (ScrollBoxReference.IsValid() && bScrollToEnd) + { + ScrollBoxReference->ScrollToEnd(); + } +} + +void UHttpGPTMessagingHandler::Destroy() +{ + ClearFlags(RF_Standalone); + +#if ENGINE_MAJOR_VERSION >= 5 + MarkAsGarbage(); +#else + MarkPendingKill(); +#endif +} \ No newline at end of file diff --git a/Source/HttpGPTEditorModule/Private/Chat/HttpGPTMessagingHandler.h b/Source/HttpGPTEditorModule/Private/Chat/HttpGPTMessagingHandler.h new file mode 100644 index 0000000..7d87131 --- /dev/null +++ b/Source/HttpGPTEditorModule/Private/Chat/HttpGPTMessagingHandler.h @@ -0,0 +1,41 @@ +// Author: Lucas Vilas-Boas +// Year: 2023 +// Repo: https://github.com/lucoiso/UEHttpGPT + +#pragma once + +#include +#include +#include "HttpGPTMessagingHandler.generated.h" + +DECLARE_DELEGATE_OneParam(FMessageContentUpdated, FString); + +UCLASS(MinimalAPI, NotBlueprintable, NotPlaceable, Category = "Implementation") +class UHttpGPTMessagingHandler : public UObject +{ + GENERATED_BODY() + +public: + explicit UHttpGPTMessagingHandler(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + FMessageContentUpdated OnMessageContentUpdated; + + UFUNCTION() + void RequestSent(); + + UFUNCTION() + void RequestFailed(); + + UFUNCTION() + void ProcessUpdated(const FHttpGPTChatResponse& Response); + + UFUNCTION() + void ProcessCompleted(const FHttpGPTChatResponse& Response); + + TSharedPtr ScrollBoxReference; + + void Destroy(); + +private: + void ProcessResponse(const FHttpGPTChatResponse& Response); +}; \ No newline at end of file diff --git a/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatItem.cpp b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatItem.cpp new file mode 100644 index 0000000..bbd71a8 --- /dev/null +++ b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatItem.cpp @@ -0,0 +1,112 @@ +// Author: Lucas Vilas-Boas +// Year: 2023 +// Repo: https://github.com/lucoiso/UEHttpGPT + +#include "SHttpGPTChatItem.h" +#include "HttpGPTMessagingHandler.h" +#include + +void SHttpGPTChatItem::Construct(const FArguments& InArgs) +{ + MessageRole = InArgs._MessageRole; + InputText = InArgs._InputText; + + if (MessageRole == EHttpGPTChatRole::Assistant) + { + MessagingHandlerObject = NewObject(); + MessagingHandlerObject->SetFlags(RF_Standalone); + MessagingHandlerObject->ScrollBoxReference = InArgs._ScrollBox; + + MessagingHandlerObject->OnMessageContentUpdated.BindLambda( + [this](FString Content) + { + if (!Message.IsValid()) + { + return; + } + +#if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1) + const FTextSelection SelectedText = Message->GetSelection(); + Message->SetText(FText::FromString(Content)); + Message->SelectText(SelectedText.GetBeginning(), SelectedText.GetEnd()); +#else + Message->SetText(FText::FromString(Content)); +#endif + } + ); + } + + ChildSlot + [ + ConstructContent() + ]; +} + +TSharedRef SHttpGPTChatItem::ConstructContent() +{ + constexpr float SlotPadding = 4.0f; + + FText RoleText = FText::FromString(TEXT("Undefined:")); + FMargin BoxMargin(SlotPadding); + + if (MessageRole == EHttpGPTChatRole::User) + { + RoleText = FText::FromString(TEXT("User:")); + BoxMargin = FMargin(SlotPadding, SlotPadding, SlotPadding * 16.f, SlotPadding); + } + else if (MessageRole == EHttpGPTChatRole::Assistant) + { + RoleText = FText::FromString(TEXT("Assistant:")); + BoxMargin = FMargin(SlotPadding * 16.f, SlotPadding, SlotPadding, SlotPadding); + } + else if (MessageRole == EHttpGPTChatRole::System) + { + RoleText = FText::FromString(TEXT("System:")); + BoxMargin = FMargin(SlotPadding * 8.f, SlotPadding); + } + + const FMargin MessageMargin(SlotPadding * 4.f, SlotPadding, SlotPadding, SlotPadding); + +#if ENGINE_MAJOR_VERSION < 5 + using FAppStyle = FEditorStyle; +#endif + + return SNew(SBox) + .Padding(BoxMargin) + [ + SNew(SBorder) + .BorderImage(FAppStyle::Get().GetBrush("Menu.Background")) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .Padding(SlotPadding) + .AutoHeight() + [ + SAssignNew(Role, STextBlock) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) + .Text(RoleText) + ] + + SVerticalBox::Slot() + .Padding(MessageMargin) + .FillHeight(1.f) + [ + SAssignNew(Message, SMultiLineEditableText) + .AllowMultiLine(true) + .AutoWrapText(true) + .IsReadOnly(true) + .AllowContextMenu(true) + .Text(FText::FromString(InputText)) + ] + ] + ]; +} + +FString SHttpGPTChatItem::GetRoleText() const +{ + return Role.IsValid() ? Role->GetText().ToString() : FString(); +} + +FString SHttpGPTChatItem::GetMessageText() const +{ + return Message.IsValid() ? Message->GetText().ToString() : FString(); +} \ No newline at end of file diff --git a/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatItem.h b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatItem.h new file mode 100644 index 0000000..1e8dfed --- /dev/null +++ b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatItem.h @@ -0,0 +1,39 @@ +// Author: Lucas Vilas-Boas +// Year: 2023 +// Repo: https://github.com/lucoiso/UEHttpGPT + +#pragma once + +#include +#include +#include "Structures/HttpGPTChatTypes.h" + +class SHttpGPTChatItem final : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SHttpGPTChatItem) : _MessageRole(), _InputText(), _ScrollBox() + { + } + SLATE_ARGUMENT(EHttpGPTChatRole, MessageRole) + SLATE_ARGUMENT(FString, InputText) + SLATE_ARGUMENT(TSharedPtr, ScrollBox) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + + FString GetRoleText() const; + FString GetMessageText() const; + + TWeakObjectPtr MessagingHandlerObject; + +private: + TSharedRef ConstructContent(); + + EHttpGPTChatRole MessageRole; + FString InputText; + + TSharedPtr Role; + TSharedPtr Message; +}; + +typedef TSharedPtr SHttpGPTChatItemPtr; \ No newline at end of file diff --git a/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatView.cpp b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatView.cpp new file mode 100644 index 0000000..75f606c --- /dev/null +++ b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatView.cpp @@ -0,0 +1,356 @@ +// Author: Lucas Vilas-Boas +// Year: 2023 +// Repo: https://github.com/lucoiso/UEHttpGPT + +#include "SHttpGPTChatView.h" +#include "HttpGPTMessagingHandler.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "SHttpGPTChatItem.h" + +void SHttpGPTChatView::Construct([[maybe_unused]] const FArguments&) +{ + ModelsComboBox = SNew(STextComboBox) + .OptionsSource(&AvailableModels) + .ToolTipText(FText::FromString(TEXT("GPT Model"))); + + for (const FName& ModelName : UHttpGPTHelper::GetAvailableGPTModels()) + { + AvailableModels.Add(MakeShared(ModelName.ToString())); + + if (ModelsComboBox.IsValid() && ModelName.IsEqual(UHttpGPTHelper::ModelToName(EHttpGPTChatModel::gpt35turbo))) + { + ModelsComboBox->SetSelectedItem(AvailableModels.Top()); + } + } + + ChildSlot + [ + ConstructContent() + ]; + + LoadChatHistory(); +} + +SHttpGPTChatView::~SHttpGPTChatView() +{ + SaveChatHistory(); +} + +bool SHttpGPTChatView::IsSendMessageEnabled() const +{ + return (!RequestReference.IsValid() || !UHttpGPTTaskStatus::IsTaskActive(RequestReference.Get())) && !HttpGPT::Internal::HasEmptyParam(InputTextBox->GetText()); +} + +bool SHttpGPTChatView::IsClearChatEnabled() const +{ + return !HttpGPT::Internal::HasEmptyParam(ChatItems); +} + +FString SHttpGPTChatView::GetHistoryPath() const +{ + return FPaths::Combine(FPaths::ProjectSavedDir(), "HttpGPT", "HttpGPTChatHistory.json"); +} + +TSharedRef SHttpGPTChatView::ConstructContent() +{ + constexpr float SlotPadding = 4.0f; + + return SNew(SVerticalBox) + + SVerticalBox::Slot() + .Padding(SlotPadding) + .FillHeight(1.f) + [ + SAssignNew(ChatScrollBox, SScrollBox) + + SScrollBox::Slot() + [ + SAssignNew(ChatBox, SVerticalBox) + ] + ] + + SVerticalBox::Slot() + .Padding(SlotPadding) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(SlotPadding) + .FillWidth(1.f) + [ + SAssignNew(InputTextBox, SEditableTextBox) + .AllowContextMenu(true) + .IsReadOnly(false) + .OnTextCommitted_Lambda( + [this]([[maybe_unused]] const FText& Text, ETextCommit::Type CommitType) + { + if (IsSendMessageEnabled() && CommitType == ETextCommit::OnEnter) + { + HandleSendMessageButton(EHttpGPTChatRole::User); + } + } + ) + ] + + SHorizontalBox::Slot() + .Padding(SlotPadding) + .AutoWidth() + [ + SNew(SButton) + .Text(FText::FromString(TEXT("Send"))) + .ToolTipText(FText::FromString(TEXT("Send Message"))) + .OnClicked(this, &SHttpGPTChatView::HandleSendMessageButton, EHttpGPTChatRole::User) + .IsEnabled(this, &SHttpGPTChatView::IsSendMessageEnabled) + ] + + SHorizontalBox::Slot() + .Padding(SlotPadding) + .AutoWidth() + [ + SNew(SButton) + .Text(FText::FromString(TEXT("System"))) + .ToolTipText(FText::FromString(TEXT("Send Message as System Context"))) + .OnClicked(this, &SHttpGPTChatView::HandleSendMessageButton, EHttpGPTChatRole::System) + .IsEnabled(this, &SHttpGPTChatView::IsSendMessageEnabled) + ] + + SHorizontalBox::Slot() + .Padding(SlotPadding) + .AutoWidth() + [ + ModelsComboBox.ToSharedRef() + ] + + SHorizontalBox::Slot() + .Padding(SlotPadding) + .AutoWidth() + [ + SNew(SButton) + .Text(FText::FromString(TEXT("Clear"))) + .ToolTipText(FText::FromString(TEXT("Clear Chat History"))) + .OnClicked(this, &SHttpGPTChatView::HandleClearChatButton) + .IsEnabled(this, &SHttpGPTChatView::IsClearChatEnabled) + ] + ]; +} + +FReply SHttpGPTChatView::HandleSendMessageButton(const EHttpGPTChatRole Role) +{ + const SHttpGPTChatItemPtr NewMessage = SNew(SHttpGPTChatItem) + .MessageRole(Role) + .InputText(InputTextBox->GetText().ToString()); + + ChatBox->AddSlot() + .AutoHeight() + [ + NewMessage.ToSharedRef() + ]; + ChatItems.Add(NewMessage); + + if (Role == EHttpGPTChatRole::System) + { + ChatScrollBox->ScrollToEnd(); + InputTextBox->SetText(FText::GetEmpty()); + + return FReply::Handled(); + } + + const SHttpGPTChatItemPtr AssistantMessage = SNew(SHttpGPTChatItem) + .MessageRole(EHttpGPTChatRole::Assistant) + .ScrollBox(ChatScrollBox); + + FHttpGPTChatOptions Options; + Options.Model = UHttpGPTHelper::NameToModel(*(*ModelsComboBox->GetSelectedItem().Get())); + Options.bStream = true; + + RequestReference = UHttpGPTChatRequest::EditorTask(GetChatHistory(), Options); + RequestReference->ProgressStarted.AddDynamic(AssistantMessage->MessagingHandlerObject.Get(), &UHttpGPTMessagingHandler::ProcessUpdated); + RequestReference->ProgressUpdated.AddDynamic(AssistantMessage->MessagingHandlerObject.Get(), &UHttpGPTMessagingHandler::ProcessUpdated); + RequestReference->ProcessCompleted.AddDynamic(AssistantMessage->MessagingHandlerObject.Get(), &UHttpGPTMessagingHandler::ProcessCompleted); + RequestReference->ErrorReceived.AddDynamic(AssistantMessage->MessagingHandlerObject.Get(), &UHttpGPTMessagingHandler::ProcessCompleted); + RequestReference->RequestFailed.AddDynamic(AssistantMessage->MessagingHandlerObject.Get(), &UHttpGPTMessagingHandler::RequestFailed); + RequestReference->RequestSent.AddDynamic(AssistantMessage->MessagingHandlerObject.Get(), &UHttpGPTMessagingHandler::RequestSent); + RequestReference->Activate(); + + ChatBox->AddSlot() + .AutoHeight() + [ + AssistantMessage.ToSharedRef() + ]; + ChatItems.Add(AssistantMessage); + + ChatScrollBox->ScrollToEnd(); + InputTextBox->SetText(FText::GetEmpty()); + + return FReply::Handled(); +} + +FReply SHttpGPTChatView::HandleClearChatButton() +{ + ChatItems.Empty(); + ChatBox->ClearChildren(); + + if (RequestReference.IsValid()) + { + RequestReference->StopHttpGPTTask(); + } + else + { + RequestReference.Reset(); + } + + return FReply::Handled(); +} + +TArray SHttpGPTChatView::GetChatHistory() const +{ + TArray Output + { + FHttpGPTChatMessage(EHttpGPTChatRole::System, GetDefaultSystemContext()) + }; + + for (const SHttpGPTChatItemPtr& Item : ChatItems) + { + FString RoleText = Item->GetRoleText(); + RoleText.RemoveFromEnd(TEXT(":")); + Output.Add(FHttpGPTChatMessage(*RoleText, Item->GetMessageText())); + } + + return Output; +} + +FString SHttpGPTChatView::GetDefaultSystemContext() const +{ + if (const UHttpGPTSettings* const Settings = UHttpGPTSettings::Get(); Settings->bUseCustomSystemContext) + { + return Settings->CustomSystemContext; + } + + FString SupportedModels; + for (const TSharedPtr& Model : AvailableModels) + { + SupportedModels.Append(*Model.Get() + ", "); + } + SupportedModels.RemoveFromEnd(", "); + + const TSharedPtr PluginInterface = IPluginManager::Get().FindPlugin("HttpGPT"); + + const FString PluginShortName = "HttpGPT"; + const FString EngineVersion = FString::Printf(TEXT("%d.%d"), ENGINE_MAJOR_VERSION, ENGINE_MINOR_VERSION); + + const FStringFormatOrderedArguments Arguments_PluginInfo{ + EngineVersion, + PluginShortName, + PluginInterface->GetDescriptor().VersionName, + PluginInterface->GetDescriptor().CreatedBy, + PluginInterface->GetDescriptor().Description + }; + + const FString PluginInformation = FString::Format(TEXT("You are in the Unreal Engine {0} plugin {1} version {2}, which was developed by {3}. The description of HttpGPT is: \"{4}\""), Arguments_PluginInfo); + + const FStringFormatOrderedArguments Arguments_SupportInfo{ + PluginInterface->GetDescriptor().DocsURL, + PluginInterface->GetDescriptor().SupportURL, + }; + + const FString PluginSupport = FString::Format(TEXT("You can find the HttpGPT documentation at {0} and support at {1}."), Arguments_SupportInfo); + + const FStringFormatOrderedArguments Arguments_Models{ + *ModelsComboBox->GetSelectedItem().Get(), + SupportedModels + }; + + const FString ModelsInformation = FString::Format(TEXT("You're using the model {0} and HttpGPT currently supports all these OpenAI Models: {1}."), Arguments_Models); + + const FStringFormatOrderedArguments Arguments_EngineDocumentation{ + EngineVersion + }; + + const FString EngineDocumentation_General = FString::Format(TEXT("You can find the Unreal Engine {0} general documentation at https://docs.unrealengine.com/{0}/en-US/."), Arguments_EngineDocumentation); + const FString EngineDocumentation_CPP = FString::Format(TEXT("You can find the Unreal Engine {0} API documentation for C++ at https://docs.unrealengine.com/{0}/en-US/API/."), Arguments_EngineDocumentation); + const FString EngineDocumentation_BP = FString::Format(TEXT("You can find the Unreal Engine {0} API documentation for Blueprints at https://docs.unrealengine.com/{0}/en-US/BlueprintAPI/."), Arguments_EngineDocumentation); + + const FStringFormatOrderedArguments Arguments_SystemContext{ + PluginInformation, + PluginSupport, + ModelsInformation, + EngineDocumentation_General, + EngineDocumentation_CPP, + EngineDocumentation_BP + }; + + return FString::Format(TEXT("You are an assistant that will help with the development of projects in Unreal Engine in general.\n{0}\n{1}\n{2}\n{3}\n{4}\n{5}"), Arguments_SystemContext); +} + +void SHttpGPTChatView::LoadChatHistory() +{ + if (const FString LoadPath = GetHistoryPath(); FPaths::FileExists(LoadPath)) + { + FString FileContent; + if (!FFileHelper::LoadFileToString(FileContent, *LoadPath)) + { + return; + } + + TSharedPtr JsonParsed; + TSharedRef> Reader = TJsonReaderFactory<>::Create(FileContent); + if (FJsonSerializer::Deserialize(Reader, JsonParsed)) + { + const TArray> Data = JsonParsed->GetArrayField("Data"); + for (const TSharedPtr& Item : Data) + { + if (const TSharedPtr MessageItObj = Item->AsObject()) + { + if (FString RoleString; MessageItObj->TryGetStringField("role", RoleString)) + { + const EHttpGPTChatRole Role = UHttpGPTHelper::NameToRole(*RoleString); + + if (FString Message; MessageItObj->TryGetStringField("content", Message)) + { + if (Role == EHttpGPTChatRole::System && Message == GetDefaultSystemContext()) + { + continue; + } + + ChatItems.Add( + SNew(SHttpGPTChatItem) + .MessageRole(Role) + .InputText(Message) + ); + } + } + } + } + } + } + + for (const SHttpGPTChatItemPtr& Item : ChatItems) + { + ChatBox->AddSlot().AutoHeight()[Item.ToSharedRef()]; + } +} + +void SHttpGPTChatView::SaveChatHistory() const +{ + const TSharedPtr JsonRequest = MakeShared(); + + TArray> Data; + for (const FHttpGPTChatMessage& Item : GetChatHistory()) + { + Data.Add(Item.GetMessage()); + } + + JsonRequest->SetArrayField("Data", Data); + + FString RequestContentString; + const TSharedRef> Writer = TJsonWriterFactory<>::Create(&RequestContentString); + + if (FJsonSerializer::Serialize(JsonRequest.ToSharedRef(), Writer)) + { + FFileHelper::SaveStringToFile(RequestContentString, *GetHistoryPath()); + } +} \ No newline at end of file diff --git a/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatView.h b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatView.h new file mode 100644 index 0000000..97cf174 --- /dev/null +++ b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatView.h @@ -0,0 +1,47 @@ +// Author: Lucas Vilas-Boas +// Year: 2023 +// Repo: https://github.com/lucoiso/UEHttpGPT + +#pragma once + +#include +#include + +class SHttpGPTChatView final : public SCompoundWidget +{ +public: + SLATE_USER_ARGS(SHttpGPTChatView) + { + } + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + ~SHttpGPTChatView(); + + bool IsSendMessageEnabled() const; + bool IsClearChatEnabled() const; + FString GetHistoryPath() const; + +private: + TSharedRef ConstructContent(); + + FReply HandleSendMessageButton(const EHttpGPTChatRole Role); + FReply HandleClearChatButton(); + + TArray GetChatHistory() const; + FString GetDefaultSystemContext() const; + + void LoadChatHistory(); + void SaveChatHistory() const; + + TSharedPtr ChatBox; + TArray ChatItems; + TSharedPtr ChatScrollBox; + + TSharedPtr InputTextBox; + + TSharedPtr ModelsComboBox; + TArray> AvailableModels; + + TWeakObjectPtr RequestReference; +}; diff --git a/Source/HttpGPTEditorModule/Private/HttpGPTEditorModule.cpp b/Source/HttpGPTEditorModule/Private/HttpGPTEditorModule.cpp index 2ee5710..a153b2e 100644 --- a/Source/HttpGPTEditorModule/Private/HttpGPTEditorModule.cpp +++ b/Source/HttpGPTEditorModule/Private/HttpGPTEditorModule.cpp @@ -3,8 +3,8 @@ // Repo: https://github.com/lucoiso/UEHttpGPT #include "HttpGPTEditorModule.h" -#include "SHttpGPTChatView.h" -#include "SHttpGPTImageGenView.h" +#include "Chat/SHttpGPTChatView.h" +#include "ImageGen/SHttpGPTImageGenView.h" #include #include #include @@ -69,14 +69,14 @@ void FHttpGPTEditorModule::RegisterMenus() const TSharedPtr Menu = WorkspaceMenu::GetMenuStructure().GetToolsCategory()->AddGroup(LOCTEXT("HttpGPTCategory", "HttpGPT"), LOCTEXT("HttpGPTCategoryTooltip", "HttpGPT Plugin Tabs"), FSlateIcon(AppStyleName, "Icons.Documentation")); FGlobalTabmanager::Get()->RegisterNomadTabSpawner(HttpGPTChatTabName, EditorTabSpawnerDelegate) - .SetDisplayName(FText::FromString("HttpGPT Chat")) - .SetTooltipText(FText::FromString("Open HttpGPT Chat")) + .SetDisplayName(FText::FromString(TEXT("HttpGPT Chat"))) + .SetTooltipText(FText::FromString(TEXT("Open HttpGPT Chat"))) .SetIcon(FSlateIcon(AppStyleName, "DerivedData.ResourceUsage")) .SetGroup(Menu.ToSharedRef()); FGlobalTabmanager::Get()->RegisterNomadTabSpawner(HttpGPTImageGeneratorTabName, EditorTabSpawnerDelegate) - .SetDisplayName(FText::FromString("HttpGPT Image Generator")) - .SetTooltipText(FText::FromString("Open HttpGPT Image Generator")) + .SetDisplayName(FText::FromString(TEXT("HttpGPT Image Generator"))) + .SetTooltipText(FText::FromString(TEXT("Open HttpGPT Image Generator"))) .SetIcon(FSlateIcon(AppStyleName, "LevelEditor.Tabs.Viewports")) .SetGroup(Menu.ToSharedRef()); } diff --git a/Source/HttpGPTEditorModule/Private/ImageGen/HttpGPTImageGetter.cpp b/Source/HttpGPTEditorModule/Private/ImageGen/HttpGPTImageGetter.cpp new file mode 100644 index 0000000..37e9cd6 --- /dev/null +++ b/Source/HttpGPTEditorModule/Private/ImageGen/HttpGPTImageGetter.cpp @@ -0,0 +1,87 @@ +// Author: Lucas Vilas-Boas +// Year: 2023 +// Repo: https://github.com/lucoiso/UEHttpGPT + +#include "HttpGPTImageGetter.h" +#include +#include + +#ifdef UE_INLINE_GENERATED_CPP_BY_NAME +#include UE_INLINE_GENERATED_CPP_BY_NAME(HttpGPTImageGetter) +#endif + +UHttpGPTImageGetter::UHttpGPTImageGetter(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer), GeneratedImages(0u), DataSize(0u) +{ +} + +void UHttpGPTImageGetter::RequestSent() +{ + OnStatusChanged.ExecuteIfBound("Request Sent. Waiting for the response..."); +} + +void UHttpGPTImageGetter::RequestFailed() +{ + OnStatusChanged.ExecuteIfBound("Request Failed. Check the Logs."); + Destroy(); +} + +void UHttpGPTImageGetter::ProcessCompleted(const FHttpGPTImageResponse& Response) +{ + if (!Response.bSuccess) + { + const FStringFormatOrderedArguments Arguments_ErrorDetails{ + FString("Request Failed."), + FString("Please check the logs. (Enable internal logs in Project Settings -> Plugins -> HttpGPT)."), + FString("Error Details: "), + FString("\tError Code: ") + Response.Error.Code.ToString(), + FString("\tError Type: ") + Response.Error.Type.ToString(), + FString("\tError Message: ") + Response.Error.Message + }; + + OnStatusChanged.ExecuteIfBound(FString::Format(TEXT("{0}\n{1}\n\n{2}\n{3}\n{4}\n{5}"), Arguments_ErrorDetails)); + Destroy(); + return; + } + + DataSize = Response.Data.Num(); + OnStatusChanged.ExecuteIfBound("Request Completed."); + + OnImageGenerated_Internal.BindUFunction(this, TEXT("ImageGenerated")); + + for (const FHttpGPTImageData& Data : Response.Data) + { + ProcessImage(Data); + } +} + +void UHttpGPTImageGetter::ProcessImage(const FHttpGPTImageData& Data) +{ + UHttpGPTImageHelper::GenerateImage(Data, OnImageGenerated_Internal); +} + +void UHttpGPTImageGetter::ImageGenerated(UTexture2D* const Texture) +{ + OnImageGenerated.ExecuteIfBound(Texture); + + ++GeneratedImages; + if (GeneratedImages >= DataSize) + { + if (OutScrollBox.IsValid()) + { + OutScrollBox->ScrollToEnd(); + } + + Destroy(); + } +} + +void UHttpGPTImageGetter::Destroy() +{ + ClearFlags(RF_Standalone); + +#if ENGINE_MAJOR_VERSION >= 5 + MarkAsGarbage(); +#else + MarkPendingKill(); +#endif +} \ No newline at end of file diff --git a/Source/HttpGPTEditorModule/Private/ImageGen/HttpGPTImageGetter.h b/Source/HttpGPTEditorModule/Private/ImageGen/HttpGPTImageGetter.h new file mode 100644 index 0000000..f58dd18 --- /dev/null +++ b/Source/HttpGPTEditorModule/Private/ImageGen/HttpGPTImageGetter.h @@ -0,0 +1,49 @@ +// Author: Lucas Vilas-Boas +// Year: 2023 +// Repo: https://github.com/lucoiso/UEHttpGPT + +#pragma once + +#include +#include +#include +#include "HttpGPTImageGetter.generated.h" + +DECLARE_DELEGATE_OneParam(FImageGenerated, UTexture2D*); +DECLARE_DELEGATE_OneParam(FImageStatusChanged, FString); + +UCLASS(MinimalAPI, NotBlueprintable, NotPlaceable, Category = "Implementation") +class UHttpGPTImageGetter : public UObject +{ + GENERATED_BODY() + +public: + explicit UHttpGPTImageGetter(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + FImageGenerated OnImageGenerated; + FImageStatusChanged OnStatusChanged; + + UFUNCTION() + void RequestSent(); + + UFUNCTION() + void RequestFailed(); + + UFUNCTION() + void ProcessCompleted(const FHttpGPTImageResponse& Response); + + void Destroy(); + + TSharedPtr OutScrollBox; + +private: + void ProcessImage(const FHttpGPTImageData& Data); + + FHttpGPTImageGenerate OnImageGenerated_Internal; + + UFUNCTION() + void ImageGenerated(UTexture2D* const Texture); + + uint8 GeneratedImages; + uint8 DataSize; +}; \ No newline at end of file diff --git a/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenItem.cpp b/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenItem.cpp new file mode 100644 index 0000000..ca5d0d1 --- /dev/null +++ b/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenItem.cpp @@ -0,0 +1,124 @@ +// Author: Lucas Vilas-Boas +// Year: 2023 +// Repo: https://github.com/lucoiso/UEHttpGPT + +#include "SHttpGPTImageGenItem.h" +#include "SHttpGPTImageGenItemData.h" +#include "HttpGPTImageGetter.h" +#include +#include +#include +#include +#include + +void SHttpGPTImageGenItem::Construct(const FArguments& InArgs) +{ + Prompt = InArgs._Prompt; + + HttpGPTImageGetterObject = NewObject(); + HttpGPTImageGetterObject->SetFlags(RF_Standalone); + HttpGPTImageGetterObject->OutScrollBox = InArgs._OutScrollBox; + + HttpGPTImageGetterObject->OnImageGenerated.BindLambda( + [this](UTexture2D* const Texture) + { + if (Texture && ItemViewBox.IsValid()) + { + ItemViewBox->AddSlot() + .AutoWidth() + [ + SNew(SHttpGPTImageGenItemData) + .Texture(Texture) + ]; + } + } + ); + + HttpGPTImageGetterObject->OnStatusChanged.BindLambda( + [this](FString NewStatus) + { + if (HttpGPT::Internal::HasEmptyParam(NewStatus) || !Status.IsValid()) + { + return; + } + + Status->SetText(FText::FromString(TEXT("Status: ") + NewStatus)); + } + ); + + FHttpGPTImageOptions Options; + Options.Format = EHttpGPTResponseFormat::b64_json; + Options.Size = UHttpGPTHelper::NameToSize(*InArgs._Size); + Options.ImagesNum = FCString::Atoi(*InArgs._Num); + + RequestReference = UHttpGPTImageRequest::EditorTask(Prompt, Options); + + RequestReference->ProcessCompleted.AddDynamic(HttpGPTImageGetterObject.Get(), &UHttpGPTImageGetter::ProcessCompleted); + RequestReference->ErrorReceived.AddDynamic(HttpGPTImageGetterObject.Get(), &UHttpGPTImageGetter::ProcessCompleted); + RequestReference->RequestFailed.AddDynamic(HttpGPTImageGetterObject.Get(), &UHttpGPTImageGetter::RequestFailed); + RequestReference->RequestSent.AddDynamic(HttpGPTImageGetterObject.Get(), &UHttpGPTImageGetter::RequestSent); + + RequestReference->Activate(); + + ChildSlot + [ + ConstructContent() + ]; +} + +SHttpGPTImageGenItem::~SHttpGPTImageGenItem() +{ + if (RequestReference.IsValid()) + { + RequestReference->StopHttpGPTTask(); + } +} + +TSharedRef SHttpGPTImageGenItem::ConstructContent() +{ + constexpr float SlotPadding = 4.0f; + +#if ENGINE_MAJOR_VERSION < 5 + using FAppStyle = FEditorStyle; +#endif + + return SNew(SBox) + .Padding(SlotPadding) + [ + SNew(SBorder) + .BorderImage(FAppStyle::Get().GetBrush("Menu.Background")) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .Padding(SlotPadding) + .AutoHeight() + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(STextBlock) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) + .Text(FText::FromString(TEXT("Prompt: ") + Prompt)) + ] + + SVerticalBox::Slot() + .AutoHeight() + [ + SAssignNew(Status, STextBlock) + .Text(FText::FromString(TEXT("Status: Sending request..."))) + ] + ] + + SVerticalBox::Slot() + .Padding(SlotPadding) + .FillHeight(1.f) + [ + SNew(SScrollBox) + .Orientation(EOrientation::Orient_Horizontal) + + SScrollBox::Slot() + [ + SAssignNew(ItemViewBox, SHorizontalBox) + ] + ] + ] + ]; +} \ No newline at end of file diff --git a/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenItem.h b/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenItem.h new file mode 100644 index 0000000..66cdd25 --- /dev/null +++ b/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenItem.h @@ -0,0 +1,38 @@ +// Author: Lucas Vilas-Boas +// Year: 2023 +// Repo: https://github.com/lucoiso/UEHttpGPT + +#pragma once + +#include +#include + +class SHttpGPTImageGenItem final : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SHttpGPTImageGenItem) : _OutScrollBox(), _Prompt(), _Num(), _Size() + { + } + SLATE_ARGUMENT(TSharedPtr, OutScrollBox) + SLATE_ARGUMENT(FString, Prompt) + SLATE_ARGUMENT(FString, Num) + SLATE_ARGUMENT(FString, Size) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + ~SHttpGPTImageGenItem(); + + TWeakObjectPtr HttpGPTImageGetterObject; + +private: + TSharedRef ConstructContent(); + + FString Prompt; + + TSharedPtr Status; + TSharedPtr ItemViewBox; + + TWeakObjectPtr RequestReference; +}; + +typedef TSharedPtr SHttpGPTImageGenItemPtr; \ No newline at end of file diff --git a/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenItemData.cpp b/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenItemData.cpp new file mode 100644 index 0000000..61e5a00 --- /dev/null +++ b/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenItemData.cpp @@ -0,0 +1,93 @@ +// Author: Lucas Vilas-Boas +// Year: 2023 +// Repo: https://github.com/lucoiso/UEHttpGPT + +#include "SHttpGPTImageGenItemData.h" +#include +#include +#include +#include + +void SHttpGPTImageGenItemData::Construct(const FArguments& InArgs) +{ + Texture = InArgs._Texture; + + ChildSlot + [ + ConstructContent() + ]; +} + +TSharedRef SHttpGPTImageGenItemData::ConstructContent() +{ + constexpr float SlotPadding = 4.0f; + + return SNew(SVerticalBox) + + SVerticalBox::Slot() + .Padding(SlotPadding) + .FillHeight(1.f) + [ + SNew(SImage) + .Image(Texture.IsValid() ? new FSlateImageBrush(Texture.Get(), FVector2D(256.f, 256.f)) : nullptr) + ] + + SVerticalBox::Slot() + .Padding(SlotPadding) + .AutoHeight() + [ + SNew(SButton) + .Text(FText::FromString(TEXT("Save"))) + .HAlign(HAlign_Center) + .OnClicked(this, &SHttpGPTImageGenItemData::HandleSaveButton) + .IsEnabled(this, &SHttpGPTImageGenItemData::IsSaveEnabled) + ]; +} + +FReply SHttpGPTImageGenItemData::HandleSaveButton() +{ + const FString AssetName = FString::FromInt(Texture->GetUniqueID()); + FString TargetFilename = FPaths::Combine(TEXT("/Game/"), UHttpGPTSettings::Get()->GeneratedImagesDir, AssetName); + FPaths::NormalizeFilename(TargetFilename); + + UPackage* const Package = CreatePackage(*TargetFilename); + UTexture2D* const SavedTexture = NewObject(Package, *AssetName, RF_Public | RF_Standalone); + +#if ENGINE_MAJOR_VERSION >= 5 + SavedTexture->SetPlatformData(Texture->GetPlatformData()); +#else + SavedTexture->PlatformData = Texture->PlatformData; +#endif + + SavedTexture->UpdateResource(); + +#if ENGINE_MAJOR_VERSION >= 5 + SavedTexture->Source.Init(Texture->GetSizeX(), Texture->GetSizeY(), 1, Texture->GetPlatformData()->Mips.Num(), ETextureSourceFormat::TSF_BGRA8); +#else + SavedTexture->Source.Init(Texture->GetSizeX(), Texture->GetSizeY(), 1, Texture->PlatformData->Mips.Num(), ETextureSourceFormat::TSF_BGRA8); +#endif + + SavedTexture->PostEditChange(); + + SavedTexture->MarkPackageDirty(); + FAssetRegistryModule::AssetCreated(SavedTexture); + + const FString TempPackageFilename = FPackageName::LongPackageNameToFilename(Package->GetName(), FPackageName::GetAssetPackageExtension()); + +#if ENGINE_MAJOR_VERSION >= 5 + FSavePackageArgs SaveArgs; + SaveArgs.SaveFlags = RF_Public | RF_Standalone; + UPackage::SavePackage(Package, SavedTexture, *TempPackageFilename, SaveArgs); +#else + UPackage::SavePackage(Package, SavedTexture, RF_Public | RF_Standalone, *TempPackageFilename); +#endif + + TArray SyncAssets; + SyncAssets.Add(FAssetData(SavedTexture)); + GEditor->SyncBrowserToObjects(SyncAssets); + + return FReply::Handled(); +} + +bool SHttpGPTImageGenItemData::IsSaveEnabled() const +{ + return Texture.IsValid(); +} \ No newline at end of file diff --git a/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenItemData.h b/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenItemData.h new file mode 100644 index 0000000..1d1dddb --- /dev/null +++ b/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenItemData.h @@ -0,0 +1,30 @@ +// Author: Lucas Vilas-Boas +// Year: 2023 +// Repo: https://github.com/lucoiso/UEHttpGPT + +#pragma once + +#include +#include + +class SHttpGPTImageGenItemData final : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SHttpGPTImageGenItemData) : _Texture() + { + } + SLATE_ARGUMENT(class UTexture2D*, Texture) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + + FReply HandleSaveButton(); + bool IsSaveEnabled() const; + +private: + TSharedRef ConstructContent(); + + TWeakObjectPtr Texture; +}; + +typedef TSharedPtr SHttpGPTImageGenItemDataPtr; \ No newline at end of file diff --git a/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenView.cpp b/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenView.cpp new file mode 100644 index 0000000..64e6239 --- /dev/null +++ b/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenView.cpp @@ -0,0 +1,152 @@ +// Author: Lucas Vilas-Boas +// Year: 2023 +// Repo: https://github.com/lucoiso/UEHttpGPT + +#include "SHttpGPTImageGenView.h" +#include "SHttpGPTImageGenItem.h" +#include +#include +#include +#include + +void SHttpGPTImageGenView::Construct(const FArguments& InArgs) +{ + InitializeImageNumOptions(); + ImageNumComboBox = SNew(STextComboBox) + .OptionsSource(&ImageNum) + .InitiallySelectedItem(ImageNum[0]) + .ToolTipText(FText::FromString(TEXT("Number of Generated Images"))); + + InitializeImageSizeOptions(); + ImageSizeComboBox = SNew(STextComboBox) + .OptionsSource(&ImageSize) + .InitiallySelectedItem(ImageSize[0]) + .ToolTipText(FText::FromString(TEXT("Size of Generated Images"))); + + ChildSlot + [ + ConstructContent() + ]; +} + +TSharedRef SHttpGPTImageGenView::ConstructContent() +{ + constexpr float SlotPadding = 4.0f; + + return SNew(SVerticalBox) + + SVerticalBox::Slot() + .Padding(SlotPadding) + .FillHeight(1.f) + [ + SAssignNew(ViewScrollBox, SScrollBox) + + SScrollBox::Slot() + [ + SAssignNew(ViewBox, SVerticalBox) + ] + ] + + SVerticalBox::Slot() + .Padding(SlotPadding) + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(SlotPadding) + .FillWidth(1.f) + [ + SAssignNew(InputTextBox, SEditableTextBox) + .AllowContextMenu(true) + .IsReadOnly(false) + .OnTextCommitted_Lambda( + [this]([[maybe_unused]] const FText& Text, ETextCommit::Type CommitType) + { + if (IsSendRequestEnabled() && CommitType == ETextCommit::OnEnter) + { + HandleSendRequestButton(); + } + } + ) + ] + + SHorizontalBox::Slot() + .Padding(SlotPadding) + .AutoWidth() + [ + SNew(SButton) + .Text(FText::FromString(TEXT("Generate"))) + .ToolTipText(FText::FromString(TEXT("Request Images Generation"))) + .OnClicked(this, &SHttpGPTImageGenView::HandleSendRequestButton) + .IsEnabled(this, &SHttpGPTImageGenView::IsSendRequestEnabled) + ] + + SHorizontalBox::Slot() + .Padding(SlotPadding) + .AutoWidth() + [ + ImageNumComboBox.ToSharedRef() + ] + + SHorizontalBox::Slot() + .Padding(SlotPadding) + .AutoWidth() + [ + ImageSizeComboBox.ToSharedRef() + ] + + SHorizontalBox::Slot() + .Padding(SlotPadding) + .AutoWidth() + [ + SNew(SButton) + .Text(FText::FromString(TEXT("Clear"))) + .ToolTipText(FText::FromString(TEXT("Clear Generation History"))) + .OnClicked(this, &SHttpGPTImageGenView::HandleClearViewButton) + .IsEnabled(this, &SHttpGPTImageGenView::IsClearViewEnabled) + ] + ]; +} + +FReply SHttpGPTImageGenView::HandleSendRequestButton() +{ + ViewBox->AddSlot() + .AutoHeight() + [ + SNew(SHttpGPTImageGenItem) + .OutScrollBox(ViewScrollBox) + .Prompt(InputTextBox->GetText().ToString()) + .Num(*ImageNumComboBox->GetSelectedItem().Get()) + .Size(*ImageSizeComboBox->GetSelectedItem().Get()) + ]; + + ViewScrollBox->ScrollToEnd(); + InputTextBox->SetText(FText::GetEmpty()); + + return FReply::Handled(); +} + +bool SHttpGPTImageGenView::IsSendRequestEnabled() const +{ + return !HttpGPT::Internal::HasEmptyParam(InputTextBox->GetText()); +} + +FReply SHttpGPTImageGenView::HandleClearViewButton() +{ + ViewBox->ClearChildren(); + return FReply::Handled(); +} + +bool SHttpGPTImageGenView::IsClearViewEnabled() const +{ + return ViewBox->NumSlots() > 0; +} + +void SHttpGPTImageGenView::InitializeImageNumOptions() +{ + constexpr uint8 MaxNum = 10u; + for (uint8 Iterator = 1u; Iterator <= MaxNum; ++Iterator) + { + ImageNum.Add(MakeShared(FString::FromInt(Iterator))); + } +} + +void SHttpGPTImageGenView::InitializeImageSizeOptions() +{ + ImageSize.Add(MakeShared(UHttpGPTHelper::SizeToName(EHttpGPTImageSize::x256).ToString())); + ImageSize.Add(MakeShared(UHttpGPTHelper::SizeToName(EHttpGPTImageSize::x512).ToString())); + ImageSize.Add(MakeShared(UHttpGPTHelper::SizeToName(EHttpGPTImageSize::x1024).ToString())); +} \ No newline at end of file diff --git a/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenView.h b/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenView.h new file mode 100644 index 0000000..025bad8 --- /dev/null +++ b/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenView.h @@ -0,0 +1,42 @@ +// Author: Lucas Vilas-Boas +// Year: 2023 +// Repo: https://github.com/lucoiso/UEHttpGPT + +#pragma once + +#include +#include + +class SHttpGPTImageGenView final : public SCompoundWidget +{ +public: + SLATE_USER_ARGS(SHttpGPTImageGenView) + { + } + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + + bool IsSendRequestEnabled() const; + bool IsClearViewEnabled() const; + +private: + TSharedRef ConstructContent(); + + FReply HandleSendRequestButton(); + FReply HandleClearViewButton(); + + void InitializeImageNumOptions(); + void InitializeImageSizeOptions(); + + TSharedPtr ViewBox; + TSharedPtr ViewScrollBox; + + TSharedPtr InputTextBox; + + TSharedPtr ImageNumComboBox; + TArray> ImageNum; + + TSharedPtr ImageSizeComboBox; + TArray> ImageSize; +}; diff --git a/Source/HttpGPTEditorModule/Private/SHttpGPTChatView.cpp b/Source/HttpGPTEditorModule/Private/SHttpGPTChatView.cpp deleted file mode 100644 index c39e876..0000000 --- a/Source/HttpGPTEditorModule/Private/SHttpGPTChatView.cpp +++ /dev/null @@ -1,488 +0,0 @@ -// Author: Lucas Vilas-Boas -// Year: 2023 -// Repo: https://github.com/lucoiso/UEHttpGPT - -#include "SHttpGPTChatView.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef UE_INLINE_GENERATED_CPP_BY_NAME -#include UE_INLINE_GENERATED_CPP_BY_NAME(SHttpGPTChatView) -#endif - -UHttpGPTMessagingHandler::UHttpGPTMessagingHandler(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) -{ -} - -void UHttpGPTMessagingHandler::RequestSent() -{ - OnMessageContentUpdated.ExecuteIfBound("Waiting for response..."); -} - -void UHttpGPTMessagingHandler::RequestFailed() -{ - OnMessageContentUpdated.ExecuteIfBound("Request Failed.\nPlease check the logs. (Enable internal logs in Project Settings -> Plugins -> HttpGPT)."); - Destroy(); -} - -void UHttpGPTMessagingHandler::ProcessUpdated(const FHttpGPTChatResponse& Response) -{ - ProcessResponse(Response); -} - -void UHttpGPTMessagingHandler::ProcessCompleted(const FHttpGPTChatResponse& Response) -{ - ProcessResponse(Response); - Destroy(); -} - -void UHttpGPTMessagingHandler::ProcessResponse(const FHttpGPTChatResponse& Response) -{ - bool bScrollToEnd = false; - if (ScrollBoxReference.IsValid()) - { - bScrollToEnd = FMath::Abs(ScrollBoxReference->GetScrollOffsetOfEnd() - ScrollBoxReference->GetScrollOffset()) <= 8.f; - } - - if (!Response.bSuccess) - { - const FStringFormatOrderedArguments Arguments_ErrorDetails{ - FString("Request Failed."), - FString("Please check the logs. (Enable internal logs in Project Settings -> Plugins -> HttpGPT)."), - FString("Error Details: "), - FString("\tError Code: ") + Response.Error.Code.ToString(), - FString("\tError Type: ") + Response.Error.Type.ToString(), - FString("\tError Message: ") + Response.Error.Message - }; - - OnMessageContentUpdated.ExecuteIfBound(FString::Format(TEXT("{0}\n{1}\n\n{2}\n{3}\n{4}\n{5}"), Arguments_ErrorDetails)); - } - else if (Response.bSuccess && !HttpGPT::Internal::HasEmptyParam(Response.Choices)) - { - OnMessageContentUpdated.ExecuteIfBound(Response.Choices[0].Message.Content); - } - else - { - return; - } - - if (ScrollBoxReference.IsValid() && bScrollToEnd) - { - ScrollBoxReference->ScrollToEnd(); - } -} - -void UHttpGPTMessagingHandler::Destroy() -{ - ClearFlags(RF_Standalone); - -#if ENGINE_MAJOR_VERSION >= 5 - MarkAsGarbage(); -#else - MarkPendingKill(); -#endif -} - -void SHttpGPTChatItem::Construct(const FArguments& InArgs) -{ - constexpr float Slot_Padding = 4.0f; - - if (InArgs._MessageRole == EHttpGPTChatRole::Assistant) - { - MessagingHandlerObject = NewObject(); - MessagingHandlerObject->SetFlags(RF_Standalone); - - MessagingHandlerObject->OnMessageContentUpdated.BindLambda( - [this](FString Content) - { - if (!Message.IsValid()) - { - return; - } - -#if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 1) - const FTextSelection SelectedText = Message->GetSelection(); - Message->SetText(FText::FromString(Content)); - Message->SelectText(SelectedText.GetBeginning(), SelectedText.GetEnd()); -#else - Message->SetText(FText::FromString(Content)); -#endif - } - ); - } - - const FText RoleText = FText::FromString(InArgs._MessageRole == EHttpGPTChatRole::User ? "User:" : "Assistant:"); - const FMargin SlotPadding = InArgs._MessageRole == EHttpGPTChatRole::User ? FMargin(Slot_Padding * 16.f, Slot_Padding, Slot_Padding, Slot_Padding) : FMargin(Slot_Padding, Slot_Padding, Slot_Padding * 16.f, Slot_Padding); - -#if ENGINE_MAJOR_VERSION < 5 - using FAppStyle = FEditorStyle; -#endif - - const ISlateStyle& AppStyle = FAppStyle::Get(); - - ChildSlot - [ - SNew(SVerticalBox) - + SVerticalBox::Slot() - .Padding(SlotPadding) - [ - SNew(SBorder) - .BorderImage(AppStyle.GetBrush("Menu.Background")) - [ - SNew(SVerticalBox) - + SVerticalBox::Slot() - .Padding(Slot_Padding) - .AutoHeight() - [ - SAssignNew(Role, STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) - .Text(RoleText) - ] - + SVerticalBox::Slot() - .Padding(FMargin(Slot_Padding * 4, Slot_Padding, Slot_Padding, Slot_Padding)) - .FillHeight(1.f) - [ - SAssignNew(Message, SMultiLineEditableText) - .AllowMultiLine(true) - .AutoWrapText(true) - .IsReadOnly(true) - .AllowContextMenu(true) - .Text(FText::FromString(InArgs._InputText)) - ] - ] - ] - ]; -} - -FString SHttpGPTChatItem::GetRoleText() const -{ - return Role.IsValid() ? Role->GetText().ToString() : FString(); -} - -FString SHttpGPTChatItem::GetMessageText() const -{ - return Message.IsValid() ? Message->GetText().ToString() : FString(); -} - -void SHttpGPTChatView::Construct([[maybe_unused]] const FArguments&) -{ - constexpr float Slot_Padding = 4.0f; - -#if ENGINE_MAJOR_VERSION < 5 - using FAppStyle = FEditorStyle; -#endif - - const ISlateStyle& AppStyle = FAppStyle::Get(); - - ModelsComboBox = SNew(STextComboBox) - .OptionsSource(&AvailableModels) - .ToolTipText(FText::FromString("Selected GPT Model")); - - InitializeModelsOptions(); - - ChildSlot - [ - SNew(SVerticalBox) - + SVerticalBox::Slot() - .Padding(Slot_Padding) - .FillHeight(1.f) - [ - SNew(SBorder) - .BorderImage(AppStyle.GetBrush("NoBorder")) - [ - SAssignNew(ChatScrollBox, SScrollBox) - + SScrollBox::Slot() - [ - SAssignNew(ChatBox, SVerticalBox) - ] - ] - ] - + SVerticalBox::Slot() - .Padding(Slot_Padding) - .AutoHeight() - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .Padding(Slot_Padding) - .FillWidth(1.f) - [ - SAssignNew(InputTextBox, SEditableTextBox) - .AllowContextMenu(true) - .IsReadOnly(false) - .OnTextCommitted_Lambda( - [this]([[maybe_unused]] const FText& Text, ETextCommit::Type CommitType) - { - if (IsSendMessageEnabled() && CommitType == ETextCommit::OnEnter) - { - HandleSendMessageButton(); - } - } - ) - ] - + SHorizontalBox::Slot() - .Padding(Slot_Padding) - .AutoWidth() - [ - SNew(SButton) - .Text(FText::FromString("Send")) - .ToolTipText(FText::FromString("Send Message")) - .OnClicked(this, &SHttpGPTChatView::HandleSendMessageButton) - .IsEnabled(this, &SHttpGPTChatView::IsSendMessageEnabled) - ] - + SHorizontalBox::Slot() - .Padding(Slot_Padding) - .AutoWidth() - [ - ModelsComboBox.ToSharedRef() - ] - + SHorizontalBox::Slot() - .Padding(Slot_Padding) - .AutoWidth() - [ - SNew(SButton) - .Text(FText::FromString("Clear")) - .ToolTipText(FText::FromString("Clear Chat History")) - .OnClicked(this, &SHttpGPTChatView::HandleClearChatButton) - .IsEnabled(this, &SHttpGPTChatView::IsClearChatEnabled) - ] - ] - ]; - - LoadChatHistory(); -} - -SHttpGPTChatView::~SHttpGPTChatView() -{ - SaveChatHistory(); -} - -FReply SHttpGPTChatView::HandleSendMessageButton() -{ - SHttpGPTChatItemPtr UserMessage = SNew(SHttpGPTChatItem).MessageRole(EHttpGPTChatRole::User).InputText(InputTextBox->GetText().ToString()); - ChatBox->AddSlot().AutoHeight()[UserMessage.ToSharedRef()]; - ChatItems.Add(UserMessage); - - SHttpGPTChatItemPtr AssistantMessage = SNew(SHttpGPTChatItem).MessageRole(EHttpGPTChatRole::Assistant); - AssistantMessage->MessagingHandlerObject->ScrollBoxReference = ChatScrollBox; - - FHttpGPTChatOptions Options; - Options.Model = UHttpGPTHelper::NameToModel(*(*ModelsComboBox->GetSelectedItem().Get())); - Options.bStream = true; - - RequestReference = UHttpGPTChatRequest::EditorTask(GetChatHistory(), Options); - - RequestReference->ProgressStarted.AddDynamic(AssistantMessage->MessagingHandlerObject.Get(), &UHttpGPTMessagingHandler::ProcessUpdated); - RequestReference->ProgressUpdated.AddDynamic(AssistantMessage->MessagingHandlerObject.Get(), &UHttpGPTMessagingHandler::ProcessUpdated); - RequestReference->ProcessCompleted.AddDynamic(AssistantMessage->MessagingHandlerObject.Get(), &UHttpGPTMessagingHandler::ProcessCompleted); - RequestReference->ErrorReceived.AddDynamic(AssistantMessage->MessagingHandlerObject.Get(), &UHttpGPTMessagingHandler::ProcessCompleted); - RequestReference->RequestFailed.AddDynamic(AssistantMessage->MessagingHandlerObject.Get(), &UHttpGPTMessagingHandler::RequestFailed); - RequestReference->RequestSent.AddDynamic(AssistantMessage->MessagingHandlerObject.Get(), &UHttpGPTMessagingHandler::RequestSent); - - RequestReference->Activate(); - - ChatBox->AddSlot().AutoHeight()[AssistantMessage.ToSharedRef()]; - ChatItems.Add(AssistantMessage); - - ChatScrollBox->ScrollToEnd(); - - InputTextBox->SetText(FText::GetEmpty()); - - return FReply::Handled(); -} - -bool SHttpGPTChatView::IsSendMessageEnabled() const -{ - return (!RequestReference.IsValid() || !UHttpGPTTaskStatus::IsTaskActive(RequestReference.Get())) && !HttpGPT::Internal::HasEmptyParam(InputTextBox->GetText()); -} - -FReply SHttpGPTChatView::HandleClearChatButton() -{ - ChatItems.Empty(); - ChatBox->ClearChildren(); - - if (RequestReference.IsValid()) - { - RequestReference->StopHttpGPTTask(); - } - else - { - RequestReference.Reset(); - } - - return FReply::Handled(); -} - -bool SHttpGPTChatView::IsClearChatEnabled() const -{ - return !HttpGPT::Internal::HasEmptyParam(ChatItems); -} - -TArray SHttpGPTChatView::GetChatHistory() const -{ - TArray Output - { - FHttpGPTChatMessage(EHttpGPTChatRole::System, GetSystemContext()) - }; - - for (const SHttpGPTChatItemPtr& Item : ChatItems) - { - FString RoleText = Item->GetRoleText(); - RoleText.RemoveFromEnd(TEXT(":")); - Output.Add(FHttpGPTChatMessage(*RoleText, Item->GetMessageText())); - } - - return Output; -} - -FString SHttpGPTChatView::GetSystemContext() const -{ - if (const UHttpGPTSettings* const Settings = UHttpGPTSettings::Get(); Settings->bUseCustomSystemContext) - { - return Settings->CustomSystemContext; - } - - FString SupportedModels; - for (const TSharedPtr& Model : AvailableModels) - { - SupportedModels.Append(*Model.Get() + ", "); - } - SupportedModels.RemoveFromEnd(", "); - - const TSharedPtr PluginInterface = IPluginManager::Get().FindPlugin("HttpGPT"); - - const FString PluginShortName = "HttpGPT"; - const FString EngineVersion = FString::Printf(TEXT("%d.%d"), ENGINE_MAJOR_VERSION, ENGINE_MINOR_VERSION); - - const FStringFormatOrderedArguments Arguments_PluginInfo{ - EngineVersion, - PluginShortName, - PluginInterface->GetDescriptor().VersionName, - PluginInterface->GetDescriptor().CreatedBy, - PluginInterface->GetDescriptor().Description - }; - - const FString PluginInformation = FString::Format(TEXT("You are in the Unreal Engine {0} plugin {1} version {2}, which was developed by {3}. The description of HttpGPT is: \"{4}\""), Arguments_PluginInfo); - - const FStringFormatOrderedArguments Arguments_SupportInfo{ - PluginInterface->GetDescriptor().DocsURL, - PluginInterface->GetDescriptor().SupportURL, - }; - - const FString PluginSupport = FString::Format(TEXT("You can find the HttpGPT documentation at {0} and support at {1}."), Arguments_SupportInfo); - - const FStringFormatOrderedArguments Arguments_Models{ - *ModelsComboBox->GetSelectedItem().Get(), - SupportedModels - }; - - const FString ModelsInformation = FString::Format(TEXT("You're using the model {0} and HttpGPT currently supports all these OpenAI Models: {1}."), Arguments_Models); - - const FStringFormatOrderedArguments Arguments_EngineDocumentation{ - EngineVersion - }; - - const FString EngineDocumentation_General = FString::Format(TEXT("You can find the Unreal Engine {0} general documentation at https://docs.unrealengine.com/{0}/en-US/."), Arguments_EngineDocumentation); - const FString EngineDocumentation_CPP = FString::Format(TEXT("You can find the Unreal Engine {0} API documentation for C++ at https://docs.unrealengine.com/{0}/en-US/API/."), Arguments_EngineDocumentation); - const FString EngineDocumentation_BP = FString::Format(TEXT("You can find the Unreal Engine {0} API documentation for Blueprints at https://docs.unrealengine.com/{0}/en-US/BlueprintAPI/."), Arguments_EngineDocumentation); - - const FStringFormatOrderedArguments Arguments_SystemContext{ - PluginInformation, - PluginSupport, - ModelsInformation, - EngineDocumentation_General, - EngineDocumentation_CPP, - EngineDocumentation_BP - }; - - return FString::Format(TEXT("You are an assistant that will help with the development of projects in Unreal Engine in general.\n{0}\n{1}\n{2}\n{3}\n{4}\n{5}"), Arguments_SystemContext); -} - -void SHttpGPTChatView::LoadChatHistory() -{ - if (const FString LoadPath = GetHistoryPath(); FPaths::FileExists(LoadPath)) - { - FString FileContent; - if (!FFileHelper::LoadFileToString(FileContent, *LoadPath)) - { - return; - } - - TSharedPtr JsonParsed; - TSharedRef> Reader = TJsonReaderFactory<>::Create(FileContent); - if (FJsonSerializer::Deserialize(Reader, JsonParsed)) - { - const TArray> Data = JsonParsed->GetArrayField("Data"); - for (const TSharedPtr& Item : Data) - { - if (const TSharedPtr MessageItObj = Item->AsObject()) - { - const FString RoleString = MessageItObj->GetStringField("role"); - const EHttpGPTChatRole Role = UHttpGPTHelper::NameToRole(*RoleString); - if (Role == EHttpGPTChatRole::System) - { - continue; - } - - const FString Message = MessageItObj->GetStringField("content"); - - ChatItems.Add( - SNew(SHttpGPTChatItem) - .MessageRole(Role) - .InputText(Message) - ); - } - } - } - } - - for (const SHttpGPTChatItemPtr& Item : ChatItems) - { - ChatBox->AddSlot().AutoHeight()[Item.ToSharedRef()]; - } -} - -void SHttpGPTChatView::SaveChatHistory() const -{ - const TSharedPtr JsonRequest = MakeShared(); - - TArray> Data; - for (const FHttpGPTChatMessage& Item : GetChatHistory()) - { - Data.Add(Item.GetMessage()); - } - - JsonRequest->SetArrayField("Data", Data); - - FString RequestContentString; - const TSharedRef> Writer = TJsonWriterFactory<>::Create(&RequestContentString); - - if (FJsonSerializer::Serialize(JsonRequest.ToSharedRef(), Writer)) - { - FFileHelper::SaveStringToFile(RequestContentString, *GetHistoryPath()); - } -} - -FString SHttpGPTChatView::GetHistoryPath() const -{ - return FPaths::Combine(FPaths::ProjectSavedDir(), "HttpGPT", "HttpGPTChatHistory.json"); -} - -void SHttpGPTChatView::InitializeModelsOptions() -{ - for (const FName& ModelName : UHttpGPTHelper::GetAvailableGPTModels()) - { - AvailableModels.Add(MakeShared(ModelName.ToString())); - - if (ModelsComboBox.IsValid() && ModelName.IsEqual(UHttpGPTHelper::ModelToName(EHttpGPTChatModel::gpt35turbo))) - { - ModelsComboBox->SetSelectedItem(AvailableModels.Top()); - } - } -} \ No newline at end of file diff --git a/Source/HttpGPTEditorModule/Private/SHttpGPTChatView.h b/Source/HttpGPTEditorModule/Private/SHttpGPTChatView.h deleted file mode 100644 index 60dff78..0000000 --- a/Source/HttpGPTEditorModule/Private/SHttpGPTChatView.h +++ /dev/null @@ -1,111 +0,0 @@ -// Author: Lucas Vilas-Boas -// Year: 2023 -// Repo: https://github.com/lucoiso/UEHttpGPT - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include "SHttpGPTChatView.generated.h" - -DECLARE_DELEGATE_OneParam(FMessageContentUpdated, FString); - -UCLASS(MinimalAPI, NotBlueprintable, NotPlaceable, Category = "Implementation") -class UHttpGPTMessagingHandler : public UObject -{ - GENERATED_BODY() - -public: - explicit UHttpGPTMessagingHandler(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); - - FMessageContentUpdated OnMessageContentUpdated; - - UFUNCTION() - void RequestSent(); - - UFUNCTION() - void RequestFailed(); - - UFUNCTION() - void ProcessUpdated(const FHttpGPTChatResponse& Response); - - UFUNCTION() - void ProcessCompleted(const FHttpGPTChatResponse& Response); - - TSharedPtr ScrollBoxReference; - - void Destroy(); - -private: - void ProcessResponse(const FHttpGPTChatResponse& Response); -}; - -class SHttpGPTChatItem final : public SCompoundWidget -{ -public: - SLATE_BEGIN_ARGS(SHttpGPTChatItem) : _MessageRole(), _InputText() - { - } - SLATE_ARGUMENT(EHttpGPTChatRole, MessageRole) - SLATE_ARGUMENT(FString, InputText) - SLATE_END_ARGS() - - void Construct(const FArguments& InArgs); - - FString GetRoleText() const; - FString GetMessageText() const; - - TWeakObjectPtr MessagingHandlerObject; - -private: - TSharedPtr Role; - TSharedPtr Message; -}; - -typedef TSharedPtr SHttpGPTChatItemPtr; - -class SHttpGPTChatView final : public SCompoundWidget -{ -public: - SLATE_USER_ARGS(SHttpGPTChatView) - { - } - SLATE_END_ARGS() - - void Construct(const FArguments& InArgs); - ~SHttpGPTChatView(); - - FReply HandleSendMessageButton(); - bool IsSendMessageEnabled() const; - - FReply HandleClearChatButton(); - bool IsClearChatEnabled() const; - -protected: - TArray GetChatHistory() const; - FString GetSystemContext() const; - - void LoadChatHistory(); - void SaveChatHistory() const; - - FString GetHistoryPath() const; - - void InitializeModelsOptions(); - -private: - TSharedPtr ChatBox; - TArray ChatItems; - TSharedPtr ChatScrollBox; - - TSharedPtr InputTextBox; - - TSharedPtr ModelsComboBox; - TArray> AvailableModels; - - TWeakObjectPtr RequestReference; -}; diff --git a/Source/HttpGPTEditorModule/Private/SHttpGPTImageGenView.cpp b/Source/HttpGPTEditorModule/Private/SHttpGPTImageGenView.cpp deleted file mode 100644 index 185f6d1..0000000 --- a/Source/HttpGPTEditorModule/Private/SHttpGPTImageGenView.cpp +++ /dev/null @@ -1,409 +0,0 @@ -// Author: Lucas Vilas-Boas -// Year: 2023 -// Repo: https://github.com/lucoiso/UEHttpGPT - -#include "SHttpGPTImageGenView.h" -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef UE_INLINE_GENERATED_CPP_BY_NAME -#include UE_INLINE_GENERATED_CPP_BY_NAME(SHttpGPTImageGenView) -#endif - -UHttpGPTImageGetter::UHttpGPTImageGetter(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) -{ -} - -void UHttpGPTImageGetter::RequestSent() -{ - OnStatusChanged.ExecuteIfBound("Request Sent. Waiting for the response..."); -} - -void UHttpGPTImageGetter::RequestFailed() -{ - OnStatusChanged.ExecuteIfBound("Request Failed. Check the Logs."); - Destroy(); -} - -void UHttpGPTImageGetter::ProcessCompleted(const FHttpGPTImageResponse& Response) -{ - if (!Response.bSuccess) - { - const FStringFormatOrderedArguments Arguments_ErrorDetails{ - FString("Request Failed."), - FString("Please check the logs. (Enable internal logs in Project Settings -> Plugins -> HttpGPT)."), - FString("Error Details: "), - FString("\tError Code: ") + Response.Error.Code.ToString(), - FString("\tError Type: ") + Response.Error.Type.ToString(), - FString("\tError Message: ") + Response.Error.Message - }; - - OnStatusChanged.ExecuteIfBound(FString::Format(TEXT("{0}\n{1}\n\n{2}\n{3}\n{4}\n{5}"), Arguments_ErrorDetails)); - Destroy(); - return; - } - - DataSize = Response.Data.Num(); - OnStatusChanged.ExecuteIfBound("Request Completed."); - - OnImageGenerated_Internal.BindUFunction(this, TEXT("ImageGenerated")); - - for (const FHttpGPTImageData& Data : Response.Data) - { - ProcessImage(Data); - } -} - -void UHttpGPTImageGetter::ProcessImage(const FHttpGPTImageData& Data) -{ - UHttpGPTImageHelper::GenerateImage(Data, OnImageGenerated_Internal); -} - -void UHttpGPTImageGetter::ImageGenerated(UTexture2D* Texture) -{ - OnImageGenerated.ExecuteIfBound(Texture); - - ++GeneratedImages; - if (GeneratedImages >= DataSize) - { - Destroy(); - } -} - -void UHttpGPTImageGetter::Destroy() -{ - ClearFlags(RF_Standalone); - -#if ENGINE_MAJOR_VERSION >= 5 - MarkAsGarbage(); -#else - MarkPendingKill(); -#endif -} - -void SHttpGPTImageGenItemData::Construct(const FArguments& InArgs) -{ - Texture = InArgs._Texture; - - constexpr float Slot_Padding = 4.0f; - - ChildSlot - [ - SNew(SVerticalBox) - + SVerticalBox::Slot() - .Padding(Slot_Padding) - .FillHeight(1.f) - [ - SAssignNew(Image, SImage) - .Image(Texture.IsValid() ? new FSlateImageBrush(Texture.Get(), FVector2D(256, 256)) : nullptr) - ] - + SVerticalBox::Slot() - .Padding(Slot_Padding) - .AutoHeight() - [ - SAssignNew(SaveButton, SButton) - .Text(FText::FromString("Save")) - .HAlign(HAlign_Center) - .OnClicked(this, &SHttpGPTImageGenItemData::HandleSaveButton) - .IsEnabled(this, &SHttpGPTImageGenItemData::IsSaveEnabled) - ] - ]; -} - -FReply SHttpGPTImageGenItemData::HandleSaveButton() -{ - const FString AssetName = FString::FromInt(Texture->GetUniqueID()); - FString TargetFilename = FPaths::Combine(TEXT("/Game/"), UHttpGPTSettings::Get()->GeneratedImagesDir, AssetName); - FPaths::NormalizeFilename(TargetFilename); - - UPackage* const Package = CreatePackage(*TargetFilename); - UTexture2D* const SavedTexture = NewObject(Package, *AssetName, RF_Public | RF_Standalone); - -#if ENGINE_MAJOR_VERSION >= 5 - SavedTexture->SetPlatformData(Texture->GetPlatformData()); -#else - SavedTexture->PlatformData = Texture->PlatformData; -#endif - - SavedTexture->UpdateResource(); - -#if ENGINE_MAJOR_VERSION >= 5 - SavedTexture->Source.Init(Texture->GetSizeX(), Texture->GetSizeY(), 1, Texture->GetPlatformData()->Mips.Num(), ETextureSourceFormat::TSF_BGRA8); -#else - SavedTexture->Source.Init(Texture->GetSizeX(), Texture->GetSizeY(), 1, Texture->PlatformData->Mips.Num(), ETextureSourceFormat::TSF_BGRA8); -#endif - - SavedTexture->PostEditChange(); - - SavedTexture->MarkPackageDirty(); - FAssetRegistryModule::AssetCreated(SavedTexture); - - const FString TempPackageFilename = FPackageName::LongPackageNameToFilename(Package->GetName(), FPackageName::GetAssetPackageExtension()); - -#if ENGINE_MAJOR_VERSION >= 5 - FSavePackageArgs SaveArgs; - SaveArgs.SaveFlags = RF_Public | RF_Standalone; - UPackage::SavePackage(Package, SavedTexture, *TempPackageFilename, SaveArgs); -#else - UPackage::SavePackage(Package, SavedTexture, RF_Public | RF_Standalone, *TempPackageFilename); -#endif - - TArray SyncAssets; - SyncAssets.Add(FAssetData(SavedTexture)); - GEditor->SyncBrowserToObjects(SyncAssets); - - return FReply::Handled(); -} - -bool SHttpGPTImageGenItemData::IsSaveEnabled() const -{ - return Texture.IsValid(); -} - -void SHttpGPTImageGenItem::Construct(const FArguments& InArgs) -{ - HttpGPTImageGetterObject = NewObject(); - HttpGPTImageGetterObject->SetFlags(RF_Standalone); - - HttpGPTImageGetterObject->OnImageGenerated.BindLambda( - [this](UTexture2D* Texture) - { - if (Texture) - { - ItemViewBox->AddSlot().AutoWidth()[SNew(SHttpGPTImageGenItemData).Texture(Texture)]; - } - } - ); - - HttpGPTImageGetterObject->OnStatusChanged.BindLambda( - [this](FString NewStatus) - { - if (HttpGPT::Internal::HasEmptyParam(NewStatus) || !Status.IsValid()) - { - return; - } - - Status->SetText(FText::FromString("Status: " + NewStatus)); - } - ); - - FHttpGPTImageOptions Options; - Options.Format = EHttpGPTResponseFormat::b64_json; - Options.Size = UHttpGPTHelper::NameToSize(*InArgs._Size); - Options.ImagesNum = FCString::Atoi(*InArgs._Num); - - RequestReference = UHttpGPTImageRequest::EditorTask(InArgs._Prompt, Options); - - RequestReference->ProcessCompleted.AddDynamic(HttpGPTImageGetterObject.Get(), &UHttpGPTImageGetter::ProcessCompleted); - RequestReference->ErrorReceived.AddDynamic(HttpGPTImageGetterObject.Get(), &UHttpGPTImageGetter::ProcessCompleted); - RequestReference->RequestFailed.AddDynamic(HttpGPTImageGetterObject.Get(), &UHttpGPTImageGetter::RequestFailed); - RequestReference->RequestSent.AddDynamic(HttpGPTImageGetterObject.Get(), &UHttpGPTImageGetter::RequestSent); - - RequestReference->Activate(); - - constexpr float Slot_Padding = 4.0f; - -#if ENGINE_MAJOR_VERSION < 5 - using FAppStyle = FEditorStyle; -#endif - - const ISlateStyle& AppStyle = FAppStyle::Get(); - - ChildSlot - [ - SNew(SVerticalBox) - + SVerticalBox::Slot() - .Padding(Slot_Padding) - [ - SNew(SBorder) - .BorderImage(AppStyle.GetBrush("Menu.Background")) - [ - SNew(SVerticalBox) - + SVerticalBox::Slot() - .Padding(Slot_Padding) - .AutoHeight() - [ - SNew(SVerticalBox) - + SVerticalBox::Slot() - .AutoHeight() - [ - SAssignNew(Prompt, STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) - .Text(FText::FromString("Prompt: " + InArgs._Prompt)) - ] - + SVerticalBox::Slot() - .AutoHeight() - [ - SAssignNew(Status, STextBlock) - .Text(FText::FromString("Status: Sending request...")) - ] - ] - + SVerticalBox::Slot() - .Padding(Slot_Padding) - .FillHeight(1.f) - [ - SAssignNew(ItemScrollBox, SScrollBox) - .Orientation(EOrientation::Orient_Horizontal) - + SScrollBox::Slot() - [ - SAssignNew(ItemViewBox, SHorizontalBox) - ] - ] - ] - ] - ]; -} - -SHttpGPTImageGenItem::~SHttpGPTImageGenItem() -{ - if (RequestReference.IsValid()) - { - RequestReference->StopHttpGPTTask(); - } -} - -void SHttpGPTImageGenView::Construct(const FArguments& InArgs) -{ - constexpr float Slot_Padding = 4.0f; - -#if ENGINE_MAJOR_VERSION < 5 - using FAppStyle = FEditorStyle; -#endif - - const ISlateStyle& AppStyle = FAppStyle::Get(); - - InitializeImageNumOptions(); - ImageNumComboBox = SNew(STextComboBox).OptionsSource(&ImageNum).InitiallySelectedItem(ImageNum[0]).ToolTipText(FText::FromString("Number of Generated Images")); - - InitializeImageSizeOptions(); - ImageSizeComboBox = SNew(STextComboBox).OptionsSource(&ImageSize).InitiallySelectedItem(ImageSize[0]).ToolTipText(FText::FromString("Size of Generated Images")); - - ChildSlot - [ - SNew(SVerticalBox) - + SVerticalBox::Slot() - .Padding(Slot_Padding) - .FillHeight(1.f) - [ - SNew(SBorder) - .BorderImage(AppStyle.GetBrush("NoBorder")) - [ - SAssignNew(ViewScrollBox, SScrollBox) - + SScrollBox::Slot() - [ - SAssignNew(ViewBox, SVerticalBox) - ] - ] - ] - + SVerticalBox::Slot() - .Padding(Slot_Padding) - .AutoHeight() - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .Padding(Slot_Padding) - .FillWidth(1.f) - [ - SAssignNew(InputTextBox, SEditableTextBox) - .AllowContextMenu(true) - .IsReadOnly(false) - .OnTextCommitted_Lambda( - [this]([[maybe_unused]] const FText& Text, ETextCommit::Type CommitType) - { - if (IsSendRequestEnabled() && CommitType == ETextCommit::OnEnter) - { - HandleSendRequestButton(); - } - } - ) - ] - + SHorizontalBox::Slot() - .Padding(Slot_Padding) - .AutoWidth() - [ - SNew(SButton) - .Text(FText::FromString("Generate")) - .ToolTipText(FText::FromString("Request Images Generation")) - .OnClicked(this, &SHttpGPTImageGenView::HandleSendRequestButton) - .IsEnabled(this, &SHttpGPTImageGenView::IsSendRequestEnabled) - ] - + SHorizontalBox::Slot() - .Padding(Slot_Padding) - .AutoWidth() - [ - ImageNumComboBox.ToSharedRef() - ] - + SHorizontalBox::Slot() - .Padding(Slot_Padding) - .AutoWidth() - [ - ImageSizeComboBox.ToSharedRef() - ] - + SHorizontalBox::Slot() - .Padding(Slot_Padding) - .AutoWidth() - [ - SNew(SButton) - .Text(FText::FromString("Clear")) - .ToolTipText(FText::FromString("Clear Generation History")) - .OnClicked(this, &SHttpGPTImageGenView::HandleClearViewButton) - .IsEnabled(this, &SHttpGPTImageGenView::IsClearViewEnabled) - ] - ] - ]; -} - -FReply SHttpGPTImageGenView::HandleSendRequestButton() -{ - ViewBox->AddSlot() - .AutoHeight() - [ - SNew(SHttpGPTImageGenItem) - .Prompt(InputTextBox->GetText().ToString()) - .Num(*ImageNumComboBox->GetSelectedItem().Get()) - .Size(*ImageSizeComboBox->GetSelectedItem().Get()) - ]; - - ViewScrollBox->ScrollToEnd(); - InputTextBox->SetText(FText::GetEmpty()); - - return FReply::Handled(); -} - -bool SHttpGPTImageGenView::IsSendRequestEnabled() const -{ - return !HttpGPT::Internal::HasEmptyParam(InputTextBox->GetText()); -} - -FReply SHttpGPTImageGenView::HandleClearViewButton() -{ - ViewBox->ClearChildren(); - return FReply::Handled(); -} - -bool SHttpGPTImageGenView::IsClearViewEnabled() const -{ - return ViewBox->NumSlots() > 0; -} - -void SHttpGPTImageGenView::InitializeImageNumOptions() -{ - constexpr uint8 MaxNum = 10u; - for (uint8 Iterator = 1u; Iterator <= MaxNum; ++Iterator) - { - ImageNum.Add(MakeShared(FString::FromInt(Iterator))); - } -} - -void SHttpGPTImageGenView::InitializeImageSizeOptions() -{ - ImageSize.Add(MakeShared(UHttpGPTHelper::SizeToName(EHttpGPTImageSize::x256).ToString())); - ImageSize.Add(MakeShared(UHttpGPTHelper::SizeToName(EHttpGPTImageSize::x512).ToString())); - ImageSize.Add(MakeShared(UHttpGPTHelper::SizeToName(EHttpGPTImageSize::x1024).ToString())); -} \ No newline at end of file diff --git a/Source/HttpGPTEditorModule/Private/SHttpGPTImageGenView.h b/Source/HttpGPTEditorModule/Private/SHttpGPTImageGenView.h deleted file mode 100644 index 1600b0c..0000000 --- a/Source/HttpGPTEditorModule/Private/SHttpGPTImageGenView.h +++ /dev/null @@ -1,134 +0,0 @@ -// Author: Lucas Vilas-Boas -// Year: 2023 -// Repo: https://github.com/lucoiso/UEHttpGPT - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include "SHttpGPTImageGenView.generated.h" - -DECLARE_DELEGATE_OneParam(FImageGenerated, UTexture2D*); -DECLARE_DELEGATE_OneParam(FImageStatusChanged, FString); - -UCLASS(MinimalAPI, NotBlueprintable, NotPlaceable, Category = "Implementation") -class UHttpGPTImageGetter : public UObject -{ - GENERATED_BODY() - -public: - explicit UHttpGPTImageGetter(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); - - FImageGenerated OnImageGenerated; - FImageStatusChanged OnStatusChanged; - - UFUNCTION() - void RequestSent(); - - UFUNCTION() - void RequestFailed(); - - UFUNCTION() - void ProcessCompleted(const FHttpGPTImageResponse& Response); - - void Destroy(); - -private: - void ProcessImage(const FHttpGPTImageData& Data); - - FHttpGPTImageGenerate OnImageGenerated_Internal; - - UFUNCTION() - void ImageGenerated(UTexture2D* Texture); - - uint8 GeneratedImages = 0u; - uint8 DataSize = 0u; -}; - -class SHttpGPTImageGenItemData final : public SCompoundWidget -{ -public: - SLATE_BEGIN_ARGS(SHttpGPTImageGenItemData) : _Texture() - { - } - SLATE_ARGUMENT(UTexture2D*, Texture) - SLATE_END_ARGS() - - void Construct(const FArguments& InArgs); - - FReply HandleSaveButton(); - bool IsSaveEnabled() const; - -private: - TSharedPtr Image; - TSharedPtr SaveButton; - - TWeakObjectPtr Texture; -}; - -typedef TSharedPtr SHttpGPTImageGenItemDataPtr; - -class SHttpGPTImageGenItem final : public SCompoundWidget -{ -public: - SLATE_BEGIN_ARGS(SHttpGPTImageGenItem) : _Prompt(), _Num(), _Size() - { - } - SLATE_ARGUMENT(FString, Prompt) - SLATE_ARGUMENT(FString, Num) - SLATE_ARGUMENT(FString, Size) - SLATE_END_ARGS() - - void Construct(const FArguments& InArgs); - ~SHttpGPTImageGenItem(); - - TWeakObjectPtr HttpGPTImageGetterObject; - -private: - TSharedPtr Prompt; - TSharedPtr Status; - TSharedPtr ItemScrollBox; - TSharedPtr ItemViewBox; - - TWeakObjectPtr RequestReference; -}; - -typedef TSharedPtr SHttpGPTImageGenItemPtr; - -class SHttpGPTImageGenView final : public SCompoundWidget -{ -public: - SLATE_USER_ARGS(SHttpGPTImageGenView) - { - } - SLATE_END_ARGS() - - void Construct(const FArguments& InArgs); - - FReply HandleSendRequestButton(); - bool IsSendRequestEnabled() const; - - FReply HandleClearViewButton(); - bool IsClearViewEnabled() const; - -protected: - void InitializeImageNumOptions(); - void InitializeImageSizeOptions(); - -private: - TSharedPtr ViewBox; - TSharedPtr ViewScrollBox; - - TSharedPtr InputTextBox; - - TSharedPtr ImageNumComboBox; - TArray> ImageNum; - - TSharedPtr ImageSizeComboBox; - TArray> ImageSize; -};