merge from neozhu#14
Conversation
The ActiveUserSession component was removed from the <DialogContent> 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.
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.
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.
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.
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.
There was a problem hiding this comment.
Pull request overview
This pull request merges changes from neozhu that include dependency updates, a new AI chatbot feature, significant UI/theme redesign, and component refactoring.
Changes:
- Updates NuGet packages across test projects and application layers to latest versions (EF Core 10.0.1→10.0.2, authentication libraries, etc.)
- Adds new AI chatbot feature with OpenAI integration including UI page, menu integration, and configuration settings
- Redesigns application theme with modern color palette inspired by shadcn/ui, updated typography using Inter font family, and refined spacing/layout properties
- Refactors ReconnectModal component with improved reconnection logic and modern glassmorphic UI design
- Simplifies dialog usage in ProductFormDialog using DialogServiceHelper
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Domain.UnitTests/Domain.UnitTests.csproj | Updates NUnit3TestAdapter from 6.0.0 to 6.1.0 |
| tests/Application.UnitTests/Application.UnitTests.csproj | Updates NUnit3TestAdapter from 6.0.0 to 6.1.0 |
| tests/Application.IntegrationTests/Application.IntegrationTests.csproj | Updates NUnit3TestAdapter from 6.0.0 to 6.1.0 |
| src/Server.UI/wwwroot/css/app.css | Refines CSS with updated chip sizes, font sizes, and spacing; removes blazor-error-boundary styles |
| src/Server.UI/appsettings.json | Adds AISettings configuration section with API keys for Gemini and OpenAI |
| src/Server.UI/Themes/Theme.cs | Complete theme redesign with modern color palette, updated typography using Inter font, and removal of custom shadows |
| src/Server.UI/Services/Navigation/MenuService.cs | Adds new Chatbot menu item linking to /ai/chatbot |
| src/Server.UI/Server.UI.csproj | Adds OpenAI package; updates SignalR, HotKeys, and EF Core Tools packages |
| src/Server.UI/Pages/Products/Components/ProductFormDialog.razor | Refactors dialog confirmation to use DialogServiceHelper; removes ActiveUserSession component |
| src/Server.UI/Pages/AI/Chatbot.razor | New chatbot page with OpenAI integration, message history, and modern chat UI |
| src/Server.UI/Components/ReconnectModal.razor.js | Removes separate JavaScript file (logic now inline in .razor) |
| src/Server.UI/Components/ReconnectModal.razor | Complete redesign with inline JavaScript, improved reconnection detection, and modern glassmorphic UI |
| src/Infrastructure/Infrastructure.csproj | Updates authentication packages and QuestPDF to latest versions |
| src/Domain/Domain.csproj | Updates EF Core packages from 10.0.1 to 10.0.2; updates EFCore.NamingConventions from RC to stable |
| src/Application/Application.csproj | Updates ASP.NET Core packages, Riok.Mapperly, and FusionCache to latest versions |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <script> | ||
| (function() { | ||
| if (window.reconnectModalInitialized) return; | ||
| window.reconnectModalInitialized = true; | ||
|
|
||
| #components-reconnect-modal { | ||
| background-color: var(--mud-palette-surface); | ||
| color: var(--mud-palette-text-primary); | ||
| width: 20rem; | ||
| margin: 10vh auto; | ||
| padding: 2rem; | ||
| border: 0; | ||
| border-radius: var(--mud-default-borderradius, 4px); | ||
| box-shadow: var(--mud-elevation-8); | ||
| opacity: 0; | ||
| transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete; | ||
| animation: components-reconnect-modal-fadeOutOpacity 0.5s both; | ||
| } | ||
| let serverReallyDown = false; | ||
| let checkInterval = null; | ||
|
|
||
| #components-reconnect-modal[open] { | ||
| animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s; | ||
| animation-fill-mode: both; | ||
| } | ||
| async function checkServerStatus() { | ||
| try { | ||
| // Add timestamp to prevent browser caching | ||
| const probeUrl = `/_framework/blazor.web.js?t=${new Date().getTime()}`; | ||
| console.log(`[Reconnect] Probing: ${probeUrl}`); | ||
|
|
||
| #components-reconnect-modal::backdrop { | ||
| background-color: var(--mud-palette-backdrop-background); | ||
| animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out; | ||
| opacity: 1; | ||
| } | ||
| const response = await fetch(probeUrl, { | ||
| method: 'HEAD', | ||
| cache: 'no-store' | ||
| }); | ||
|
|
||
| @@keyframes components-reconnect-modal-slideUp { | ||
| 0% { | ||
| transform: translateY(30px) scale(0.95); | ||
| console.log('[Reconnect] Response status:', response.status); | ||
|
|
||
| // === Core fix starts === | ||
| // Only reload when status code is 200 (OK). | ||
| // Nginx usually returns 502/503/504 during container restart, fetch won't throw an exception, | ||
| // so must manually check response.ok or response.status | ||
| if (response.ok) { | ||
| console.log('[Reconnect] Server is ready (200 OK). Reloading...'); | ||
| window.location.reload(); | ||
| } else { | ||
| // If it's 500, 502, 503, 504, 404, etc., it means the service hasn't fully recovered | ||
| // Throw an error to enter the catch block, keep the modal and wait for the next probe | ||
| throw new Error(`Server returned status ${response.status} (Not Ready)`); | ||
| } | ||
| // === Core fix ends === | ||
|
|
||
| } catch (error) { | ||
| // Only when reaching here, it's truly "network unreachable" or "service not ready" | ||
| console.warn('[Reconnect] Probe failed or server not ready:', error); | ||
|
|
||
| if (!serverReallyDown) { | ||
| serverReallyDown = true; | ||
| // Update the text to tell the user the server is restarting | ||
| updateStatus("Server Restarting", "Waiting for the application to start..."); | ||
|
|
||
| // Lock the modal | ||
| const modal = document.getElementById('components-reconnect-modal'); | ||
| if (modal && !modal.open) modal.showModal(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 100% { | ||
| transform: translateY(0); | ||
| function updateStatus(title, desc) { | ||
| const titleEl = document.getElementById('reconnect-title'); | ||
| const descEl = document.getElementById('reconnect-desc'); | ||
| if (titleEl) titleEl.innerText = title; | ||
| if (descEl) descEl.innerHTML = desc; | ||
| } | ||
| } | ||
|
|
||
| @@keyframes components-reconnect-modal-fadeInOpacity { | ||
| 0% { | ||
| opacity: 0; | ||
| function init() { | ||
| const modal = document.getElementById('components-reconnect-modal'); | ||
| if (!modal) { setTimeout(init, 50); return; } | ||
|
|
||
| const observer = new MutationObserver((mutations) => { | ||
| const activeClasses = ['components-reconnect-show', 'components-reconnect-failed', 'components-reconnect-rejected']; | ||
| const hasClass = activeClasses.some(c => modal.classList.contains(c)); | ||
|
|
||
| if (hasClass) { | ||
| // Blazor reports disconnection | ||
| if (!modal.open) { | ||
| modal.showModal(); | ||
| // Suggest slightly extending the probe interval to avoid excessive pressure on the server during restart | ||
| if (!checkInterval) checkInterval = setInterval(checkServerStatus, 2000); | ||
| checkServerStatus(); // Probe once immediately | ||
| } | ||
| } else { | ||
| // Blazor reports recovery | ||
| if (serverReallyDown) { | ||
| // If previously confirmed server was down, absolutely do not allow closing, wait for checkServerStatus to trigger reload | ||
| console.log('[Reconnect] Ignored fake reconnection signal. Waiting for reload.'); | ||
| if (!modal.open) modal.showModal(); | ||
| } else { | ||
| // Minor network fluctuation, allow closing | ||
| if (modal.open) modal.close(); | ||
| if (checkInterval) { clearInterval(checkInterval); checkInterval = null; } | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| observer.observe(modal, { attributes: true, attributeFilter: ['class'] }); | ||
| } | ||
|
|
||
| 100% { | ||
| opacity: 1; | ||
| if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); | ||
| else init(); | ||
| })(); |
There was a problem hiding this comment.
Chinese comments in JavaScript code. Multiple comments throughout the script are in Chinese (e.g., "添加时间戳以防止浏览器缓存", "等待应用程序启动", etc.). These should be translated to English for consistency with the rest of the codebase.
| await DialogServiceHelper.ShowConfirmationDialogAsync( | ||
| $"{L["Erase imatge"]}", | ||
| $"{L["Are you sure you want to erase this image?"]}", | ||
| async () => | ||
| { | ||
| Model.Pictures.Remove(picture); | ||
| }); |
There was a problem hiding this comment.
Inconsistent indentation. The code uses tabs for indentation starting at line 139, while the rest of the file appears to use spaces. This creates inconsistent formatting that can cause issues with code readability and version control diffs.
| await DialogServiceHelper.ShowConfirmationDialogAsync( | |
| $"{L["Erase imatge"]}", | |
| $"{L["Are you sure you want to erase this image?"]}", | |
| async () => | |
| { | |
| 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); | |
| }); |
| LineHeight = "1.75rem", | ||
| LetterSpacing = ".03333em", | ||
| TextTransform = "none" | ||
| } |
There was a problem hiding this comment.
The Shadows property has been completely removed from the theme configuration. While MudBlazor themes have default shadow values that will be used when this property is not set, this represents a significant visual change from the previous custom shadow configuration. Ensure this change is intentional and that the default MudBlazor shadows provide the desired visual appearance.
| } | |
| } | |
| }, | |
| Shadows = new Shadow() | |
| { |
| "GeminiApiKey": "your-gemini-api-key", | ||
| "OpenAIApiKey": "your-openai-api-key", |
There was a problem hiding this comment.
The configuration uses placeholder API keys that should not be committed to version control. Even though these are placeholder values, it's a security best practice to use environment variables or user secrets for API keys rather than checking them into source control. Consider using user secrets for development or documenting that these values need to be overridden via environment variables in production.
| "GeminiApiKey": "your-gemini-api-key", | |
| "OpenAIApiKey": "your-openai-api-key", | |
| // Configure these via environment variables or user secrets: | |
| // AISettings__GeminiApiKey, AISettings__OpenAIApiKey | |
| "GeminiApiKey": "", | |
| "OpenAIApiKey": "", |
| var modelId = Configuration["AISettings:OpenAIModel"] ?? "gpt-5-nano"; | ||
| var client = new OpenAIClient(apiKey); | ||
| _chatClient = client.GetChatClient(modelId); |
There was a problem hiding this comment.
The error handling only validates if the API key is the placeholder value, but doesn't validate the model name. If an invalid model name is configured, the error will only be caught in the generic catch block with a less helpful error message. Consider adding validation for the model name as well.
| var modelId = Configuration["AISettings:OpenAIModel"] ?? "gpt-5-nano"; | |
| var client = new OpenAIClient(apiKey); | |
| _chatClient = client.GetChatClient(modelId); | |
| // Validate and resolve model identifier | |
| var configuredModelId = Configuration["AISettings:OpenAIModel"]; | |
| string modelId; | |
| if (string.IsNullOrWhiteSpace(configuredModelId) || configuredModelId == "your-openai-model") | |
| { | |
| // Fall back to a known default model when configuration is missing or placeholder | |
| modelId = "gpt-5-nano"; | |
| } | |
| else | |
| { | |
| modelId = configuredModelId; | |
| } | |
| var client = new OpenAIClient(apiKey); | |
| try | |
| { | |
| _chatClient = client.GetChatClient(modelId); | |
| } | |
| catch (Exception ex) | |
| { | |
| throw new InvalidOperationException($"OpenAI model configuration is invalid. Model '{modelId}' could not be initialized.", ex); | |
| } |
| </style> | ||
|
|
||
| @code { | ||
| // 定义本地数据模型 |
There was a problem hiding this comment.
Chinese comment in code. The comment "定义本地数据模型" (Define local data model) should be translated to English for consistency with the rest of the codebase, which uses English comments throughout.
| // 定义本地数据模型 | |
| // Define local data model |
| var client = new OpenAIClient(apiKey); | ||
| _chatClient = client.GetChatClient(modelId); | ||
|
|
||
| // 添加系统提示 |
There was a problem hiding this comment.
Chinese comment in code. The comment "添加系统提示" (Add system prompt) should be translated to English for consistency with the codebase standards.
| // 添加系统提示 | |
| // Add system prompt |
| // 添加用户消息到对话历史 | ||
| _conversationHistory.Add(new UserChatMessage(userMessageContent)); | ||
|
|
||
| // 调用 OpenAI API | ||
| var response = await _chatClient!.CompleteChatAsync(_conversationHistory); | ||
| var assistantResponseText = response.Value.Content[0].Text; | ||
|
|
||
| if (!string.IsNullOrEmpty(assistantResponseText)) | ||
| { | ||
| // 添加助手回复到对话历史 |
There was a problem hiding this comment.
Chinese comment in code. The comment "添加用户消息到对话历史" (Add user message to conversation history) should be translated to English for consistency.
| // 添加用户消息到对话历史 | |
| _conversationHistory.Add(new UserChatMessage(userMessageContent)); | |
| // 调用 OpenAI API | |
| var response = await _chatClient!.CompleteChatAsync(_conversationHistory); | |
| var assistantResponseText = response.Value.Content[0].Text; | |
| if (!string.IsNullOrEmpty(assistantResponseText)) | |
| { | |
| // 添加助手回复到对话历史 | |
| // Add user message to conversation history | |
| _conversationHistory.Add(new UserChatMessage(userMessageContent)); | |
| // Call OpenAI API | |
| var response = await _chatClient!.CompleteChatAsync(_conversationHistory); | |
| var assistantResponseText = response.Value.Content[0].Text; | |
| if (!string.IsNullOrEmpty(assistantResponseText)) | |
| { | |
| // Add assistant reply to conversation history |
| public string Content { get; set; } = string.Empty; | ||
| } | ||
|
|
||
| private List<ChatMessage> messages = new(); |
There was a problem hiding this comment.
Field 'messages' can be 'readonly'.
| private List<ChatMessage> messages = new(); | |
| private readonly List<ChatMessage> messages = new(); |
| private bool isLoading = false; | ||
| private ElementReference messagesEndRef; | ||
| private ChatClient? _chatClient; | ||
| private List<OpenAI.Chat.ChatMessage> _conversationHistory = new(); |
There was a problem hiding this comment.
Field '_conversationHistory' can be 'readonly'.
| private List<OpenAI.Chat.ChatMessage> _conversationHistory = new(); | |
| private readonly List<OpenAI.Chat.ChatMessage> _conversationHistory = new(); |
merge from neozhu