Skip to content

Commit

Permalink
Merge branch 'feature/dashboard-page' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
KarmaKamikaze committed Nov 14, 2023
2 parents 299b120 + b10f271 commit 2e6f14f
Show file tree
Hide file tree
Showing 33 changed files with 957 additions and 271 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
name: Build

on:
Expand Down
6 changes: 6 additions & 0 deletions ChatRPG.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ VisualStudioVersion = 17.5.33627.172
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatRPG", "ChatRPG\ChatRPG.csproj", "{02177064-5C6F-4AD0-A3E2-8FDE212D1B8E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatRPGTests", "ChatRPGTests\ChatRPGTests.csproj", "{38AA38FA-5260-4499-8448-97F1D962035B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -15,6 +17,10 @@ Global
{02177064-5C6F-4AD0-A3E2-8FDE212D1B8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{02177064-5C6F-4AD0-A3E2-8FDE212D1B8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{02177064-5C6F-4AD0-A3E2-8FDE212D1B8E}.Release|Any CPU.Build.0 = Release|Any CPU
{38AA38FA-5260-4499-8448-97F1D962035B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{38AA38FA-5260-4499-8448-97F1D962035B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{38AA38FA-5260-4499-8448-97F1D962035B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{38AA38FA-5260-4499-8448-97F1D962035B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
25 changes: 20 additions & 5 deletions ChatRPG/Areas/Identity/Pages/Account/LogOut.cshtml
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
@page
@page "/Logout"
@attribute [IgnoreAntiforgeryToken]
@inject SignInManager<IdentityUser> SignInManager
@inject SignInManager<User> SignInManager

@functions {

public async Task<IActionResult> OnPost()
{
await SignOut();

return Redirect("~/");
}

public async Task<IActionResult> OnGet()
{
await SignOut();

return Redirect("~/");
}

private async Task SignOut()
{
if (SignInManager.IsSignedIn(User))
{
await SignInManager.SignOutAsync();
}

return Redirect("~/");
}
}

}
2 changes: 1 addition & 1 deletion ChatRPG/Areas/Identity/Pages/Account/Login.cshtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@page
@page "/Login"
@using Microsoft.AspNetCore.Authentication
@model LoginModel

Expand Down
2 changes: 1 addition & 1 deletion ChatRPG/Areas/Identity/Pages/Account/Register.cshtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@page
@page "/Register"
@using Microsoft.AspNetCore.Authentication
@model RegisterModel
@{
Expand Down
4 changes: 4 additions & 0 deletions ChatRPG/ChatRPG.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@
<PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="ChatRPGTests" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion ChatRPG/Data/ApplicationDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.Entity<CharacterAbility>(builder =>
{
builder.HasKey("CharacterId", "AbilityId");
builder.HasOne(c => c.Character).WithMany(c => c.CharacterAbilities);
builder.HasOne(c => c.Character).WithMany(c => c.CharacterAbilities!);
})
.Entity<Event>(builder => builder.Property(x => x.Ordering).HasDefaultValue(1))
;
Expand Down
34 changes: 31 additions & 3 deletions ChatRPG/Data/FileUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

namespace ChatRPG.Data;

public record MessagePair(string PlayerMessage, string AssistantMessage);

public class FileUtility
public class FileUtility : IFileUtility
{
private readonly string _currentUser;
private readonly string _path;
Expand All @@ -14,13 +12,19 @@ public class FileUtility
private readonly string _playerKeyword = "#<Player>: ";
private readonly string _gameKeyword = "#<Game>: ";

/// <summary>
/// Initializes a new instance of the FileUtility class with the specified user and save directory.
/// </summary>
/// <param name="currentUser">The username of the current user.</param>
/// <param name="saveDir">The directory where conversation files are saved (default: "Saves/").</param>
public FileUtility(string currentUser, string saveDir = "Saves/")
{
_currentUser = currentUser;
_saveDir = Path.Join(AppDomain.CurrentDomain.BaseDirectory, saveDir);
_path = SetPath(DateTime.Now);
}

/// <inheritdoc />
public async Task UpdateSaveFileAsync(MessagePair messages)
{
// According to .NET docs, you do not need to check if directory exists first
Expand All @@ -37,12 +41,19 @@ public async Task UpdateSaveFileAsync(MessagePair messages)
await fs.WriteAsync(encodedAssistantMessage, 0, encodedAssistantMessage.Length);
}

/// <inheritdoc />
public async Task<List<string>> GetMostRecentConversationAsync(string playerTag, string assistantTag)
{
string filePath = GetMostRecentFile(Directory.GetFiles(_saveDir, $"{_currentUser}*"));
return await GetConversationsStringFromSaveFileAsync(filePath, playerTag, assistantTag);
}

/// <summary>
/// Prepares a message for saving by ensuring a newline character and specifying the author.
/// </summary>
/// <param name="message">The message to prepare for saving.</param>
/// <param name="isPlayerMessage">A boolean indicating if the message is from the player (default: false).</param>
/// <returns>The prepared message with a newline character and author tag.</returns>
private string PrepareMessageForSave(string message, bool isPlayerMessage = false)
{
if (!message.EndsWith("\n"))
Expand All @@ -52,6 +63,13 @@ private string PrepareMessageForSave(string message, bool isPlayerMessage = fals
return message;
}

/// <summary>
/// Reads a conversation string from a save file asynchronously and converts it into a list of messages.
/// </summary>
/// <param name="path">The path to the save file to read.</param>
/// <param name="playerTag">The tag to mark a player message.</param>
/// <param name="assistantTag">The tag to mark an assistant message.</param>
/// <returns>A list of messages extracted from the save file.</returns>
private async Task<List<string>> GetConversationsStringFromSaveFileAsync(string path, string playerTag,
string assistantTag)
{
Expand All @@ -70,6 +88,11 @@ private async Task<List<string>> GetConversationsStringFromSaveFileAsync(string
return ConvertConversationStringToList(sb.ToString(), playerTag, assistantTag);
}

/// <summary>
/// Gets the most recent file from a list of files based on their timestamps.
/// </summary>
/// <param name="files">An array of file paths to choose from.</param>
/// <returns>The path to the most recent file.</returns>
private string GetMostRecentFile(string[] files)
{
DateTime mostRecent = new DateTime(1, 1, 1, 0, 0, 0); // Hello Jesus
Expand Down Expand Up @@ -130,6 +153,11 @@ private List<string> ConvertConversationStringToList(string conversation, string
return fullConversation;
}

/// <summary>
/// Sets the path for a save file based on the user and a timestamp.
/// </summary>
/// <param name="timestamp">The timestamp used to create the file name.</param>
/// <returns>The complete path to the save file.</returns>
private string SetPath(DateTime timestamp)
{
// Add save directory and file name to path
Expand Down
21 changes: 21 additions & 0 deletions ChatRPG/Data/IFileUtility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace ChatRPG.Data;

public interface IFileUtility
{
/// <summary>
/// Updates the conversation save file asynchronously with the provided message pair.
/// </summary>
/// <param name="messages">A pair of messages (player and assistant) to save in the file.</param>
/// <returns>A task representing the asynchronous file update process.</returns>
Task UpdateSaveFileAsync(MessagePair messages);
/// <summary>
/// Retrieves the most recent conversation from a save file asynchronously, parsing and inserting
/// into messages the player and assistant tags.
/// </summary>
/// <param name="playerTag">The tag to mark a player message.</param>
/// <param name="assistantTag">The tag to mark an assistant message.</param>
/// <returns>A list of messages representing the most recent conversation.</returns>
Task<List<string>> GetMostRecentConversationAsync(string playerTag, string assistantTag);
}

public record MessagePair(string PlayerMessage, string AssistantMessage);
4 changes: 2 additions & 2 deletions ChatRPG/Data/Models/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ namespace ChatRPG.Data.Models;

public class User : IdentityUser
{
private User()
public User()
{
}

public User(string username) : base(username)
{
}

public ICollection<Campaign> Campaigns { get; } = new List<Campaign>();
public virtual ICollection<Campaign> Campaigns { get; } = new List<Campaign>();
}
110 changes: 9 additions & 101 deletions ChatRPG/Pages/Campaign.razor
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
@page "/Campaign"
@using ChatRPG.Data
@using ChatRPG.API
@using Microsoft.IdentityModel.Tokens
@using OpenAiGptMessage = ChatRPG.API.OpenAiGptMessage
@inject IConfiguration Configuration
@inject IOpenAiLlmClient OpenAiLlmClient;
@inject IJSRuntime JsRuntime
@inject AuthenticationStateProvider AuthenticationStateProvider

<PageTitle>Campaign</PageTitle>

<div class="text-center mx-auto">
<Navbar Username="@_loggedInUsername"/>

<div class="text-center campaign-title">
<h1>Campaign</h1>
</div>

Expand All @@ -34,7 +30,7 @@
<input type="text" style="resize:vertical;" class="form-control user-prompt custom-text-field"
@bind="_userInput" placeholder="What do you do?" @onkeyup="@EnterKeyHandler" disabled="@_isWaitingForResponse"/>
<div class="input-group-append">
<button class="btn btn-primary ml-2" type="button" @onclick="SendPrompt" disabled="@_isWaitingForResponse">
<button class="btn btn-send ml-2" type="button" @onclick="SendPrompt" disabled="@_isWaitingForResponse">
Send
</button>
</div>
Expand All @@ -43,96 +39,8 @@

<span id="bottom-id"></span>

@code {
private string? _loggedInUsername;
private bool _shouldSave;
private IJSObjectReference? _scrollJsScript;
private FileUtility? _fileUtil;
readonly List<OpenAiGptMessage> _conversation = new();
private string _userInput = "";
private string _tempMessage = "";
private bool _shouldStream;
private bool _isWaitingForResponse;

protected override async Task OnInitializedAsync()
{
AuthenticationState authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
_loggedInUsername = authenticationState.User.Identity?.Name;
if (_loggedInUsername != null) _fileUtil = new FileUtility(_loggedInUsername);
_shouldSave = Configuration.GetValue<bool>("SaveConversationsToFile");
_shouldStream = !Configuration.GetValue<bool>("UseMocks") && Configuration.GetValue<bool>("StreamChatCompletions");
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_scrollJsScript ??= await JsRuntime.InvokeAsync<IJSObjectReference>("import", "./js/scroll.js");
}
}

async Task EnterKeyHandler(KeyboardEventArgs e)
{
if (e.Code is "Enter" or "NumpadEnter")
{
await SendPrompt();
}
}

private async Task SendPrompt()
{
if (string.IsNullOrWhiteSpace(_userInput))
{
return;
}
_isWaitingForResponse = true;

OpenAiGptMessage userInput = new OpenAiGptMessage("user", _userInput);
_conversation.Add(userInput);

if (_shouldStream)
{
await HandleStreamedResponse(OpenAiLlmClient.GetStreamedChatCompletion(userInput));
}
else
{
string response = await OpenAiLlmClient.GetChatCompletion(userInput);
HandleResponse(response);
}

if (_shouldSave && _fileUtil != null)
{
string assistantOutput = _conversation.Last().Content;
await _fileUtil.UpdateSaveFileAsync(new MessagePair(_userInput, assistantOutput));
}

_userInput = "";
StateHasChanged();
await ScrollToElement("bottom-id");
_isWaitingForResponse = false;
}

private void HandleResponse(string response)
{
OpenAiGptMessage assistantOutput = new OpenAiGptMessage("assistant", response);
_conversation.Add(assistantOutput);
}

private async Task HandleStreamedResponse(IAsyncEnumerable<string> streamedResponse)
{
await foreach (string res in streamedResponse)
{
_tempMessage += res;
StateHasChanged();
await ScrollToElement("bottom-id");
}
HandleResponse(_tempMessage);
_tempMessage = "";
}

private async Task ScrollToElement(string elementId)
{
await _scrollJsScript!.InvokeVoidAsync("ScrollToId", elementId);
}

}
<footer class="footer pl-3 text-muted footer-formatting fixed-bottom custom-gray-container">
<div class="container">
&copy; 2023 - ChatRPG
</div>
</footer>
Loading

0 comments on commit 2e6f14f

Please sign in to comment.