跳转到正文

Blazor 前端

本导读介绍位于零售分析 API 前端的 Blazor WebAssembly 客户端。你将了解它如何连接 API、将聊天响应流式写入消息列表、持久化本地设置,并使模型选择器与实时模型列表保持一致。

应用结构与端口

前端是位于 AgentHQDemo.Web 下的 Blazor WebAssembly 应用。

在整个仓库中,这两个服务都使用显式的 --urls启动,从而覆盖启动配置文件:

dotnet run --project src/AgentOrchestrator/AgentHQDemo.Web --urls "http://localhost:5051"

因此,文档、图表和实验都将 5051 用于 UI,将 5050 用于 API。⚠️ 仓库中提交的启动配置文件默认使用 不同的 端口——Web 项目使用 5240,API 使用 5167——因此,如果运行时未指定 --urls (或在 IDE 中按 F5),服务将改用这些端口,UI 的默认 API 基址 http://localhost:5050 将不再匹配。请按文档所示传入 --urls ,或者设置 ApiBaseUrl 使其匹配。

AgentHQDemo.Web/Program.cs 配置 API 基址:

var apiBase = builder.Configuration["ApiBaseUrl"] ?? "http://localhost:5050";
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(apiBase) });

API 在 AgentHQDemo.Api/Program.cs 中启用 CORS,并采用允许任意源、方法和请求头的默认策略。这样,在演示期间,从本地开发 URL 提供的 WebAssembly 应用就可以调用已配置的 API 基址。

Home.razor

Home.razor 是主聊天页面,负责管理以下页面状态:

  • Messages:按顺序排列的聊天记录。
  • SelectedModel:当前选择的 Copilot 模型。
  • Models:模型 ID 到显示名称的字典。
  • IsDarkTheme:当前主题标志。
  • IsStreaming:助手响应是否正在进行。

没有消息时,页面会显示欢迎面板和 SuggestionChips。出现消息后,页面会使用 Message 组件渲染每条消息。

流式状态与滚动

Home.razor.SendMessage 添加并存储用户消息,然后追加一个空的助手占位消息。当 IsStreaming 为 true 时,输入框会被禁用;随着数据块从 ChatService.StreamChatAsync 到达,助手消息会不断更新。

页面使用 Timer 以大约每秒 20 帧的频率批量更新 UI,而不是针对每个词元都执行渲染。渲染后,页面会调用 JavaScript 辅助函数,将消息容器滚动到底部并高亮代码块。

本地存储持久化

StorageService 封装了 Blazored.LocalStorage,并存储三个本地值:

用途
chat_messages 持久化的 ChatMessage 聊天记录。
selected_model 下次加载页面时恢复的模型。
theme 已保存的 darklight 主题。

Home.razor.OnInitializedAsync 会在获取模型列表之前加载这三个值。

模型选择器

Header.razor 渲染模型选择器。它接收实时 Models 字典,并将所选选项绑定到 SelectedModel

<select id="model-select" @bind="SelectedModel" @bind:after="OnModelChanged">

当用户更改选择时, Header.OnModelChanged 会调用 SelectedModelChanged 回调。Home.razor.OnModelChanged 更新本地状态,并通过 StorageService.SetSelectedModelAsync 保存新模型。

获取实时可用模型

ChatService.GetModelsAsync 从 API 加载模型元数据:

var models = await _http.GetFromJsonAsync<List<ApiModel>>("/api/chat/models");

如果 API 至少返回一个模型,服务会将其转换为 Header.razor 使用的 Dictionary<string, string>。如果 API 无法访问或返回不可用数据,则回退到 ChatService.AvailableModels,即用于离线容错的静态目录。

防止使用过时的已保存模型

本地存储中保存的模型可能已不再对当前登录账户可用。 Home.razor.OnInitializedAsync 会在获取实时模型后处理这种情况:

if (!Models.ContainsKey(SelectedModel))
{
    SelectedModel = Models.ContainsKey("claude-haiku-4.5")
        ? "claude-haiku-4.5"
        : Models.Keys.First();
}

随后,页面会持久化替代模型。这样可避免旧的 localStorage 值导致聊天请求使用 API 已不再提供的模型。

辅助组件

ChatInput 提供文本区域和发送按钮。单击按钮或按下不带 Shift 的 Enter 键时会发送消息;加载期间禁用输入;移除空白消息;并在首次渲染后聚焦输入框。

Message 渲染用户和助手消息。助手内容为空时显示输入指示器;非空内容使用 Markdig 从 Markdown 渲染,并标记代码块以供 JavaScript 执行高亮。

SuggestionChips 显示预定义的零售分析提示。选择一个提示标签后,会通过与手动输入相同的 Home.razor.SendMessage 路径发送该提示。