From 20bf98b131a713070c59a270eac7baa14872b110 Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Tue, 28 Jul 2026 15:42:07 +0200 Subject: [PATCH] Improve copy-to-clipboard UX in Ask.razor Refactored the copy button to use per-message state, displaying a checkmark icon and "Copied!" tooltip when a message is copied. Simplified state management, removed unused references, and updated the Reset method to clear copied state. Enhanced Ask.razor.css to minimize button chrome and improve interactive states for a cleaner UI. --- .../Components/Pages/Ask.razor | 44 ++++++++++--------- .../Components/Pages/Ask.razor.css | 23 ++++++++++ 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/SqlDatabaseVectorSearch/Components/Pages/Ask.razor b/SqlDatabaseVectorSearch/Components/Pages/Ask.razor index c581879..c160568 100644 --- a/SqlDatabaseVectorSearch/Components/Pages/Ask.razor +++ b/SqlDatabaseVectorSearch/Components/Pages/Ask.razor @@ -77,10 +77,10 @@ -
- - - @@ -128,8 +128,6 @@ @code { - private Button askButton = default!; - private Button resetButton = default!; private ElementReference askInput = default!; private ElementReference chat = default!; @@ -137,10 +135,9 @@ private string? question; private Guid conversationId = Guid.NewGuid(); - private bool isAsking = false; + private bool isAsking; - private bool showCopyConfirmation = false; - private string toolTipText = "Copy to clipboard"; + private Message? copiedMessage; protected override async Task OnAfterRenderAsync(bool firstRender) { @@ -222,9 +219,10 @@ } finally { - await EnsureMessageIsVisibleAsync(); - isAsking = false; + await InvokeAsync(StateHasChanged); + + await EnsureMessageIsVisibleAsync(); } } @@ -232,25 +230,29 @@ { question = null; conversationId = Guid.NewGuid(); + copiedMessage = null; messages.Clear(); } - private async Task CopyToClipboardAsync(string text) + private async Task CopyToClipboardAsync(Message message) { - if (text is null) + if (string.IsNullOrEmpty(message.Text)) + { return; + } - await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", text); + await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", message.Text); - showCopyConfirmation = true; - toolTipText = "Copied!"; + copiedMessage = message; await InvokeAsync(StateHasChanged); await Task.Delay(3000); // Shows the checkmark for 3 seconds - toolTipText = "Copy to clipboard"; - showCopyConfirmation = false; - await InvokeAsync(StateHasChanged); + if (copiedMessage == message) + { + copiedMessage = null; + await InvokeAsync(StateHasChanged); + } } private static string FormatTokenUsage(TokenUsageResponse? tokenUsageResponse) diff --git a/SqlDatabaseVectorSearch/Components/Pages/Ask.razor.css b/SqlDatabaseVectorSearch/Components/Pages/Ask.razor.css index 4ea9bdb..f1bdafb 100644 --- a/SqlDatabaseVectorSearch/Components/Pages/Ask.razor.css +++ b/SqlDatabaseVectorSearch/Components/Pages/Ask.razor.css @@ -119,6 +119,29 @@ input[type="checkbox"] + label { z-index: 10; } +/* The copy button only shows an icon, so the default button chrome is removed and just the pointer cursor is kept. */ +.copy-button ::deep .btn { + background-color: transparent; + border-color: transparent; + box-shadow: none; + color: var(--bs-secondary-color); +} + + .copy-button ::deep .btn:hover { + background-color: transparent; + border-color: transparent; + color: var(--bs-body-color); + } + + .copy-button ::deep .btn:focus, + .copy-button ::deep .btn:focus-visible, + .copy-button ::deep .btn:active { + background-color: transparent; + border-color: transparent; + box-shadow: none; + outline: none; + } + .btn-clipboard { line-height: 1; color: var(--bs-body-color);