diff --git a/docker-compose.yml b/docker-compose.yml index 2698bd0be..9db0b1f47 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,7 @@ services: - DatabaseSettings__DBProvider=${DB_PROVIDER} - DatabaseSettings__ConnectionString=${DB_CONNECTION_STRING} - AISettings__GeminiApiKey=${GEMINI_API_KEY} + - AISettings__OpenAIApiKey=${OPENAI_API_KEY} - SmtpClientOptions__UserName=${SMTP_USERNAME} - SmtpClientOptions__Port=${SMTP_PORT} - SmtpClientOptions__Host=${SMTP_HOST} diff --git a/src/Application/Common/Interfaces/IAISettings.cs b/src/Application/Common/Interfaces/IAISettings.cs index a37ac10a5..dc227ff97 100644 --- a/src/Application/Common/Interfaces/IAISettings.cs +++ b/src/Application/Common/Interfaces/IAISettings.cs @@ -1,4 +1,4 @@ -namespace CleanArchitecture.Blazor.Application.Common.Interfaces; +namespace CleanArchitecture.Blazor.Application.Common.Interfaces; /// /// AI configuration settings interface @@ -9,4 +9,8 @@ public interface IAISettings /// Gets the Gemini API key /// string GeminiApiKey { get; } -} + /// + /// Gets the API key used to authenticate requests to the OpenAI service. + /// + string OpenAIApiKey { get; } + } diff --git a/src/Infrastructure/Configurations/AISettings.cs b/src/Infrastructure/Configurations/AISettings.cs index 0f83d3063..3623ff949 100644 --- a/src/Infrastructure/Configurations/AISettings.cs +++ b/src/Infrastructure/Configurations/AISettings.cs @@ -19,4 +19,11 @@ public class AISettings : IAISettings /// Gets or sets the Gemini API key /// public string GeminiApiKey { get; set; } = string.Empty; + + /// + /// Gets or sets the API key used to authenticate requests to the OpenAI service. + /// + /// The API key must be valid and authorized for the intended OpenAI operations. Storing + /// sensitive credentials such as API keys should be done securely to prevent unauthorized access. + public string OpenAIApiKey { get; set; } = string.Empty; } diff --git a/src/Server.UI/DependencyInjection.cs b/src/Server.UI/DependencyInjection.cs index e89d6d21e..be5242eff 100644 --- a/src/Server.UI/DependencyInjection.cs +++ b/src/Server.UI/DependencyInjection.cs @@ -22,6 +22,7 @@ using MudBlazor.Services; using QuestPDF; using QuestPDF.Infrastructure; +using Betalgo.Ranul.OpenAI.Extensions; @@ -103,6 +104,9 @@ public static IServiceCollection AddServerUI(this IServiceCollection services, I c.DefaultRequestHeaders.Add("x-goog-api-key", aiSettings.GeminiApiKey); }); + services.AddOpenAIService(s => { + s.ApiKey = config.GetValue("AISettings:OpenAIApiKey")??string.Empty; + }); services.AddScoped(); services.AddScoped(); services diff --git a/src/Server.UI/Pages/AI/Chatbot.razor b/src/Server.UI/Pages/AI/Chatbot.razor new file mode 100644 index 000000000..021eb5848 --- /dev/null +++ b/src/Server.UI/Pages/AI/Chatbot.razor @@ -0,0 +1,377 @@ +@page "/ai/chatbot" + +@using Betalgo.Ranul.OpenAI.Interfaces +@using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels +@using Betalgo.Ranul.OpenAI.ObjectModels + +@inject IOpenAIService OpenAIService + + + +Chatbot + + + +
+ @if (!messages.Any()) + { +
+ + Start Conversation + Chat with AI Assistant +
+ } + @foreach (var message in messages) + { +
+
+ @if (message.Role == "user") + { + + @if (!string.IsNullOrEmpty(UserProfile?.ProfilePictureDataUrl ?? "")) + { + + } + else + { + + } + + } + else + { + + + + } +
+
+ @{ + var bubbleClass = $"message-bubble {(message.Role == "user" ? "message-bubble-user" : "message-bubble-assistant")}"; + } + + @message.Content + + @if (message.Role == "assistant") + { +
+ +
+ } +
+
+ } + @if (isLoading) + { +
+
+ + + +
+
+ + + +
+
+ } +
+
+
+ +
+ + + + Send + + +
+
+ + + +@code { + private List messages = new(); + private string userInput = string.Empty; + private bool isLoading = false; + private ElementReference messagesEndRef; + [CascadingParameter] + private UserProfile? UserProfile { get; set; } + + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + await ScrollToBottomAsync(); + } + + private async Task HandleKeyDown(KeyboardEventArgs e) + { + if (e.Key == "Enter" && e.CtrlKey) + { + await SendAsync(); + } + } + + private async Task SendAsync() + { + if (string.IsNullOrWhiteSpace(userInput) || isLoading) + { + return; + } + + var userMessage = userInput.Trim(); + userInput = string.Empty; + + messages.Add(new ChatMessage + { + Role = "user", + Content = userMessage + }); + + StateHasChanged(); + await ScrollToBottomAsync(); + + isLoading = true; + StateHasChanged(); + + try + { + var chatMessages = messages.Select(m => new Betalgo.Ranul.OpenAI.ObjectModels.RequestModels.ChatMessage(m.Role, m.Content)).ToList(); + + var completionResult = await OpenAIService.ChatCompletion.CreateCompletion(new ChatCompletionCreateRequest + { + Messages = chatMessages, + Model = Models.Gpt_4o_mini, + MaxTokens = 2000 + }); + + if (completionResult.Successful) + { + var assistantMessage = completionResult.Choices.FirstOrDefault()?.Message.Content ?? "No response received"; + + messages.Add(new ChatMessage + { + Role = "assistant", + Content = assistantMessage + }); + } + else + { + var errorMessage = $"Error: {completionResult.Error?.Message ?? "Unknown error"}"; + messages.Add(new ChatMessage + { + Role = "assistant", + Content = errorMessage + }); + Snackbar.Add(errorMessage, Severity.Error); + } + } + catch (Exception ex) + { + var errorMessage = $"Exception occurred: {ex.Message}"; + messages.Add(new ChatMessage + { + Role = "assistant", + Content = errorMessage + }); + Snackbar.Add(errorMessage, Severity.Error); + } + finally + { + isLoading = false; + StateHasChanged(); + await ScrollToBottomAsync(); + } + } + + private async Task CopyToClipboardAsync(string text) + { + try + { + await JS.InvokeVoidAsync("navigator.clipboard.writeText", text); + Snackbar.Add("Copied to clipboard", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"Copy failed: {ex.Message}", Severity.Error); + } + } + + private async Task ScrollToBottomAsync() + { + try + { + await JS.InvokeVoidAsync("eval", @" + var element = document.querySelector('.chat-messages-container'); + if (element) { + element.scrollTop = element.scrollHeight; + } + "); + } + catch + { + // Ignore JS interop errors during pre-rendering + } + } +} diff --git a/src/Server.UI/Pages/Identity/Login/LoginWithRecoveryCode.razor b/src/Server.UI/Pages/Identity/Login/LoginWithRecoveryCode.razor index b0528de3b..472a392b3 100644 --- a/src/Server.UI/Pages/Identity/Login/LoginWithRecoveryCode.razor +++ b/src/Server.UI/Pages/Identity/Login/LoginWithRecoveryCode.razor @@ -1,4 +1,4 @@ -@page "/account/loginwithrecoverycode" +@page "/account/loginwithrecoverycode" @using CleanArchitecture.Blazor.Domain.Identity @using System.ComponentModel.DataAnnotations diff --git a/src/Server.UI/Server.UI.csproj b/src/Server.UI/Server.UI.csproj index a4cdf96aa..ac867ffad 100644 --- a/src/Server.UI/Server.UI.csproj +++ b/src/Server.UI/Server.UI.csproj @@ -22,9 +22,12 @@ + + + diff --git a/src/Server.UI/Services/Navigation/MenuService.cs b/src/Server.UI/Services/Navigation/MenuService.cs index bf08677f9..1cc0d919d 100644 --- a/src/Server.UI/Services/Navigation/MenuService.cs +++ b/src/Server.UI/Services/Navigation/MenuService.cs @@ -40,6 +40,14 @@ public class MenuService : IMenuService PageStatus = PageStatus.Completed } } + }, + new() + { + Title = "Chatbot", + Roles = new[] { Roles.Admin, Roles.Users }, + Icon = Icons.Material.Filled.ChatBubble, + Href ="/ai/chatbot", + PageStatus = PageStatus.Completed }, new() { @@ -56,15 +64,8 @@ public class MenuService : IMenuService Icon = Icons.Material.Filled.Money, Href = "/banking", PageStatus = PageStatus.ComingSoon - }, - new() - { - Title = "Booking", - Roles = new[] { Roles.Admin, Roles.Users }, - Icon = Icons.Material.Filled.CalendarToday, - Href = "/booking", - PageStatus = PageStatus.ComingSoon } + } }, new MenuSectionModel diff --git a/src/Server.UI/appsettings.json b/src/Server.UI/appsettings.json index a4cbfed3c..7d4a2cccc 100644 --- a/src/Server.UI/appsettings.json +++ b/src/Server.UI/appsettings.json @@ -1,10 +1,11 @@ -{ +{ "DatabaseSettings": { "DBProvider": "mssql", "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=BlazorDashboardDb;Trusted_Connection=True;MultipleActiveResultSets=true;" }, "AISettings": { - "GeminiApiKey": "your-gemini-api-key" + "GeminiApiKey": "your-gemini-api-key", + "OpenAIApiKey": "your-openai-api-key" }, "Authentication": { "Microsoft": { @@ -26,7 +27,7 @@ }, "AppConfigurationSettings": { "ApplicationUrl": "https://architecture.blazorserver.com", - "Version": "1.17", + "Version": "1.18", "App": "Blazor", "AppName": "Blazor Studio", "Company": "Company",