Compare commits
1 Commits
7a5c22593a
...
chatendpoi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
711df97ce9 |
96
ARCHITECTURE.md
Normal file
96
ARCHITECTURE.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Architecture Decisions
|
||||
|
||||
## Blazor Hosting Model: Server vs WebAssembly
|
||||
|
||||
### How They Differ
|
||||
|
||||
| Aspect | Blazor Server | Blazor WASM |
|
||||
|--------|--------------|-------------|
|
||||
| Code runs | On the server | In the browser |
|
||||
| UI updates | Via SignalR WebSocket (server pushes diffs) | Locally in browser (no round-trip) |
|
||||
| Calling backend services | Direct — code is already server-side | Needs HTTP calls if accessing server resources |
|
||||
| Offline capable | No (requires persistent connection) | Yes |
|
||||
| Startup speed | Fast | Slower (downloads .NET runtime to browser) |
|
||||
|
||||
### Decision: Blazor WASM
|
||||
|
||||
This project uses **Blazor WebAssembly (standalone)** with a separate ASP.NET Core Web API backend.
|
||||
|
||||
## When Do You Need a Separate API Project?
|
||||
|
||||
There are two distinct concerns that are easy to conflate:
|
||||
|
||||
### 1. Consuming an External API (e.g. OpenAI)
|
||||
|
||||
Both Blazor Server and WASM can call external APIs directly from C# — no controller needed. The difference is **where that code executes**:
|
||||
|
||||
- **Blazor Server**: Code runs on the server. A service class calls OpenAI directly. API keys live safely in server memory. No controller, no exposed endpoint required.
|
||||
- **Blazor WASM**: Code runs in the browser. You *can* call OpenAI directly, but any **API key would be embedded in the browser download** — anyone could inspect it via browser dev tools. This is the primary reason to add a backend proxy.
|
||||
|
||||
### 2. Exposing an API Endpoint (e.g. ChatAgent.Api)
|
||||
|
||||
This is a separate concern. You expose an API endpoint when:
|
||||
|
||||
- Another application needs to communicate with your chat system
|
||||
- You want a REST API for mobile clients, scripts, or integrations
|
||||
- You need a server-side proxy to protect secrets from the browser (the WASM case above)
|
||||
|
||||
**Key insight:** You don't need an API endpoint to *use* an external service. You only need one in WASM to **keep secrets out of the browser**.
|
||||
|
||||
### 3. Component Updates Do Not Require an API
|
||||
|
||||
In both hosting models, Blazor components update themselves locally:
|
||||
|
||||
- Components hold state in fields/properties
|
||||
- Calling `StateHasChanged()` triggers a re-render
|
||||
- Components can call injected C# services directly
|
||||
- No HTTP round-trip is needed for UI updates
|
||||
|
||||
For example, a chat component that echoes "success msg!" back needs no API at all — a simple injected service handles it entirely within the client project.
|
||||
|
||||
## API Key Management Scenarios
|
||||
|
||||
The need for an API project in WASM depends on how secrets are managed:
|
||||
|
||||
| Scenario | WASM needs API project? | Why |
|
||||
|----------|------------------------|-----|
|
||||
| Raw API key in config | Yes | To keep the key out of browser-downloadable code |
|
||||
| Azure Key Vault | Yes | Browser sandbox cannot access Key Vault or managed identity |
|
||||
| API Management gateway (Azure APIM) with token auth | No | WASM calls the gateway directly; gateway handles auth via managed identity |
|
||||
| Blazor Server (any scenario) | No | Code is already server-side; secrets never leave the server |
|
||||
|
||||
### Enterprise Pattern: API Gateway
|
||||
|
||||
In enterprise environments, a common pattern avoids the need for a custom API proxy entirely:
|
||||
|
||||
1. An **API Management gateway** (e.g. Azure APIM) sits in front of the external service (e.g. OpenAI)
|
||||
2. The gateway authenticates via managed identity and handles secret retrieval
|
||||
3. The gateway exposes a public endpoint requiring only a subscription key or OAuth token
|
||||
4. WASM calls the gateway directly — no secrets in the browser, no custom API project needed
|
||||
|
||||
The "proxy" becomes infrastructure rather than application code.
|
||||
|
||||
## Current Project Structure
|
||||
|
||||
```
|
||||
ChatAgent.sln
|
||||
src/
|
||||
ChatAgent.Client/ -- Blazor WASM (standalone)
|
||||
Pages/ -- Routable page components
|
||||
Layout/ -- MainLayout, NavMenu
|
||||
Services/ -- Client-side services (e.g. ChatApiClient)
|
||||
Program.cs -- Client entry point, DI registration
|
||||
ChatAgent.Api/ -- ASP.NET Core Web API (backend proxy)
|
||||
Controllers/ -- API controllers (e.g. HealthController)
|
||||
Program.cs -- Server entry point, middleware config
|
||||
ChatAgent.Shared/ -- Models shared between Client and Api
|
||||
Models/ -- DTOs (e.g. HealthResponse)
|
||||
```
|
||||
|
||||
### Why Three Projects?
|
||||
|
||||
- **ChatAgent.Client**: The Blazor WASM app running in the browser
|
||||
- **ChatAgent.Api**: Exists to proxy requests that require server-side secrets (e.g. future OpenAI calls). Not needed for basic component interactions.
|
||||
- **ChatAgent.Shared**: Models referenced by both Client and Api, avoiding duplication
|
||||
|
||||
For the initial "echo success" phase, only ChatAgent.Client is actively used. The Api and Shared projects exist to support future integration with external services that require secret management.
|
||||
156
CLAUDE.md
156
CLAUDE.md
@@ -1,147 +1,43 @@
|
||||
<!-- GSD:project-start source:PROJECT.md -->
|
||||
## Project
|
||||
|
||||
**Chat Agent WebApp**
|
||||
|
||||
A personal AI chat web application built with Blazor WebAssembly and the OpenAI GPT API. Users send messages, receive streaming AI responses rendered as markdown, and manage multiple persistent conversations. The project doubles as an incremental learning journey — each phase introduces one concept with well-documented, explained code, making it suitable as a Blazor tutorial for a developer experienced in C# but new to the framework.
|
||||
A personal AI chat web application built with Blazor WebAssembly and MudBlazor. Users send messages through a ChatGPT-style interface and receive responses from a backend service. The project is an incremental learning journey — each phase introduces one concept at a time, making it suitable for a C# developer experienced in backend work but new to web application frameworks.
|
||||
|
||||
**Core Value:** A working, well-understood AI chat interface — every line of code is intentional and explained, so the builder learns Blazor patterns while shipping a real product.
|
||||
**Core Value:** A working chat interface where every line of code is intentional and explained, so the builder learns Blazor patterns while shipping a real product.
|
||||
|
||||
**Current Phase:** Echo — the backend returns "success msg!" for every user message. No external API integration yet.
|
||||
|
||||
### Constraints
|
||||
|
||||
- **Tech stack**: .NET / C# / Blazor WebAssembly — non-negotiable
|
||||
- **LLM provider**: OpenAI GPT API
|
||||
- **Storage**: JSON files on local disk
|
||||
- **Architecture**: WASM client + backend API (API key stays server-side)
|
||||
- **Code style**: Every Blazor concept introduced must have inline comments explaining what it does and why
|
||||
<!-- GSD:project-end -->
|
||||
- **Tech stack**: C# / Blazor WebAssembly — non-negotiable
|
||||
- **Hosting model**: Blazor WASM (standalone) with separate ASP.NET Core Web API backend
|
||||
- **UI library**: MudBlazor
|
||||
- **Code style**: Simple, well-documented. Every Blazor concept introduced must have inline comments explaining what it does, why it's done that way, and what idiomatic alternatives exist
|
||||
|
||||
<!-- GSD:stack-start source:research/STACK.md -->
|
||||
## Technology Stack
|
||||
|
||||
## Recommended Stack
|
||||
### Core Technologies
|
||||
| Technology | Version | Purpose | Why Recommended |
|
||||
|------------|---------|---------|-----------------|
|
||||
| .NET 9 SDK | 9.x (latest patch) | Runtime, tooling, SDK | LTS-adjacent, stable, .NET 10 is in preview — stay on 9 for a tutorial project targeting a stable foundation |
|
||||
| Blazor WebAssembly Standalone | .NET 9 | Client SPA running in-browser | Non-negotiable per project constraints; client-side execution with no server round-trip for UI |
|
||||
| ASP.NET Core Web API | .NET 9 | Backend proxy for OpenAI calls | Required to keep the OpenAI API key server-side; WASM cannot access secrets directly |
|
||||
| C# 13 | Included with .NET 9 | Application language | Included in .NET 9 SDK; no separate install needed |
|
||||
### OpenAI Integration
|
||||
| Library | Version | Purpose | Why Recommended |
|
||||
|---------|---------|---------|-----------------|
|
||||
| `OpenAI` (official) | 2.9.1 | OpenAI API client with streaming | The official OpenAI-published .NET library; supports `CompleteChatStreamingAsync()` returning `AsyncCollectionResult<StreamingChatCompletionUpdate>` via `await foreach`; stable release as of 2026-03-02 |
|
||||
### Markdown Rendering
|
||||
| Library | Version | Purpose | Why Recommended |
|
||||
|---------|---------|---------|-----------------|
|
||||
| `Markdig` | 1.1.1 | Parse markdown text to HTML | The de facto standard markdown processor for .NET; CommonMark-compliant, fast, extensible, targets .NET Standard 2.0 so works in WASM; used by Microsoft and Syncfusion as the underlying engine |
|
||||
### UI Component Library
|
||||
| Library | Version | Purpose | Why Recommended |
|
||||
|---------|---------|---------|-----------------|
|
||||
| `MudBlazor` | 9.2.0 | Material Design component library | Full .NET 9 support confirmed; pure C# with minimal JavaScript; comprehensive chat-friendly components (MudTextField, MudPaper, MudScrollToBottom, MudList); large community; no per-seat licensing |
|
||||
### JSON Storage (Server-side)
|
||||
| Technology | Version | Purpose | Why Recommended |
|
||||
|------------|---------|---------|-----------------|
|
||||
| `System.Text.Json` | Built into .NET 9 | Serialize/deserialize conversation history | Built-in, no extra dependency; `JsonSerializerOptions` with `WriteIndented = true` for human-readable files; async file I/O via `File.ReadAllTextAsync` / `File.WriteAllTextAsync` |
|
||||
## Supporting Libraries
|
||||
| Library | Version | Purpose | When to Use |
|
||||
|---------|---------|---------|-------------|
|
||||
| `Microsoft.Extensions.AI` (abstractions) | 9.x preview | Optional AI abstraction layer | Skip for v1 — adds indirection before the core chat pattern is understood. Relevant for v2 when adding multi-provider support |
|
||||
| `Blazored.LocalStorage` | latest | Browser local storage | Not needed for this project — persistence is on the server via JSON files, not the browser |
|
||||
| `System.Net.ServerSentEvents` | Built into .NET 9 | SSE parser for streaming | Used automatically by the `OpenAI` library on the server; no direct usage needed |
|
||||
## Development Tools
|
||||
| Tool | Purpose | Notes |
|
||||
|------|---------|-------|
|
||||
| Visual Studio 2022 (v17.12+) | IDE with Blazor hot reload | Recommended for tutorial builder; full Blazor debugging, component preview, and hot reload support |
|
||||
| VS Code + C# Dev Kit | Lighter-weight alternative | Works well; use `dotnet watch` for hot reload |
|
||||
| `dotnet watch run` | Hot reload during development | Run in both Client and Server project directories simultaneously |
|
||||
| `dotnet-dev-certs` | HTTPS dev certificate | Required for local HTTPS; run `dotnet dev-certs https --trust` once |
|
||||
## Installation
|
||||
# Create solution
|
||||
# Create Blazor WASM client (standalone)
|
||||
# Create ASP.NET Core Web API backend
|
||||
# Install OpenAI SDK in the API project
|
||||
# Install Markdig in the Client project
|
||||
## Alternatives Considered
|
||||
| Recommended | Alternative | When to Use Alternative |
|
||||
|-------------|-------------|-------------------------|
|
||||
| `OpenAI` 2.9.1 (official) | `OpenAI-DotNet` 8.8.8 (unofficial) | Never — the official package is now stable and maintained by OpenAI directly |
|
||||
| `OpenAI` 2.9.1 (official) | `Azure.AI.OpenAI` 2.1.0 | When targeting Azure OpenAI Service specifically (e.g., enterprise, EU data residency, private endpoints) — overkill for this project |
|
||||
| `Markdig` | `CommonMark.NET` | Only if strict CommonMark compliance matters more than extensions; Markdig is a superset and the ecosystem standard |
|
||||
| `MudBlazor` | Radzen Blazor | Radzen is fine; choose it if you already know it; MudBlazor has more learning resources |
|
||||
| `MudBlazor` | Telerik UI for Blazor | Telerik requires a paid license; not appropriate for a personal tool |
|
||||
| Standalone WASM + separate Web API | Blazor Web App template (unified) | Use the unified Blazor Web App template when you want mixed Server+WASM render modes on a single project; overkill for this project and obscures the WASM-specific patterns the tutorial aims to teach |
|
||||
| JSON flat files (server-side) | SQLite via EF Core | SQLite is a better choice at scale; JSON is simpler for single-user personal tools and avoids introducing a migration workflow |
|
||||
## What NOT to Use
|
||||
| Avoid | Why | Use Instead |
|
||||
|-------|-----|-------------|
|
||||
| `OpenAI-DotNet` (unofficial) | Different API surface, not maintained by OpenAI, version numbers create confusion | Official `OpenAI` NuGet package |
|
||||
| `Microsoft.SemanticKernel` | Adds significant abstraction and dependency weight for a tutorial; streaming works but is complex to explain | Direct `OpenAI` SDK calls; add SK in v2 when orchestration is needed |
|
||||
| JavaScript `EventSource` API via JSInterop for streaming | Blazor WASM has `SetBrowserResponseStreamingEnabled` which avoids JS interop; adding JSInterop for streaming increases complexity significantly | `HttpCompletionOption.ResponseHeadersRead` + `SetBrowserResponseStreamingEnabled(true)` in the HTTP handler |
|
||||
| `Newtonsoft.Json` | Unnecessary dependency; `System.Text.Json` is built into .NET 9 and is faster; Newtonsoft was the pre-.NET Core standard | `System.Text.Json` (built-in) |
|
||||
| `Blazored.LocalStorage` for persistence | Browser storage is limited (~5MB), cleared by users, and not suitable for chat history of any meaningful length; also exposes all data client-side | Server-side JSON file storage via the Web API |
|
||||
| AOT compilation during learning phase | Dramatically increases build times; not needed until production optimization is a concern; confusing to introduce in a tutorial | Default IL interpretation; add AOT opt-in note in the final phase |
|
||||
## Stack Patterns by Variant
|
||||
- Backend streams OpenAI tokens as `text/event-stream` (SSE) or `application/x-ndjson`
|
||||
- Client uses `SetBrowserResponseStreamingEnabled(true)` on `HttpRequestMessage`
|
||||
- Client reads with `HttpCompletionOption.ResponseHeadersRead` and iterates the stream
|
||||
- Trigger `StateHasChanged()` in the component after each token to update the UI
|
||||
- Define a `ConversationRepository` service on the API that reads/writes from a configurable base path
|
||||
- Register as `Singleton` (not `Scoped`) since there is only one user and file access must be serialized
|
||||
- Use `SemaphoreSlim(1,1)` to prevent concurrent write conflicts even in single-user mode
|
||||
- Use `Markdig.Markdown.ToHtml(text, pipeline)` where `pipeline` is built with `MarkdownPipelineBuilder` enabling extensions (e.g., `UseAutoLinks()`, `UseEmojiAndSmiley()`)
|
||||
- Render the HTML string using `@((MarkupString)html)` inside a `<div class="markdown-body">` element
|
||||
- Apply CSS (GitHub Markdown CSS or custom) scoped to `.markdown-body` for code blocks and tables
|
||||
## Version Compatibility
|
||||
| Package | Compatible With | Notes |
|
||||
|---------|-----------------|-------|
|
||||
| `OpenAI` 2.9.1 | .NET Standard 2.0+ (.NET 9 confirmed) | Published 2026-03-02; requires `System.Net.ServerSentEvents` (built into .NET 9) |
|
||||
| `Markdig` 1.1.1 | .NET 8.0, .NET Standard 2.0, .NET Framework 4.6.2 | .NET 9 compatible via .NET 8 TFM; published 2026-03-04 |
|
||||
| `MudBlazor` 9.2.0 | .NET 8.0, .NET 9.0, .NET 10.0 | Published 2026-03-18; version 9.x = full support for .NET 9 |
|
||||
| .NET 9 SDK | Blazor WASM + Web API in same solution | Both project types target `net9.0`; no cross-framework issues |
|
||||
## Sources
|
||||
- https://www.nuget.org/packages/OpenAI — Official OpenAI NuGet package; version 2.9.1 confirmed (2026-03-02)
|
||||
- https://github.com/openai/openai-dotnet — Official OpenAI .NET SDK; streaming API verified (`CompleteChatStreamingAsync`, `await foreach`)
|
||||
- https://www.nuget.org/packages/Markdig — Markdig version 1.1.1 confirmed (2026-03-04)
|
||||
- https://www.nuget.org/packages/MudBlazor — MudBlazor 9.2.0 confirmed; .NET 8/9/10 full support (2026-03-18)
|
||||
- https://learn.microsoft.com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-9.0 — Official Blazor hosting model docs; standalone WASM vs Blazor Web App distinction verified
|
||||
- https://learn.microsoft.com/en-us/dotnet/core/compatibility/networking/10.0/default-http-streaming — Breaking change: WASM streaming opt-in (.NET 9) vs default (.NET 10)
|
||||
- https://www.strathweb.com/2024/07/built-in-support-for-server-sent-events-in-net-9/ — SSE native support in .NET 9 via `System.Net.ServerSentEvents`; used internally by OpenAI SDK (MEDIUM confidence, single source)
|
||||
- https://github.com/openai/openai-dotnet/issues/65 — Confirmed streaming issue in Blazor WASM requires `SetBrowserResponseStreamingEnabled(true)` (MEDIUM confidence, GitHub issue thread)
|
||||
- https://devblogs.microsoft.com/dotnet/openai-dotnet-library/ — Official .NET Blog announcement of the OpenAI library
|
||||
- https://dev.to/kazinix/blazor-web-app-webassembly-hosted-in-net8-and-net9-1k6g — Hosted template removal in .NET 8+, manual solution structure (MEDIUM confidence)
|
||||
<!-- GSD:stack-end -->
|
||||
| Technology | Version | Purpose |
|
||||
|------------|---------|---------|
|
||||
| .NET SDK | 9.0.x | Runtime and tooling |
|
||||
| Blazor WebAssembly Standalone | .NET 9 | Client SPA running in-browser |
|
||||
| ASP.NET Core Web API | .NET 9 | Backend proxy (for future external API calls) |
|
||||
| MudBlazor | latest | Material Design component library |
|
||||
| System.Text.Json | built-in | JSON serialization |
|
||||
|
||||
<!-- GSD:conventions-start source:CONVENTIONS.md -->
|
||||
## Conventions
|
||||
|
||||
Conventions not yet established. Will populate as patterns emerge during development.
|
||||
<!-- GSD:conventions-end -->
|
||||
|
||||
<!-- GSD:architecture-start source:ARCHITECTURE.md -->
|
||||
## Architecture
|
||||
|
||||
Architecture not yet mapped. Follow existing patterns found in the codebase.
|
||||
<!-- GSD:architecture-end -->
|
||||
See [ARCHITECTURE.md](ARCHITECTURE.md) for detailed hosting model discussion, API design decisions, and project structure rationale.
|
||||
|
||||
<!-- GSD:workflow-start source:GSD defaults -->
|
||||
## GSD Workflow Enforcement
|
||||
**Summary:**
|
||||
- Three-project solution: Client (WASM), Api (backend proxy), Shared (models)
|
||||
- Components update locally — no API needed for UI rendering
|
||||
- The Api project exists for future use when external services require server-side secrets
|
||||
- For the current echo phase, only the Client project is actively used
|
||||
|
||||
Before using Edit, Write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync.
|
||||
## Conventions
|
||||
|
||||
Use these entry points:
|
||||
- `/gsd:quick` for small fixes, doc updates, and ad-hoc tasks
|
||||
- `/gsd:debug` for investigation and bug fixing
|
||||
- `/gsd:execute-phase` for planned phase work
|
||||
|
||||
Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it.
|
||||
<!-- GSD:workflow-end -->
|
||||
|
||||
|
||||
|
||||
<!-- GSD:profile-start -->
|
||||
## Developer Profile
|
||||
|
||||
> Profile not yet configured. Run `/gsd:profile-user` to generate your developer profile.
|
||||
> This section is managed by `generate-claude-profile` -- do not edit manually.
|
||||
<!-- GSD:profile-end -->
|
||||
- Inline comments on every new Blazor concept: what it does, why, and idiomatic alternatives
|
||||
- Emphasize framework idiom and explain choices — written for a C# developer new to web/Blazor
|
||||
- Keep code simple; avoid abstractions until they are clearly needed
|
||||
- One concept per phase — do not introduce multiple new patterns at once
|
||||
|
||||
58
README.md
58
README.md
@@ -1,2 +1,58 @@
|
||||
# AgenticCode
|
||||
# ChatAgent
|
||||
|
||||
A personal AI chat web application built with Blazor WebAssembly and MudBlazor. Currently in the **Echo phase** — the bot responds with "success msg!" to every message.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
ChatAgent.Client/ Blazor WASM app (runs in the browser)
|
||||
ChatAgent.Api/ ASP.NET Core Web API (backend proxy)
|
||||
ChatAgent.Shared/ Shared models between Client and Api
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
### Chat UI only (Echo phase)
|
||||
|
||||
The echo phase doesn't need the API backend — the client handles everything locally.
|
||||
|
||||
```bash
|
||||
cd src/ChatAgent.Client
|
||||
dotnet run
|
||||
```
|
||||
|
||||
Open http://localhost:5100 in your browser.
|
||||
|
||||
### Full stack (Client + API)
|
||||
|
||||
Run both projects in separate terminals:
|
||||
|
||||
```bash
|
||||
# Terminal 1 — API backend
|
||||
cd src/ChatAgent.Api
|
||||
dotnet run
|
||||
|
||||
# Terminal 2 — Blazor WASM client
|
||||
cd src/ChatAgent.Client
|
||||
dotnet run
|
||||
```
|
||||
|
||||
| Service | HTTP | HTTPS |
|
||||
|---------|------|-------|
|
||||
| Client | http://localhost:5100 | https://localhost:5200 |
|
||||
| API | http://localhost:7000 | https://localhost:7100 |
|
||||
|
||||
The health check page is available at `/health` when the API is running.
|
||||
|
||||
## Build
|
||||
|
||||
From the repo root:
|
||||
|
||||
```bash
|
||||
dotnet build
|
||||
```
|
||||
|
||||
@@ -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>
|
||||
|
||||
91
src/ChatAgent.Client/Components/ChatInput.razor
Normal file
91
src/ChatAgent.Client/Components/ChatInput.razor
Normal file
@@ -0,0 +1,91 @@
|
||||
@*
|
||||
ChatInput.razor -- The message input bar at the bottom of the chat.
|
||||
|
||||
KEY BLAZOR CONCEPTS:
|
||||
- @bind-Value: Two-way data binding. When the user types, _messageText updates.
|
||||
When _messageText changes in code, the input reflects it. This is Blazor's
|
||||
equivalent of React's controlled components or Angular's [(ngModel)].
|
||||
- EventCallback: A typed delegate for child-to-parent communication. When the
|
||||
user clicks Send, this component invokes OnMessageSent, and the parent handles it.
|
||||
EventCallback is the Blazor pattern for "events flow up" (opposite of parameters
|
||||
which flow data down).
|
||||
- @onkeydown: Blazor's way to handle DOM events. The @ prefix binds a C# method
|
||||
to a JavaScript event. Blazor intercepts it via JS interop and calls your C# code.
|
||||
*@
|
||||
|
||||
@* max-width and margin:auto center the input bar to match the message list width. *@
|
||||
<div class="pa-3" style="border-top: 1px solid var(--mud-palette-lines-default); background: var(--mud-palette-background);">
|
||||
<div style="max-width: 768px; width: 100%; margin: 0 auto;">
|
||||
@* MudStack arranges children in a row. Spacing="2" adds gap between items.
|
||||
AlignItems centers them vertically within the row. *@
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
@* MudTextField is a Material Design text input.
|
||||
@bind-Value creates two-way binding to _messageText.
|
||||
Immediate="true" means the binding updates on every keystroke
|
||||
(default is on blur/focus-loss). We need this so Enter key handling
|
||||
always sees the latest typed text.
|
||||
Variant.Outlined draws a bordered input (vs Filled or Text). *@
|
||||
<MudTextField @bind-Value="_messageText"
|
||||
Placeholder="Type a message..."
|
||||
Variant="Variant.Outlined"
|
||||
Immediate="true"
|
||||
@onkeydown="HandleKeyDown"
|
||||
FullWidth="true"
|
||||
Disabled="@_isSending" />
|
||||
@* MudIconButton renders a Material icon as a clickable button.
|
||||
Icons.Material.Filled.Send is a built-in MudBlazor icon constant.
|
||||
Disabled prevents double-sends while processing. *@
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Send"
|
||||
Color="Color.Primary"
|
||||
OnClick="SendMessage"
|
||||
Disabled="@(string.IsNullOrWhiteSpace(_messageText) || _isSending)" />
|
||||
</MudStack>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
// Private field bound to the text input via @bind-Value.
|
||||
// The underscore prefix is a C# convention for private fields.
|
||||
private string _messageText = string.Empty;
|
||||
private bool _isSending;
|
||||
|
||||
/// <summary>
|
||||
/// EventCallback for notifying the parent when the user sends a message.
|
||||
/// The parent provides a handler method:
|
||||
/// <ChatInput OnMessageSent="@HandleNewMessage" />
|
||||
/// EventCallback<string> means the event carries a string payload (the message text).
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<string> OnMessageSent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Send button click (or Enter key). Invokes the parent's callback
|
||||
/// with the message text, then clears the input.
|
||||
/// </summary>
|
||||
private async Task SendMessage()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_messageText) || _isSending)
|
||||
return;
|
||||
|
||||
_isSending = true;
|
||||
|
||||
// InvokeAsync triggers the parent's event handler and passes the message text.
|
||||
// The parent (Chat.razor) will call ChatService.SendMessageAsync with this text.
|
||||
await OnMessageSent.InvokeAsync(_messageText);
|
||||
|
||||
_messageText = string.Empty;
|
||||
_isSending = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the message when the user presses Enter (without Shift).
|
||||
/// Shift+Enter can be used for multi-line input in a future phase.
|
||||
/// </summary>
|
||||
private async Task HandleKeyDown(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Key == "Enter" && !e.ShiftKey)
|
||||
{
|
||||
await SendMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
77
src/ChatAgent.Client/Components/ChatMessageList.razor
Normal file
77
src/ChatAgent.Client/Components/ChatMessageList.razor
Normal file
@@ -0,0 +1,77 @@
|
||||
@*
|
||||
ChatMessageList.razor -- Renders the scrollable list of chat messages.
|
||||
|
||||
This is a "presentational" component -- it receives data via a [Parameter]
|
||||
and renders it. It does not manage state or call services directly.
|
||||
|
||||
KEY BLAZOR CONCEPTS:
|
||||
- [Parameter]: A property decorated with [Parameter] receives its value from
|
||||
the parent component's markup (like an HTML attribute). This is how data
|
||||
flows downward in Blazor's component tree (parent -> child).
|
||||
- @foreach: Razor syntax for looping. The @ prefix switches from HTML to C#.
|
||||
- Conditional CSS classes: We use a ternary expression to pick alignment
|
||||
based on IsUser, so user messages appear on the right and bot messages on the left.
|
||||
*@
|
||||
|
||||
@using ChatAgent.Client.Models
|
||||
|
||||
@* flex:1 fills available space; overflow-y:auto enables scrolling when messages exceed
|
||||
the visible area. flex-direction:column-reverse anchors content to the bottom,
|
||||
so the newest messages are always visible (like ChatGPT). *@
|
||||
<div style="flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column-reverse;">
|
||||
@* We wrap the messages in an inner div because column-reverse reverses the visual order
|
||||
of direct children. By putting all messages in a single child div, we keep their
|
||||
natural top-to-bottom order while the outer container anchors to the bottom. *@
|
||||
<div style="max-width: 768px; width: 100%; margin: 0 auto;">
|
||||
@if (Messages is null || Messages.Count == 0)
|
||||
{
|
||||
@* MudText is a MudBlazor typography component. Typo.Body1 sets the text style.
|
||||
Using MudBlazor components instead of raw HTML gives consistent Material Design styling. *@
|
||||
<MudText Typo="Typo.body1" Align="Align.Center" Class="mt-4" Style="color: var(--mud-palette-text-secondary);">
|
||||
Send a message to get started.
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var message in Messages)
|
||||
{
|
||||
@* d-flex = display:flex (MudBlazor utility class, similar to Bootstrap).
|
||||
justify-end/justify-start control horizontal alignment of the chat bubble. *@
|
||||
<div class="d-flex @(message.IsUser ? "justify-end" : "justify-start") mb-3">
|
||||
@* MudPaper is a Material Design surface (a "card" with elevation/shadow).
|
||||
Elevation="1" adds a subtle shadow. Class applies padding and max-width. *@
|
||||
<MudPaper Elevation="1" Class="pa-3" Style="@GetBubbleStyle(message.IsUser)">
|
||||
<MudText Typo="Typo.body1">@message.Text</MudText>
|
||||
<MudText Typo="Typo.caption" Style="opacity: 0.6; margin-top: 4px;">
|
||||
@message.Timestamp.ToString("h:mm tt")
|
||||
</MudText>
|
||||
</MudPaper>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The list of messages to display. Passed in from the parent Chat page.
|
||||
/// [Parameter] tells Blazor this value comes from the parent's markup:
|
||||
/// <ChatMessageList Messages="@someList" />
|
||||
/// Blazor re-renders this component when the parameter value changes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public IReadOnlyList<ChatMessage>? Messages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Builds the inline style for a chat bubble. User messages get the primary color;
|
||||
/// bot messages get the surface color. This is in a method because Blazor component
|
||||
/// attributes do not support mixed C# and markup inline (RZ9986).
|
||||
/// </summary>
|
||||
private static string GetBubbleStyle(bool isUser)
|
||||
{
|
||||
var bg = isUser
|
||||
? "background-color: var(--mud-palette-primary); color: var(--mud-palette-primary-text);"
|
||||
: "background-color: var(--mud-palette-surface);";
|
||||
return $"max-width: 75%; border-radius: 16px; {bg}";
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,12 @@
|
||||
is rendered inside the layout's @Body placeholder. This is similar to
|
||||
_Layout.cshtml in MVC or master pages in Web Forms.
|
||||
|
||||
Phase 1 uses a minimal layout -- just centered content with padding.
|
||||
Later phases will add a sidebar for conversation management.
|
||||
MudBlazor requires certain provider components to be placed at the layout level:
|
||||
- MudThemeProvider: Applies the Material Design theme (colors, typography, spacing)
|
||||
- MudPopoverProvider: Renders popovers/tooltips outside the normal DOM flow
|
||||
so they are not clipped by parent overflow styles
|
||||
- MudDialogProvider: Renders dialogs (modal windows) at the root level
|
||||
- MudSnackbarProvider: Renders toast notifications at the root level
|
||||
*@
|
||||
|
||||
@* @inherits LayoutComponentBase makes this a layout component.
|
||||
@@ -14,10 +18,26 @@
|
||||
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>
|
||||
@* MudBlazor providers -- must be in the layout so they wrap all page content *@
|
||||
<MudThemeProvider />
|
||||
<MudPopoverProvider />
|
||||
<MudDialogProvider />
|
||||
<MudSnackbarProvider />
|
||||
|
||||
<MudLayout>
|
||||
@* MudAppBar is a Material Design top bar. Elevation adds shadow depth.
|
||||
Fixed="true" keeps it pinned at the top when the page scrolls. *@
|
||||
<MudAppBar Elevation="1" Fixed="true">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Chat" Class="mr-2" />
|
||||
<MudText Typo="Typo.h6">ChatAgent</MudText>
|
||||
</MudAppBar>
|
||||
|
||||
@* MudMainContent automatically adds top padding to account for the fixed app bar,
|
||||
so page content doesn't render behind it. *@
|
||||
<MudMainContent Style="height: 100vh; display: flex; flex-direction: column;">
|
||||
@* @Body is where the routed page content renders.
|
||||
When the user navigates to "/", the Chat.razor component's markup
|
||||
appears here. The layout stays the same -- only @Body changes. *@
|
||||
@Body
|
||||
</MudMainContent>
|
||||
</MudLayout>
|
||||
|
||||
29
src/ChatAgent.Client/Models/ChatMessage.cs
Normal file
29
src/ChatAgent.Client/Models/ChatMessage.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
// ChatMessage.cs -- Represents a single message in the chat conversation.
|
||||
//
|
||||
// This is a plain C# model (sometimes called a DTO -- Data Transfer Object).
|
||||
// It holds the data for one chat bubble: who sent it, what they said, and when.
|
||||
// Both the UI components and the ChatService reference this model.
|
||||
|
||||
namespace ChatAgent.Client.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A single message in the chat conversation.
|
||||
/// </summary>
|
||||
public class ChatMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The display text of the message.
|
||||
/// </summary>
|
||||
public string Text { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// True if the message was sent by the user; false if it came from the bot.
|
||||
/// The UI uses this to style and align the bubble differently.
|
||||
/// </summary>
|
||||
public bool IsUser { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the message was created. Used for display ordering.
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; } = DateTime.Now;
|
||||
}
|
||||
56
src/ChatAgent.Client/Pages/Chat.razor
Normal file
56
src/ChatAgent.Client/Pages/Chat.razor
Normal file
@@ -0,0 +1,56 @@
|
||||
@*
|
||||
Chat.razor -- The main chat page that composes ChatMessageList and ChatInput.
|
||||
|
||||
KEY BLAZOR CONCEPTS:
|
||||
- @page "/": This directive registers the component as a routable page.
|
||||
When the browser navigates to "/", Blazor's Router (in App.razor) renders
|
||||
this component inside MainLayout's @Body. The URL path is how Blazor picks
|
||||
which page component to show.
|
||||
- @inject: Requests a service from the DI container. This is the Razor syntax
|
||||
equivalent of constructor injection in regular C# classes. The service must be
|
||||
registered in Program.cs first.
|
||||
- StateHasChanged(): Tells Blazor "my state changed, please re-render."
|
||||
Blazor calls this automatically after event handlers (like button clicks),
|
||||
but since we update state inside an awaited service call, we call it explicitly
|
||||
to ensure the UI reflects the new messages immediately.
|
||||
*@
|
||||
|
||||
@page "/"
|
||||
@using ChatAgent.Client.Components
|
||||
@using ChatAgent.Client.Models
|
||||
|
||||
@* @inject pulls ChatService from the DI container registered in Program.cs.
|
||||
"ChatService" is the type; "ChatService" after it is the property name we use in code. *@
|
||||
@inject ChatService ChatService
|
||||
|
||||
@* PageTitle sets the browser tab title. It works via the HeadOutlet
|
||||
registered in Program.cs (which manages <head> elements from components). *@
|
||||
<PageTitle>Chat</PageTitle>
|
||||
|
||||
@* flex:1 makes this container fill all remaining space below the app bar.
|
||||
The inner flex-column stacks the message list (scrollable) above the input (fixed). *@
|
||||
<div style="flex: 1; display: flex; flex-direction: column; min-height: 0;">
|
||||
@* ChatMessageList is our presentational component. We pass the message list as a parameter.
|
||||
The = sign in Messages="@..." assigns the value; the @ prefix evaluates the C# expression. *@
|
||||
<ChatMessageList Messages="@ChatService.Messages" />
|
||||
|
||||
@* ChatInput fires OnMessageSent when the user clicks Send or presses Enter.
|
||||
We bind that event to our HandleNewMessage method. *@
|
||||
<ChatInput OnMessageSent="@HandleNewMessage" />
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Called when ChatInput fires its OnMessageSent event.
|
||||
/// Delegates to ChatService, then triggers a re-render so the new messages appear.
|
||||
/// </summary>
|
||||
private async Task HandleNewMessage(string messageText)
|
||||
{
|
||||
await ChatService.SendMessageAsync(messageText);
|
||||
|
||||
// StateHasChanged() tells Blazor to re-render this component.
|
||||
// After re-render, the updated ChatService.Messages list flows down
|
||||
// to ChatMessageList via its [Parameter], updating the UI.
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@
|
||||
*@
|
||||
|
||||
@* @page directive maps this component to a URL route.
|
||||
"/" means this is the default/home page. *@
|
||||
@page "/"
|
||||
"/health" keeps the health check accessible while "/" is now the chat page. *@
|
||||
@page "/health"
|
||||
|
||||
@* Import the service and model namespaces for this component.
|
||||
These could also be in _Imports.razor for global access. *@
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -49,4 +50,13 @@ builder.Services.AddHttpClient<ChatApiClient>(client =>
|
||||
client.BaseAddress = new Uri(apiBaseUrl);
|
||||
});
|
||||
|
||||
// AddMudServices registers MudBlazor's internal services (theme, popover, scroll, etc.)
|
||||
// into the DI container. Required for MudBlazor components to function.
|
||||
builder.Services.AddMudServices();
|
||||
|
||||
// Register ChatService as a Singleton. In Blazor WASM, Singleton means "one instance per
|
||||
// browser tab" -- there is no server-side shared state. This keeps the message list
|
||||
// alive across page navigations within the same tab session.
|
||||
builder.Services.AddSingleton<ChatService>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
|
||||
65
src/ChatAgent.Client/Services/ChatService.cs
Normal file
65
src/ChatAgent.Client/Services/ChatService.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
// ChatService.cs -- Client-side service that manages chat state and produces responses.
|
||||
//
|
||||
// WHY A SERVICE? In Blazor, services registered in DI live for the lifetime of the app
|
||||
// (when registered as Singleton in WASM -- there is only one "user" per browser tab).
|
||||
// This keeps chat state (the message list) separate from UI components, following the
|
||||
// "service extracts logic from components" pattern. Components inject this service
|
||||
// and call its methods, rather than holding all state themselves.
|
||||
//
|
||||
// CURRENT PHASE (Echo): SendMessageAsync always returns "success msg!".
|
||||
// FUTURE: This service will call ChatApiClient to reach the backend, which will
|
||||
// forward the message to an external AI service.
|
||||
|
||||
using ChatAgent.Client.Models;
|
||||
|
||||
namespace ChatAgent.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the chat conversation state and produces responses.
|
||||
/// Registered as a Singleton in Program.cs so all components share the same message list.
|
||||
/// In Blazor WASM, Singleton means "one instance per browser tab" (there is no shared server state).
|
||||
/// </summary>
|
||||
public class ChatService
|
||||
{
|
||||
// The conversation history. Components read this list to render messages.
|
||||
// Using a List<T> (not IReadOnlyList) for simplicity in this phase.
|
||||
private readonly List<ChatMessage> _messages = new();
|
||||
|
||||
/// <summary>
|
||||
/// The full conversation history, oldest first.
|
||||
/// Components bind to this to render the chat bubbles.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ChatMessage> Messages => _messages;
|
||||
|
||||
/// <summary>
|
||||
/// Adds the user's message to the conversation, generates a response, and adds that too.
|
||||
/// Returns the bot's response message.
|
||||
///
|
||||
/// The method is async (returns Task) even though the current echo implementation is synchronous.
|
||||
/// This is intentional -- when we later replace the echo with an HTTP call to the API,
|
||||
/// the method signature won't need to change, and all callers already await it.
|
||||
/// </summary>
|
||||
public Task<ChatMessage> SendMessageAsync(string userText)
|
||||
{
|
||||
// Add the user's message to the conversation
|
||||
var userMessage = new ChatMessage
|
||||
{
|
||||
Text = userText,
|
||||
IsUser = true,
|
||||
Timestamp = DateTime.Now
|
||||
};
|
||||
_messages.Add(userMessage);
|
||||
|
||||
// Echo phase: always respond with "success msg!"
|
||||
// Future: replace this with an HTTP call via ChatApiClient
|
||||
var botMessage = new ChatMessage
|
||||
{
|
||||
Text = "success msg!",
|
||||
IsUser = false,
|
||||
Timestamp = DateTime.Now
|
||||
};
|
||||
_messages.Add(botMessage);
|
||||
|
||||
return Task.FromResult(botMessage);
|
||||
}
|
||||
}
|
||||
@@ -17,3 +17,4 @@
|
||||
@using ChatAgent.Client.Layout
|
||||
@using ChatAgent.Client.Services
|
||||
@using ChatAgent.Shared.Models
|
||||
@using MudBlazor
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
<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" />
|
||||
<!-- MudBlazor CSS: Material Design styles for all MudBlazor components -->
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
|
||||
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -26,6 +28,8 @@
|
||||
<a href="." class="reload">Reload</a>
|
||||
<span class="dismiss">🗙</span>
|
||||
</div>
|
||||
<!-- MudBlazor JS: required for interactive features (popover positioning, scroll, etc.) -->
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user