From a5bcc4c8fa59af2343b6bd42c47fe6eac93a3c2f Mon Sep 17 00:00:00 2001 From: Vikram Reddy Date: Sun, 29 Sep 2024 23:08:28 +0530 Subject: [PATCH 01/23] Chat component + Markdown component (#893) * AI Chat component + Markdown component --- .../Components/Layout/MainLayout.razor.cs | 1 + .../Pages/AI/AIChat/AIChatDocumentation.razor | 21 +++ .../AI/AIChat/AIChat_Demo_01_Examples.razor | 4 + .../Grid_Demo_14_A_DetailView.razor | 101 ++++++----- ...id_Demo_14_B_DetailView_Dynamic_Data.razor | 66 +++++++ .../Grid_DetailView_Documentation.razor | 4 + .../Components/Pages/Index.razor | 19 ++ .../Markdown/MarkdownDocumentation.razor | 45 +++++ .../Markdown/Markdown_Demo_01_Examples.razor | 15 ++ .../Markdown/Markdown_Demo_02_Headers.razor | 7 + ...n_Demo_03_Paragraphs_and_Line_Breaks.razor | 8 + .../Markdown_Demo_04_Blockquotes.razor | 6 + .../Markdown_Demo_05_Horizontal_Rules.razor | 6 + ..._Emphasis_bold_italics_strikethrough.razor | 5 + .../Markdown_Demo_07_Code_Highlighting.razor | 11 ++ BlazorBootstrap.Demo.Server/appsettings.json | 6 + blazorbootstrap/BlazorBootstrap.csproj | 2 + .../Components/AI/Chat/AIChat.razor | 63 +++++++ .../Components/AI/Chat/AIChat.razor.cs | 155 ++++++++++++++++ .../Components/AI/Chat/AIChatInterop.cs.cs | 12 ++ .../Core/BlazorBootstrapComponentBase.cs | 6 +- .../Components/Core/BlazorBootstrapInterop.cs | 14 ++ .../Components/Markdown/Markdown.razor | 6 + .../Components/Markdown/Markdown.razor.cs | 165 ++++++++++++++++++ blazorbootstrap/Config.cs | 19 +- blazorbootstrap/Constants/BootstrapClass.cs | 2 + .../Models/AI/OpenAIChatMessage.cs | 3 + .../Models/AI/OpenAIChatPayload.cs | 56 ++++++ blazorbootstrap/Models/MarkdownPattern.cs | 3 + .../wwwroot/blazor.bootstrap.ai.js | 57 ++++++ blazorbootstrap/wwwroot/blazor.bootstrap.js | 90 ++++++++++ 31 files changed, 927 insertions(+), 51 deletions(-) create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/AI/AIChat/AIChatDocumentation.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/AI/AIChat/AIChat_Demo_01_Examples.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Grid/14-detail-view/Grid_Demo_14_B_DetailView_Dynamic_Data.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/MarkdownDocumentation.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_01_Examples.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_02_Headers.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_03_Paragraphs_and_Line_Breaks.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_04_Blockquotes.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_05_Horizontal_Rules.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_06_Emphasis_bold_italics_strikethrough.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_07_Code_Highlighting.razor create mode 100644 blazorbootstrap/Components/AI/Chat/AIChat.razor create mode 100644 blazorbootstrap/Components/AI/Chat/AIChat.razor.cs create mode 100644 blazorbootstrap/Components/AI/Chat/AIChatInterop.cs.cs create mode 100644 blazorbootstrap/Components/Core/BlazorBootstrapInterop.cs create mode 100644 blazorbootstrap/Components/Markdown/Markdown.razor create mode 100644 blazorbootstrap/Components/Markdown/Markdown.razor.cs create mode 100644 blazorbootstrap/Models/AI/OpenAIChatMessage.cs create mode 100644 blazorbootstrap/Models/AI/OpenAIChatPayload.cs create mode 100644 blazorbootstrap/Models/MarkdownPattern.cs create mode 100644 blazorbootstrap/wwwroot/blazor.bootstrap.ai.js diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs index 97b8ebb37..d4797337d 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs @@ -62,6 +62,7 @@ internal override IEnumerable GetNavItems() #endregion Grid + new (){ Id = "514", Text = "Markdown", Href = "/markdown", IconName = IconName.MarkdownFill, ParentId = "5" }, new (){ Id = "514", Text = "Modals", Href = "/modals", IconName = IconName.WindowStack, ParentId = "5" }, new (){ Id = "515", Text = "Offcanvas", Href = "/offcanvas", IconName = IconName.LayoutSidebarReverse, ParentId = "5" }, new (){ Id = "516", Text = "Pagination", Href = "/pagination", IconName = IconName.ThreeDots, ParentId = "5" }, diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/AI/AIChat/AIChatDocumentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/AI/AIChat/AIChatDocumentation.razor new file mode 100644 index 000000000..559e797cd --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/AI/AIChat/AIChatDocumentation.razor @@ -0,0 +1,21 @@ +@page "/ai/open-ai-chat" + +@title + + + +

Blazor Open AI Chat

+
Provide contextual feedback messages for typical user actions with a handful of available and flexible alert messages.
+ + + + +
Alerts are available for any length of text, as well as an optional close button. For proper styling, use one of the eight colors.
+ + +@code{ + private string pageUrl = "/ai/open-ai-chat"; + private string title = "Blazor Open AI Chat Component"; + private string description = "Provide contextual feedback messages for typical user actions with the handful of available and flexible Blazor Bootstrap alert messages."; // TODO: update + private string imageUrl = "https://i.imgur.com/FGgEMp6.jpg"; // TODO: update +} \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/AI/AIChat/AIChat_Demo_01_Examples.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/AI/AIChat/AIChat_Demo_01_Examples.razor new file mode 100644 index 000000000..a9cdd295b --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/AI/AIChat/AIChat_Demo_01_Examples.razor @@ -0,0 +1,4 @@ + diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Grid/14-detail-view/Grid_Demo_14_A_DetailView.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Grid/14-detail-view/Grid_Demo_14_A_DetailView.razor index 38c47764c..a17d058ce 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Grid/14-detail-view/Grid_Demo_14_A_DetailView.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Grid/14-detail-view/Grid_Demo_14_A_DetailView.razor @@ -1,66 +1,85 @@ - - + @context.Id - + @context.Name - - @context.Designation - - - @context.DOJ - - + @context.IsActive - -
-
Id
-
@context.Id
-
-
-
Name
-
@context.Name
-
-
-
Designation
-
@context.Designation
-
-
-
DOJ
-
@context.DOJ
-
-
-
IsActive
-
@context.IsActive
-
+ + + + + + + @emp1.Id + + + @emp1.Description + + + @emp1.Unit + + + @emp1.Quantity + + + + +
@code { - private List employees = new List { - new Employee1 { Id = 107, Name = "Alice", Designation = "AI Engineer", DOJ = new DateOnly(1998, 11, 17), IsActive = true }, - new Employee1 { Id = 103, Name = "Bob", Designation = "Senior DevOps Engineer", DOJ = new DateOnly(1985, 1, 5), IsActive = true }, - new Employee1 { Id = 106, Name = "John", Designation = "Data Engineer", DOJ = new DateOnly(1995, 4, 17), IsActive = true }, - new Employee1 { Id = 104, Name = "Pop", Designation = "Associate Architect", DOJ = new DateOnly(1985, 6, 8), IsActive = false }, - new Employee1 { Id = 105, Name = "Ronald", Designation = "Senior Data Engineer", DOJ = new DateOnly(1991, 8, 23), IsActive = true } + private List products = new List { + new Product { Id = 1, Name = "Product 1", IsActive = true }, + new Product { Id = 2, Name = "Product 2", IsActive = true }, + new Product { Id = 3, Name = "Product 3", IsActive = true }, + new Product { Id = 4, Name = "Product 4", IsActive = true }, + new Product { Id = 5, Name = "Product 5", IsActive = true } }; - public record class Employee1 + private List ingredients = new List { + new Ingredient { Id = 105, ProductId = 1, Description = "Ingredient 1", Unit = "UNIT1", Quantity = 350 }, + new Ingredient { Id = 106, ProductId = 1, Description = "Ingredient 2", Unit = "UNIT1", Quantity = 600 }, + new Ingredient { Id = 107, ProductId = 1, Description = "Ingredient 3", Unit = "UNIT2", Quantity = 13 }, + new Ingredient { Id = 108, ProductId = 1, Description = "Ingredient 4", Unit = "UNIT3", Quantity = 25 }, + new Ingredient { Id = 109, ProductId = 2, Description = "Ingredient 5", Unit = "UNIT1", Quantity = 750 }, + new Ingredient { Id = 110, ProductId = 2, Description = "Ingredient 3", Unit = "UNIT2", Quantity = 13 }, + new Ingredient { Id = 111, ProductId = 1, Description = "Ingredient 4", Unit = "UNIT3", Quantity = 25 }, + new Ingredient { Id = 112, ProductId = 2, Description = "Ingredient 5", Unit = "UNIT1", Quantity = 750 }, + new Ingredient { Id = 113, ProductId = 4, Description = "Ingredient 3", Unit = "UNIT2", Quantity = 13 }, + new Ingredient { Id = 114, ProductId = 5, Description = "Ingredient 4", Unit = "UNIT3", Quantity = 25 }, + new Ingredient { Id = 115, ProductId = 2, Description = "Ingredient 5", Unit = "UNIT1", Quantity = 750 }, + }; + + private IEnumerable GetIngredients(int productId) => ingredients.Where(i => i.ProductId == productId); + + public record class Product { public int Id { get; set; } public string? Name { get; set; } - public string? Designation { get; set; } - public DateOnly DOJ { get; set; } public bool IsActive { get; set; } } + + public record class Ingredient + { + public int Id { get; set; } + public int ProductId { get; set; } + public string? Description { get; set; } + public string? Unit { get; set; } + public int Quantity { get; set; } + } } diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Grid/14-detail-view/Grid_Demo_14_B_DetailView_Dynamic_Data.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Grid/14-detail-view/Grid_Demo_14_B_DetailView_Dynamic_Data.razor new file mode 100644 index 000000000..38c47764c --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Grid/14-detail-view/Grid_Demo_14_B_DetailView_Dynamic_Data.razor @@ -0,0 +1,66 @@ + + + + + @context.Id + + + @context.Name + + + @context.Designation + + + @context.DOJ + + + @context.IsActive + + + + +
+
Id
+
@context.Id
+
+
+
Name
+
@context.Name
+
+
+
Designation
+
@context.Designation
+
+
+
DOJ
+
@context.DOJ
+
+
+
IsActive
+
@context.IsActive
+
+
+ +
+ +@code { + private List employees = new List { + new Employee1 { Id = 107, Name = "Alice", Designation = "AI Engineer", DOJ = new DateOnly(1998, 11, 17), IsActive = true }, + new Employee1 { Id = 103, Name = "Bob", Designation = "Senior DevOps Engineer", DOJ = new DateOnly(1985, 1, 5), IsActive = true }, + new Employee1 { Id = 106, Name = "John", Designation = "Data Engineer", DOJ = new DateOnly(1995, 4, 17), IsActive = true }, + new Employee1 { Id = 104, Name = "Pop", Designation = "Associate Architect", DOJ = new DateOnly(1985, 6, 8), IsActive = false }, + new Employee1 { Id = 105, Name = "Ronald", Designation = "Senior Data Engineer", DOJ = new DateOnly(1991, 8, 23), IsActive = true } + }; + + public record class Employee1 + { + public int Id { get; set; } + public string? Name { get; set; } + public string? Designation { get; set; } + public DateOnly DOJ { get; set; } + public bool IsActive { get; set; } + } +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Grid/14-detail-view/Grid_DetailView_Documentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Grid/14-detail-view/Grid_DetailView_Documentation.razor index 5e33560fc..beae70930 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Grid/14-detail-view/Grid_DetailView_Documentation.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Grid/14-detail-view/Grid_DetailView_Documentation.razor @@ -13,6 +13,10 @@
To enable detail view, set the AllowDetailView parameter to true. In the following example, existing <GridColumn> tags are nested under <GridColumns> tag to distinguish them from <GridDetailView>.
+ +
+ + @code { private const string pageUrl = "/grid/detail-view"; private const string title = "Blazor Grid Component - Detail View"; diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Index.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Index.razor index a874822ef..e63d9fc44 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Index.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Index.razor @@ -130,6 +130,11 @@

Images New

+ + +

Form Components

diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/MarkdownDocumentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/MarkdownDocumentation.razor new file mode 100644 index 000000000..ca711ea73 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/MarkdownDocumentation.razor @@ -0,0 +1,45 @@ +@page "/markdown" + +@title + + + +

Blazor Markdown

+
Use Blazor Bootstrap Markdown component to add formatting, tables, images, and more to your project pages.
+ + + +@* +
+ *@ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + +@code{ + private string pageUrl = "/markdown"; + private string title = "Blazor Markdown Component"; + private string description = "Use Blazor Bootstrap Markdown component to add formatting, tables, images, and more to your project pages."; + private string imageUrl = "https://i.imgur.com/FGgEMp6.jpg"; // TODO: update +} \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_01_Examples.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_01_Examples.razor new file mode 100644 index 000000000..1a0d708d7 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_01_Examples.razor @@ -0,0 +1,15 @@ + + Test + # Heading level 1 + ## Heading level 2 + ### Heading level 3 + #### Heading level 4 + ##### Heading level 5 + ###### Heading level 6 + + + __bold text__ + **bold text** + _italic text_ + *italic text* + \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_02_Headers.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_02_Headers.razor new file mode 100644 index 000000000..9792ee11e --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_02_Headers.razor @@ -0,0 +1,7 @@ + + # This is a H1 header + ## This is a H2 header + ### This is a H3 header + #### This is a H4 header + ##### This is a H5 header + \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_03_Paragraphs_and_Line_Breaks.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_03_Paragraphs_and_Line_Breaks.razor new file mode 100644 index 000000000..24b026e26 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_03_Paragraphs_and_Line_Breaks.razor @@ -0,0 +1,8 @@ + + Add lines between your text with the **Enter** key. + Your text gets better spaced and makes it easier to read. + + + Add lines between your text with the **Enter** key. + Your text gets better spaced and makes it easier to read. + \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_04_Blockquotes.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_04_Blockquotes.razor new file mode 100644 index 000000000..8e065dd41 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_04_Blockquotes.razor @@ -0,0 +1,6 @@ + + > Single line quote + >> Nested quote + >> multiple line + >> quote + \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_05_Horizontal_Rules.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_05_Horizontal_Rules.razor new file mode 100644 index 000000000..e7d9c79e5 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_05_Horizontal_Rules.razor @@ -0,0 +1,6 @@ + + above + + ---- + below + \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_06_Emphasis_bold_italics_strikethrough.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_06_Emphasis_bold_italics_strikethrough.razor new file mode 100644 index 000000000..9ab8f7c56 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_06_Emphasis_bold_italics_strikethrough.razor @@ -0,0 +1,5 @@ + + Use _emphasis_ in comments to express **strong** opinions and point out ~~corrections~~ + **_Bold, italicized text_** + **~~Bold, strike-through text~~** + \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_07_Code_Highlighting.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_07_Code_Highlighting.razor new file mode 100644 index 000000000..137c1352d --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Markdown/Markdown_Demo_07_Code_Highlighting.razor @@ -0,0 +1,11 @@ + + ```js + const count = records.length; + ``` + + + + ```csharp + Console.WriteLine("Hello, World!"); + ``` + \ No newline at end of file diff --git a/BlazorBootstrap.Demo.Server/appsettings.json b/BlazorBootstrap.Demo.Server/appsettings.json index 0fc7e90f8..03290f577 100644 --- a/BlazorBootstrap.Demo.Server/appsettings.json +++ b/BlazorBootstrap.Demo.Server/appsettings.json @@ -23,5 +23,11 @@ }, "GoogleMap": { "ApiKey": "AIzaSyDc110Rvu20IMJhlZcWTOPoLbVQdnjLyXs" + }, + "AzureOpenAI": { + "Endpoint": "", + "DeploymentName": "", + "ApiKey": "", + "ApiVersion": "" } } diff --git a/blazorbootstrap/BlazorBootstrap.csproj b/blazorbootstrap/BlazorBootstrap.csproj index 34c3dbc0f..7d0120794 100644 --- a/blazorbootstrap/BlazorBootstrap.csproj +++ b/blazorbootstrap/BlazorBootstrap.csproj @@ -46,10 +46,12 @@ + + \ No newline at end of file diff --git a/blazorbootstrap/Components/AI/Chat/AIChat.razor b/blazorbootstrap/Components/AI/Chat/AIChat.razor new file mode 100644 index 000000000..254561235 --- /dev/null +++ b/blazorbootstrap/Components/AI/Chat/AIChat.razor @@ -0,0 +1,63 @@ +@namespace BlazorBootstrap +@inherits BlazorBootstrapComponentBase + + \ No newline at end of file diff --git a/blazorbootstrap/Components/AI/Chat/AIChat.razor.cs b/blazorbootstrap/Components/AI/Chat/AIChat.razor.cs new file mode 100644 index 000000000..ab33601c9 --- /dev/null +++ b/blazorbootstrap/Components/AI/Chat/AIChat.razor.cs @@ -0,0 +1,155 @@ +namespace BlazorBootstrap; + +public partial class AIChat : BlazorBootstrapComponentBase +{ + #region Fields and Constants + + private readonly List conversationHistory = new(); + private string? apiKey; + private string? apiVersion; + private string? currentCompletion; + private string? deploymentName; + private string? endpoint; + private bool isRequestInProgress; + private DotNetObjectReference? objRef; + private string userPrompt = string.Empty; + + #endregion + + #region Methods + + protected override async Task OnInitializedAsync() + { + var configurationSection = Configuration.GetSection("AzureOpenAI"); + + if (Configuration is null) + throw new ArgumentException("`AzureOpenAI` section was not found in the application configuration."); + + endpoint = configurationSection["Endpoint"]; + + if (endpoint is null) + throw new ArgumentException("`Endpoint` key/value was not found in the 'AzureOpenAI' section of the application configuration."); + + deploymentName = configurationSection["DeploymentName"]; + + if (deploymentName is null) + throw new ArgumentException("`DeploymentName` key/value was not found in the 'AzureOpenAI' section of the application configuration."); + + apiKey = configurationSection["ApiKey"]; + + if (apiKey is null) + throw new ArgumentException("`ApiKey` key/value was not found in the 'AzureOpenAI' section of the application configuration."); + + apiVersion = configurationSection["ApiVersion"]; + + if (apiVersion is null) + throw new ArgumentException("`ApiVersion` key/value was not found in the 'AzureOpenAI' section of the application configuration."); + + objRef ??= DotNetObjectReference.Create(this); + + await base.OnInitializedAsync(); + } + + [JSInvokable] + public async Task ChartCompletetionsStreamJs(string content, bool done) + { + ClearInput(); + + if (isRequestInProgress) + isRequestInProgress = false; + + if (done) + { + conversationHistory.Add(new OpenAIChatMessage("system", currentCompletion!)); + currentCompletion = ""; + await InvokeAsync(StateHasChanged); + await JSRuntime.InvokeVoidAsync(BlazorBootstrapInterop.ScrollToElementBottom, Id); + return; + } + + currentCompletion += content; + await InvokeAsync(StateHasChanged); + await JSRuntime.InvokeVoidAsync(BlazorBootstrapInterop.ScrollToElementBottom, Id); + } + + private void ClearInput() => userPrompt = string.Empty; + + private async Task CreateCompletionAsync(List messages) + { + isRequestInProgress = true; + + var payload = new OpenAIChatPayload { Messages = messages, MaximumTokens = MaximumTokens, Temperature = Temperature, TopP = TopP }; + + try + { + await JSRuntime.InvokeVoidAsync( + AIChatInterop.AzureOpenAIChatCompletions, + $"{endpoint}openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion}", + apiKey, + payload, + objRef! + ); + } + catch + { + isRequestInProgress = false; + } + } + + private async Task SendPromptAsync() + { + if (string.IsNullOrWhiteSpace(userPrompt)) + return; + + var message = new OpenAIChatMessage("user", userPrompt); + conversationHistory.Add(message); + + await CreateCompletionAsync(new List { message }); + } + + #endregion + + #region Properties, Indexers + + protected override string? ClassNames => + BuildClassNames(Class, + (BootstrapClass.Container, true)); + + protected override string? StyleNames => + BuildStyleNames(Style, + //("min-height:200px", true), + //("max-height:400px", true), + ("overflow-x:hidden", true), + ("overflow-y:auto", true)); + + /// + /// The maximum number of tokens to generate shared between the prompt and completion. + /// The exact limit varies by model. (One token is roughly 4 characters for standard English text) + /// Minimum 1 and the maximum tokens is 4096. + /// + /// Default value is 2048. This value is limited by gpt-3.5-turbo. + [Parameter] + public long MaximumTokens { get; set; } = 2048; + + /// + /// Controls randomness: Lowering results in less random completions. + /// As the temperature approaches zero, the model will become deterministic and repetitive. + /// Minimum 1 and the maximum is 2. + /// + /// Default value is 1. + [Parameter] + public double Temperature { get; set; } = 1; + + /// + /// Controls diversity via nucleus sampling: 0.5 means half of all likelihood-weighted options are considered. + /// + /// Default value is 1. + [Parameter] + public double TopP { get; set; } = 1; + + #endregion +} + + + + diff --git a/blazorbootstrap/Components/AI/Chat/AIChatInterop.cs.cs b/blazorbootstrap/Components/AI/Chat/AIChatInterop.cs.cs new file mode 100644 index 000000000..8dc241100 --- /dev/null +++ b/blazorbootstrap/Components/AI/Chat/AIChatInterop.cs.cs @@ -0,0 +1,12 @@ +namespace BlazorBootstrap; + +public class AIChatInterop +{ + #region Fields and Constants + + private const string Prefix = "window.blazorBootstrap.ai."; + + public const string AzureOpenAIChatCompletions = Prefix + "azureOpenAI.chat.completions"; + + #endregion +} diff --git a/blazorbootstrap/Components/Core/BlazorBootstrapComponentBase.cs b/blazorbootstrap/Components/Core/BlazorBootstrapComponentBase.cs index e5386389f..a8bfe8dfa 100644 --- a/blazorbootstrap/Components/Core/BlazorBootstrapComponentBase.cs +++ b/blazorbootstrap/Components/Core/BlazorBootstrapComponentBase.cs @@ -1,4 +1,6 @@ -namespace BlazorBootstrap; +using Microsoft.Extensions.Configuration; + +namespace BlazorBootstrap; public abstract class BlazorBootstrapComponentBase : ComponentBase, IDisposable, IAsyncDisposable { @@ -141,6 +143,8 @@ protected virtual ValueTask DisposeAsyncCore(bool disposing) protected virtual string? ClassNames => Class; + [Inject] protected IConfiguration Configuration { get; set; } = default!; + public ElementReference Element { get; set; } [Parameter] public string? Id { get; set; } diff --git a/blazorbootstrap/Components/Core/BlazorBootstrapInterop.cs b/blazorbootstrap/Components/Core/BlazorBootstrapInterop.cs new file mode 100644 index 000000000..a3d057186 --- /dev/null +++ b/blazorbootstrap/Components/Core/BlazorBootstrapInterop.cs @@ -0,0 +1,14 @@ +namespace BlazorBootstrap; + +public class BlazorBootstrapInterop +{ + #region Fields and Constants + + private const string Prefix = "window.blazorBootstrap."; + + public const string ScrollToElementBottom = Prefix + "scrollToElementBottom"; + + public const string ScrollToElementTop = Prefix + "scrollToElementTop"; + + #endregion +} diff --git a/blazorbootstrap/Components/Markdown/Markdown.razor b/blazorbootstrap/Components/Markdown/Markdown.razor new file mode 100644 index 000000000..0a5a86345 --- /dev/null +++ b/blazorbootstrap/Components/Markdown/Markdown.razor @@ -0,0 +1,6 @@ +@namespace BlazorBootstrap +@inherits BlazorBootstrapComponentBase + + \ No newline at end of file diff --git a/blazorbootstrap/Components/Markdown/Markdown.razor.cs b/blazorbootstrap/Components/Markdown/Markdown.razor.cs new file mode 100644 index 000000000..fe95e23e7 --- /dev/null +++ b/blazorbootstrap/Components/Markdown/Markdown.razor.cs @@ -0,0 +1,165 @@ +using Microsoft.AspNetCore.Components.Rendering; +using System.Text.RegularExpressions; + +namespace BlazorBootstrap; + +public partial class Markdown : BlazorBootstrapComponentBase +{ + private string? html; + + #region Properties, Indexers + + /// + /// Gets or sets the content to be rendered within the component. + /// + /// + /// Default value is null. + /// + [Parameter] + public RenderFragment? ChildContent { get; set; } + + #endregion + + protected override void OnInitialized() + { + ParseMarkdown(); + + base.OnInitialized(); + } + + protected override void OnParametersSet() + { + if (IsRenderComplete) + ParseMarkdown(); + + base.OnParametersSet(); + } + + private void ParseMarkdown() + { + var lines = GetLines(); + if (lines.Any()) + { + // remove start and end blank lines + if (lines[0] == "") + lines.RemoveAt(0); + + if (lines[lines.Count - 1] == "") + lines.RemoveAt(lines.Count - 1); + } + + var markup = ApplyRules(lines); + html = ApplyFullMarkupRules(markup); + } + + private string ApplyRules(List lines) + { + var patterns = GetRules(); + foreach (var pattern in patterns) + { + for (var i = 0; i < lines.Count; i++) + lines[i] = Regex.Replace(lines[i], pattern.Rule, pattern.Template); + } + + return string.Join("\n", lines); + } + + private string ApplyFullMarkupRules(string markup) + { + var patterns = GetFullMarkupRules(); + + foreach (var pattern in patterns) + markup = Regex.Replace(markup, pattern.Rule, pattern.Template); + + return markup; + } + + List GetLines() + { + var inputs = new List(); + + if (ChildContent is not null) + { + var builder = new RenderTreeBuilder(); + ChildContent.Invoke(builder); + + var frames = builder.GetFrames().Array; + foreach (var frame in frames) + { + if (frame.MarkupContent is not null) + { + var lines = frame.MarkupContent.Split("\r\n"); + foreach (var line in lines) + inputs.Add(line.Trim()); + } + } + } + + return inputs; + } + + private List GetRules() + { + return new List + { + // Headers + new(@"^#{6}\s?([^\n]+)", "
$1
"), + new(@"^#{5}\s?([^\n]+)", "
$1
"), + new(@"^#{4}\s?([^\n]+)", "

$1

"), + new(@"^#{3}\s?([^\n]+)", "

$1

"), + new(@"^#{2}\s?([^\n]+)", "

$1

"), + new(@"^#{1}\s?([^\n]+)", "

$1

"), + + // Blockquotes + new(@"^>{1}\s(.*)$", "
$1
"), + //new(@"^(>>)+ (.*)$", "

$2

"), + + // Horizontal rules + new(@"^\-{3,}$", "
"), + + // Emphasis (bold, italics, strikethrough) + new(@"\*\*(.*?)\*\*", "$1"), + new(@"__(.*?)__", "$1"), + new(@"\*(.*?)\*", "$1"), + new(@"_(.*?)_", "$1"), + new(@"~~(.*?)~~", "$1"), + + // Code highlighting + new(@"\```(\w+)", "
"),
+            new(@"```", "
"), + + // Tables + + // Lists + // Ordered or numbered lists + + // Bulleted lists + + // Nested lists + + // Links + + // Anchor links + + // Images + + // Checklist or task list + + // Emoji + + // Mathematical notation and characters + + // Mermaid diagrams + }; + } + + private List GetFullMarkupRules() + { + return new List + { + // Paragraphs and line breaks + new(@"(?"), + new(@"([^\n\n]+\n?)", "

$1

"), + }; + } +} diff --git a/blazorbootstrap/Config.cs b/blazorbootstrap/Config.cs index 19fb67f36..fe8146e37 100644 --- a/blazorbootstrap/Config.cs +++ b/blazorbootstrap/Config.cs @@ -1,4 +1,5 @@ using BlazorBootstrap; +using Microsoft.Extensions.Configuration; namespace Microsoft.Extensions.DependencyInjection; @@ -9,19 +10,19 @@ public static class Config /// /// Adds a bootstrap providers and component mappings. /// - /// + /// /// IServiceCollection - public static IServiceCollection AddBlazorBootstrap(this IServiceCollection serviceCollection) + public static IServiceCollection AddBlazorBootstrap(this IServiceCollection services, IConfiguration configuration = null!) { - serviceCollection.AddScoped(); - serviceCollection.AddScoped(); - serviceCollection.AddScoped(); - serviceCollection.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); - serviceCollection.AddScoped(); - serviceCollection.AddScoped(); + services.AddScoped(); + services.AddScoped(); - return serviceCollection; + return services; } #endregion diff --git a/blazorbootstrap/Constants/BootstrapClass.cs b/blazorbootstrap/Constants/BootstrapClass.cs index 03fc8791d..73919b3ad 100644 --- a/blazorbootstrap/Constants/BootstrapClass.cs +++ b/blazorbootstrap/Constants/BootstrapClass.cs @@ -65,6 +65,8 @@ public static class BootstrapClass public const string ConfirmationModal = "modal-confirmation"; + public const string Container = "container"; + public const string Disabled = "disabled"; public const string DisplayNone = "d-none"; public const string DisplayBlock = "d-block"; diff --git a/blazorbootstrap/Models/AI/OpenAIChatMessage.cs b/blazorbootstrap/Models/AI/OpenAIChatMessage.cs new file mode 100644 index 000000000..4a3950e63 --- /dev/null +++ b/blazorbootstrap/Models/AI/OpenAIChatMessage.cs @@ -0,0 +1,3 @@ +namespace BlazorBootstrap; + +public record OpenAIChatMessage(string Role, string Content); diff --git a/blazorbootstrap/Models/AI/OpenAIChatPayload.cs b/blazorbootstrap/Models/AI/OpenAIChatPayload.cs new file mode 100644 index 000000000..92f69887d --- /dev/null +++ b/blazorbootstrap/Models/AI/OpenAIChatPayload.cs @@ -0,0 +1,56 @@ +namespace BlazorBootstrap; + +public record OpenAIChatPayload +{ + #region Properties, Indexers + + /// + /// How much to penalize new tokens based on their existing frequency in the text so far. + /// Decreases the model's likelihood to repeat the same line verbatim. + /// Minimum 1 and the maximum is 2. + /// + /// Default value is 0. + [JsonPropertyName("frequency_penalty")] + public double FrequencyPenalty { get; set; } = 0; + + /// + /// The maximum number of tokens to generate shared between the prompt and completion. + /// The exact limit varies by model. (One token is roughly 4 characters for standard English text) + /// Minimum 1 and the maximum tokens is 4096. + /// + /// Default value is 2048. This value is limited by gpt-3.5-turbo. + [JsonPropertyName("max_tokens")] + public long MaximumTokens { get; set; } = 2048; + + [JsonPropertyName("messages")] + public List? Messages { get; set; } + + /// + /// How much to penalize new tokens based on whether they appear in the text so far. + /// Increases the model's likelihood to talk about new topics. + /// Minimum 1 and the maximum is 2. + /// + /// Default value is 0. + [JsonPropertyName("presence_penalty")] + public float PresencePenalty { get; set; } = 0; + + [JsonPropertyName("stream")] public bool Stream { get; } = true; + + /// + /// Controls randomness: Lowering results in less random completions. + /// As the temperature approaches zero, the model will become deterministic and repetitive. + /// Minimum 1 and the maximum is 2. + /// + /// Default value is 1. + [JsonPropertyName("temperature")] + public double Temperature { get; set; } = 1; + + /// + /// Controls diversity via nucleus sampling: 0.5 means half of all likelihood-weighted options are considered. + /// + /// Default value is 1. + [JsonPropertyName("top_p")] + public double TopP { get; set; } = 1; + + #endregion +} diff --git a/blazorbootstrap/Models/MarkdownPattern.cs b/blazorbootstrap/Models/MarkdownPattern.cs new file mode 100644 index 000000000..c5095e5b7 --- /dev/null +++ b/blazorbootstrap/Models/MarkdownPattern.cs @@ -0,0 +1,3 @@ +namespace BlazorBootstrap; + +public record MarkdownPattern(string Rule, string Template); diff --git a/blazorbootstrap/wwwroot/blazor.bootstrap.ai.js b/blazorbootstrap/wwwroot/blazor.bootstrap.ai.js new file mode 100644 index 000000000..f9241c524 --- /dev/null +++ b/blazorbootstrap/wwwroot/blazor.bootstrap.ai.js @@ -0,0 +1,57 @@ +export async function createChatCompletions(key, messages, dotNetHelper) { + const API_KEY = key; + const API_URL = 'https://api.openai.com/v1/chat/completions'; + + try { + // Fetch the response from the OpenAI API with the signal from AbortController + const response = await fetch(API_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${API_KEY}`, + }, + body: JSON.stringify({ + model: "gpt-3.5-turbo", //"gpt-4", + messages: messages, + max_tokens: 200, + stream: true, // For streaming responses + }) + }); + + // Read the response as a stream of data + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + let i = 0; + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + + // Massage and parse the chunk of data + const chunk = decoder.decode(value); + const lines = chunk.split("\n"); + + for (const payload of lines) { + + if (payload.includes('[DONE]')) { + dotNetHelper.invokeMethodAsync('ChartCompletetionsStreamJs', '', true); + return; + } + + if (payload.startsWith("data:")) { + const data = JSON.parse(payload.replace("data:", "")); + const content = data.choices[0].delta.content; + if (content) { + dotNetHelper.invokeMethodAsync('ChartCompletetionsStreamJs', content, false); + } + } + } + } + } catch (error) { + // Handle fetch request errors + console.log(error); + } finally { + // TODO: cleanup + } +} diff --git a/blazorbootstrap/wwwroot/blazor.bootstrap.js b/blazorbootstrap/wwwroot/blazor.bootstrap.js index 1d45c7a38..055742f0d 100644 --- a/blazorbootstrap/wwwroot/blazor.bootstrap.js +++ b/blazorbootstrap/wwwroot/blazor.bootstrap.js @@ -933,6 +933,16 @@ window.blazorBootstrap = { return false; }, + scrollToElementBottom: (elementId) => { + let el = document.getElementById(elementId); + if (el) + el.scrollTop = el.scrollHeight; + }, + scrollToElementTop: (elementId) => { + let el = document.getElementById(elementId); + if (el) + el.scrollTop = 0; + } } window.blazorChart = { @@ -1992,3 +2002,83 @@ window.blazorChart.scatter = { } } } + +if (!window.blazorBootstrap.ai) { + window.blazorBootstrap.ai = {}; +} + +window.blazorBootstrap.ai = { + azureOpenAI: { + chat: { + completions: async (url, key, payload, dotNetHelper) => { + let contentArray = []; + let notificationTriggered = false; + let streamComplete = false; + + try { + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "api-key": `${key}`, + }, + body: JSON.stringify(payload) + }); + + // Read the response as a stream of data + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + let i = 0; + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + + // Message and parse the chunk of data + const chunk = decoder.decode(value); + const lines = chunk.split("\n"); + + for (const line of lines) { + + if (line.includes('[DONE]')) { + streamComplete = true; + return; + } + + if (line.startsWith("data:")) { + const data = JSON.parse(line.replace("data:", "")); + const content = data.choices[0]?.delta?.content; + if (content) { + contentArray.push(content); + if (!notificationTriggered) { + notificationTriggered = true; + triggerNotify(); + } + } + } + } + } + + function triggerNotify() { + let handler = setInterval(() => { + const content = contentArray.shift(); + if (content && content.length > 0) + dotNetHelper.invokeMethodAsync('ChartCompletetionsStreamJs', content, false); + + if (streamComplete && contentArray.length === 0) { + clearInterval(handler); + dotNetHelper.invokeMethodAsync('ChartCompletetionsStreamJs', '', true); + } + }, 100); + } + + } catch (error) { + console.log(error); + } finally { + // TODO: cleanup + } + } + } + } +} From d4cc2668ca2e42b9515c2105ad6e0ecbed657c74 Mon Sep 17 00:00:00 2001 From: Vikram Reddy Date: Sun, 6 Oct 2024 23:33:45 +0530 Subject: [PATCH 02/23] Dark mode support (#895) * dark mode support --- .../Components/Layout/EmptyLayout.razor | 116 +++------ .../Components/Layout/EmptyLayout.razor.cs | 5 + .../Components/Layout/MainLayout.razor | 1 - .../Components/Layout/MainLayoutBase.cs | 38 ++- .../Layout/MainLayoutBaseFooter.razor | 4 +- .../Components/Shared/Demo.razor | 8 +- .../Components/Shared/Demo.razor.cs | 4 +- .../Components/Shared/Demo.razor.css | 6 +- .../wwwroot/css/blazorbootstrap.demo.rcl.css | 61 ++++- .../wwwroot/css/prism-vs.min.css | 117 +++++++++ .../wwwroot/css/prism-vsc-dark-plus.min.css | 226 ++++++++++++++++++ .../wwwroot/js/blazorbootstrap.demo.rcl.js | 99 ++++++++ .../Components/App.razor | 4 +- .../Components/Callout/Callout.razor.css | 5 +- .../Components/Modals/Modal.razor.cs | 6 +- .../Components/Toasts/Toast.razor.cs | 4 +- blazorbootstrap/Enums/ModalType.cs | 1 + blazorbootstrap/Models/ModalOption.cs | 4 +- blazorbootstrap/wwwroot/blazor.bootstrap.css | 101 +++++--- 19 files changed, 658 insertions(+), 152 deletions(-) create mode 100644 BlazorBootstrap.Demo.RCL/Components/Layout/EmptyLayout.razor.cs create mode 100644 BlazorBootstrap.Demo.RCL/wwwroot/css/prism-vs.min.css create mode 100644 BlazorBootstrap.Demo.RCL/wwwroot/css/prism-vsc-dark-plus.min.css diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/EmptyLayout.razor b/BlazorBootstrap.Demo.RCL/Components/Layout/EmptyLayout.razor index 2858d1aa1..c2ad8e129 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/EmptyLayout.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/EmptyLayout.razor @@ -1,6 +1,5 @@ @namespace BlazorBootstrap.Demo.RCL - -@inherits LayoutComponentBase +@inherits MainLayoutBase @@ -64,6 +63,33 @@ Open Collective + +
@@ -74,79 +100,13 @@ @Body - - -@code { - private string version = default!; - private string docsUrl = default!; - private string blogUrl = default!; - private string githubUrl = default!; - private string twitterUrl = default!; - private string linkedInUrl = default!; - private string openCollectiveUrl = default!; - private string githubIssuesUrl = default!; - private string githubDiscussionsUrl = default!; - private string stackoverflowUrl = default!; - - [Inject] public IConfiguration Configuration { get; set; } = default!; - - protected override void OnInitialized() - { - version = $"v{Configuration["version"]}"; // example: v0.6.1 - docsUrl = $"{Configuration["urls:docs"]}"; - blogUrl = $"{Configuration["urls:blog"]}"; - githubUrl = $"{Configuration["urls:github"]}"; - twitterUrl = $"{Configuration["urls:twitter"]}"; - linkedInUrl = $"{Configuration["urls:linkedin"]}"; - openCollectiveUrl = $"{Configuration["urls:opencollective"]}"; - githubIssuesUrl = $"{Configuration["urls:github_issues"]}"; - githubDiscussionsUrl = $"{Configuration["urls:github_discussions"]}"; - stackoverflowUrl = $"{Configuration["urls:stackoverflow"]}"; - - base.OnInitialized(); - } -} + diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/EmptyLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/EmptyLayout.razor.cs new file mode 100644 index 000000000..278295750 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/EmptyLayout.razor.cs @@ -0,0 +1,5 @@ +namespace BlazorBootstrap.Demo.RCL; + +public partial class EmptyLayout : MainLayoutBase +{ +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor index a81e746b9..2298ef346 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor @@ -1,5 +1,4 @@ @namespace BlazorBootstrap.Demo.RCL - @inherits MainLayoutBase
diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayoutBase.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayoutBase.cs index 2c7a5d8db..92ce935ec 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayoutBase.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayoutBase.cs @@ -2,22 +2,32 @@ public class MainLayoutBase : LayoutComponentBase { - private string version = default!; - private string docsUrl = default!; - private string blogUrl = default!; - private string githubUrl = default!; - private string twitterUrl = default!; - private string linkedInUrl = default!; - private string openCollectiveUrl = default!; - private string githubIssuesUrl = default!; - private string githubDiscussionsUrl = default!; - private string stackoverflowUrl = default!; + internal string version = default!; + internal string docsUrl = default!; + internal string blogUrl = default!; + internal string githubUrl = default!; + internal string twitterUrl = default!; + internal string linkedInUrl = default!; + internal string openCollectiveUrl = default!; + internal string githubIssuesUrl = default!; + internal string githubDiscussionsUrl = default!; + internal string stackoverflowUrl = default!; internal Sidebar2 sidebar2 = default!; internal IEnumerable navItems = default!; [Inject] public IConfiguration Configuration { get; set; } = default!; + [Inject] protected IJSRuntime JS { get; set; } = default!; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + await JS.InvokeVoidAsync("initializeTheme"); + + await base.OnAfterRenderAsync(firstRender); + } + protected override void OnInitialized() { version = $"v{Configuration["version"]}"; // example: v0.6.1 @@ -33,6 +43,14 @@ protected override void OnInitialized() base.OnInitialized(); } + internal Task SetAutoTheme() => SetTheme("system"); + + internal Task SetDarkTheme() => SetTheme("dark"); + + internal Task SetLightTheme() => SetTheme("light"); + + internal async Task SetTheme(string themeName) => await JS.InvokeVoidAsync("setTheme", themeName); + internal virtual async Task Sidebar2DataProvider(Sidebar2DataProviderRequest request) { if (navItems is null) diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayoutBaseFooter.razor b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayoutBaseFooter.razor index 42c32ab15..c35d8360e 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayoutBaseFooter.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayoutBaseFooter.razor @@ -1,11 +1,11 @@ @namespace BlazorBootstrap.Demo.RCL @inherits ComponentBase -