feat: add basic chat interface with MudBlazor and propose responses API integration
Install MudBlazor 9.2.0, replace Bootstrap layout with MudLayout/MudAppBar, create Chat.razor with message list, text input, auto-scroll, and hardcoded responses. Add ChatMessage shared model. Remove template pages (Counter, Weather), move health check to /health. Include OpenSpec change artifacts for the upcoming wire-responses-api work. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.14" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.14" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.4" />
|
||||
<PackageReference Include="MudBlazor" Version="9.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
@* MainLayout.razor -- The root layout component for the application.
|
||||
@* MainLayout.razor -- The root layout component using MudBlazor.
|
||||
|
||||
In Blazor, layout components wrap page content. Every routed page (@page)
|
||||
is rendered inside the layout's @Body placeholder. This is similar to
|
||||
_Layout.cshtml in MVC or master pages in Web Forms.
|
||||
MudBlazor requires three providers in the component tree for its components to work:
|
||||
- MudThemeProvider: Supplies the Material Design theme (colors, typography, spacing)
|
||||
- MudPopoverProvider: Manages popover/dropdown positioning (used by MudSelect, MudMenu, etc.)
|
||||
- MudDialogProvider: Enables the dialog service to render modal dialogs
|
||||
|
||||
Phase 1 uses a minimal layout -- just centered content with padding.
|
||||
Later phases will add a sidebar for conversation management.
|
||||
The layout uses MudLayout + MudAppBar + MudMainContent to create a standard
|
||||
Material Design app shell. MudMainContent automatically accounts for the AppBar
|
||||
height so page content doesn't render underneath it.
|
||||
*@
|
||||
|
||||
@* @inherits LayoutComponentBase makes this a layout component.
|
||||
LayoutComponentBase provides the Body property, which is a RenderFragment
|
||||
containing the routed page's content. Without this base class, @Body
|
||||
would not be available. *@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<main>
|
||||
@* @Body is where the routed page content renders.
|
||||
When the user navigates to "/", the Home.razor component's markup
|
||||
appears here. When they navigate to another @page, that component
|
||||
renders here instead. The layout stays the same -- only @Body changes. *@
|
||||
@Body
|
||||
</main>
|
||||
<MudThemeProvider />
|
||||
<MudPopoverProvider />
|
||||
<MudDialogProvider />
|
||||
|
||||
<MudLayout>
|
||||
@* MudAppBar provides the top application bar. Dense reduces its height.
|
||||
The fixed position keeps it visible while scrolling. *@
|
||||
<MudAppBar Elevation="1" Dense="true">
|
||||
<MudText Typo="Typo.h6">Chat Agent</MudText>
|
||||
</MudAppBar>
|
||||
|
||||
@* MudMainContent renders the routed page content (same role as @Body in plain Blazor).
|
||||
It automatically adds top padding to clear the AppBar. *@
|
||||
<MudMainContent>
|
||||
@Body
|
||||
</MudMainContent>
|
||||
</MudLayout>
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">ChatAgent.Client</a>
|
||||
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
|
||||
<nav class="nav flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="counter">
|
||||
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="weather">
|
||||
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool collapseNavMenu = true;
|
||||
|
||||
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
|
||||
|
||||
private void ToggleNavMenu()
|
||||
{
|
||||
collapseNavMenu = !collapseNavMenu;
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
.navbar-toggler {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
min-height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a {
|
||||
color: #d7d7d7;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep a:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
164
src/ChatAgent.Client/Pages/Chat.razor
Normal file
164
src/ChatAgent.Client/Pages/Chat.razor
Normal file
@@ -0,0 +1,164 @@
|
||||
@* Chat.razor -- The main chat interface.
|
||||
|
||||
This is the primary page of the application, mapped to the root route "/".
|
||||
It displays a vertically scrolling message list and a text input at the bottom,
|
||||
styled after ChatGPT/Gemini.
|
||||
|
||||
Key Blazor concepts demonstrated:
|
||||
- @page routing (this component owns "/")
|
||||
- Two-way binding with @bind-Value on MudTextField
|
||||
- Event handling with @onclick and OnKeyDown
|
||||
- List rendering with @foreach over a List<T>
|
||||
- StateHasChanged() for manual re-render triggers
|
||||
- IJSRuntime for calling JavaScript (auto-scroll)
|
||||
- Conditional CSS classes based on data (user vs assistant styling)
|
||||
*@
|
||||
|
||||
@page "/"
|
||||
|
||||
@* IJSRuntime lets us call JavaScript from C#. We use it to scroll the message
|
||||
container to the bottom after adding a new message, because Blazor has no
|
||||
built-in scroll API. *@
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<PageTitle>Chat Agent</PageTitle>
|
||||
|
||||
@* Chat container: uses flexbox to fill available height.
|
||||
The message area grows to fill space; the input stays pinned at the bottom. *@
|
||||
<div class="chat-container">
|
||||
|
||||
@* Message list: scrollable area that grows to fill available space.
|
||||
The @ref directive captures a reference to this DOM element so we can
|
||||
scroll it programmatically via JavaScript interop. *@
|
||||
<div class="message-list" @ref="_messageListRef">
|
||||
@if (_messages.Count == 0)
|
||||
{
|
||||
@* Empty state shown before any messages are sent *@
|
||||
<div class="empty-state">
|
||||
<MudText Typo="Typo.h5" Align="Align.Center" Class="mb-2"
|
||||
Style="color: var(--mud-palette-text-secondary);">
|
||||
Chat Agent
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2" Align="Align.Center"
|
||||
Style="color: var(--mud-palette-text-disabled);">
|
||||
Type a message to get started
|
||||
</MudText>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@* Render each message as a MudPaper card.
|
||||
@foreach iterates the list; Blazor re-renders this block when _messages changes.
|
||||
The CSS class changes based on Role to align user messages right, assistant left. *@
|
||||
@foreach (var message in _messages)
|
||||
{
|
||||
<div class="message-row @(message.Role == "user" ? "message-user" : "message-assistant")">
|
||||
<MudPaper Class="@($"message-bubble {(message.Role == "user" ? "bubble-user" : "bubble-assistant")}")"
|
||||
Elevation="0">
|
||||
<MudText Typo="Typo.body1">@message.Content</MudText>
|
||||
</MudPaper>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@* Input area: pinned at the bottom of the chat container.
|
||||
MudTextField with an Adornment provides the send button inside the text field,
|
||||
similar to ChatGPT's input design. *@
|
||||
<div class="input-area">
|
||||
<MudTextField @bind-Value="_userInput"
|
||||
Placeholder="Type a message..."
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.End"
|
||||
AdornmentIcon="@Icons.Material.Filled.Send"
|
||||
AdornmentColor="Color.Primary"
|
||||
OnAdornmentClick="SendMessage"
|
||||
OnKeyDown="HandleKeyDown"
|
||||
Immediate="true"
|
||||
FullWidth="true"
|
||||
AutoFocus="true" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
// The conversation messages, displayed in the message list.
|
||||
// Using a simple List<T> since we only add to the end — no complex state management needed.
|
||||
private List<ChatMessage> _messages = new();
|
||||
|
||||
// The current text in the input field. Bound two-way via @bind-Value.
|
||||
private string _userInput = string.Empty;
|
||||
|
||||
// DOM reference to the message list div, used for auto-scrolling via JS interop.
|
||||
private ElementReference _messageListRef;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Enter key press to submit the message.
|
||||
/// KeyboardEventArgs gives us the key that was pressed.
|
||||
/// </summary>
|
||||
private async Task HandleKeyDown(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Key == "Enter" && !e.ShiftKey)
|
||||
{
|
||||
await SendMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the user's message and appends a hardcoded assistant response.
|
||||
/// In future phases, this will call the API instead of using a hardcoded reply.
|
||||
/// </summary>
|
||||
private async Task SendMessage()
|
||||
{
|
||||
// Block empty or whitespace-only submissions
|
||||
if (string.IsNullOrWhiteSpace(_userInput))
|
||||
return;
|
||||
|
||||
// Add the user's message
|
||||
_messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = _userInput.Trim(),
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
|
||||
// Clear the input field
|
||||
_userInput = string.Empty;
|
||||
|
||||
// Add a hardcoded assistant response.
|
||||
// This is the stub that will be replaced with an API call in the next phase.
|
||||
_messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "assistant",
|
||||
Content = "This is a placeholder response. AI integration coming soon!",
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
|
||||
// StateHasChanged() tells Blazor to re-render this component.
|
||||
// It's needed here because we modified _messages after the initial render cycle.
|
||||
// Without this call, the new messages wouldn't appear until the next UI event.
|
||||
StateHasChanged();
|
||||
|
||||
// Auto-scroll to the bottom after rendering the new messages.
|
||||
// We use a small delay to ensure the DOM has updated before scrolling.
|
||||
await Task.Delay(50);
|
||||
await ScrollToBottom();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scrolls the message list to the bottom using JavaScript interop.
|
||||
/// Blazor has no built-in scroll API, so we call a tiny JS snippet directly.
|
||||
/// InvokeVoidAsync calls a JS function that returns nothing (void).
|
||||
/// </summary>
|
||||
private async Task ScrollToBottom()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JS.InvokeVoidAsync("eval",
|
||||
"document.querySelector('.message-list').scrollTop = document.querySelector('.message-list').scrollHeight");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore scroll errors — non-critical UI enhancement
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/ChatAgent.Client/Pages/Chat.razor.css
Normal file
79
src/ChatAgent.Client/Pages/Chat.razor.css
Normal file
@@ -0,0 +1,79 @@
|
||||
/* Chat.razor.css -- Scoped styles for the chat interface.
|
||||
*
|
||||
* Blazor CSS isolation: this file is automatically scoped to Chat.razor.
|
||||
* Styles here only apply to elements rendered by this component, preventing
|
||||
* conflicts with other pages. The build system adds a unique attribute
|
||||
* (e.g., b-abc123) to both the CSS selectors and the rendered HTML.
|
||||
*
|
||||
* ::deep is needed for styles that target child component markup (like MudPaper)
|
||||
* because those elements are rendered by MudBlazor, not directly by this component.
|
||||
*/
|
||||
|
||||
/* Chat container: flexbox column that fills the viewport below the AppBar.
|
||||
* The message-list grows to fill available space; input-area stays at the bottom. */
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 48px); /* 48px = MudAppBar Dense height */
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Scrollable message area */
|
||||
.message-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem 1rem 0.5rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Empty state centered in the message area */
|
||||
.empty-state {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Message row: controls horizontal alignment */
|
||||
.message-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.message-user {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.message-assistant {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* Message bubbles — ::deep is required because MudPaper renders its own elements */
|
||||
::deep .message-bubble {
|
||||
max-width: 75%;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 1rem;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
::deep .bubble-user {
|
||||
background-color: var(--mud-palette-primary);
|
||||
color: white;
|
||||
border-bottom-right-radius: 0.25rem;
|
||||
}
|
||||
|
||||
::deep .bubble-assistant {
|
||||
background-color: var(--mud-palette-surface);
|
||||
border: 1px solid var(--mud-palette-lines-default);
|
||||
border-bottom-left-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/* Input area pinned at the bottom */
|
||||
.input-area {
|
||||
padding: 0.75rem 1rem 1rem 1rem;
|
||||
border-top: 1px solid var(--mud-palette-lines-default);
|
||||
background-color: var(--mud-palette-background);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
@page "/counter"
|
||||
|
||||
<PageTitle>Counter</PageTitle>
|
||||
|
||||
<h1>Counter</h1>
|
||||
|
||||
<p role="status">Current count: @currentCount</p>
|
||||
|
||||
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
|
||||
|
||||
@code {
|
||||
private int currentCount = 0;
|
||||
|
||||
private void IncrementCount()
|
||||
{
|
||||
currentCount++;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
@* Home.razor -- The landing page for ChatAgent.
|
||||
@* Home.razor -- The health check page for ChatAgent.
|
||||
|
||||
@page "/" maps this component to the root URL. When a user navigates to "/",
|
||||
the Blazor router renders this component inside MainLayout's @Body placeholder.
|
||||
@page "/health" maps this component to the /health URL. Chat.razor now owns "/".
|
||||
The Blazor router renders this component inside MainLayout's @Body placeholder.
|
||||
|
||||
This page demonstrates the health check round-trip:
|
||||
1. On load, it calls the API's /api/health endpoint via ChatApiClient
|
||||
@@ -10,8 +10,8 @@
|
||||
*@
|
||||
|
||||
@* @page directive maps this component to a URL route.
|
||||
"/" means this is the default/home page. *@
|
||||
@page "/"
|
||||
"/health" provides access to the health check page (Chat.razor now owns "/"). *@
|
||||
@page "/health"
|
||||
|
||||
@* Import the service and model namespaces for this component.
|
||||
These could also be in _Imports.razor for global access. *@
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
@page "/weather"
|
||||
@inject HttpClient Http
|
||||
|
||||
<PageTitle>Weather</PageTitle>
|
||||
|
||||
<h1>Weather</h1>
|
||||
|
||||
<p>This component demonstrates fetching data from the server.</p>
|
||||
|
||||
@if (forecasts == null)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th aria-label="Temperature in Celsius">Temp. (C)</th>
|
||||
<th aria-label="Temperature in Farenheit">Temp. (F)</th>
|
||||
<th>Summary</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var forecast in forecasts)
|
||||
{
|
||||
<tr>
|
||||
<td>@forecast.Date.ToShortDateString()</td>
|
||||
<td>@forecast.TemperatureC</td>
|
||||
<td>@forecast.TemperatureF</td>
|
||||
<td>@forecast.Summary</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@code {
|
||||
private WeatherForecast[]? forecasts;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
|
||||
}
|
||||
|
||||
public class WeatherForecast
|
||||
{
|
||||
public DateOnly Date { get; set; }
|
||||
|
||||
public int TemperatureC { get; set; }
|
||||
|
||||
public string? Summary { get; set; }
|
||||
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using MudBlazor.Services;
|
||||
using ChatAgent.Client;
|
||||
using ChatAgent.Client.Services;
|
||||
|
||||
@@ -38,6 +39,10 @@ var apiBaseUrl = isHttps
|
||||
? builder.Configuration["ApiBaseUrl_Https"] ?? "https://localhost:7100"
|
||||
: builder.Configuration["ApiBaseUrl_Http"] ?? "http://localhost:7000";
|
||||
|
||||
// AddMudServices registers MudBlazor's internal services (snackbar, dialog, popover, etc.)
|
||||
// into the DI container. This is required before any MudBlazor component will work.
|
||||
builder.Services.AddMudServices();
|
||||
|
||||
// AddHttpClient<ChatApiClient> registers a typed HttpClient using IHttpClientFactory.
|
||||
// IHttpClientFactory manages the underlying HttpMessageHandler lifetime to prevent
|
||||
// socket exhaustion (a common problem with raw HttpClient in long-running apps).
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using MudBlazor
|
||||
@using ChatAgent.Client
|
||||
@using ChatAgent.Client.Layout
|
||||
@using ChatAgent.Client.Services
|
||||
|
||||
@@ -1,53 +1,11 @@
|
||||
/* app.css -- Application styles for ChatAgent (Phase 1).
|
||||
/* app.css -- Application-wide styles for ChatAgent.
|
||||
*
|
||||
* Phase 1 uses plain HTML/CSS (D-10) with a light theme (D-11).
|
||||
* MudBlazor will be introduced in Phase 5 for UI polish.
|
||||
* These styles provide a clean, minimal appearance for the health check page.
|
||||
* MudBlazor handles most styling via its component library.
|
||||
* This file contains only:
|
||||
* - Blazor framework styles (error UI, loading progress) that must stay
|
||||
* - Global overrides if needed
|
||||
*/
|
||||
|
||||
html, body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #ffffff;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.health-status {
|
||||
padding: 1rem;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.health-status p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #d32f2f;
|
||||
padding: 1rem;
|
||||
border: 1px solid #d32f2f;
|
||||
border-radius: 8px;
|
||||
background-color: #fce4ec;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: #666666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Blazor error UI -- shown when an unhandled exception occurs.
|
||||
* This is built into the Blazor template's index.html and should be kept. */
|
||||
#blazor-error-ui {
|
||||
|
||||
@@ -1,32 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ChatAgent.Client</title>
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="css/app.css" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<link href="ChatAgent.Client.styles.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<svg class="loading-progress">
|
||||
<circle r="40%" cx="50%" cy="50%" />
|
||||
<circle r="40%" cx="50%" cy="50%" />
|
||||
</svg>
|
||||
<div class="loading-progress-text"></div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="." class="reload">Reload</a>
|
||||
<span class="dismiss">🗙</span>
|
||||
</div>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Chat Agent</title>
|
||||
<base href="/" />
|
||||
|
||||
<!-- MudBlazor CSS — provides all component styles and Material Design baseline -->
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
||||
|
||||
<link rel="stylesheet" href="css/app.css" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<link href="ChatAgent.Client.styles.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<svg class="loading-progress">
|
||||
<circle r="40%" cx="50%" cy="50%" />
|
||||
<circle r="40%" cx="50%" cy="50%" />
|
||||
</svg>
|
||||
<div class="loading-progress-text"></div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="." class="reload">Reload</a>
|
||||
<span class="dismiss">🗙</span>
|
||||
</div>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
|
||||
<!-- MudBlazor JS — required for popover positioning, scroll handling, etc. -->
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
31
src/ChatAgent.Shared/Models/ChatMessage.cs
Normal file
31
src/ChatAgent.Shared/Models/ChatMessage.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// ChatMessage.cs -- Shared DTO representing a single message in a conversation.
|
||||
//
|
||||
// This model lives in ChatAgent.Shared so both the WASM client and the API can use it.
|
||||
// In Phase 1, messages only exist in-memory on the client. Later phases will serialize
|
||||
// these to JSON files on the server for persistence.
|
||||
|
||||
namespace ChatAgent.Shared.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a single chat message with a sender role, text content, and timestamp.
|
||||
/// Role is "user" for messages the human typed, "assistant" for AI (or hardcoded) replies.
|
||||
/// </summary>
|
||||
public class ChatMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Who sent the message: "user" or "assistant".
|
||||
/// Uses string rather than enum so it matches the OpenAI API's role format directly.
|
||||
/// </summary>
|
||||
public string Role { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The text content of the message.
|
||||
/// </summary>
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// When the message was created (UTC).
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user