-
-
Notifications
You must be signed in to change notification settings - Fork 254
Add AI chat panel to Platform website (#11346) #11347
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
msynk
merged 6 commits into
bitfoundation:develop
from
msynk:11346-websites-platform-aichatpanel
Sep 6, 2025
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3 changes: 2 additions & 1 deletion
3
...Websites/Platform/src/Bit.Websites.Platform.Client/Extensions/IConfigurationExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
127 changes: 127 additions & 0 deletions
127
src/Websites/Platform/src/Bit.Websites.Platform.Client/Shared/AppAiChatPanel.razor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| @using Bit.Websites.Platform.Shared.Dtos.AiChat | ||
| @inherits AppComponentBase | ||
|
|
||
| <BitMediaQuery ScreenQuery="BitScreenQuery.LtSm" OnChange="isMatched => isSmallScreen = isMatched" /> | ||
|
|
||
| <section> | ||
| <BitButton Float | ||
| Draggable | ||
| FloatOffset="1rem" | ||
| Class="open-panel-button" | ||
| Variant="BitVariant.Outline" | ||
| OnClick="() => isOpen = true" | ||
| Color="BitColor.PrimaryBackground" | ||
| IconUrl="/images/ai-chat-icon-64.webp" | ||
| FloatPosition="BitPosition.BottomRight" /> | ||
|
|
||
| <BitProPanel ShowCloseButton | ||
| @bind-IsOpen="isOpen" | ||
| ModeFull="isSmallScreen is true" | ||
| Modeless="isSmallScreen is false" | ||
| OnDismiss="WrapHandled(HandleOnDismissPanel)"> | ||
| <Header> | ||
| <BitStack Horizontal Alignment="BitAlignment.Center"> | ||
| <BitText Typography="BitTypography.H5">AI chat panel</BitText> | ||
| @if (isLoading) | ||
| { | ||
| <BitRollingSquareLoading Size="BitSize.Small" /> | ||
| } | ||
| <BitSpacer /> | ||
| <BitButton IconOnly | ||
| FixedColor | ||
| Title="Clear" | ||
| IconName="Delete" | ||
| Style="padding:10px" | ||
| Variant="BitVariant.Text" | ||
| OnClick="WrapHandled(ClearChat)" | ||
| Color="BitColor.SecondaryBackground" /> | ||
| </BitStack> | ||
| </Header> | ||
| <Body> | ||
| <BitStack Class="body"> | ||
| <BitScrollablePane FullSize | ||
| AutoScroll | ||
| Class="scr-container" | ||
| ScrollbarWidth="BitScrollbarWidth.Thin"> | ||
| <BitStack> | ||
| @foreach (var message in chatMessages) | ||
| { | ||
| if (message.Role is AiChatMessageRole.User) | ||
| { | ||
| <BitCard Background="BitColorKind.Tertiary" Class="user-message"> | ||
| <BitText Element="pre">@message.Content</BitText> | ||
| </BitCard> | ||
| } | ||
| else | ||
| { | ||
| <BitMarkdownViewer Markdown="@message.Content" /> | ||
| @if (message.Successful is false) | ||
| { | ||
| <BitTag Color="BitColor.Error" Size="BitSize.Small" Style="min-height:18px">Canceled</BitTag> | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @if (isLoading && string.IsNullOrWhiteSpace(lastAssistantMessage?.Content)) | ||
| { | ||
| <BitEllipsisLoading Size="BitSize.Small" /> | ||
| } | ||
| </BitStack> | ||
msynk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| </BitScrollablePane> | ||
|
|
||
| @if (chatMessages.Count == 1) | ||
| { | ||
| <BitStack Alignment=" BitAlignment.Center" FitHeight FillContent Class="default-prompt-container"> | ||
| <BitButton FixedColor | ||
| Variant="BitVariant.Outline" | ||
| Class="default-prompt-button" | ||
| Color="BitColor.SecondaryBackground" | ||
| OnClick="() => SendPromptMessage(AiChatPanelPrompt1)"> | ||
| @AiChatPanelPrompt1 | ||
| </BitButton> | ||
|
|
||
| <BitButton FixedColor | ||
| Variant="BitVariant.Outline" | ||
| Class="default-prompt-button" | ||
| Color="BitColor.SecondaryBackground" | ||
| OnClick="() => SendPromptMessage(AiChatPanelPrompt2)"> | ||
| @AiChatPanelPrompt2 | ||
| </BitButton> | ||
|
|
||
| <BitButton FixedColor | ||
| Variant="BitVariant.Outline" | ||
| Class="default-prompt-button" | ||
| Color="BitColor.SecondaryBackground" | ||
| OnClick="() => SendPromptMessage(AiChatPanelPrompt3)"> | ||
| @AiChatPanelPrompt3 | ||
| </BitButton> | ||
| </BitStack> | ||
| } | ||
msynk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| <BitStack FitHeight Style="position:relative"> | ||
| <BitTextField Rows="1" | ||
| Immediate | ||
| Multiline | ||
| AutoHeight | ||
| PreventEnter | ||
| MaxLength="1024" | ||
| Style="width:100%" | ||
| @bind-Value="@userInput" | ||
| Placeholder="Write a message...." | ||
| OnEnter="WrapHandled((KeyboardEventArgs e) => HandleOnUserInputEnter(e))" | ||
| Styles="@(new() { FieldGroup = "padding:0.5rem; padding-inline-end:2.5rem", Input = "min-height:unset" })" /> | ||
msynk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| <BitButton Float | ||
| AutoLoading | ||
| FloatAbsolute | ||
| Title="Send" | ||
| IconName="Up" | ||
| FloatOffset="0.5rem" | ||
| Class="send-message-button" | ||
| OnClick="WrapHandled(SendMessage)" | ||
| IsEnabled=@(string.IsNullOrEmpty(userInput) is false) /> | ||
| </BitStack> | ||
| </BitStack> | ||
| </Body> | ||
| </BitProPanel> | ||
| </section> | ||
176 changes: 176 additions & 0 deletions
176
src/Websites/Platform/src/Bit.Websites.Platform.Client/Shared/AppAiChatPanel.razor.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| using System.Threading.Channels; | ||
| using Bit.Websites.Platform.Shared.Dtos.AiChat; | ||
| using Bit.Websites.Platform.Shared.Services; | ||
| using Microsoft.AspNetCore.Components.Web; | ||
| using Microsoft.AspNetCore.SignalR.Client; | ||
|
|
||
| namespace Bit.Websites.Platform.Client.Shared; | ||
|
|
||
| public partial class AppAiChatPanel | ||
| { | ||
| [CascadingParameter] public BitDir? CurrentDir { get; set; } | ||
|
|
||
| [AutoInject] private HubConnection hubConnection = default!; | ||
|
|
||
|
|
||
| private bool isOpen; | ||
| private bool isLoading; | ||
| private string? userInput; | ||
| private bool isSmallScreen; | ||
| private int responseCounter; | ||
| private Channel<string>? channel; | ||
| private AiChatMessage? lastAssistantMessage; | ||
| private List<AiChatMessage> chatMessages = []; // TODO: Persist these values in client-side storage to retain them across app restarts. | ||
|
|
||
| private string AiChatPanelPrompt1 = "What does bitplatform MIT license mean? Is it free to use?"; | ||
| private string AiChatPanelPrompt2 = "What are the benefits of dedicated support?"; | ||
| private string AiChatPanelPrompt3 = "What does bit Besql do?"; | ||
|
|
||
|
|
||
| protected override async Task OnAfterFirstRenderAsync() | ||
| { | ||
| SetDefaultValues(); | ||
| StateHasChanged(); | ||
| hubConnection.Reconnected += HubConnection_Reconnected; | ||
|
|
||
| await base.OnAfterFirstRenderAsync(); | ||
| } | ||
|
|
||
|
|
||
| private async Task HubConnection_Reconnected(string? _) | ||
| { | ||
| if (channel is null) return; | ||
|
|
||
| await RestartChannel(); | ||
| } | ||
|
|
||
| private async Task SendPromptMessage(string message) | ||
| { | ||
| userInput = message; | ||
| await SendMessage(); | ||
| } | ||
|
|
||
| private async Task SendMessage() | ||
| { | ||
| if (string.IsNullOrWhiteSpace(userInput)) return; | ||
|
|
||
| if (hubConnection.State is not HubConnectionState.Connected) | ||
| { | ||
| await hubConnection.StartAsync(CurrentCancellationToken); | ||
| } | ||
|
|
||
| if (channel is null) | ||
| { | ||
| _ = StartChannel(); | ||
| } | ||
|
|
||
| isLoading = true; | ||
|
|
||
| var input = userInput; | ||
| userInput = string.Empty; | ||
|
|
||
| chatMessages.Add(new() { Content = input, Role = AiChatMessageRole.User }); | ||
| lastAssistantMessage = new() { Role = AiChatMessageRole.Assistant }; | ||
| chatMessages.Add(lastAssistantMessage); | ||
|
|
||
| StateHasChanged(); | ||
|
|
||
| await channel!.Writer.WriteAsync(input!, CurrentCancellationToken); | ||
| } | ||
|
|
||
| private async Task ClearChat() | ||
| { | ||
| SetDefaultValues(); | ||
|
|
||
| await RestartChannel(); | ||
| } | ||
|
|
||
| private void SetDefaultValues() | ||
| { | ||
| isLoading = false; | ||
| responseCounter = 0; | ||
| lastAssistantMessage = new() { Role = AiChatMessageRole.Assistant }; | ||
| chatMessages = [ | ||
| new() | ||
| { | ||
| Role = AiChatMessageRole.Assistant, | ||
| Content = "I'm here to make your app experience awesome! Got a question or need a hand?", | ||
| } | ||
| ]; | ||
| } | ||
msynk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| private async Task HandleOnDismissPanel() | ||
| { | ||
| await StopChannel(); | ||
| } | ||
|
|
||
| private async Task HandleOnUserInputEnter(KeyboardEventArgs e) | ||
| { | ||
| if (e.ShiftKey) return; | ||
|
|
||
| await SendMessage(); | ||
| } | ||
|
|
||
msynk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| private async Task StartChannel() | ||
| { | ||
| channel = Channel.CreateUnbounded<string>(new() { SingleReader = true, SingleWriter = true }); | ||
|
|
||
| await foreach (var response in hubConnection.StreamAsync<string>("Chatbot", | ||
| new StartChatbotRequest() | ||
| { | ||
| ChatMessagesHistory = chatMessages | ||
| }, | ||
| channel.Reader.ReadAllAsync(CurrentCancellationToken), | ||
| cancellationToken: CurrentCancellationToken)) | ||
| { | ||
| int expectedResponsesCount = chatMessages.Count(c => c.Role is AiChatMessageRole.User); | ||
|
|
||
| if (response is SharedChatProcessMessages.MESSAGE_RPOCESS_SUCESS) | ||
| { | ||
| responseCounter++; | ||
| isLoading = false; | ||
| } | ||
| else if (response is SharedChatProcessMessages.MESSAGE_RPOCESS_ERROR) | ||
| { | ||
| responseCounter++; | ||
| if (responseCounter == expectedResponsesCount) | ||
| { | ||
| isLoading = false; // Hide loading only if this is an error for the last user's message. | ||
| } | ||
| chatMessages[responseCounter * 2].Successful = false; | ||
| } | ||
| else | ||
| { | ||
| if ((responseCounter + 1) == expectedResponsesCount) | ||
| { | ||
| lastAssistantMessage!.Content += response; | ||
| } | ||
| } | ||
|
|
||
| StateHasChanged(); | ||
| } | ||
| } | ||
|
|
||
| private async Task StopChannel() | ||
| { | ||
| if (channel is null) return; | ||
|
|
||
| channel.Writer.Complete(); | ||
| channel = null; | ||
| } | ||
|
|
||
| private async Task RestartChannel() | ||
| { | ||
| await StopChannel(); | ||
| await StartChannel(); | ||
| } | ||
|
|
||
| protected override async ValueTask DisposeAsync(bool disposing) | ||
| { | ||
| hubConnection.Reconnected -= HubConnection_Reconnected; | ||
|
|
||
| await StopChannel(); | ||
|
|
||
| await base.DisposeAsync(disposing); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.