From 0b3b291089fe2821c9eec4f3314d586f77fee88e Mon Sep 17 00:00:00 2001 From: "hualin.zhu" Date: Sun, 21 Dec 2025 09:59:05 +0800 Subject: [PATCH 1/5] Remove ActiveUserSession from product form dialog #922 The ActiveUserSession component was removed from the section of ProductFormDialog.razor, so it is no longer rendered within the product form dialog. This streamlines the dialog content and removes session tracking from this component. --- src/Server.UI/Pages/Products/Components/ProductFormDialog.razor | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Server.UI/Pages/Products/Components/ProductFormDialog.razor b/src/Server.UI/Pages/Products/Components/ProductFormDialog.razor index 5a9f697df..a3a0ba86f 100644 --- a/src/Server.UI/Pages/Products/Components/ProductFormDialog.razor +++ b/src/Server.UI/Pages/Products/Components/ProductFormDialog.razor @@ -15,7 +15,6 @@ - From 1fe957eba0e35198c8929056826db46778e15d1f Mon Sep 17 00:00:00 2001 From: "hualin.zhu" Date: Fri, 2 Jan 2026 08:58:29 +0800 Subject: [PATCH 2/5] Redesign ReconnectModal with auto-retry and modern UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completely overhaul the ReconnectModal component: - Replace legacy HTML, MudBlazor buttons, and animations with a glassmorphic modal featuring a status icon, pulse effect, and progress bar. - Remove all manual retry/resume buttons; reconnection is now fully automatic. - Inline and rewrite JS logic to probe server health and only reload when the server is truly ready (HTTP 200), improving reliability during restarts. - Use MutationObserver to trigger modal and probing based on Blazor connection state. - Rewrite CSS for a modern, responsive, and dark mode–friendly design. - Delete obsolete ReconnectModal.razor.js file. - Result: a more robust, user-friendly, and visually appealing reconnection experience. --- src/Server.UI/Components/ReconnectModal.razor | 467 +++++++++--------- .../Components/ReconnectModal.razor.js | 63 --- 2 files changed, 244 insertions(+), 286 deletions(-) delete mode 100644 src/Server.UI/Components/ReconnectModal.razor.js diff --git a/src/Server.UI/Components/ReconnectModal.razor b/src/Server.UI/Components/ReconnectModal.razor index b314c23ae..e76556579 100644 --- a/src/Server.UI/Components/ReconnectModal.razor +++ b/src/Server.UI/Components/ReconnectModal.razor @@ -1,287 +1,308 @@ - + + + -
-
- -@code { - -} + \ No newline at end of file diff --git a/src/Server.UI/Components/ReconnectModal.razor.js b/src/Server.UI/Components/ReconnectModal.razor.js deleted file mode 100644 index 916a39930..000000000 --- a/src/Server.UI/Components/ReconnectModal.razor.js +++ /dev/null @@ -1,63 +0,0 @@ -// Set up event handlers -const reconnectModal = document.getElementById("components-reconnect-modal"); -reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged); - -const retryButton = document.getElementById("components-reconnect-button"); -retryButton.addEventListener("click", retry); - -const resumeButton = document.getElementById("components-resume-button"); -resumeButton.addEventListener("click", resume); - -function handleReconnectStateChanged(event) { - if (event.detail.state === "show") { - reconnectModal.showModal(); - } else if (event.detail.state === "hide") { - reconnectModal.close(); - } else if (event.detail.state === "failed") { - document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); - } else if (event.detail.state === "rejected") { - location.reload(); - } -} - -async function retry() { - document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible); - - try { - // Reconnect will asynchronously return: - // - true to mean success - // - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID) - // - exception to mean we didn't reach the server (this can be sync or async) - const successful = await Blazor.reconnect(); - if (!successful) { - // We have been able to reach the server, but the circuit is no longer available. - // We'll reload the page so the user can continue using the app as quickly as possible. - const resumeSuccessful = await Blazor.resumeCircuit(); - if (!resumeSuccessful) { - location.reload(); - } else { - reconnectModal.close(); - } - } - } catch (err) { - // We got an exception, server is currently unavailable - document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); - } -} - -async function resume() { - try { - const successful = await Blazor.resumeCircuit(); - if (!successful) { - location.reload(); - } - } catch { - location.reload(); - } -} - -async function retryWhenDocumentBecomesVisible() { - if (document.visibilityState === "visible") { - await retry(); - } -} \ No newline at end of file From 622354c07af5a17ad15527f71563a7618bf9a0ed Mon Sep 17 00:00:00 2001 From: "hualin.zhu" Date: Fri, 2 Jan 2026 16:41:41 +0800 Subject: [PATCH 3/5] Refactor image deletion to use confirmation dialog helper Simplified the DeleteImage method by replacing manual dialog creation and result handling with DialogServiceHelper .ShowConfirmationDialogAsync. This centralizes confirmation logic and improves code readability and maintainability. --- .../Components/ProductFormDialog.razor | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/Server.UI/Pages/Products/Components/ProductFormDialog.razor b/src/Server.UI/Pages/Products/Components/ProductFormDialog.razor index a3a0ba86f..dd5ae57f0 100644 --- a/src/Server.UI/Pages/Products/Components/ProductFormDialog.razor +++ b/src/Server.UI/Pages/Products/Components/ProductFormDialog.razor @@ -136,18 +136,14 @@ { if (Model.Pictures != null) { - var parameters = new DialogParameters - { - { x => x.ContentText, $"{L["Are you sure you want to erase this image?"]}" } - }; - var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall, FullWidth = true }; - var dialog = await DialogService.ShowAsync($"{L["Erase imatge"]}", parameters, options); - var state = await dialog.Result; - - if (state is not null && !state.Canceled) - { - Model.Pictures.Remove(picture); - } + await DialogServiceHelper.ShowConfirmationDialogAsync( + $"{L["Erase imatge"]}", + $"{L["Are you sure you want to erase this image?"]}", + async () => + { + Model.Pictures.Remove(picture); + }); + } } From 1e0dcf7062524bd296f952bd108a4f903243813c Mon Sep 17 00:00:00 2001 From: "hualin.zhu" Date: Fri, 2 Jan 2026 19:57:14 +0800 Subject: [PATCH 4/5] Modernize theme and UI styles for improved clarity Updated Theme.cs to use modern palette and typography, aligning with shadcn/ui and Tailwind standards. Refined layout properties and removed shadows. Revised app.css for consistent font sizing, improved chip/input/table styles, and removed error boundary styling for a cleaner look. Enhances accessibility, consistency, and overall visual professionalism. --- src/Server.UI/Themes/Theme.cs | 271 +++++++++++++++--------------- src/Server.UI/wwwroot/css/app.css | 63 +++---- 2 files changed, 163 insertions(+), 171 deletions(-) diff --git a/src/Server.UI/Themes/Theme.cs b/src/Server.UI/Themes/Theme.cs index cf0887472..6bd8666cb 100644 --- a/src/Server.UI/Themes/Theme.cs +++ b/src/Server.UI/Themes/Theme.cs @@ -8,209 +8,212 @@ public static MudTheme ApplicationTheme() { PaletteLight = new PaletteLight { - Primary = "#5052ba", - Secondary = "#7D7D7D", - Success = "#0CAD39", - Info = "#4099f3", - Warning = "#f0c42b", - Error = "rgba(244,67,54,1)", + Primary = "#0f172a", // Modern blue, professional and trustworthy + PrimaryContrastText = "#ffffff", + PrimaryDarken = "#020617", + PrimaryLighten = "#1e293b", + Secondary = "#64748b", // Neutral gray, clean and professional + SecondaryContrastText = "#ffffff", + SecondaryLighten = "#475569", + SecondaryDarken = "#94a3b8", + Success = "#10b981", // Fresh green, success + Info = "#0ea5e9", // Info blue, clear + Tertiary = "#8b5cf6", // Purple 500 + TertiaryContrastText = "#ffffff", + TertiaryDarken = "#7c3aed", // Purple 600 + TertiaryLighten = "#a78bfa", // Purple 400 + + Warning = "#f59e0b", // Amber 500 + WarningContrastText = "#92400e", // Amber 800 + WarningDarken = "#d97706", // Amber 600 + WarningLighten = "#fbbf24", // Amber 400 + + Error = "#dc2626", // Clear red, error ErrorContrastText = "#ffffff", - ErrorDarken = "rgb(242,28,13)", - ErrorLighten = "rgb(246,96,85)", - Tertiary = "#20c997", - Black = "#111", - White = "#ffffff", - AppbarBackground = "rgba(245, 245, 245, 0.8)", - AppbarText = "#424242", - Background = "#f5f5f5", - Surface = "#ffffff", + ErrorDarken = "#b91c1c", + ErrorLighten = "#ef4444", + + Black = "#020617", // Deep blue-black, more texture + White = "#ffffff", + AppbarBackground = "#f8fafc", // Very light blue-gray, modern + AppbarText = "#0a0a0a", + Background = "#f8fafc", // Very light blue-gray, modern + Surface = "#ffffff", DrawerBackground = "#ffffff", - TextPrimary = "#2E2E2E", - TextSecondary = "#6c757d", - SecondaryContrastText = "#F5F5F5", - TextDisabled = "#B0B0B0", - ActionDefault = "#80838b", - ActionDisabled = "rgba(128, 131, 139, 0.3)", - ActionDisabledBackground = "rgba(128, 131, 139, 0.12)", - Divider = "#e2e5e8", - DividerLight = "rgba(128, 131, 139, 0.15)", - TableLines = "#eff0f2", - LinesDefault = "#e2e5e8", - LinesInputs = "#e2e5e8", - + TextPrimary = "#0f172a", // Deep blue-gray, modern professional + TextSecondary = "#64748b", // Neutral gray, hierarchy + TextDisabled = "#94a3b8", // Soft gray + ActionDefault = "#262626", + ActionDisabled = "rgba(100, 116, 139, 0.4)", + ActionDisabledBackground = "rgba(100, 116, 139, 0.1)", + Divider = "#e2e8f0", // Elegant divider + DividerLight = "#f1f5f9", + TableLines = "#e2e8f0", // Table lines, elegant + LinesDefault = "#e2e8f0", + LinesInputs = "#cbd5e1", }, PaletteDark = new PaletteDark { - Primary = "#5052ba", - Secondary = "#A5A5A5", - Success = "#0CAD39", - Info = "#4099f3", - Warning = "#f0c42b", - Error = "#e02d48", - ErrorContrastText = "#ffffff", - ErrorDarken = "#e02d48", - ErrorLighten = "#ff3333", - Tertiary = "#20c997", - Black = "#000000", - White = "#ffffff", - Background = "#202124", - Surface = "#303134", - AppbarBackground = "rgba(32, 33, 36, 0.8)", - AppbarText = "rgba(255, 255, 255, 0.7)", - DrawerBackground = "#303134", - TextPrimary = "#DADADA", - TextSecondary = "#A8A8A8", - TextDisabled = "rgba(255, 255, 255, 0.3)", - SecondaryContrastText = "#D5D5D5", - ActionDefault = "#e8eaed", - ActionDisabled = "rgba(255, 255, 255, 0.26)", - ActionDisabledBackground = "rgba(255, 255, 255, 0.12)", - Divider = "#3F4452", - DividerLight = "rgba(255, 255, 255, 0.06)", - TableLines = "rgba(63, 68, 82, 0.6)", - LinesDefault = "#3F4452", - LinesInputs = "rgba(255, 255, 255, 0.3)", + Primary = "#fafafa", // shadcn/ui white primary + PrimaryContrastText = "#020817", + PrimaryDarken = "#e4e4e7", + PrimaryLighten = "#ffffff", + Secondary = "#78716c", // Neutral gray + Success = "#22c55e", // Green for success + Info = "#0ea5e9", // Sky blue for info (shadcn sky-500) + InfoDarken = "#0284c7", // Darker sky blue (shadcn sky-600) + InfoLighten = "#38bdf8", // Lighter sky blue (shadcn sky-400) + + Tertiary = "#6366f1", + TertiaryContrastText = "#fafafa", + TertiaryDarken = "#4f46e5", + TertiaryLighten = "#818cf8", + + Warning = "#f59e0b", // Orange for warning + WarningContrastText = "#fafafa", + WarningDarken = "#d97706", + WarningLighten = "#fbbf24", + + Error = "#dc2626", // Red for error + ErrorContrastText = "#fafafa", + ErrorDarken = "#b91c1c", + ErrorLighten = "#ef4444", + + Black = "#020817", + White = "#fafafa", + Background = "#0c0a09", // shadcn/ui dark background + Surface = "#171717", // Deeper surface color + AppbarBackground = "#0c0a09", + AppbarText = "#fafafa", + DrawerText = "#fafafa", + DrawerBackground = "#0c0a09", + TextPrimary = "#fafafa", // shadcn/ui white text + TextSecondary = "#a1a1aa", // Neutral gray secondary text + TextDisabled = "rgba(161, 161, 170, 0.5)", + ActionDefault = "#e5e5e5", + ActionDisabled = "rgba(161, 161, 170, 0.3)", + ActionDisabledBackground = "rgba(161, 161, 170, 0.1)", + Divider = "rgba(255, 255, 255, 0.1)", // shadcn/ui divider color + DividerLight = "rgba(161, 161, 170, 0.1)", + TableLines = "rgba(255, 255, 255, 0.1)", + LinesDefault = "rgba(255, 255, 255, 0.1)", + LinesInputs = "rgba(161, 161, 170, 0.2)", + DarkContrastText = "#020817", + SecondaryContrastText = "#fafafa", + SecondaryDarken = "#57534e", + SecondaryLighten = "#a8a29e" + }, LayoutProperties = new LayoutProperties { - AppbarHeight = "80px", - DefaultBorderRadius = "6px" + AppbarHeight = "64px", // More modern height + DefaultBorderRadius = "8px", // More modern border radius + DrawerWidthLeft = "280px", // Wider sidebar + DrawerMiniWidthRight = "260px" }, Typography = new Typography { Default = new DefaultTypography { - FontSize = ".8125rem", + FontSize = ".875rem", FontWeight = "400", - LineHeight = "1.4", + LineHeight = "1.43", LetterSpacing = "normal", - FontFamily = ["Public Sans", "Roboto", "Arial", "sans-serif"] + FontFamily = ["Inter var", "Inter", "ui-sans-serif", "system-ui", "-apple-system", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "Noto Sans", "sans-serif", "Apple Color Emoji", "Segoe UI Emoji"] }, H1 = new H1Typography { - FontSize = "2.2rem", - FontWeight = "700", - LineHeight = "2.8", - LetterSpacing = "-.01562em" + FontSize = "2.25rem", + FontWeight = "800", + LineHeight = "2.5rem", + LetterSpacing = "-0.025em" }, H2 = new H2Typography { - FontSize = "2rem", + FontSize = "1.875rem", FontWeight = "600", - LineHeight = "2.5", - LetterSpacing = "-.00833em" + LineHeight = "2.25rem", + LetterSpacing = "-0.025em" }, H3 = new H3Typography { - FontSize = "1.75rem", + FontSize = "1.5rem", FontWeight = "600", - LineHeight = "2.23", - LetterSpacing = "0" + LineHeight = "2rem", + LetterSpacing = "-0.025em" }, H4 = new H4Typography { - FontSize = "1.5rem", - FontWeight = "500", - LineHeight = "2", - LetterSpacing = ".00735em" + FontSize = "1.25rem", + FontWeight = "600", + LineHeight = "1.75rem", + LetterSpacing = "-0.025em" }, H5 = new H5Typography { - FontSize = "1.25rem", - FontWeight = "500", - LineHeight = "1.8", - LetterSpacing = "0" + FontSize = "1.125rem", + FontWeight = "600", + LineHeight = "1.75rem", + LetterSpacing = "-0.025em" }, H6 = new H6Typography { FontSize = "1rem", - FontWeight = "400", - LineHeight = "1.6", - LetterSpacing = ".0075em" + FontWeight = "600", + LineHeight = "1.25rem", + LetterSpacing = "-0.025em" }, Button = new ButtonTypography { - FontSize = ".8125rem", + FontSize = ".875rem", FontWeight = "500", - LineHeight = "1.75", - LetterSpacing = ".02857em", - TextTransform = "uppercase" + LineHeight = "1.75rem", + LetterSpacing = "normal", + TextTransform = "none" }, Subtitle1 = new Subtitle1Typography { FontSize = ".875rem", FontWeight = "400", - LineHeight = "1.5", - LetterSpacing = "normal", - FontFamily = ["Public Sans", "Roboto", "Arial", "sans-serif"] + LineHeight = "1.5rem", + LetterSpacing = ".00938em", }, Subtitle2 = new Subtitle2Typography { - FontSize = ".8125rem", + FontSize = "1rem", FontWeight = "500", - LineHeight = "1.57", + LineHeight = "1.75rem", LetterSpacing = ".00714em" }, Body1 = new Body1Typography { - FontSize = "0.8125rem", + FontSize = ".875rem", FontWeight = "400", - LineHeight = "1.5", + LineHeight = "1.5rem", LetterSpacing = ".00938em" }, Body2 = new Body2Typography { FontSize = ".75rem", - FontWeight = "300", - LineHeight = "1.43", + FontWeight = "400", + LineHeight = "1.25rem", LetterSpacing = ".01071em" }, Caption = new CaptionTypography { - FontSize = "0.625rem", + FontSize = "0.75rem", FontWeight = "400", - LineHeight = "1.5", + LineHeight = "1.5rem", LetterSpacing = ".03333em" }, Overline = new OverlineTypography { - FontSize = "0.625rem", - FontWeight = "300", - LineHeight = "2", - LetterSpacing = ".08333em" - } - }, - Shadows = new Shadow - { - Elevation = new[] - { - "none", - "0 2px 4px -1px rgba(6, 24, 44, 0.2)", - "0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12)", - "0 30px 60px rgba(0,0,0,0.12)", - "0 6px 12px -2px rgba(50,50,93,0.25),0 3px 7px -3px rgba(0,0,0,0.3)", - "0 50px 100px -20px rgba(50,50,93,0.25),0 30px 60px -30px rgba(0,0,0,0.3)", - "0px 3px 5px -1px rgba(0,0,0,0.2),0px 6px 10px 0px rgba(0,0,0,0.14),0px 1px 18px 0px rgba(0,0,0,0.12)", - "0px 4px 5px -2px rgba(0,0,0,0.2),0px 7px 10px 1px rgba(0,0,0,0.14),0px 2px 16px 1px rgba(0,0,0,0.12)", - "0px 5px 5px -3px rgba(0,0,0,0.2),0px 8px 10px 1px rgba(0,0,0,0.14),0px 3px 14px 2px rgba(0,0,0,0.12)", - "0px 5px 6px -3px rgba(0,0,0,0.2),0px 9px 12px 1px rgba(0,0,0,0.14),0px 3px 16px 2px rgba(0,0,0,0.12)", - "0px 6px 6px -3px rgba(0,0,0,0.2),0px 10px 14px 1px rgba(0,0,0,0.14),0px 4px 18px 3px rgba(0,0,0,0.12)", - "0px 6px 7px -4px rgba(0,0,0,0.2),0px 11px 15px 1px rgba(0,0,0,0.14),0px 4px 20px 3px rgba(0,0,0,0.12)", - "0px 7px 8px -4px rgba(0,0,0,0.2),0px 12px 17px 2px rgba(0,0,0,0.14),0px 5px 22px 4px rgba(0,0,0,0.12)", - "0px 7px 8px -4px rgba(0,0,0,0.2),0px 13px 19px 2px rgba(0,0,0,0.14),0px 5px 24px 4px rgba(0,0,0,0.12)", - "0px 7px 9px -4px rgba(0,0,0,0.2),0px 14px 21px 2px rgba(0,0,0,0.14),0px 5px 26px 4px rgba(0,0,0,0.12)", - "0px 8px 9px -5px rgba(0,0,0,0.2),0px 15px 22px 2px rgba(0,0,0,0.14),0px 6px 28px 5px rgba(0,0,0,0.12)", - "0px 8px 10px -5px rgba(0,0,0,0.2),0px 16px 24px 2px rgba(0,0,0,0.14),0px 6px 30px 5px rgba(0,0,0,0.12)", - "0px 8px 11px -5px rgba(0,0,0,0.2),0px 17px 26px 2px rgba(0,0,0,0.14),0px 6px 32px 5px rgba(0,0,0,0.12)", - "0px 9px 11px -5px rgba(0,0,0,0.2),0px 18px 28px 2px rgba(0,0,0,0.14),0px 7px 34px 6px rgba(0,0,0,0.12)", - "0px 9px 12px -6px rgba(0,0,0,0.2),0px 19px 29px 2px rgba(0,0,0,0.14),0px 7px 36px 6px rgba(0,0,0,0.12)", - "0px 10px 13px -6px rgba(0,0,0,0.2),0px 20px 31px 3px rgba(0,0,0,0.14),0px 8px 38px 7px rgba(0,0,0,0.12)", - "0px 10px 13px -6px rgba(0,0,0,0.2),0px 21px 33px 3px rgba(0,0,0,0.14),0px 8px 40px 7px rgba(0,0,0,0.12)", - "0px 10px 14px -6px rgba(0,0,0,0.2),0px 22px 35px 3px rgba(0,0,0,0.14),0px 8px 42px 7px rgba(0,0,0,0.12)", - "0 50px 100px -20px rgba(50, 50, 93, 0.25), 0 30px 60px -30px rgba(0, 0, 0, 0.30)", - "2.8px 2.8px 2.2px rgba(0, 0, 0, 0.02),6.7px 6.7px 5.3px rgba(0, 0, 0, 0.028),12.5px 12.5px 10px rgba(0, 0, 0, 0.035),22.3px 22.3px 17.9px rgba(0, 0, 0, 0.042),41.8px 41.8px 33.4px rgba(0, 0, 0, 0.05),100px 100px 80px rgba(0, 0, 0, 0.07)", - "0px 0px 20px 0px rgba(0, 0, 0, 0.05)" + FontSize = "0.75rem", + FontWeight = "400", + LineHeight = "1.75rem", + LetterSpacing = ".03333em", + TextTransform = "none" } } }; diff --git a/src/Server.UI/wwwroot/css/app.css b/src/Server.UI/wwwroot/css/app.css index 91ee5c6da..3f85a9804 100644 --- a/src/Server.UI/wwwroot/css/app.css +++ b/src/Server.UI/wwwroot/css/app.css @@ -1,36 +1,45 @@ -.mud-main-content { +html { + font-size: 14px; /* 默认:1rem = 14px */ +} + +.mud-main-content { height: 100%; min-height: 100vh; display: flex; } - -.mud-chip { - font-size: var(--mud-typography-default-size); + +.mud-chip.mud-chip-size-medium { + font-size: var(--mud-typography-default-size) !important; } + .mud-chip.mud-chip-size-small { - font-size: var(--mud-typography-body1-size); + font-size: var(--mud-typography-body2-size) !important; } + +.mud-chip { + font-size: var(--mud-typography-default-size); +} + .mud-tabs { background-color: var(--mud-palette-surface); } -.mud-input-control > .mud-input-control-input-container > .mud-input-label-inputcontrol { - font-size: var(--mud-typography-subtitle1-size); +.mud-input-control-margin-dense .mud-input > input.mud-input-root, +.mud-input-control-margin-dense div.mud-input-slot.mud-input-root { + font-size: 0.90em; } -.mud-input > input.mud-input-root, div.mud-input-slot.mud-input-root { - font-size: var(--mud-typography-default-size) !important; -} -.mud-input > textarea.mud-input-root { + +.mud-input-control > .mud-input-control-input-container > .mud-input-label-inputcontrol { font-size: var(--mud-typography-default-size) !important; } - .mud-simple-table table * tr > td, .mud-simple-table table * tr th { + + +.mud-simple-table table * tr > td, .mud-simple-table table * tr th { font-size: var(--mud-typography-default-size) !important; - } .mud-expand-panel .mud-expand-panel-header { font-size: var(--mud-typography-default-size) !important; - } .mud-button-year { @@ -41,19 +50,10 @@ font-size: var(--mud-typography-default-size) !important; } - -.mud-typography-subtitle2 { - font-size: var(--mud-typography-subtitle2-size); - color: var(--mud-palette-text-secondary); -} -.mud-typography-body1 { - font-size: var(--mud-typography-body1-size); +.mud-table-dense .mud-table-cell { + font-size: var(--mud-typography-body2-size) !important; } -.mud-typography-body2 { - font-size: var(--mud-typography-body2-size); - color: var(--mud-palette-text-secondary); -} .mud-button-outlined-size-small { font-size: var(--mud-typography-body2-size); @@ -61,7 +61,7 @@ .mud-grid.readonly-grid > .mud-grid-item { border-bottom: 1px solid var(--mud-palette-table-lines); - padding-bottom:2px; + padding-bottom: 2px; } .mud-nav-link { @@ -101,14 +101,3 @@ - - -.blazor-error-boundary { - background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; - padding: 1rem 1rem 1rem 3.7rem; - color: white; -} - - .blazor-error-boundary::after { - content: "An error has occurred." - } \ No newline at end of file From 51981d336783640f68c589fce195824eca7bf8d8 Mon Sep 17 00:00:00 2001 From: "hualin.zhu" Date: Fri, 16 Jan 2026 14:45:54 +0800 Subject: [PATCH 5/5] Add AI Chatbot page with OpenAI integration Introduced a new /ai/chatbot page featuring a chat UI for interacting with an AI assistant powered by OpenAI. Integrated the OpenAI .NET SDK and added configuration for API keys and model selection in appsettings.json. Updated the navigation menu to include the Chatbot, and added the OpenAI NuGet package dependency. The chat interface supports avatars, message bubbles, copy-to-clipboard, auto-scroll, and error handling. --- src/Application/Application.csproj | 8 +- src/Domain/Domain.csproj | 20 +- src/Infrastructure/Infrastructure.csproj | 8 +- src/Server.UI/Pages/AI/Chatbot.razor | 397 ++++++++++++++++++ src/Server.UI/Server.UI.csproj | 7 +- .../Services/Navigation/MenuService.cs | 7 + src/Server.UI/appsettings.json | 5 + .../Application.IntegrationTests.csproj | 2 +- .../Application.UnitTests.csproj | 2 +- .../Domain.UnitTests/Domain.UnitTests.csproj | 2 +- 10 files changed, 434 insertions(+), 24 deletions(-) create mode 100644 src/Server.UI/Pages/AI/Chatbot.razor diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index e5d9bc37d..2d2f4c99c 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -17,13 +17,13 @@ - - + + - + - + diff --git a/src/Domain/Domain.csproj b/src/Domain/Domain.csproj index b0cc30c1a..16871d85f 100644 --- a/src/Domain/Domain.csproj +++ b/src/Domain/Domain.csproj @@ -11,23 +11,23 @@ - - + + - + - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index fa9eb79e6..bb9e2047c 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -8,10 +8,10 @@ default - - - - + + + + diff --git a/src/Server.UI/Pages/AI/Chatbot.razor b/src/Server.UI/Pages/AI/Chatbot.razor new file mode 100644 index 000000000..2818e2c5a --- /dev/null +++ b/src/Server.UI/Pages/AI/Chatbot.razor @@ -0,0 +1,397 @@ +@page "/ai/chatbot" + +@using OpenAI +@using OpenAI.Chat +@using Microsoft.Extensions.Configuration + +@inject IConfiguration Configuration + + +Chatbot + + + +
+ @if (!messages.Any()) + { +
+ + Start Conversation + Chat with AI Assistant +
+ } + @foreach (var message in messages) + { +
+
+ @if (message.Role == "user") + { + + @if (!string.IsNullOrEmpty(UserProfile?.ProfilePictureDataUrl ?? "")) + { + + } + else + { + + } + + } + else + { + + + + } +
+
+ @{ + var bubbleClass = $"message-bubble {(message.Role == "user" ? "message-bubble-user" : "message-bubble-assistant")}"; + } + + @message.Content + + @if (message.Role == "assistant") + { +
+ +
+ } +
+
+ } + @if (isLoading) + { +
+
+ + + +
+
+ + + +
+
+ } +
+
+
+ +
+ + + + Send + + +
+
+ + + +@code { + // 定义本地数据模型 + private class ChatMessage + { + public string Role { get; set; } = "user"; + public string Content { get; set; } = string.Empty; + } + + private List messages = new(); + private string userInput = string.Empty; + private bool isLoading = false; + private ElementReference messagesEndRef; + private ChatClient? _chatClient; + private List _conversationHistory = new(); + + [CascadingParameter] + private UserProfile? UserProfile { get; set; } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + await ScrollToBottomAsync(); + } + + private async Task HandleKeyDown(KeyboardEventArgs e) + { + if (e.Key == "Enter" && e.CtrlKey) + { + await SendAsync(); + } + } + + private void InitializeChatClient() + { + if (_chatClient is not null) return; + + var apiKey = Configuration["AISettings:OpenAIApiKey"]; + if (string.IsNullOrEmpty(apiKey) || apiKey == "your-openai-api-key") + { + throw new InvalidOperationException("OpenAI API Key is missing or invalid in AISettings."); + } + + var modelId = Configuration["AISettings:OpenAIModel"] ?? "gpt-5-nano"; + var client = new OpenAIClient(apiKey); + _chatClient = client.GetChatClient(modelId); + + // 添加系统提示 + _conversationHistory.Add(new SystemChatMessage("You are a helpful AI assistant. Be concise and accurate in your responses.")); + } + + private async Task SendAsync() + { + if (string.IsNullOrWhiteSpace(userInput) || isLoading) + { + return; + } + + var userMessageContent = userInput.Trim(); + userInput = string.Empty; + + messages.Add(new ChatMessage + { + Role = "user", + Content = userMessageContent + }); + + StateHasChanged(); + await ScrollToBottomAsync(); + + isLoading = true; + StateHasChanged(); + + try + { + InitializeChatClient(); + + // 添加用户消息到对话历史 + _conversationHistory.Add(new UserChatMessage(userMessageContent)); + + // 调用 OpenAI API + var response = await _chatClient!.CompleteChatAsync(_conversationHistory); + var assistantResponseText = response.Value.Content[0].Text; + + if (!string.IsNullOrEmpty(assistantResponseText)) + { + // 添加助手回复到对话历史 + _conversationHistory.Add(new AssistantChatMessage(assistantResponseText)); + + messages.Add(new ChatMessage + { + Role = "assistant", + Content = assistantResponseText + }); + } + else + { + throw new Exception("No response received from the API."); + } + } + catch (Exception ex) + { + var errorMessage = $"Error: {ex.Message}"; + messages.Add(new ChatMessage + { + Role = "assistant", + Content = errorMessage + }); + Snackbar.Add(errorMessage, Severity.Error); + } + finally + { + isLoading = false; + StateHasChanged(); + await ScrollToBottomAsync(); + } + } + + private async Task CopyToClipboardAsync(string text) + { + try + { + await JS.InvokeVoidAsync("navigator.clipboard.writeText", text); + Snackbar.Add("Copied to clipboard", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"Copy failed: {ex.Message}", Severity.Error); + } + } + + private async Task ScrollToBottomAsync() + { + try + { + await JS.InvokeVoidAsync("eval", @" + var element = document.querySelector('.chat-messages-container'); + if (element) { + element.scrollTop = element.scrollHeight; + } + "); + } + catch + { + // Ignore JS interop errors during pre-rendering + } + } +} \ No newline at end of file diff --git a/src/Server.UI/Server.UI.csproj b/src/Server.UI/Server.UI.csproj index ea5218596..39d8abe42 100644 --- a/src/Server.UI/Server.UI.csproj +++ b/src/Server.UI/Server.UI.csproj @@ -15,15 +15,16 @@ default + - + - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Server.UI/Services/Navigation/MenuService.cs b/src/Server.UI/Services/Navigation/MenuService.cs index 2c7273919..4fed68734 100644 --- a/src/Server.UI/Services/Navigation/MenuService.cs +++ b/src/Server.UI/Services/Navigation/MenuService.cs @@ -42,6 +42,13 @@ public class MenuService : IMenuService } }, new() + { + Title = "Chatbot", + Icon = Icons.Material.Filled.ChatBubble, + Href ="/ai/chatbot", + PageStatus = PageStatus.Completed + }, + new() { Title = "Analytics", Roles = new[] { RoleName.Admin, RoleName.Users }, diff --git a/src/Server.UI/appsettings.json b/src/Server.UI/appsettings.json index f6145a520..ffe82fe64 100644 --- a/src/Server.UI/appsettings.json +++ b/src/Server.UI/appsettings.json @@ -8,6 +8,11 @@ //"DBProvider": "postgresql", //"ConnectionString": "Server=127.0.0.1;Database=BlazorDashboardDb;User Id=root;Password=root;Port=5432" }, + "AISettings": { + "GeminiApiKey": "your-gemini-api-key", + "OpenAIApiKey": "your-openai-api-key", + "OpenAIModel": "gpt-5-nano" + }, "Authentication": { "Microsoft": { "ClientId": "***", diff --git a/tests/Application.IntegrationTests/Application.IntegrationTests.csproj b/tests/Application.IntegrationTests/Application.IntegrationTests.csproj index 4cc383975..d0eee0467 100644 --- a/tests/Application.IntegrationTests/Application.IntegrationTests.csproj +++ b/tests/Application.IntegrationTests/Application.IntegrationTests.csproj @@ -23,7 +23,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Application.UnitTests/Application.UnitTests.csproj b/tests/Application.UnitTests/Application.UnitTests.csproj index a6a6db845..5e1014c9c 100644 --- a/tests/Application.UnitTests/Application.UnitTests.csproj +++ b/tests/Application.UnitTests/Application.UnitTests.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Domain.UnitTests/Domain.UnitTests.csproj b/tests/Domain.UnitTests/Domain.UnitTests.csproj index ed39de0ef..8d1460c0e 100644 --- a/tests/Domain.UnitTests/Domain.UnitTests.csproj +++ b/tests/Domain.UnitTests/Domain.UnitTests.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive