diff --git a/LM-Kit-Maestro/UI/Pages/ChatPage.xaml b/LM-Kit-Maestro/UI/Pages/ChatPage.xaml index 663e62ba..9bd22bec 100644 --- a/LM-Kit-Maestro/UI/Pages/ChatPage.xaml +++ b/LM-Kit-Maestro/UI/Pages/ChatPage.xaml @@ -120,20 +120,13 @@ - - @@ -143,8 +136,7 @@ diff --git a/LM-Kit-Maestro/UI/Razor/Components/Chat.razor b/LM-Kit-Maestro/UI/Razor/Components/Chat.razor index 5506c698..c323dd37 100644 --- a/LM-Kit-Maestro/UI/Razor/Components/Chat.razor +++ b/LM-Kit-Maestro/UI/Razor/Components/Chat.razor @@ -1,4 +1,4 @@ - @page "/chat" +@page "/chat" @inject LMKitService LMKitService @inject HttpClient Http @@ -10,22 +10,39 @@ @inherits MvvmComponentBase
-
-
+
+ +
+ + @foreach (var conversation in ViewModel.ConversationListViewModel.Conversations) + { + + } +
+
+ +
+
@if (ViewModel?.ConversationListViewModel.CurrentConversation != null) { if (ViewModel.ConversationListViewModel.CurrentConversation.IsEmpty) { -
+
-
Maestro at your service—let’s orchestrate something amazing!
+
Maestro at your service—let’s orchestrate something + amazing! +
Feel free to ask questions, explore ideas, or engage in meaningful conversations.
- Whether you need assistance, inspiration, or just some lighthearted chat, I'm here to help. + Whether you need assistance, inspiration, or just some lighthearted chat, I'm here to + help.
@@ -44,317 +61,49 @@ } else { -
+
@foreach (var message in ViewModel.ConversationListViewModel.CurrentConversation.Messages) { - + } + +
+
+ @if (!IsScrolledToEnd) + { + + } +
+
} }
-
-
- @if (!IsScrolledToEnd) - { - - } -
+
+
-
- -
- -
- -
- @if (ViewModel?.ConversationListViewModel?.CurrentConversation?.LMKitConversation?.ContextSize > 0) - { - - Tokens: @ViewModel.ConversationListViewModel.CurrentConversation.LMKitConversation.ContextUsedSpace / - @ViewModel.ConversationListViewModel.CurrentConversation.LMKitConversation.ContextSize - (@CalculateUsagePercentage(ViewModel.ConversationListViewModel.CurrentConversation.LMKitConversation.ContextUsedSpace, - ViewModel.ConversationListViewModel.CurrentConversation.LMKitConversation.ContextSize)%) - - } -
-
- -@code -{ - private const int UIUpdateDelayMilliseconds = 50; - - private ConversationViewModel? _previousConversationViewModel; - private MessageViewModel? _latestAssistantResponse; - private bool _hasPendingScrollEnd; - private bool _ignoreScrollsUntilNextScrollUp; - private double? _previousScrollTop; - private bool _shouldAutoScrollEnd; - private double _scrollTop; - - private bool _isScrolledToEnd = false; - - public bool IsScrolledToEnd - { - get => _isScrolledToEnd; - set - { - _isScrolledToEnd = value; - - if (_isScrolledToEnd && - ViewModel.ConversationListViewModel.CurrentConversation != null && - ViewModel.ConversationListViewModel.CurrentConversation!.AwaitingResponse) - { - // Assistant is currently generating a response, enforce auto-scroll. - _shouldAutoScrollEnd |= true; - } - - UpdateUIAsync(); - } - } - - protected override async void OnInitialized() - { - base.OnInitialized(); - - if (ViewModel.ConversationListViewModel.CurrentConversation != null) - { - OnConversationSet(); - } - - ViewModel.ConversationListViewModel.PropertyChanged += OnConversationListViewModelPropertyChanged; - ViewModel.ConversationListViewModel.CurrentConversation.LMKitConversation.PropertyChanged += OnLMKitConversationPropertyChanged; - - await ResizeHandler.RegisterPageResizeAsync(Resized); - await JS.InvokeVoidAsync("initializeScrollHandler", DotNetObjectReference.Create(this)); - } - - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - await base.OnAfterRenderAsync(firstRender); - - if (_hasPendingScrollEnd) - { - _hasPendingScrollEnd = false; - await ScrollToEnd(); - } - } - - private bool _refreshScheduled = false; - - private Task UpdateUIAsync(bool forceRedraw = false) - { - if (forceRedraw) - { - return InvokeAsync(() => StateHasChanged()); - } - else - { - if (!_refreshScheduled) - { - _refreshScheduled = true; - - return InvokeAsync(async () => - { - await Task.Delay(UIUpdateDelayMilliseconds); - StateHasChanged(); - _refreshScheduled = false; - }); - } - } - return Task.CompletedTask; - } - - private void OnConversationListViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(ConversationListViewModel.CurrentConversation)) - { - OnConversationSet(); - } - } - - private void OnLMKitConversationPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(LMKitService.Conversation.ContextRemainingSpace)) - { - UpdateUIAsync(); - } - else if (e.PropertyName == nameof(LMKitService.Conversation.InTextCompletion)) - { - UpdateUIAsync(forceRedraw: true); - } - } - - private void OnLatestAssistantMessagePropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(MessageViewModel.Content)) - { - OnLatestAssistantResponseProgressed(); - } - else if (e.PropertyName == nameof(MessageViewModel.MessageInProgress)) - { - if (sender == _latestAssistantResponse && !_latestAssistantResponse!.MessageInProgress && _shouldAutoScrollEnd) - { - Task.Run(async () => - { - // Note: Adding delay to sync with current UI behavior - // (show latest assistant response footer for 3 sec). - await Task.Delay(50); - await ScrollToEnd(); - }); - } - } - } - - private void OnConversationSet() - { - if (_previousConversationViewModel != null) - { - _previousConversationViewModel.Messages.CollectionChanged -= OnConversationMessagesCollectionChanged; - _previousConversationViewModel.TextGenerationCompleted -= OnTextGenerationCompleted; - } - - _previousConversationViewModel = ViewModel.ConversationListViewModel.CurrentConversation; - - if (ViewModel.ConversationListViewModel.CurrentConversation != null) - { - ViewModel.ConversationListViewModel.CurrentConversation.Messages.CollectionChanged += OnConversationMessagesCollectionChanged; - ViewModel.ConversationListViewModel.CurrentConversation.TextGenerationCompleted += OnTextGenerationCompleted; - - _previousScrollTop = null; - _ignoreScrollsUntilNextScrollUp = true; - IsScrolledToEnd = true; - - // Awaiting for the component to be rendered before scrolling to bottom. - _hasPendingScrollEnd = true; - } - } - - private async void OnConversationMessagesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - await UpdateUIAsync(forceRedraw: true); - - await ScrollToEnd(); - - var latestMessage = ViewModel.ConversationListViewModel.CurrentConversation!.Messages.LastOrDefault(); - - if (latestMessage != null && latestMessage.Sender == MessageSender.Assistant && latestMessage.MessageInProgress) - { - _latestAssistantResponse = latestMessage; - _shouldAutoScrollEnd = true; - latestMessage.PropertyChanged += OnLatestAssistantMessagePropertyChanged; - } - } - - private void OnTextGenerationCompleted(object? sender, ConversationViewModel.TextGenerationCompletedEventArgs e) - { - if (e.Exception != null && - e.Status == LMKitRequestStatus.GenericError) - { - Snackbar.Clear(); - Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; - Snackbar.Add($"Text generation failed unexpectedly:\n{e.Exception.Message}", Severity.Error); - } - - UpdateUIAsync(); - } - - private async void OnLatestAssistantResponseProgressed() - { - if (_shouldAutoScrollEnd) - { - await ScrollToEnd(); - } - } - - private async Task Resized(ResizeEventArgs args) - { - if (IsScrolledToEnd) - { - await ScrollToEnd(); - } - else - { - await CheckIsScrolledToEnd(); - } - } - - private async Task ScrollToEnd(bool smooth = false) - { - IsScrolledToEnd = true; - _ignoreScrollsUntilNextScrollUp = true; - - await JS.InvokeVoidAsync("scrollToEnd", smooth); - } - - private async Task CheckIsScrolledToEnd() - { - var viewHeight = await JS.InvokeAsync("getConversationViewHeight"); - var chatContentHeight = await JS.InvokeAsync("getScrollHeight"); - - var value = chatContentHeight - viewHeight - _scrollTop; - IsScrolledToEnd = Math.Abs(chatContentHeight - viewHeight - _scrollTop) < 5 || chatContentHeight <= viewHeight; - } - - public async Task OnScrollToEndButtonClicked() - { - await ScrollToEnd(true); - } - - public void OnSubmit() - { - ViewModel.ConversationListViewModel.CurrentConversation!.Submit(); - } - - private int CalculateUsagePercentage(int used, int total) - { - if (total == 0) return 0; - return (int)(100 * used / total); - } - - - [JSInvokable] - public async Task OnConversationContainerScrolled(double scrollTop) - { - _scrollTop = scrollTop; - - bool shouldCheckIsScrolledToEnd = true; - bool? isScrollUp = null; - - if (_previousScrollTop != null) - { - isScrollUp = scrollTop < _previousScrollTop; - - if (isScrollUp.Value && _shouldAutoScrollEnd) - { - _shouldAutoScrollEnd = false; - } - } - - if (_ignoreScrollsUntilNextScrollUp) - { - if (isScrollUp == null || !isScrollUp.Value) - { - shouldCheckIsScrolledToEnd = false; - } - else +
+ @if (ViewModel?.ConversationListViewModel?.CurrentConversation?.LMKitConversation?.ContextSize > 0) { - _ignoreScrollsUntilNextScrollUp = false; + + Tokens: @ViewModel.ConversationListViewModel.CurrentConversation.LMKitConversation.ContextUsedSpace / + @ViewModel.ConversationListViewModel.CurrentConversation.LMKitConversation.ContextSize + (@CalculateUsagePercentage(ViewModel.ConversationListViewModel.CurrentConversation.LMKitConversation.ContextUsedSpace, + ViewModel.ConversationListViewModel.CurrentConversation.LMKitConversation.ContextSize)%) + } - } - - if (shouldCheckIsScrolledToEnd) - { - await CheckIsScrolledToEnd(); - } +
+
- _previousScrollTop = _scrollTop; - } -} + @*
+ +
*@ +
diff --git a/LM-Kit-Maestro/UI/Razor/Components/Chat.razor.cs b/LM-Kit-Maestro/UI/Razor/Components/Chat.razor.cs new file mode 100644 index 00000000..2e94898b --- /dev/null +++ b/LM-Kit-Maestro/UI/Razor/Components/Chat.razor.cs @@ -0,0 +1,303 @@ +using LMKit.Maestro.Models; +using LMKit.Maestro.Services; +using LMKit.Maestro.ViewModels; +using Majorsoft.Blazor.Components.Common.JsInterop.GlobalMouseEvents; +using Microsoft.JSInterop; +using MudBlazor; +using System.Collections.Specialized; +using System.ComponentModel; + +namespace LMKit.Maestro.UI.Razor.Components; + +public partial class Chat +{ + private const int UIUpdateDelayMilliseconds = 50; + + private ConversationViewModel? _previousConversationViewModel; + private MessageViewModel? _latestAssistantResponse; + + private bool _autoScrolling; + private bool _hasPendingScrollToEnd; + private bool _ignoreScrollsUntilNextScrollUp; + private double? _previousScrollTop; + private double _scrollTop; + + private bool _refreshScheduled; + + private bool _isScrolledToEnd; + public bool IsScrolledToEnd + { + get => _isScrolledToEnd; + set + { + _isScrolledToEnd = value; + + if (_isScrolledToEnd && + ViewModel.ConversationListViewModel.CurrentConversation != null && + ViewModel.ConversationListViewModel.CurrentConversation!.AwaitingResponse) + { + // Assistant is currently generating a response, enforce auto-scroll. + _autoScrolling |= true; + } + + UpdateUIAsync(); + } + } + + protected override async void OnInitialized() + { + base.OnInitialized(); + + if (ViewModel.ConversationListViewModel.CurrentConversation != null) + { + OnConversationSet(); + } + + ViewModel.ConversationListViewModel.ConversationPropertyChanged += OnConversationPropertyChanged; + ViewModel.ConversationListViewModel.PropertyChanged += OnConversationListViewModelPropertyChanged; + + await ResizeHandler.RegisterPageResizeAsync(Resized); + await JS.InvokeVoidAsync("initializeScrollHandler", DotNetObjectReference.Create(this)); + } + + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (_hasPendingScrollToEnd) + { + _hasPendingScrollToEnd = false; + await ScrollToEnd(); + } + } + + + private Task UpdateUIAsync(bool forceRedraw = false) + { + if (forceRedraw) + { + return InvokeAsync(() => StateHasChanged()); + } + else + { + if (!_refreshScheduled) + { + _refreshScheduled = true; + + return InvokeAsync(async () => + { + await Task.Delay(UIUpdateDelayMilliseconds); + StateHasChanged(); + _refreshScheduled = false; + }); + } + } + + return Task.CompletedTask; + } + + private void OnConversationListViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(ConversationListViewModel.CurrentConversation)) + { + OnConversationSet(); + } + } + + private void OnConversationPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(ConversationViewModel.IsShowingActionPopup)) + { + UpdateUIAsync(); + } + } + private void OnCurrentLMKitConversationPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(LMKitService.Conversation.ContextRemainingSpace)) + { + UpdateUIAsync(); + } + else if (e.PropertyName == nameof(LMKitService.Conversation.InTextCompletion)) + { + UpdateUIAsync(forceRedraw: true); + } + } + + private void OnLatestAssistantMessagePropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(MessageViewModel.Content)) + { + OnLatestAssistantResponseProgressed(); + } + else if (e.PropertyName == nameof(MessageViewModel.MessageInProgress)) + { + if (sender == _latestAssistantResponse && !_latestAssistantResponse!.MessageInProgress && _autoScrolling) + { + Task.Run(async () => + { + // Note: Adding delay to sync with current UI behavior + // (show latest assistant response footer for 3 sec). + await Task.Delay(50); + await ScrollToEnd(); + }); + } + } + } + + private void OnConversationSet() + { + if (_previousConversationViewModel != null) + { + _previousConversationViewModel.Messages.CollectionChanged -= OnConversationMessagesCollectionChanged; + _previousConversationViewModel.TextGenerationCompleted -= OnTextGenerationCompleted; + } + + _previousConversationViewModel = ViewModel.ConversationListViewModel.CurrentConversation; + + if (ViewModel.ConversationListViewModel.CurrentConversation != null) + { + ViewModel.ConversationListViewModel.CurrentConversation.Messages.CollectionChanged += OnConversationMessagesCollectionChanged; + ViewModel.ConversationListViewModel.CurrentConversation.TextGenerationCompleted += OnTextGenerationCompleted; + ViewModel.ConversationListViewModel.CurrentConversation.LMKitConversation.PropertyChanged += OnCurrentLMKitConversationPropertyChanged; + + _previousScrollTop = null; + _ignoreScrollsUntilNextScrollUp = true; + IsScrolledToEnd = true; + + // Awaiting for the component to be rendered before scrolling to bottom. + _hasPendingScrollToEnd = true; + } + } + + private async void OnConversationMessagesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + await UpdateUIAsync(forceRedraw: true); + + await ScrollToEnd(); + + var latestMessage = ViewModel.ConversationListViewModel.CurrentConversation!.Messages.LastOrDefault(); + + if (latestMessage != null && latestMessage.Sender == MessageSender.Assistant && latestMessage.MessageInProgress) + { + _latestAssistantResponse = latestMessage; + _autoScrolling = true; + latestMessage.PropertyChanged += OnLatestAssistantMessagePropertyChanged; + } + } + + private void OnTextGenerationCompleted(object? sender, ConversationViewModel.TextGenerationCompletedEventArgs e) + { + if (e.Exception != null && + e.Status == LMKitRequestStatus.GenericError) + { + Snackbar.Clear(); + Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + Snackbar.Add($"Text generation failed unexpectedly:\n{e.Exception.Message}", Severity.Error); + } + + UpdateUIAsync(); + } + + private async void OnLatestAssistantResponseProgressed() + { + if (_autoScrolling) + { + await ScrollToEnd(); + } + } + + private async Task Resized(ResizeEventArgs args) + { + if (IsScrolledToEnd) + { + await ScrollToEnd(); + } + else + { + await CheckIsScrolledToEnd(); + } + } + + private async Task ScrollToEnd(bool smooth = false) + { + IsScrolledToEnd = true; + _ignoreScrollsUntilNextScrollUp = true; + + await JS.InvokeVoidAsync("scrollToEnd", smooth); + } + + private async Task CheckIsScrolledToEnd() + { + var viewHeight = await JS.InvokeAsync("getConversationViewHeight"); + var chatContentHeight = await JS.InvokeAsync("getScrollHeight"); + + var value = chatContentHeight - viewHeight - _scrollTop; + IsScrolledToEnd = Math.Abs(chatContentHeight - viewHeight - _scrollTop) < 5 || chatContentHeight <= viewHeight; + } + + private async Task OnScrollToEndButtonClicked() + { + await ScrollToEnd(true); + } + + + private int CalculateUsagePercentage(int used, int total) + { + if (total == 0) return 0; + return (int)(100 * used / total); + } + + + [JSInvokable] + public async Task OnChatScrolled(double scrollTop) + { + _scrollTop = scrollTop; + + bool shouldCheckIsScrolledToEnd = true; + bool? isScrollUp = null; + + if (_previousScrollTop != null) + { + isScrollUp = scrollTop < _previousScrollTop; + + if (isScrollUp.Value && _autoScrolling) + { + _autoScrolling = false; + } + } + + if (_ignoreScrollsUntilNextScrollUp) + { + if (isScrollUp == null || !isScrollUp.Value) + { + shouldCheckIsScrolledToEnd = false; + } + else + { + _ignoreScrollsUntilNextScrollUp = false; + } + } + + if (shouldCheckIsScrolledToEnd) + { + await CheckIsScrolledToEnd(); + } + + _previousScrollTop = _scrollTop; + } + + private void OnConversationItemSelected(ConversationViewModel conversationViewModel) + { + if (conversationViewModel != ViewModel.ConversationListViewModel.CurrentConversation) + { + ViewModel.ConversationListViewModel.CurrentConversation = conversationViewModel; + } + } + + + private void OnConversationItemDeleteClicked(ConversationViewModel conversationViewModel) + { + Task.Run(async () => await ViewModel.ConversationListViewModel.DeleteConversation(conversationViewModel)); + } +} diff --git a/LM-Kit-Maestro/UI/Razor/Components/Chat.razor.css b/LM-Kit-Maestro/UI/Razor/Components/Chat.razor.css index f60d0a49..48591465 100644 --- a/LM-Kit-Maestro/UI/Razor/Components/Chat.razor.css +++ b/LM-Kit-Maestro/UI/Razor/Components/Chat.razor.css @@ -1,41 +1,29 @@ #chat-container { height: 100%; width: 100%; - display: grid; - grid-template-rows: 1fr auto; - /*padding-inline: 8px;*/ - /*padding-block: 8px;*/ - /*position: fixed;*/ - /* width: auto; - left: 0; - right: 0;*/ -} - -#conversation-container { - background-color: transparent; + display: flex +} + +#chat-body { display: flex; flex-direction: column; - position: relative; + height: 100%; + width: 100%; +} + +#conversation-content { overflow-y: auto; + height: 100%; + width: 100%; scrollbar-gutter: both-edges stable; + margin: auto; + display: flex; + flex-direction: column; } - #conversation-container::-webkit-scrollbar { - width: 10px; - } - - #conversation-container::-webkit-scrollbar-thumb { - background-color: var(--OutlineVariant); - border-radius: 0; - border: 10px solid transparent; - } - - #conversation-container::-webkit-scrollbar-track { - background-color: transparent; - } - - #conversation-container::-webkit-scrollbar-thumb:hover { - background-color: var(--Outline); + #chat-messages { + display: flex; + flex-direction: column; } #bottom-space { @@ -46,7 +34,10 @@ align-items: center; text-align: center; padding: 0; - margin-block: 8px; +} + +#conversation-list { + padding-inline: 12px; } .chat-element { @@ -77,23 +68,30 @@ body.mac .chat-element { justify-content: center; } -#message-list { - margin: auto; +#empty-conversation { + height: 100%; +/* gap: 32px; display: flex; - flex-direction: column; + align-items: center; + height: 100%; + justify-content: center; + background-color: red; + margin-inline: 0 auto; + width: 100%; + text-align: center;*/ } -#empty-conversation { - flex-direction: column; - gap: 32px; - display: flex; +#user-input{ + padding-inline: 10px; } -#chat-info { +#chat-messages-bottom-space { position: sticky; display: flex; - bottom: 8px; - height: 100px; + height: 48px; + bottom: 0; + margin-top: -48px; + align-self: center; } .scroll-to-end-button { @@ -108,14 +106,6 @@ body.mac .chat-element { color: var(--OnSurface); } -body .windows .scroll-to-end-button { - margin-top: -32px; /* Setting negative margin to the height of the button so that it does not add height to parent containers. */ -} - -body.mac .scroll-to-end-button { - margin-top: -40px; /* Setting negative margin to the height of the button so that it does not add height to parent containers. */ -} - .vertical-stack { gap: 8px; display: flex; @@ -136,3 +126,24 @@ body.mac .scroll-to-end-button { border-color: var(--Surface4); color: var(--Outline); } + +.sidebar-hide { + display: none; +} + +.sidebar { + overflow-y: auto; + background-color: var(--Surface); + width: 300px; + min-width: 300px; + max-width: 300px; + height: 100%; +} + +body.windows .chat-element { + max-width: 800px; +} + +body.mac .chat-element { + max-width: 1000px; +} \ No newline at end of file diff --git a/LM-Kit-Maestro/UI/Razor/Components/ChatMessage.razor.css b/LM-Kit-Maestro/UI/Razor/Components/ChatMessage.razor.css index dcc70cf5..8fa6440d 100644 --- a/LM-Kit-Maestro/UI/Razor/Components/ChatMessage.razor.css +++ b/LM-Kit-Maestro/UI/Razor/Components/ChatMessage.razor.css @@ -18,9 +18,6 @@ max-width: 75%; align-self: flex-end; } -.user-message-container:hover .user-message-content { -} - .message-content { padding-inline: 16px; @@ -36,17 +33,6 @@ border-radius: 32px; } -.assistant-message-content { -} - -.show { - visibility: visible; -} - -.hide { - visibility: hidden; -} - .chat-action-button { align-self: center; border-radius: 50%; diff --git a/LM-Kit-Maestro/UI/Razor/Components/ChatSettings.razor b/LM-Kit-Maestro/UI/Razor/Components/ChatSettings.razor new file mode 100644 index 00000000..8fcc3f1e --- /dev/null +++ b/LM-Kit-Maestro/UI/Razor/Components/ChatSettings.razor @@ -0,0 +1,7 @@ +

+ Setting 1 +

+ +

+ Setting 2 +

diff --git a/LM-Kit-Maestro/UI/Razor/Components/ConversationListItem.razor b/LM-Kit-Maestro/UI/Razor/Components/ConversationListItem.razor new file mode 100644 index 00000000..f118ec6a --- /dev/null +++ b/LM-Kit-Maestro/UI/Razor/Components/ConversationListItem.razor @@ -0,0 +1,42 @@ +@inherits ComponentBase + +
+ + + +
+
+ +
+ + +
+ + Rename + + + + Select + + + + Delete + +
+
+ + +
+
diff --git a/LM-Kit-Maestro/UI/Razor/Components/ConversationListItem.razor.cs b/LM-Kit-Maestro/UI/Razor/Components/ConversationListItem.razor.cs new file mode 100644 index 00000000..7dd8670e --- /dev/null +++ b/LM-Kit-Maestro/UI/Razor/Components/ConversationListItem.razor.cs @@ -0,0 +1,89 @@ +using LMKit.Maestro.ViewModels; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using System.Diagnostics; + +namespace LMKit.Maestro.UI.Razor.Components; + +public partial class ConversationListItem : ComponentBase +{ + [Parameter] public EventCallback OnSelect { get; set; } + [Parameter] public EventCallback OnDelete { get; set; } + [Parameter] public required ConversationViewModel ViewModel { get; set; } + [Parameter] public bool IsSelected { get; set; } + + private MudBlazor.MudTextField? ItemTitleRef; + + public string Title { get; private set; } = ""; + + + protected override void OnParametersSet() + { + base.OnParametersSet(); + + if (!ViewModel.IsRenaming) + { + Title = ViewModel.Title; + IsSelected = ViewModel.IsSelected; + } + } + + private void OnShowMoreClicked() + { + ViewModel.IsShowingActionPopup = true; + } + + private void OnClickOutsideShowMore() + { + ViewModel.IsShowingActionPopup = false; + } + + private async void OnRenameClicked() + { + ViewModel.IsShowingActionPopup = false; + ViewModel.IsRenaming = true; + + await ItemTitleRef!.FocusAsync(); + } + + private void OnSelected() + { + ViewModel.IsShowingActionPopup = false; + OnSelect.InvokeAsync(ViewModel); + } + + private void OnDeleteClicked() + { + ViewModel.IsShowingActionPopup = false; + OnDelete.InvokeAsync(ViewModel); + } + + private void OnTitleFocusOut(Microsoft.AspNetCore.Components.Web.FocusEventArgs e) + { + ValidateTitle(); + } + + private void OnKeyPressed(KeyboardEventArgs e) + { + Trace.WriteLine(Title); + + if (e.Key == "Enter") + { + ValidateTitle(); + } + } + + private void ValidateTitle() + { + if (!string.IsNullOrWhiteSpace(Title)) + { + ViewModel!.Title = Title.TrimStart().TrimEnd(); + } + else + { + Title = ViewModel.Title; + } + + ViewModel!.IsRenaming = false; + } +} diff --git a/LM-Kit-Maestro/UI/Razor/Components/ConversationListItem.razor.css b/LM-Kit-Maestro/UI/Razor/Components/ConversationListItem.razor.css new file mode 100644 index 00000000..4d5722d4 --- /dev/null +++ b/LM-Kit-Maestro/UI/Razor/Components/ConversationListItem.razor.css @@ -0,0 +1,69 @@ +#item-container { + margin-block: 8px; + border-radius: 4px; + border: 0 solid transparent; + display: flex; + flex-direction: row; + justify-content: space-between; + padding-block: 12px; + padding-inline: 8px; + cursor: pointer !important; +} + + #item-container:hover { + background-color: var(--Surface15); + } + + #item-container.item-selected:hover { + background-color: var(--Surface2); + } + + #item-container:hover #show-more-button { + visibility: visible; + } + + #item-container.item-selected { + background-color: var(--Surface2); + } + +::deep .mud-button-label { + text-transform: initial !important; +} + +::deep .mud-input { + cursor: pointer !important; +} + +::deep .mud-input-slot { + cursor: pointer !important; +} + +::deep .mud-icon-button:hover { + color: var(--OnSurface) !important; + background-color: transparent !important; +} + +/*::deep.item-actions{ + background-color: var(--Primary); + text-transform: initial !important; + color: red !important; +}*/ + +/* Unable to override mud css. todo: find a way to do so. */ +/*#show-more-button .mud-icon-button { + margin-left: 12px; + color: var(--Outline) !important; +} + +#show-more-button{ + background-color: pink !important; +} + +#show-more-button:hover .mud-icon-button { + color: var(--OnSurface) !important; +} + +*/ +/*.mud-icon-button-label { + color: red !important; +}*/ diff --git a/LM-Kit-Maestro/UI/Razor/Components/Translation.razor b/LM-Kit-Maestro/UI/Razor/Components/Translation.razor index 65d939ec..e53f4440 100644 --- a/LM-Kit-Maestro/UI/Razor/Components/Translation.razor +++ b/LM-Kit-Maestro/UI/Razor/Components/Translation.razor @@ -66,7 +66,7 @@
-
+
diff --git a/LM-Kit-Maestro/UI/Razor/Components/UserInput.razor.css b/LM-Kit-Maestro/UI/Razor/Components/UserInput.razor.css index a2e1c5a9..edfd5859 100644 --- a/LM-Kit-Maestro/UI/Razor/Components/UserInput.razor.css +++ b/LM-Kit-Maestro/UI/Razor/Components/UserInput.razor.css @@ -78,24 +78,6 @@ body.mac #input-text { border-color: var(--Primary); } -#input-text::-webkit-scrollbar { - width: 10px; -} - -#input-text::-webkit-scrollbar-thumb { - background-color: var(--OutlineVariant); - border-radius: 0; - border: 10px solid transparent; -} - -#input-text::-webkit-scrollbar-track { - background-color: transparent; -} - -#input-text::-webkit-scrollbar-thumb:hover { - background-color: var(--Outline); -} - .send-button { position: absolute; right: 0; diff --git a/LM-Kit-Maestro/ViewModels/ConversationListViewModel.cs b/LM-Kit-Maestro/ViewModels/ConversationListViewModel.cs index 77e8ba1d..08fdac14 100644 --- a/LM-Kit-Maestro/ViewModels/ConversationListViewModel.cs +++ b/LM-Kit-Maestro/ViewModels/ConversationListViewModel.cs @@ -2,7 +2,10 @@ using LMKit.Maestro.Data; using LMKit.Maestro.Models; using Microsoft.Extensions.Logging; +using CommunityToolkit.Mvvm.ComponentModel; using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; namespace LMKit.Maestro.ViewModels { @@ -16,6 +19,7 @@ public partial class ConversationListViewModel : ViewModelBase private readonly IAppSettingsService _appSettingsService; private ConversationViewModel? _currentConversation; + public ConversationViewModel? CurrentConversation { get => _currentConversation; @@ -37,10 +41,14 @@ public ConversationViewModel? CurrentConversation } } + public event PropertyChangedEventHandler? ConversationPropertyChanged; - public ObservableCollection Conversations { get; } = new ObservableCollection(); + public ObservableCollection Conversations { get; } = + new ObservableCollection(); - public ConversationListViewModel(IMainThread mainThread, IPopupService popupService, ILogger logger, IMaestroDatabase database, LMKitService lmKitService, IAppSettingsService appSettingsService) + public ConversationListViewModel(IMainThread mainThread, IPopupService popupService, + ILogger logger, IMaestroDatabase database, LMKitService lmKitService, + IAppSettingsService appSettingsService) { _mainThread = mainThread; _logger = logger; @@ -48,6 +56,8 @@ public ConversationListViewModel(IMainThread mainThread, IPopupService popupServ _database = database; _lmKitService = lmKitService; _appSettingsService = appSettingsService; + + Conversations.CollectionChanged += OnConversationCollectionChanged; } public async Task LoadConversationLogs() @@ -67,7 +77,8 @@ public async Task LoadConversationLogs() foreach (var conversation in conversations) { - ConversationViewModel conversationViewModel = new ConversationViewModel(_popupService, _lmKitService, _database, conversation); + ConversationViewModel conversationViewModel = + new ConversationViewModel(_popupService, _lmKitService, _database, conversation); if (conversation.ChatHistoryData != null) { @@ -131,5 +142,28 @@ public async Task DeleteConversation(ConversationViewModel conversationViewModel CurrentConversation = Conversations.First(); } } + + private void OnConversationCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + foreach (var item in e.NewItems) + { + ((ConversationViewModel)item).PropertyChanged += OnConversationPropertyChanged; + } + } + else if (e.Action == NotifyCollectionChangedAction.Remove) + { + foreach (var item in e.OldItems) + { + (item as ConversationViewModel).PropertyChanged -= OnConversationPropertyChanged; + } + } + } + + private void OnConversationPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + ConversationPropertyChanged?.Invoke(sender, e); + } } } \ No newline at end of file diff --git a/LM-Kit-Maestro/ViewModels/Pages/ChatPageViewModel.cs b/LM-Kit-Maestro/ViewModels/Pages/ChatPageViewModel.cs index 0ae24165..31c695f2 100644 --- a/LM-Kit-Maestro/ViewModels/Pages/ChatPageViewModel.cs +++ b/LM-Kit-Maestro/ViewModels/Pages/ChatPageViewModel.cs @@ -1,4 +1,5 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using System.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LMKit.Maestro.Data; using Microsoft.Extensions.Logging; @@ -23,7 +24,7 @@ public partial class ChatPageViewModel : PageViewModelBase [ObservableProperty] private SettingsViewModel _settingsViewModel; - public LMKitService LMKitService { get; } + public LMKitService LmKitService { get; } public ConversationListViewModel ConversationListViewModel { get; } public ModelListViewModel ModelListViewModel { get; } @@ -37,7 +38,7 @@ public ChatPageViewModel(INavigationService navigationService, IPopupService pop ModelListViewModel = modelListViewModel; _database = database; _llmFileManager = llmFileManager; - LMKitService = lmKitService; + LmKitService = lmKitService; SettingsViewModel = settingsViewModel; ConversationListViewModel.Conversations.CollectionChanged += OnConversationListChanged; @@ -75,15 +76,6 @@ public async Task DeleteConversation(ConversationViewModel conversationViewModel ConversationListViewModel.CurrentConversation = ConversationListViewModel.Conversations.First(); } } - - private void InitializeCurrentConversation() - { - if (ConversationListViewModel.Conversations.Count == 0) - { - StartNewConversation(); - } - } - private void OnConversationListChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add && ConversationListViewModel.Conversations.Count == 1) diff --git a/LM-Kit-Maestro/wwwroot/css/lmkitmaestro.css b/LM-Kit-Maestro/wwwroot/css/lmkitmaestro.css index cd5da8ff..eb90304f 100644 --- a/LM-Kit-Maestro/wwwroot/css/lmkitmaestro.css +++ b/LM-Kit-Maestro/wwwroot/css/lmkitmaestro.css @@ -18,9 +18,9 @@ } }*/ - body.windows .small-text { - font-size: 12px; - } + body.windows .small-text { + font-size: 12px; + } body.mac .small-text { font-size: 14px; @@ -242,6 +242,28 @@ body.windows .primary-button { display: inline-block; } -.hidden { +.hide { visibility: hidden; } + +.show { + visibility: visible; +} + +::-webkit-scrollbar { + width: 10px; +} + +::-webkit-scrollbar-thumb { + background-color: var(--OutlineVariant); + border-radius: 0; + border: 10px solid transparent; +} + +::-webkit-scrollbar-track { + background-color: transparent; +} + +::-webkit-scrollbar-thumb:hover { + background-color: var(--Outline); +} diff --git a/LM-Kit-Maestro/wwwroot/js/lmkitmaestro.js b/LM-Kit-Maestro/wwwroot/js/lmkitmaestro.js index 46731704..bf5b9281 100644 --- a/LM-Kit-Maestro/wwwroot/js/lmkitmaestro.js +++ b/LM-Kit-Maestro/wwwroot/js/lmkitmaestro.js @@ -21,34 +21,37 @@ document.addEventListener("DOMContentLoaded", function () { Chat */ function initializeScrollHandler(dotNetHelper) { - const container = document.getElementById('conversation-container'); + const container = document.getElementById('conversation-content'); container.addEventListener('scroll', () => { - dotNetHelper.invokeMethodAsync('OnConversationContainerScrolled', container.scrollTop); + dotNetHelper.invokeMethodAsync('OnChatScrolled', container.scrollTop); }); } function getScrollHeight() { - const element = document.getElementById('conversation-container'); + const element = document.getElementById('conversation-content'); return element.scrollHeight; }; function getConversationViewHeight() { - const element = document.getElementById('conversation-container'); + const element = document.getElementById('conversation-content'); return element.clientHeight; }; function setUserInputFocus() { - const element = document.getElementById('input-text'); + const element = document.getElementById('conversation-content'); element.focus(); } function scrollToEnd(smooth) { - const container = document.getElementById('conversation-container'); - container.scrollTo({ - top: container.scrollHeight, - behavior: smooth ? 'smooth' : 'auto' - }); + const element = document.getElementById('conversation-content'); + + if (element != null) { + element.scrollTo({ + top: element.scrollHeight, + behavior: smooth ? 'smooth' : 'auto' + }); + } }