From 3ab6d3830f50b2490cc73a15be6d26f5683589c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Vilas-B=C3=B4as?= Date: Sat, 19 Aug 2023 17:13:55 +0100 Subject: [PATCH 1/3] Support for multiple chat sessions in Editor Tool (#80) * Const + Remove enum ref * #62 Multiple chat sessions * Fix session delete --- HttpGPT.uplugin | 4 +- .../Private/Tasks/HttpGPTChatRequest.cpp | 10 +- .../Public/Tasks/HttpGPTChatRequest.h | 10 +- .../Private/Utils/HttpGPTHelper.cpp | 12 +- .../Public/Utils/HttpGPTHelper.h | 12 +- .../Private/Chat/SHttpGPTChatItem.cpp | 19 +- .../Private/Chat/SHttpGPTChatItem.h | 4 +- .../Private/Chat/SHttpGPTChatShell.cpp | 291 ++++++++++++++++++ .../Private/Chat/SHttpGPTChatShell.h | 41 +++ .../Private/Chat/SHttpGPTChatView.cpp | 88 ++++-- .../Private/Chat/SHttpGPTChatView.h | 13 +- .../Private/HttpGPTEditorModule.cpp | 14 +- .../Private/ImageGen/SHttpGPTImageGenView.h | 4 +- .../Private/Tasks/HttpGPTImageRequest.cpp | 6 +- .../Public/Tasks/HttpGPTImageRequest.h | 6 +- 15 files changed, 463 insertions(+), 71 deletions(-) create mode 100644 Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatShell.cpp create mode 100644 Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatShell.h diff --git a/HttpGPT.uplugin b/HttpGPT.uplugin index a295026..15c1bc9 100644 --- a/HttpGPT.uplugin +++ b/HttpGPT.uplugin @@ -1,7 +1,7 @@ { "FileVersion": 3, - "Version": 18, - "VersionName": "1.5.5", + "Version": 19, + "VersionName": "1.5.6", "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/HttpGPTChatModule/Private/Tasks/HttpGPTChatRequest.cpp b/Source/HttpGPTChatModule/Private/Tasks/HttpGPTChatRequest.cpp index ff5ce44..49513f2 100644 --- a/Source/HttpGPTChatModule/Private/Tasks/HttpGPTChatRequest.cpp +++ b/Source/HttpGPTChatModule/Private/Tasks/HttpGPTChatRequest.cpp @@ -36,22 +36,22 @@ UHttpGPTChatRequest* UHttpGPTChatRequest::EditorTask(const TArray& Messages) +UHttpGPTChatRequest* UHttpGPTChatRequest::SendMessages_DefaultOptions(UObject* const WorldContextObject, const TArray& Messages) { return SendMessages_CustomOptions(WorldContextObject, Messages, FHttpGPTCommonOptions(), FHttpGPTChatOptions()); } -UHttpGPTChatRequest* UHttpGPTChatRequest::SendMessage_CustomOptions(UObject* WorldContextObject, const FString& Message, const FHttpGPTCommonOptions CommonOptions, const FHttpGPTChatOptions ChatOptions) +UHttpGPTChatRequest* UHttpGPTChatRequest::SendMessage_CustomOptions(UObject* const WorldContextObject, const FString& Message, const FHttpGPTCommonOptions CommonOptions, const FHttpGPTChatOptions ChatOptions) { return SendMessages_CustomOptions(WorldContextObject, { FHttpGPTChatMessage(EHttpGPTChatRole::User, Message) }, CommonOptions, ChatOptions); } -UHttpGPTChatRequest* UHttpGPTChatRequest::SendMessages_CustomOptions(UObject* WorldContextObject, const TArray& Messages, const FHttpGPTCommonOptions CommonOptions, const FHttpGPTChatOptions ChatOptions) +UHttpGPTChatRequest* UHttpGPTChatRequest::SendMessages_CustomOptions(UObject* const WorldContextObject, const TArray& Messages, const FHttpGPTCommonOptions CommonOptions, const FHttpGPTChatOptions ChatOptions) { UHttpGPTChatRequest* const NewAsyncTask = NewObject(); NewAsyncTask->Messages = Messages; @@ -388,7 +388,7 @@ void UHttpGPTChatRequest::DeserializeSingleResponse(const FString& Content) } } -UHttpGPTChatRequest* UHttpGPTChatHelper::CastToHttpGPTChatRequest(UObject* Object) +UHttpGPTChatRequest* UHttpGPTChatHelper::CastToHttpGPTChatRequest(UObject* const Object) { return Cast(Object); } \ No newline at end of file diff --git a/Source/HttpGPTChatModule/Public/Tasks/HttpGPTChatRequest.h b/Source/HttpGPTChatModule/Public/Tasks/HttpGPTChatRequest.h index d949d24..745b5b8 100644 --- a/Source/HttpGPTChatModule/Public/Tasks/HttpGPTChatRequest.h +++ b/Source/HttpGPTChatModule/Public/Tasks/HttpGPTChatRequest.h @@ -39,16 +39,16 @@ class HTTPGPTCHATMODULE_API UHttpGPTChatRequest : public UHttpGPTBaseTask #endif UFUNCTION(BlueprintCallable, Category = "HttpGPT | Chat | Default", meta = (BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject", DisplayName = "Send Message with Default Options")) - static UHttpGPTChatRequest* SendMessage_DefaultOptions(UObject* WorldContextObject, const FString& Message); + static UHttpGPTChatRequest* SendMessage_DefaultOptions(UObject* const WorldContextObject, const FString& Message); UFUNCTION(BlueprintCallable, Category = "HttpGPT | Chat | Default", meta = (BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject", DisplayName = "Send Messages with Default Options")) - static UHttpGPTChatRequest* SendMessages_DefaultOptions(UObject* WorldContextObject, const TArray& Messages); + static UHttpGPTChatRequest* SendMessages_DefaultOptions(UObject* const WorldContextObject, const TArray& Messages); UFUNCTION(BlueprintCallable, Category = "HttpGPT | Chat | Custom", meta = (BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject", DisplayName = "Send Message with Custom Options")) - static UHttpGPTChatRequest* SendMessage_CustomOptions(UObject* WorldContextObject, const FString& Message, const FHttpGPTCommonOptions CommonOptions, const FHttpGPTChatOptions ChatOptions); + static UHttpGPTChatRequest* SendMessage_CustomOptions(UObject* const WorldContextObject, const FString& Message, const FHttpGPTCommonOptions CommonOptions, const FHttpGPTChatOptions ChatOptions); UFUNCTION(BlueprintCallable, Category = "HttpGPT | Chat | Custom", meta = (BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject", DisplayName = "Send Messages with Custom Options")) - static UHttpGPTChatRequest* SendMessages_CustomOptions(UObject* WorldContextObject, const TArray& Messages, const FHttpGPTCommonOptions CommonOptions, const FHttpGPTChatOptions ChatOptions); + static UHttpGPTChatRequest* SendMessages_CustomOptions(UObject* const WorldContextObject, const TArray& Messages, const FHttpGPTCommonOptions CommonOptions, const FHttpGPTChatOptions ChatOptions); UFUNCTION(BlueprintPure, Category = "HttpGPT | Chat") const FHttpGPTChatOptions GetChatOptions() const; @@ -81,5 +81,5 @@ class HTTPGPTCHATMODULE_API UHttpGPTChatHelper final : public UBlueprintFunction public: UFUNCTION(BlueprintPure, Category = "HttpGPT | Chat", Meta = (DisplayName = "Cast to HttpGPT Chat Request")) - static UHttpGPTChatRequest* CastToHttpGPTChatRequest(UObject* Object); + static UHttpGPTChatRequest* CastToHttpGPTChatRequest(UObject* const Object); }; diff --git a/Source/HttpGPTCommonModule/Private/Utils/HttpGPTHelper.cpp b/Source/HttpGPTCommonModule/Private/Utils/HttpGPTHelper.cpp index eaf2013..1acd253 100644 --- a/Source/HttpGPTCommonModule/Private/Utils/HttpGPTHelper.cpp +++ b/Source/HttpGPTCommonModule/Private/Utils/HttpGPTHelper.cpp @@ -9,7 +9,7 @@ #include UE_INLINE_GENERATED_CPP_BY_NAME(HttpGPTHelper) #endif -const FName UHttpGPTHelper::ModelToName(const EHttpGPTChatModel& Model) +const FName UHttpGPTHelper::ModelToName(const EHttpGPTChatModel Model) { switch (Model) { @@ -74,7 +74,7 @@ const EHttpGPTChatModel UHttpGPTHelper::NameToModel(const FName Model) return EHttpGPTChatModel::gpt35turbo; } -const FName UHttpGPTHelper::RoleToName(const EHttpGPTChatRole& Role) +const FName UHttpGPTHelper::RoleToName(const EHttpGPTChatRole Role) { switch (Role) { @@ -124,7 +124,7 @@ const TArray UHttpGPTHelper::GetAvailableGPTModels() return Output; } -const FName UHttpGPTHelper::GetEndpointForModel(const EHttpGPTChatModel& Model) +const FName UHttpGPTHelper::GetEndpointForModel(const EHttpGPTChatModel Model) { switch (Model) { @@ -145,7 +145,7 @@ const FName UHttpGPTHelper::GetEndpointForModel(const EHttpGPTChatModel& Model) return NAME_None; } -const bool UHttpGPTHelper::ModelSupportsChat(const EHttpGPTChatModel& Model) +const bool UHttpGPTHelper::ModelSupportsChat(const EHttpGPTChatModel Model) { switch (Model) { @@ -166,7 +166,7 @@ const bool UHttpGPTHelper::ModelSupportsChat(const EHttpGPTChatModel& Model) return false; } -const FName UHttpGPTHelper::SizeToName(const EHttpGPTImageSize& Size) +const FName UHttpGPTHelper::SizeToName(const EHttpGPTImageSize Size) { switch (Size) { @@ -204,7 +204,7 @@ const EHttpGPTImageSize UHttpGPTHelper::NameToSize(const FName Size) return EHttpGPTImageSize::x256; } -const FName UHttpGPTHelper::FormatToName(const EHttpGPTResponseFormat& Format) +const FName UHttpGPTHelper::FormatToName(const EHttpGPTResponseFormat Format) { switch (Format) { diff --git a/Source/HttpGPTCommonModule/Public/Utils/HttpGPTHelper.h b/Source/HttpGPTCommonModule/Public/Utils/HttpGPTHelper.h index b1b4ed6..dbcf4cc 100644 --- a/Source/HttpGPTCommonModule/Public/Utils/HttpGPTHelper.h +++ b/Source/HttpGPTCommonModule/Public/Utils/HttpGPTHelper.h @@ -20,13 +20,13 @@ class HTTPGPTCOMMONMODULE_API UHttpGPTHelper final : public UBlueprintFunctionLi public: UFUNCTION(BlueprintPure, Category = "HttpGPT | Chat", meta = (DisplayName = "Convert HttpGPT Model to Name")) - static const FName ModelToName(const EHttpGPTChatModel& Model); + static const FName ModelToName(const EHttpGPTChatModel Model); UFUNCTION(BlueprintPure, Category = "HttpGPT | Chat", meta = (DisplayName = "Convert Name to HttpGPT Model")) static const EHttpGPTChatModel NameToModel(const FName Model); UFUNCTION(BlueprintPure, Category = "HttpGPT | Chat", meta = (DisplayName = "Convert HttpGPT Role to Name")) - static const FName RoleToName(const EHttpGPTChatRole& Role); + static const FName RoleToName(const EHttpGPTChatRole Role); UFUNCTION(BlueprintPure, Category = "HttpGPT | Chat", meta = (DisplayName = "Convert Name to HttpGPT Role")) static const EHttpGPTChatRole NameToRole(const FName Role); @@ -35,19 +35,19 @@ class HTTPGPTCOMMONMODULE_API UHttpGPTHelper final : public UBlueprintFunctionLi static const TArray GetAvailableGPTModels(); UFUNCTION(BlueprintPure, Category = "HttpGPT | Chat", meta = (DisplayName = "Get Endpoint for Model")) - static const FName GetEndpointForModel(const EHttpGPTChatModel& Model); + static const FName GetEndpointForModel(const EHttpGPTChatModel Model); UFUNCTION(BlueprintPure, Category = "HttpGPT | Chat", meta = (DisplayName = "Model Supports Chat")) - static const bool ModelSupportsChat(const EHttpGPTChatModel& Model); + static const bool ModelSupportsChat(const EHttpGPTChatModel Model); UFUNCTION(BlueprintPure, Category = "HttpGPT | Image", meta = (DisplayName = "Convert HttpGPT Size to Name")) - static const FName SizeToName(const EHttpGPTImageSize& Size); + static const FName SizeToName(const EHttpGPTImageSize Size); UFUNCTION(BlueprintPure, Category = "HttpGPT | Image", meta = (DisplayName = "Convert Name to HttpGPT Size")) static const EHttpGPTImageSize NameToSize(const FName Size); UFUNCTION(BlueprintPure, Category = "HttpGPT | Image", meta = (DisplayName = "Convert HttpGPT Format to Name")) - static const FName FormatToName(const EHttpGPTResponseFormat& Format); + static const FName FormatToName(const EHttpGPTResponseFormat Format); UFUNCTION(BlueprintPure, Category = "HttpGPT | Image", meta = (DisplayName = "Convert Name to HttpGPT Format")) static const EHttpGPTResponseFormat NameToFormat(const FName Format); diff --git a/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatItem.cpp b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatItem.cpp index 59cce0d..4e052ef 100644 --- a/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatItem.cpp +++ b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatItem.cpp @@ -45,25 +45,23 @@ void SHttpGPTChatItem::Construct(const FArguments& InArgs) TSharedRef SHttpGPTChatItem::ConstructContent() { constexpr float SlotPadding = 4.0f; - constexpr float PaddingMultiplier = 16.0f; + constexpr float PaddingMultiplier = 32.0f; - FText RoleText = FText::FromString(TEXT("Undefined:")); - FMargin BoxMargin(SlotPadding); + FText RoleText = FText::FromString(TEXT("User:")); + FMargin BoxMargin(SlotPadding * PaddingMultiplier, SlotPadding, SlotPadding, SlotPadding); + FSlateColor MessageColor(FLinearColor::White); - if (MessageRole == EHttpGPTChatRole::User) - { - RoleText = FText::FromString(TEXT("User:")); - BoxMargin = FMargin(SlotPadding * PaddingMultiplier, SlotPadding, SlotPadding, SlotPadding); - } - else if (MessageRole == EHttpGPTChatRole::Assistant) + if (MessageRole == EHttpGPTChatRole::Assistant) { RoleText = FText::FromString(TEXT("Assistant:")); BoxMargin = FMargin(SlotPadding, SlotPadding, SlotPadding * PaddingMultiplier, SlotPadding); + MessageColor = FLinearColor::Gray; } else if (MessageRole == EHttpGPTChatRole::System) { RoleText = FText::FromString(TEXT("System:")); - BoxMargin = FMargin(SlotPadding * (PaddingMultiplier * 0.5f), SlotPadding); + BoxMargin = FMargin(SlotPadding * PaddingMultiplier * 0.5f, SlotPadding); + MessageColor = FLinearColor::Black; } const FMargin MessageMargin(SlotPadding * 4.f, SlotPadding, SlotPadding, SlotPadding); @@ -77,6 +75,7 @@ TSharedRef SHttpGPTChatItem::ConstructContent() [ SNew(SBorder) .BorderImage(FAppStyle::Get().GetBrush("Menu.Background")) + .BorderBackgroundColor(MessageColor) [ SNew(SVerticalBox) + SVerticalBox::Slot() diff --git a/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatItem.h b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatItem.h index 1e8dfed..2a6ff2c 100644 --- a/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatItem.h +++ b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatItem.h @@ -6,7 +6,9 @@ #include #include -#include "Structures/HttpGPTChatTypes.h" +#include + +static const FName NewSessionName = TEXT("New Session"); class SHttpGPTChatItem final : public SCompoundWidget { diff --git a/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatShell.cpp b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatShell.cpp new file mode 100644 index 0000000..249bc86 --- /dev/null +++ b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatShell.cpp @@ -0,0 +1,291 @@ +// Author: Lucas Vilas-Boas +// Year: 2023 +// Repo: https://github.com/lucoiso/UEHttpGPT + +#include "SHttpGPTChatShell.h" +#include "SHttpGPTChatView.h" +#include +#include +#include + +typedef TDelegate FOnChatSessionNameChanged; + +class SHttpGPTChatSessionOption : public STableRow +{ +public: + SLATE_BEGIN_ARGS(SHttpGPTChatSessionOption) + { + } + SLATE_EVENT(FOnChatSessionNameChanged, OnNameChanged) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView, FNamePtr InItem) + { + OnNameChanged = InArgs._OnNameChanged; + Item = InItem; + + STableRow::Construct(STableRow::FArguments() + .Padding(8.f) + .Content() + [ + SAssignNew(SessionName, STextBlock) + .Text(this, &SHttpGPTChatSessionOption::GetName) + .ToolTipText(this, &SHttpGPTChatSessionOption::GetName) + ], InOwnerTableView); + } + + FText GetName() const + { + return FText::FromName(Item.IsValid() ? *Item : TEXT("Invalid")); + } + + void EnableEditMode() + { + TSharedRef TextEntry = + SNew(STextEntryPopup) + .Label(FText::FromString("Rename Session")) + .OnTextCommitted(this, &SHttpGPTChatSessionOption::OnNameCommited); + + FSlateApplication& SlateApp = FSlateApplication::Get(); + + SlateApp.PushMenu( + AsShared(), + FWidgetPath(), + TextEntry, + SlateApp.GetCursorPos(), + FPopupTransitionEffect::TypeInPopup + ); + } + +private: + FNamePtr Item; + FOnChatSessionNameChanged OnNameChanged; + + TSharedPtr SessionName; + + void OnNameCommited(const FText& NewText, ETextCommit::Type CommitInfo) + { + if (!SessionName.IsValid()) + { + return; + } + + if (CommitInfo == ETextCommit::OnEnter) + { + OnNameChanged.ExecuteIfBound(Item, FName(*NewText.ToString())); + *Item = *NewText.ToString(); + + FSlateApplication::Get().DismissAllMenus(); + } + else if (CommitInfo == ETextCommit::OnCleared) + { + FSlateApplication::Get().DismissAllMenus(); + } + } +}; + +void SHttpGPTChatShell::Construct([[maybe_unused]] const FArguments&) +{ + ChildSlot + [ + ConstructContent() + ]; + + InitializeChatSessionOptions(); +} + +SHttpGPTChatShell::~SHttpGPTChatShell() = default; + +TSharedRef SHttpGPTChatShell::ConstructContent() +{ + return SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(0.2f) + [ + SAssignNew(ChatSessionListView, SListView) + .ListItemsSource(&ChatSessions) + .OnGenerateRow(this, &SHttpGPTChatShell::OnGenerateChatSessionRow) + .OnSelectionChanged(this, &SHttpGPTChatShell::OnChatSessionSelectionChanged) + .SelectionMode(ESelectionMode::Single) + .ClearSelectionOnClick(false) + .OnMouseButtonDoubleClick(this, &SHttpGPTChatShell::OnChatSessionDoubleClicked) + .OnKeyDownHandler(this, &SHttpGPTChatShell::OnChatSessionKeyDown) + + ] + + SHorizontalBox::Slot() + .FillWidth(0.8f) + [ + SAssignNew(ShellBox, SBox) + .HAlign(HAlign_Fill) + .VAlign(VAlign_Fill) + ]; +} + +void SHttpGPTChatShell::InitializeChatSessionOptions() +{ + ChatSessions.Empty(); + + if (const FString SessionsPath = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("HttpGPT")); FPaths::DirectoryExists(SessionsPath)) + { + TArray FoundFiles; + IFileManager::Get().FindFilesRecursive(FoundFiles, *SessionsPath, TEXT("*.json"), true, false, true); + + TArray FoundBaseFileNames; + Algo::Transform(FoundFiles, FoundBaseFileNames, [](const FString& Iterator) { return FPaths::GetBaseFilename(Iterator); }); + + for (const FString& FileIt : FoundBaseFileNames) + { + ChatSessions.EmplaceAt(FileIt.Equals(NewSessionName.ToString()) ? 0 : ChatSessions.Num(), MakeShared(FileIt)); + } + + if (FoundBaseFileNames.IsEmpty() || !FoundBaseFileNames.Contains(NewSessionName.ToString())) + { + ChatSessions.EmplaceAt(0, MakeShared(NewSessionName)); + } + + InitializeChatSession(ChatSessions[0]); + } + + if (ChatSessionListView.IsValid()) + { + ChatSessionListView->RequestListRefresh(); + } +} + +void SHttpGPTChatShell::InitializeChatSession(FNamePtr InItem) +{ + if (!ShellBox.IsValid()) + { + return; + } + + ShellBox->SetContent(SAssignNew(CurrentView, SHttpGPTChatView).SessionID(*InItem)); + + if (ChatSessionListView.IsValid()) + { + if (!ChatSessionListView->IsItemSelected(InItem)) + { + ChatSessionListView->SetSelection(InItem); + } + + ChatSessionListView->RequestListRefresh(); + } +} + +TSharedRef SHttpGPTChatShell::OnGenerateChatSessionRow(FNamePtr InItem, const TSharedRef& OwnerTable) +{ + return SNew(SHttpGPTChatSessionOption, OwnerTable, InItem) + .OnNameChanged(this, &SHttpGPTChatShell::OnChatSessionNameChanged); +} + +void SHttpGPTChatShell::OnChatSessionSelectionChanged(FNamePtr InItem, [[maybe_unused]] ESelectInfo::Type SelectInfo) +{ + if (!InItem.IsValid()) + { + return; + } + + InitializeChatSession(InItem); +} + +void SHttpGPTChatShell::OnChatSessionNameChanged(FNamePtr InItem, const FName& NewName) +{ + if (!InItem.IsValid()) + { + return; + } + + if (InItem->IsEqual(NewSessionName)) + { + ChatSessions.EmplaceAt(0, MakeShared(NewSessionName)); + } + + if (const FString SessionPath = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("HttpGPT"), InItem->ToString()); FPaths::FileExists(SessionPath)) + { + const FString NewPath = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("HttpGPT"), NewName.ToString()); + IFileManager::Get().Move(*SessionPath, *NewPath, true, true, false, false); + } + + if (!CurrentView.IsValid()) + { + return; + } + + if (CurrentView.IsValid() && CurrentView->GetSessionID().IsEqual(*InItem)) + { + CurrentView->SetSessionID(NewName); + } + + *InItem = NewName; + + if (ChatSessionListView.IsValid()) + { + ChatSessionListView->RequestListRefresh(); + } +} + +void SHttpGPTChatShell::OnChatSessionDoubleClicked(FNamePtr InItem) +{ + if (!InItem.IsValid() || !ChatSessionListView.IsValid()) + { + return; + } + + if (const TSharedPtr Row = ChatSessionListView->WidgetFromItem(InItem); Row.IsValid()) + { + if (const TSharedPtr Session = StaticCastSharedPtr(Row); Session.IsValid()) + { + Session->EnableEditMode(); + ChatSessionListView->RequestListRefresh(); + } + } +} + +FReply SHttpGPTChatShell::OnChatSessionKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) +{ + if (!ChatSessionListView.IsValid()) + { + return FReply::Unhandled(); + } + + if (InKeyEvent.GetKey() != EKeys::Delete) + { + return FReply::Unhandled(); + } + + if (FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString("Are you sure you want to delete this session?")) == EAppReturnType::No) + { + return FReply::Unhandled(); + } + + const FNamePtr SelectedItem = ChatSessionListView->GetNumItemsSelected() == 0 ? nullptr : ChatSessionListView->GetSelectedItems()[0]; + if (!SelectedItem.IsValid()) + { + return FReply::Unhandled(); + } + + if (CurrentView.IsValid()) + { + CurrentView->ClearChat(); + } + + if (const FString SessionPath = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("HttpGPT"), SelectedItem->ToString() + TEXT(".json")); FPaths::FileExists(SessionPath)) + { + IFileManager::Get().Delete(*SessionPath, true, true); + } + + if (SelectedItem->IsEqual(NewSessionName) || !ChatSessions.ContainsByPredicate([](const FNamePtr& Item) { return Item->IsEqual(NewSessionName); })) + { + ChatSessions.EmplaceAt(0, MakeShared(NewSessionName)); + } + + ChatSessions.Remove(SelectedItem); + + if (ChatSessionListView.IsValid()) + { + ChatSessionListView->RequestListRefresh(); + ChatSessionListView->SetSelection(ChatSessions[0]); + } + + return FReply::Handled(); +} \ No newline at end of file diff --git a/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatShell.h b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatShell.h new file mode 100644 index 0000000..d3a190a --- /dev/null +++ b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatShell.h @@ -0,0 +1,41 @@ +// Author: Lucas Vilas-Boas +// Year: 2023 +// Repo: https://github.com/lucoiso/UEHttpGPT + +#pragma once + +#include +#include + +typedef TSharedPtr FNamePtr; + +class SHttpGPTChatShell final : public SCompoundWidget +{ +public: + SLATE_USER_ARGS(SHttpGPTChatShell) + { + } + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + ~SHttpGPTChatShell(); + +private: + TSharedRef ConstructContent(); + + void InitializeChatSessionOptions(); + void InitializeChatSession(FNamePtr InItem); + + TSharedPtr ShellBox; + TSharedPtr CurrentView; + + TSharedPtr> ChatSessionListView; + TArray ChatSessions; + + TSharedRef OnGenerateChatSessionRow(FNamePtr InItem, const TSharedRef& OwnerTable); + + void OnChatSessionSelectionChanged(FNamePtr InItem, ESelectInfo::Type SelectInfo); + void OnChatSessionNameChanged(FNamePtr InItem, const FName& NewName); + void OnChatSessionDoubleClicked(FNamePtr InItem); + FReply OnChatSessionKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent); +}; diff --git a/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatView.cpp b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatView.cpp index 75f606c..10b6f13 100644 --- a/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatView.cpp +++ b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatView.cpp @@ -16,10 +16,11 @@ #include #include #include -#include "SHttpGPTChatItem.h" -void SHttpGPTChatView::Construct([[maybe_unused]] const FArguments&) +void SHttpGPTChatView::Construct(const FArguments& InArgs) { + SetSessionID(InArgs._SessionID); + ModelsComboBox = SNew(STextComboBox) .OptionsSource(&AvailableModels) .ToolTipText(FText::FromString(TEXT("GPT Model"))); @@ -59,7 +60,50 @@ bool SHttpGPTChatView::IsClearChatEnabled() const FString SHttpGPTChatView::GetHistoryPath() const { - return FPaths::Combine(FPaths::ProjectSavedDir(), "HttpGPT", "HttpGPTChatHistory.json"); + return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("HttpGPT"), SessionID.ToString() + TEXT(".json")); +} + +void SHttpGPTChatView::SetSessionID(const FName& NewSessionID) +{ + const FName NewValidSessionID = *FPaths::MakeValidFileName(NewSessionID.ToString()); + if (SessionID == NewValidSessionID) + { + return; + } + + if (SessionID.IsNone()) + { + SessionID = NewValidSessionID; + return; + } + + if (const FString OldPath = GetHistoryPath(); FPaths::FileExists(OldPath)) + { + IFileManager::Get().Delete(*OldPath, true, true, true); + } + + SessionID = NewValidSessionID; + SaveChatHistory(); +} + +FName SHttpGPTChatView::GetSessionID() const +{ + return SessionID; +} + +void SHttpGPTChatView::ClearChat() +{ + ChatItems.Empty(); + if (ChatBox.IsValid()) + { + ChatBox->ClearChildren(); + } + + if (RequestReference.IsValid()) + { + RequestReference->StopHttpGPTTask(); + RequestReference.Reset(); + } } TSharedRef SHttpGPTChatView::ConstructContent() @@ -191,18 +235,7 @@ FReply SHttpGPTChatView::HandleSendMessageButton(const EHttpGPTChatRole Role) FReply SHttpGPTChatView::HandleClearChatButton() { - ChatItems.Empty(); - ChatBox->ClearChildren(); - - if (RequestReference.IsValid()) - { - RequestReference->StopHttpGPTTask(); - } - else - { - RequestReference.Reset(); - } - + ClearChat(); return FReply::Handled(); } @@ -231,7 +264,7 @@ FString SHttpGPTChatView::GetDefaultSystemContext() const } FString SupportedModels; - for (const TSharedPtr& Model : AvailableModels) + for (const FTextDisplayStringPtr& Model : AvailableModels) { SupportedModels.Append(*Model.Get() + ", "); } @@ -288,6 +321,11 @@ FString SHttpGPTChatView::GetDefaultSystemContext() const void SHttpGPTChatView::LoadChatHistory() { + if (SessionID.IsNone()) + { + return; + } + if (const FString LoadPath = GetHistoryPath(); FPaths::FileExists(LoadPath)) { FString FileContent; @@ -300,8 +338,8 @@ void SHttpGPTChatView::LoadChatHistory() TSharedRef> Reader = TJsonReaderFactory<>::Create(FileContent); if (FJsonSerializer::Deserialize(Reader, JsonParsed)) { - const TArray> Data = JsonParsed->GetArrayField("Data"); - for (const TSharedPtr& Item : Data) + const TArray> SessionData = JsonParsed->GetArrayField("Data"); + for (const TSharedPtr& Item : SessionData) { if (const TSharedPtr MessageItObj = Item->AsObject()) { @@ -316,10 +354,11 @@ void SHttpGPTChatView::LoadChatHistory() continue; } - ChatItems.Add( + ChatItems.Emplace( SNew(SHttpGPTChatItem) .MessageRole(Role) .InputText(Message) + .ScrollBox(ChatScrollBox) ); } } @@ -330,12 +369,21 @@ void SHttpGPTChatView::LoadChatHistory() for (const SHttpGPTChatItemPtr& Item : ChatItems) { - ChatBox->AddSlot().AutoHeight()[Item.ToSharedRef()]; + ChatBox->AddSlot() + .AutoHeight() + [ + Item.ToSharedRef() + ]; } } void SHttpGPTChatView::SaveChatHistory() const { + if (SessionID.IsNone() || ChatItems.IsEmpty()) + { + return; + } + const TSharedPtr JsonRequest = MakeShared(); TArray> Data; diff --git a/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatView.h b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatView.h index 97cf174..eacb7d6 100644 --- a/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatView.h +++ b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatView.h @@ -5,14 +5,18 @@ #pragma once #include +#include #include +#include "SHttpGPTChatItem.h" class SHttpGPTChatView final : public SCompoundWidget { public: SLATE_USER_ARGS(SHttpGPTChatView) + : _SessionID(NAME_None) { } + SLATE_ARGUMENT(FName, SessionID) SLATE_END_ARGS() void Construct(const FArguments& InArgs); @@ -22,6 +26,11 @@ class SHttpGPTChatView final : public SCompoundWidget bool IsClearChatEnabled() const; FString GetHistoryPath() const; + void SetSessionID(const FName& NewSessionID); + FName GetSessionID() const; + + void ClearChat(); + private: TSharedRef ConstructContent(); @@ -34,6 +43,8 @@ class SHttpGPTChatView final : public SCompoundWidget void LoadChatHistory(); void SaveChatHistory() const; + FName SessionID; + TSharedPtr ChatBox; TArray ChatItems; TSharedPtr ChatScrollBox; @@ -41,7 +52,7 @@ class SHttpGPTChatView final : public SCompoundWidget TSharedPtr InputTextBox; TSharedPtr ModelsComboBox; - TArray> AvailableModels; + TArray AvailableModels; TWeakObjectPtr RequestReference; }; diff --git a/Source/HttpGPTEditorModule/Private/HttpGPTEditorModule.cpp b/Source/HttpGPTEditorModule/Private/HttpGPTEditorModule.cpp index a153b2e..836428a 100644 --- a/Source/HttpGPTEditorModule/Private/HttpGPTEditorModule.cpp +++ b/Source/HttpGPTEditorModule/Private/HttpGPTEditorModule.cpp @@ -3,7 +3,7 @@ // Repo: https://github.com/lucoiso/UEHttpGPT #include "HttpGPTEditorModule.h" -#include "Chat/SHttpGPTChatView.h" +#include "Chat/SHttpGPTChatShell.h" #include "ImageGen/SHttpGPTImageGenView.h" #include #include @@ -17,7 +17,7 @@ static const FName HttpGPTImageGeneratorTabName("HttpGPTImageGenerator"); void FHttpGPTEditorModule::StartupModule() { - const auto RegisterDelegate = FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FHttpGPTEditorModule::RegisterMenus); + const FSimpleDelegate RegisterDelegate = FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FHttpGPTEditorModule::RegisterMenus); UToolMenus::RegisterStartupCallback(RegisterDelegate); } @@ -36,7 +36,7 @@ TSharedRef FHttpGPTEditorModule::OnSpawnTab(const FSpawnTabArgs& Spawn TSharedPtr OutContent; if (TabId.IsEqual(HttpGPTChatTabName)) { - OutContent = SNew(SHttpGPTChatView); + OutContent = SNew(SHttpGPTChatShell); } else if (TabId.IsEqual(HttpGPTImageGeneratorTabName)) { @@ -58,7 +58,7 @@ TSharedRef FHttpGPTEditorModule::OnSpawnTab(const FSpawnTabArgs& Spawn void FHttpGPTEditorModule::RegisterMenus() { FToolMenuOwnerScoped OwnerScoped(this); - const auto EditorTabSpawnerDelegate = FOnSpawnTab::CreateRaw(this, &FHttpGPTEditorModule::OnSpawnTab); + const FOnSpawnTab EditorTabSpawnerDelegate = FOnSpawnTab::CreateRaw(this, &FHttpGPTEditorModule::OnSpawnTab); #if ENGINE_MAJOR_VERSION < 5 const FName AppStyleName = FEditorStyle::GetStyleSetName(); @@ -66,19 +66,19 @@ void FHttpGPTEditorModule::RegisterMenus() const FName AppStyleName = FAppStyle::GetAppStyleSetName(); #endif - const TSharedPtr Menu = WorkspaceMenu::GetMenuStructure().GetToolsCategory()->AddGroup(LOCTEXT("HttpGPTCategory", "HttpGPT"), LOCTEXT("HttpGPTCategoryTooltip", "HttpGPT Plugin Tabs"), FSlateIcon(AppStyleName, "Icons.Documentation")); + const TSharedRef 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(TEXT("HttpGPT Chat"))) .SetTooltipText(FText::FromString(TEXT("Open HttpGPT Chat"))) .SetIcon(FSlateIcon(AppStyleName, "DerivedData.ResourceUsage")) - .SetGroup(Menu.ToSharedRef()); + .SetGroup(Menu); FGlobalTabmanager::Get()->RegisterNomadTabSpawner(HttpGPTImageGeneratorTabName, EditorTabSpawnerDelegate) .SetDisplayName(FText::FromString(TEXT("HttpGPT Image Generator"))) .SetTooltipText(FText::FromString(TEXT("Open HttpGPT Image Generator"))) .SetIcon(FSlateIcon(AppStyleName, "LevelEditor.Tabs.Viewports")) - .SetGroup(Menu.ToSharedRef()); + .SetGroup(Menu); } #undef LOCTEXT_NAMESPACE diff --git a/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenView.h b/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenView.h index 025bad8..489a8fb 100644 --- a/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenView.h +++ b/Source/HttpGPTEditorModule/Private/ImageGen/SHttpGPTImageGenView.h @@ -35,8 +35,8 @@ class SHttpGPTImageGenView final : public SCompoundWidget TSharedPtr InputTextBox; TSharedPtr ImageNumComboBox; - TArray> ImageNum; + TArray ImageNum; TSharedPtr ImageSizeComboBox; - TArray> ImageSize; + TArray ImageSize; }; diff --git a/Source/HttpGPTImageModule/Private/Tasks/HttpGPTImageRequest.cpp b/Source/HttpGPTImageModule/Private/Tasks/HttpGPTImageRequest.cpp index 9606686..56db9d1 100644 --- a/Source/HttpGPTImageModule/Private/Tasks/HttpGPTImageRequest.cpp +++ b/Source/HttpGPTImageModule/Private/Tasks/HttpGPTImageRequest.cpp @@ -39,12 +39,12 @@ UHttpGPTImageRequest* UHttpGPTImageRequest::EditorTask(const FString& Prompt, co } #endif -UHttpGPTImageRequest* UHttpGPTImageRequest::RequestImages_DefaultOptions(UObject* WorldContextObject, const FString& Prompt) +UHttpGPTImageRequest* UHttpGPTImageRequest::RequestImages_DefaultOptions(UObject* const WorldContextObject, const FString& Prompt) { return RequestImages_CustomOptions(WorldContextObject, Prompt, FHttpGPTCommonOptions(), FHttpGPTImageOptions()); } -UHttpGPTImageRequest* UHttpGPTImageRequest::RequestImages_CustomOptions(UObject* WorldContextObject, const FString& Prompt, const FHttpGPTCommonOptions CommonOptions, const FHttpGPTImageOptions ImageOptions) +UHttpGPTImageRequest* UHttpGPTImageRequest::RequestImages_CustomOptions(UObject* const WorldContextObject, const FString& Prompt, const FHttpGPTCommonOptions CommonOptions, const FHttpGPTImageOptions ImageOptions) { UHttpGPTImageRequest* const NewAsyncTask = NewObject(); NewAsyncTask->Prompt = Prompt; @@ -197,7 +197,7 @@ void UHttpGPTImageRequest::DeserializeResponse(const FString& Content) } } -UHttpGPTImageRequest* UHttpGPTImageHelper::CastToHttpGPTImageRequest(UObject* Object) +UHttpGPTImageRequest* UHttpGPTImageHelper::CastToHttpGPTImageRequest(UObject* const Object) { return Cast(Object); } diff --git a/Source/HttpGPTImageModule/Public/Tasks/HttpGPTImageRequest.h b/Source/HttpGPTImageModule/Public/Tasks/HttpGPTImageRequest.h index 5ba76c9..6e879c3 100644 --- a/Source/HttpGPTImageModule/Public/Tasks/HttpGPTImageRequest.h +++ b/Source/HttpGPTImageModule/Public/Tasks/HttpGPTImageRequest.h @@ -45,10 +45,10 @@ class HTTPGPTIMAGEMODULE_API UHttpGPTImageRequest : public UHttpGPTBaseTask #endif UFUNCTION(BlueprintCallable, Category = "HttpGPT | Image | Default", meta = (BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject", DisplayName = "Request Images with Default Options")) - static UHttpGPTImageRequest* RequestImages_DefaultOptions(UObject* WorldContextObject, const FString& Prompt); + static UHttpGPTImageRequest* RequestImages_DefaultOptions(UObject* const WorldContextObject, const FString& Prompt); UFUNCTION(BlueprintCallable, Category = "HttpGPT | Image | Custom", meta = (BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject", DisplayName = "Request Images with Custom Options")) - static UHttpGPTImageRequest* RequestImages_CustomOptions(UObject* WorldContextObject, const FString& Prompt, const FHttpGPTCommonOptions CommonOptions, const FHttpGPTImageOptions ImageOptions); + static UHttpGPTImageRequest* RequestImages_CustomOptions(UObject* const WorldContextObject, const FString& Prompt, const FHttpGPTCommonOptions CommonOptions, const FHttpGPTImageOptions ImageOptions); UFUNCTION(BlueprintPure, Category = "HttpGPT | Image") const FHttpGPTImageOptions GetImageOptions() const; @@ -82,7 +82,7 @@ class HTTPGPTIMAGEMODULE_API UHttpGPTImageHelper final : public UBlueprintFuncti public: UFUNCTION(BlueprintPure, Category = "HttpGPT | Image", Meta = (DisplayName = "Cast to HttpGPT Image Request")) - static UHttpGPTImageRequest* CastToHttpGPTImageRequest(UObject* Object); + static UHttpGPTImageRequest* CastToHttpGPTImageRequest(UObject* const Object); UFUNCTION(BlueprintCallable, Category = "HttpGPT | Image") static void GenerateImage(const FHttpGPTImageData& ImageData, const FHttpGPTImageGenerate& Callback); From ca314ba7b28d31265f896f9c2f92921e748f9308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Vilas-B=C3=B4as?= Date: Sat, 19 Aug 2023 17:17:35 +0100 Subject: [PATCH 2/3] PlatformAllowList #75 (#81) --- HttpGPT.uplugin | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/HttpGPT.uplugin b/HttpGPT.uplugin index 15c1bc9..238926d 100644 --- a/HttpGPT.uplugin +++ b/HttpGPT.uplugin @@ -19,7 +19,7 @@ "Name": "HttpGPTChatModule", "Type": "Runtime", "LoadingPhase": "Default", - "WhitelistPlatforms": [ + "PlatformAllowList": [ "Win64", "Mac", "Linux", @@ -32,7 +32,7 @@ "Name": "HttpGPTImageModule", "Type": "Runtime", "LoadingPhase": "Default", - "WhitelistPlatforms": [ + "PlatformAllowList": [ "Win64", "Mac", "Linux", @@ -45,7 +45,7 @@ "Name": "HttpGPTCommonModule", "Type": "Runtime", "LoadingPhase": "Default", - "WhitelistPlatforms": [ + "PlatformAllowList": [ "Win64", "Mac", "Linux", @@ -57,7 +57,7 @@ { "Name": "HttpGPTEditorModule", "Type": "Editor", - "WhitelistPlatforms": [ + "PlatformAllowList": [ "Win64", "Mac", "Linux" From f3ed85ca06ba884a66ebf9c024ea1bbecdda3e49 Mon Sep 17 00:00:00 2001 From: lucoiso Date: Sat, 19 Aug 2023 17:23:44 +0100 Subject: [PATCH 3/3] Hotfix: Message box colors --- .../Private/Chat/SHttpGPTChatItem.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatItem.cpp b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatItem.cpp index 4e052ef..db88be3 100644 --- a/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatItem.cpp +++ b/Source/HttpGPTEditorModule/Private/Chat/SHttpGPTChatItem.cpp @@ -42,6 +42,15 @@ void SHttpGPTChatItem::Construct(const FArguments& InArgs) ]; } +static FSlateColor& operator*=(FSlateColor& Lhs, const float Rhs) +{ + FLinearColor NewColor = Lhs.GetSpecifiedColor() * Rhs; + NewColor.A = 1.f; + Lhs = FSlateColor(NewColor); + + return Lhs; +} + TSharedRef SHttpGPTChatItem::ConstructContent() { constexpr float SlotPadding = 4.0f; @@ -55,13 +64,13 @@ TSharedRef SHttpGPTChatItem::ConstructContent() { RoleText = FText::FromString(TEXT("Assistant:")); BoxMargin = FMargin(SlotPadding, SlotPadding, SlotPadding * PaddingMultiplier, SlotPadding); - MessageColor = FLinearColor::Gray; + MessageColor *= 0.3f; } else if (MessageRole == EHttpGPTChatRole::System) { RoleText = FText::FromString(TEXT("System:")); BoxMargin = FMargin(SlotPadding * PaddingMultiplier * 0.5f, SlotPadding); - MessageColor = FLinearColor::Black; + MessageColor *= 0.f; } const FMargin MessageMargin(SlotPadding * 4.f, SlotPadding, SlotPadding, SlotPadding);