diff --git a/openspec/changes/archive/2026-04-04-basic-chat-interface/.openspec.yaml b/openspec/changes/archive/2026-04-04-basic-chat-interface/.openspec.yaml new file mode 100644 index 0000000..c430c5f --- /dev/null +++ b/openspec/changes/archive/2026-04-04-basic-chat-interface/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-03 diff --git a/openspec/changes/archive/2026-04-04-basic-chat-interface/design.md b/openspec/changes/archive/2026-04-04-basic-chat-interface/design.md new file mode 100644 index 0000000..41e8944 --- /dev/null +++ b/openspec/changes/archive/2026-04-04-basic-chat-interface/design.md @@ -0,0 +1,48 @@ +## Context + +The project has a working Blazor WASM client and ASP.NET Core API with health check connectivity proven. The client currently uses Bootstrap for layout and has template pages (Counter, Weather). No UI component library is installed. This change introduces MudBlazor and builds the first real feature — a chat interface with hardcoded responses. + +## Goals / Non-Goals + +**Goals:** +- Install and configure MudBlazor as the UI component library +- Build a ChatGPT/Gemini-inspired chat interface +- Establish the message model and UI patterns that future phases will build on +- Keep hardcoded responses so the UI is testable without API wiring + +**Non-Goals:** +- OpenAI API integration (future phase) +- Markdown rendering of messages (future phase — Markdig) +- Conversation persistence or history (future phase) +- Multiple conversations / sidebar navigation (future phase) +- Responsive mobile layout optimization + +## Decisions + +### Decision 1: MudBlazor component choices + +**Chat message list**: Use `MudPaper` cards inside a scrollable `div` (or `MudStack`). Each message gets a `MudPaper` with `Elevation="0"` and a background color to distinguish user vs assistant. + +**Input area**: `MudTextField` with `Variant="Outlined"` and an `Adornment` send button (icon). This gives a single-line input with integrated send — similar to ChatGPT. + +**Layout**: `MudLayout` + `MudAppBar` + `MudMainContent`. No drawer/sidebar yet — that comes when we add conversation management. + +**Alternative considered**: Building with raw HTML/CSS. Rejected because MudBlazor is in the tech stack spec and provides the component patterns needed for later phases (dialogs, drawers, lists). + +### Decision 2: ChatMessage model location + +Place `ChatMessage.cs` in `ChatAgent.Shared` so it's available to both Client and API when API integration comes. Fields: `Role` (string: "user" or "assistant"), `Content` (string), `Timestamp` (DateTime). + +### Decision 3: Chat page structure + +The Chat.razor component owns the message list (`List`) and handles input. No separate service layer yet — the hardcoded response is inline in the component. When AI integration comes, a service will be extracted. + +### Decision 4: Template page cleanup + +Remove Counter.razor and Weather.razor. Move Home.razor from `/` to `/health` so the health check is still accessible but the chat page takes the root route. + +## Risks / Trade-offs + +- [MudBlazor WASM bundle size] → Acceptable for a personal tool; AOT is deferred per stack spec +- [No service abstraction for responses] → Intentional; extracting too early adds complexity before the pattern is clear. Will refactor when adding API integration. +- [Removing template pages] → Low risk; they were scaffolding. Health check preserved at `/health`. diff --git a/openspec/changes/archive/2026-04-04-basic-chat-interface/proposal.md b/openspec/changes/archive/2026-04-04-basic-chat-interface/proposal.md new file mode 100644 index 0000000..b168948 --- /dev/null +++ b/openspec/changes/archive/2026-04-04-basic-chat-interface/proposal.md @@ -0,0 +1,35 @@ +## Why + +The project exists to be a working AI chat interface, but there is no chat UI yet — only a health check page. This change builds the foundational chat experience using MudBlazor components, with hardcoded responses so the UI can be developed and tested independently of the OpenAI API integration. + +## What Changes + +- Install and configure MudBlazor in the Client project (NuGet, CSS/JS, providers, imports) +- Replace the Bootstrap navbar/layout with a MudBlazor layout (MudLayout, MudAppBar, MudMainContent) +- Create a Chat page with a message list and text input, styled after ChatGPT/Gemini +- Add a shared `ChatMessage` model (role + content + timestamp) +- Wire the input to append user messages and reply with a hardcoded bot response +- Remove template pages (Counter, Weather) that are no longer needed + +## Capabilities + +### New Capabilities +- `chat-ui`: The chat interface — message display, input handling, auto-scroll, and layout +- `mudblazor-setup`: MudBlazor installation, theming, and provider configuration + +### Modified Capabilities + + +## Impact + +- `src/ChatAgent.Client/ChatAgent.Client.csproj`: Add MudBlazor package +- `src/ChatAgent.Client/wwwroot/index.html`: Add MudBlazor CSS/JS/font links +- `src/ChatAgent.Client/_Imports.razor`: Add MudBlazor using +- `src/ChatAgent.Client/Program.cs`: Add MudBlazor services +- `src/ChatAgent.Client/Layout/MainLayout.razor`: Replace with MudBlazor layout +- `src/ChatAgent.Client/Layout/NavMenu.razor`: Replace or remove Bootstrap nav +- `src/ChatAgent.Client/Pages/Chat.razor`: New chat page (becomes default route) +- `src/ChatAgent.Client/Pages/Home.razor`: Demote from `/` route or keep as `/health` +- `src/ChatAgent.Shared/Models/ChatMessage.cs`: New shared message model +- `src/ChatAgent.Client/Pages/Counter.razor`: Remove +- `src/ChatAgent.Client/Pages/Weather.razor`: Remove diff --git a/openspec/changes/archive/2026-04-04-basic-chat-interface/specs/chat-ui/spec.md b/openspec/changes/archive/2026-04-04-basic-chat-interface/specs/chat-ui/spec.md new file mode 100644 index 0000000..fa7c46c --- /dev/null +++ b/openspec/changes/archive/2026-04-04-basic-chat-interface/specs/chat-ui/spec.md @@ -0,0 +1,66 @@ +## ADDED Requirements + +### Requirement: Message display + +The chat page SHALL display messages in a vertically scrolling list, with each message showing the sender role (user or assistant), the message content, and a visual distinction between user and assistant messages (e.g., alignment, color, or avatar). + +#### Scenario: User message displayed + +- **WHEN** the user sends a message +- **THEN** the message appears in the message list aligned or styled to indicate it is from the user + +#### Scenario: Assistant message displayed + +- **WHEN** the assistant responds +- **THEN** the response appears in the message list with distinct styling from user messages (different alignment, color, or avatar) + +#### Scenario: Message ordering + +- **WHEN** multiple messages exist in the conversation +- **THEN** messages are displayed in chronological order, oldest at top + +### Requirement: Message input + +The chat page SHALL provide a text input area at the bottom of the page where the user can type and submit messages. + +#### Scenario: Submit via button + +- **WHEN** the user types text and clicks the send button +- **THEN** the message is added to the conversation and the input is cleared + +#### Scenario: Submit via Enter key + +- **WHEN** the user types text and presses Enter +- **THEN** the message is submitted (same as clicking send) + +#### Scenario: Empty input blocked + +- **WHEN** the user attempts to send an empty or whitespace-only message +- **THEN** nothing is sent and no message is added + +### Requirement: Hardcoded response + +In this phase, the assistant SHALL reply with a hardcoded message to every user input. This stubs the AI integration point for future phases. + +#### Scenario: Bot replies to any input + +- **WHEN** the user sends any message +- **THEN** the assistant replies with a hardcoded response (e.g., "This is a placeholder response. AI integration coming soon!") + +### Requirement: Auto-scroll + +The message list SHALL automatically scroll to the newest message when a new message is added. + +#### Scenario: New message scrolls into view + +- **WHEN** a new message (user or assistant) is added to the conversation +- **THEN** the message list scrolls to the bottom so the new message is visible + +### Requirement: Chat page is default route + +The chat page SHALL be the default route (`/`) of the application. + +#### Scenario: App opens to chat + +- **WHEN** the user navigates to the root URL +- **THEN** the chat page is displayed diff --git a/openspec/changes/archive/2026-04-04-basic-chat-interface/specs/mudblazor-setup/spec.md b/openspec/changes/archive/2026-04-04-basic-chat-interface/specs/mudblazor-setup/spec.md new file mode 100644 index 0000000..2d4e713 --- /dev/null +++ b/openspec/changes/archive/2026-04-04-basic-chat-interface/specs/mudblazor-setup/spec.md @@ -0,0 +1,46 @@ +## ADDED Requirements + +### Requirement: MudBlazor package installed + +The Client project SHALL have MudBlazor 9.2.0 installed as a NuGet dependency. + +#### Scenario: Package reference present + +- **WHEN** the Client project is built +- **THEN** MudBlazor 9.2.0 is resolved as a dependency + +### Requirement: MudBlazor services registered + +MudBlazor services SHALL be registered in the Client's DI container via `AddMudServices()`. + +#### Scenario: Services available + +- **WHEN** the application starts +- **THEN** MudBlazor services (snackbar, dialog, etc.) are available for injection + +### Requirement: MudBlazor assets loaded + +The Client's `index.html` SHALL include MudBlazor CSS, JS, and font references. + +#### Scenario: Styles and scripts present + +- **WHEN** the application loads in the browser +- **THEN** MudBlazor CSS (`_content/MudBlazor/MudBlazor.min.css`), JS (`_content/MudBlazor/MudBlazor.min.js`), and Material Design Icons font are loaded + +### Requirement: MudBlazor layout providers + +The app root SHALL include `MudThemeProvider`, `MudPopoverProvider`, and `MudDialogProvider` so MudBlazor components function correctly. + +#### Scenario: Providers present + +- **WHEN** any MudBlazor component is rendered +- **THEN** it functions correctly because the required providers are in the component tree + +### Requirement: MudBlazor layout replaces Bootstrap + +The application layout SHALL use MudBlazor layout components (`MudLayout`, `MudAppBar`, `MudMainContent`) instead of the current Bootstrap navbar. + +#### Scenario: Layout renders with MudBlazor + +- **WHEN** any page is displayed +- **THEN** the page is wrapped in a MudBlazor layout with an app bar showing the application name diff --git a/openspec/changes/archive/2026-04-04-basic-chat-interface/tasks.md b/openspec/changes/archive/2026-04-04-basic-chat-interface/tasks.md new file mode 100644 index 0000000..bd4667e --- /dev/null +++ b/openspec/changes/archive/2026-04-04-basic-chat-interface/tasks.md @@ -0,0 +1,38 @@ +## 1. MudBlazor Setup + +- [x] 1.1 Install MudBlazor 9.2.0 NuGet package in ChatAgent.Client +- [x] 1.2 Add MudBlazor CSS, JS, and Material Design Icons font to index.html (remove Bootstrap CSS) +- [x] 1.3 Add `@using MudBlazor` to _Imports.razor +- [x] 1.4 Register MudBlazor services (`AddMudServices()`) in Program.cs +- [x] 1.5 Add MudThemeProvider, MudPopoverProvider, MudDialogProvider to MainLayout.razor + +## 2. Layout Migration + +- [x] 2.1 Replace MainLayout.razor with MudBlazor layout (MudLayout, MudAppBar, MudMainContent) +- [x] 2.2 Remove NavMenu.razor (Bootstrap navbar no longer needed) +- [x] 2.3 Remove MainLayout.razor.css (MudBlazor handles styling) + +## 3. Shared Model + +- [x] 3.1 Create ChatMessage.cs in ChatAgent.Shared/Models with Role, Content, Timestamp + +## 4. Chat Page + +- [x] 4.1 Create Chat.razor at route `/` with message list and input area +- [x] 4.2 Implement message display with MudPaper cards (distinct styling for user vs assistant) +- [x] 4.3 Implement text input with MudTextField and send button adornment +- [x] 4.4 Wire Enter key and send button to submit handler +- [x] 4.5 Block empty/whitespace-only submissions +- [x] 4.6 Add hardcoded assistant response after each user message +- [x] 4.7 Implement auto-scroll to bottom on new messages + +## 5. Cleanup + +- [x] 5.1 Move Home.razor route from `/` to `/health` +- [x] 5.2 Remove Counter.razor and Weather.razor +- [x] 5.3 Update app.css — remove Bootstrap-specific styles, keep custom styles that still apply + +## 6. Verify + +- [x] 6.1 Run `dotnet build` on the solution to confirm no errors +- [ ] 6.2 Manually verify: chat page loads at `/`, messages display correctly, hardcoded response works diff --git a/openspec/changes/wire-responses-api/.openspec.yaml b/openspec/changes/wire-responses-api/.openspec.yaml new file mode 100644 index 0000000..c54c137 --- /dev/null +++ b/openspec/changes/wire-responses-api/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-04 diff --git a/openspec/changes/wire-responses-api/design.md b/openspec/changes/wire-responses-api/design.md new file mode 100644 index 0000000..f047042 --- /dev/null +++ b/openspec/changes/wire-responses-api/design.md @@ -0,0 +1,66 @@ +## Context + +The chat UI has a hardcoded response stub. A local OpenAI-compatible proxy at `localhost:8317` serves the Responses API (`POST /v1/responses`) with Claude models. The existing architecture has a WASM client calling the API backend — we add a new endpoint that proxies to the Responses API and streams tokens back. + +The Responses API streaming format uses SSE with events like `response.output_text.delta` carrying a `delta` field with text fragments. + +## Goals / Non-Goals + +**Goals:** +- Wire real AI responses through the existing client → backend → proxy chain +- Stream tokens to the UI for responsive feel +- Keep the proxy URL and model configurable (server-side only) +- Show a thinking indicator while waiting for first token + +**Non-Goals:** +- Conversation history / multi-turn context (future phase) +- Model selection UI (future phase) +- Retry logic or rate limiting +- Markdown rendering of responses (future phase — Markdig) + +## Decisions + +### Decision 1: Backend proxies the Responses API + +The WASM client cannot call `localhost:8317` directly (different origin, and we keep external service URLs server-side). The API backend gets a new `ChatController` that: +1. Receives messages from the client +2. Forwards them to `POST /v1/responses` with `"stream": true` +3. Reads the SSE stream, extracts `response.output_text.delta` events +4. Re-emits the text deltas as simple SSE events to the client + +**Format for client SSE**: `data: {"text": ""}\n\n` for each token, and `data: [DONE]\n\n` at the end. This is simpler than forwarding the full Responses API event structure. + +**Alternative considered**: Having the client call `localhost:8317` directly via CORS. Rejected — breaks the architecture constraint of keeping external URLs server-side. + +### Decision 2: Client-side streaming with SetBrowserResponseStreamingEnabled + +Per the stack spec, the client uses: +- `SetBrowserResponseStreamingEnabled(true)` on the `HttpRequestMessage` +- `HttpCompletionOption.ResponseHeadersRead` to start reading before the full response arrives +- Line-by-line iteration of the response stream + +This avoids any JavaScript interop for streaming. + +### Decision 3: Simple DTOs in Shared project + +Add `ChatRequest` (list of messages) and keep the existing `ChatMessage` model. The SSE parsing happens in `ChatApiClient` — no DTO needed for individual stream events since they're parsed inline. + +### Decision 4: Configuration via appsettings.json + +The API's `appsettings.json` gets: +```json +{ + "ResponsesApi": { + "BaseUrl": "http://localhost:8317", + "Model": "claude-sonnet-4-6" + } +} +``` + +This is the API project's appsettings (server-side, not exposed to the browser). + +## Risks / Trade-offs + +- [Proxy adds latency] → Minimal for localhost; acceptable tradeoff for keeping URLs server-side +- [No conversation history] → Intentional; each request is single-turn for now. Multi-turn comes in a future phase. +- [No retry on stream failure] → If the stream breaks mid-response, the partial text stays visible and an error is shown. Good enough for phase 1. diff --git a/openspec/changes/wire-responses-api/proposal.md b/openspec/changes/wire-responses-api/proposal.md new file mode 100644 index 0000000..3a4127f --- /dev/null +++ b/openspec/changes/wire-responses-api/proposal.md @@ -0,0 +1,30 @@ +## Why + +The chat UI currently returns hardcoded responses. A local OpenAI-compatible proxy is running at `localhost:8317` that exposes the Responses API (`POST /v1/responses`) backed by Anthropic Claude models. This change wires the chat to produce real AI responses via streaming, replacing the hardcoded stub. + +## What Changes + +- Add a chat endpoint to the API backend that proxies requests to the local Responses API +- Stream tokens from the Responses API back to the WASM client as SSE +- Update ChatApiClient with a streaming chat method +- Replace the hardcoded response in Chat.razor with live streaming from the API +- Add a "thinking" indicator while the assistant is responding +- Disable input during streaming to prevent overlapping requests + +## Capabilities + +### New Capabilities +- `chat-streaming`: Streaming AI responses from the Responses API proxy through the backend to the WASM client + +### Modified Capabilities +- `chat-ui`: Replace hardcoded response with streaming AI response, add typing indicator, disable input during streaming + +## Impact + +- `src/ChatAgent.Api/ChatAgent.Api.csproj`: Add no new packages (uses built-in HttpClient) +- `src/ChatAgent.Api/Controllers/ChatController.cs`: New controller proxying to Responses API +- `src/ChatAgent.Api/Program.cs`: Register HttpClient for the proxy, add configuration +- `src/ChatAgent.Api/appsettings.json`: New — configure Responses API base URL and model +- `src/ChatAgent.Client/Services/ChatApiClient.cs`: Add streaming chat method +- `src/ChatAgent.Client/Pages/Chat.razor`: Replace hardcoded response with streaming call +- `src/ChatAgent.Shared/Models/`: New request/response DTOs for the chat endpoint diff --git a/openspec/changes/wire-responses-api/specs/chat-streaming/spec.md b/openspec/changes/wire-responses-api/specs/chat-streaming/spec.md new file mode 100644 index 0000000..88fa313 --- /dev/null +++ b/openspec/changes/wire-responses-api/specs/chat-streaming/spec.md @@ -0,0 +1,51 @@ +## ADDED Requirements + +### Requirement: Chat endpoint proxies to Responses API + +The API backend SHALL expose `POST /api/chat` that accepts a list of messages and proxies the request to the local Responses API at a configurable base URL using the `POST /v1/responses` endpoint. + +#### Scenario: Successful proxy request + +- **WHEN** the client sends a POST to `/api/chat` with a message list +- **THEN** the API forwards the messages to the Responses API with the configured model and returns the response + +### Requirement: Streaming response delivery + +The API backend SHALL stream the Responses API's SSE events back to the WASM client as `text/event-stream`, forwarding `response.output_text.delta` events so the client can render tokens incrementally. + +#### Scenario: Tokens stream to client + +- **WHEN** the Responses API emits `response.output_text.delta` events +- **THEN** the backend forwards each delta as an SSE event to the client containing the text fragment + +#### Scenario: Stream completes + +- **WHEN** the Responses API emits `response.completed` +- **THEN** the backend signals stream completion to the client + +### Requirement: Configurable proxy target + +The Responses API base URL and model name SHALL be configurable via `appsettings.json` in the API project, not hardcoded. + +#### Scenario: Configuration read at startup + +- **WHEN** the API starts +- **THEN** it reads `ResponsesApi:BaseUrl` and `ResponsesApi:Model` from configuration + +### Requirement: Client streams from backend + +The WASM client SHALL call `POST /api/chat` with `SetBrowserResponseStreamingEnabled(true)` and `HttpCompletionOption.ResponseHeadersRead`, then iterate the SSE stream to update the UI token by token. + +#### Scenario: Client reads streaming response + +- **WHEN** the client sends a chat request +- **THEN** it reads the response stream incrementally and appends each text delta to the assistant message in real time + +### Requirement: Error propagation + +If the Responses API returns an error or is unreachable, the API backend SHALL return an appropriate HTTP error status and the client SHALL display the error to the user. + +#### Scenario: Proxy unreachable + +- **WHEN** the Responses API is not running +- **THEN** the client displays an error message instead of an assistant response diff --git a/openspec/changes/wire-responses-api/specs/chat-ui/spec.md b/openspec/changes/wire-responses-api/specs/chat-ui/spec.md new file mode 100644 index 0000000..4bae96b --- /dev/null +++ b/openspec/changes/wire-responses-api/specs/chat-ui/spec.md @@ -0,0 +1,50 @@ +## MODIFIED Requirements + +### Requirement: Hardcoded response + +The assistant SHALL reply with a real AI response streamed from the backend API, replacing the previous hardcoded stub. Tokens appear incrementally as they arrive. + +#### Scenario: Bot replies with streamed AI response + +- **WHEN** the user sends any message +- **THEN** the assistant message appears and grows token by token as the stream delivers text + +### Requirement: Message input + +The chat page SHALL provide a text input area at the bottom of the page where the user can type and submit messages. + +#### Scenario: Submit via button + +- **WHEN** the user types text and clicks the send button +- **THEN** the message is added to the conversation and the input is cleared + +#### Scenario: Submit via Enter key + +- **WHEN** the user types text and presses Enter +- **THEN** the message is submitted (same as clicking send) + +#### Scenario: Empty input blocked + +- **WHEN** the user attempts to send an empty or whitespace-only message +- **THEN** nothing is sent and no message is added + +#### Scenario: Input disabled during streaming + +- **WHEN** the assistant is currently streaming a response +- **THEN** the input field and send button are disabled until streaming completes + +## ADDED Requirements + +### Requirement: Thinking indicator + +The chat page SHALL show a visual indicator while waiting for the first token from the assistant. + +#### Scenario: Indicator shown during wait + +- **WHEN** the user sends a message and the assistant has not yet started streaming +- **THEN** a thinking indicator (e.g., animated dots) is shown in the assistant message area + +#### Scenario: Indicator replaced by content + +- **WHEN** the first token arrives from the stream +- **THEN** the thinking indicator is replaced by the streamed text diff --git a/openspec/changes/wire-responses-api/tasks.md b/openspec/changes/wire-responses-api/tasks.md new file mode 100644 index 0000000..23b7c07 --- /dev/null +++ b/openspec/changes/wire-responses-api/tasks.md @@ -0,0 +1,29 @@ +## 1. Shared Models + +- [ ] 1.1 Create ChatRequest.cs in ChatAgent.Shared/Models with a Messages list property + +## 2. API Backend + +- [ ] 2.1 Add appsettings.json to ChatAgent.Api with ResponsesApi:BaseUrl and ResponsesApi:Model +- [ ] 2.2 Register an HttpClient for the Responses API proxy in Api Program.cs +- [ ] 2.3 Create ChatController with POST /api/chat that proxies to the Responses API with streaming +- [ ] 2.4 Parse Responses API SSE stream, extract response.output_text.delta events, re-emit as simplified SSE to client + +## 3. Client Streaming + +- [ ] 3.1 Add a streaming SendChatAsync method to ChatApiClient that uses SetBrowserResponseStreamingEnabled and HttpCompletionOption.ResponseHeadersRead +- [ ] 3.2 Parse the simplified SSE stream line-by-line, yielding text deltas + +## 4. Chat Page Updates + +- [ ] 4.1 Replace hardcoded response in Chat.razor with a call to ChatApiClient.SendChatAsync +- [ ] 4.2 Append tokens to the assistant message incrementally with StateHasChanged after each delta +- [ ] 4.3 Add a thinking indicator shown until the first token arrives +- [ ] 4.4 Disable input field and send button while streaming is in progress +- [ ] 4.5 Handle errors — display error message if API call fails +- [ ] 4.6 Auto-scroll during streaming (not just at the end) + +## 5. Verify + +- [ ] 5.1 Run dotnet build to confirm no errors +- [ ] 5.2 Manually verify: send a message, see streaming response from Claude diff --git a/src/ChatAgent.Client/ChatAgent.Client.csproj b/src/ChatAgent.Client/ChatAgent.Client.csproj index 5ba2fba..06c18e0 100644 --- a/src/ChatAgent.Client/ChatAgent.Client.csproj +++ b/src/ChatAgent.Client/ChatAgent.Client.csproj @@ -10,6 +10,7 @@ + diff --git a/src/ChatAgent.Client/Layout/MainLayout.razor b/src/ChatAgent.Client/Layout/MainLayout.razor index f946585..c37dead 100644 --- a/src/ChatAgent.Client/Layout/MainLayout.razor +++ b/src/ChatAgent.Client/Layout/MainLayout.razor @@ -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 -
- @* @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 -
+ + + + + + @* MudAppBar provides the top application bar. Dense reduces its height. + The fixed position keeps it visible while scrolling. *@ + + Chat Agent + + + @* MudMainContent renders the routed page content (same role as @Body in plain Blazor). + It automatically adds top padding to clear the AppBar. *@ + + @Body + + diff --git a/src/ChatAgent.Client/Layout/MainLayout.razor.css b/src/ChatAgent.Client/Layout/MainLayout.razor.css deleted file mode 100644 index baef3ee..0000000 --- a/src/ChatAgent.Client/Layout/MainLayout.razor.css +++ /dev/null @@ -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; - } -} diff --git a/src/ChatAgent.Client/Layout/NavMenu.razor b/src/ChatAgent.Client/Layout/NavMenu.razor deleted file mode 100644 index 18398fa..0000000 --- a/src/ChatAgent.Client/Layout/NavMenu.razor +++ /dev/null @@ -1,39 +0,0 @@ - - - - -@code { - private bool collapseNavMenu = true; - - private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; - - private void ToggleNavMenu() - { - collapseNavMenu = !collapseNavMenu; - } -} diff --git a/src/ChatAgent.Client/Layout/NavMenu.razor.css b/src/ChatAgent.Client/Layout/NavMenu.razor.css deleted file mode 100644 index 6724fd1..0000000 --- a/src/ChatAgent.Client/Layout/NavMenu.razor.css +++ /dev/null @@ -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; - } -} diff --git a/src/ChatAgent.Client/Pages/Chat.razor b/src/ChatAgent.Client/Pages/Chat.razor new file mode 100644 index 0000000..86324c5 --- /dev/null +++ b/src/ChatAgent.Client/Pages/Chat.razor @@ -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 + - 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 + +Chat Agent + +@* Chat container: uses flexbox to fill available height. + The message area grows to fill space; the input stays pinned at the bottom. *@ +
+ + @* 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. *@ +
+ @if (_messages.Count == 0) + { + @* Empty state shown before any messages are sent *@ +
+ + Chat Agent + + + Type a message to get started + +
+ } + 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) + { +
+ + @message.Content + +
+ } + } +
+ + @* 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. *@ +
+ +
+
+ +@code { + // The conversation messages, displayed in the message list. + // Using a simple List since we only add to the end — no complex state management needed. + private List _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; + + /// + /// Handles the Enter key press to submit the message. + /// KeyboardEventArgs gives us the key that was pressed. + /// + private async Task HandleKeyDown(KeyboardEventArgs e) + { + if (e.Key == "Enter" && !e.ShiftKey) + { + await SendMessage(); + } + } + + /// + /// 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. + /// + 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(); + } + + /// + /// 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). + /// + 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 + } + } +} diff --git a/src/ChatAgent.Client/Pages/Chat.razor.css b/src/ChatAgent.Client/Pages/Chat.razor.css new file mode 100644 index 0000000..11ba293 --- /dev/null +++ b/src/ChatAgent.Client/Pages/Chat.razor.css @@ -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); +} diff --git a/src/ChatAgent.Client/Pages/Counter.razor b/src/ChatAgent.Client/Pages/Counter.razor deleted file mode 100644 index b21f052..0000000 --- a/src/ChatAgent.Client/Pages/Counter.razor +++ /dev/null @@ -1,18 +0,0 @@ -@page "/counter" - -Counter - -

Counter

- -

Current count: @currentCount

- - - -@code { - private int currentCount = 0; - - private void IncrementCount() - { - currentCount++; - } -} diff --git a/src/ChatAgent.Client/Pages/Home.razor b/src/ChatAgent.Client/Pages/Home.razor index f3d3630..87098b4 100644 --- a/src/ChatAgent.Client/Pages/Home.razor +++ b/src/ChatAgent.Client/Pages/Home.razor @@ -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. *@ diff --git a/src/ChatAgent.Client/Pages/Weather.razor b/src/ChatAgent.Client/Pages/Weather.razor deleted file mode 100644 index 70a1c0f..0000000 --- a/src/ChatAgent.Client/Pages/Weather.razor +++ /dev/null @@ -1,57 +0,0 @@ -@page "/weather" -@inject HttpClient Http - -Weather - -

Weather

- -

This component demonstrates fetching data from the server.

- -@if (forecasts == null) -{ -

Loading...

-} -else -{ - - - - - - - - - - - @foreach (var forecast in forecasts) - { - - - - - - - } - -
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
-} - -@code { - private WeatherForecast[]? forecasts; - - protected override async Task OnInitializedAsync() - { - forecasts = await Http.GetFromJsonAsync("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); - } -} diff --git a/src/ChatAgent.Client/Program.cs b/src/ChatAgent.Client/Program.cs index 83ce5ba..49ac850 100644 --- a/src/ChatAgent.Client/Program.cs +++ b/src/ChatAgent.Client/Program.cs @@ -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 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). diff --git a/src/ChatAgent.Client/_Imports.razor b/src/ChatAgent.Client/_Imports.razor index 28adc3a..81fa8b1 100644 --- a/src/ChatAgent.Client/_Imports.razor +++ b/src/ChatAgent.Client/_Imports.razor @@ -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 diff --git a/src/ChatAgent.Client/wwwroot/css/app.css b/src/ChatAgent.Client/wwwroot/css/app.css index 60635aa..be8532e 100644 --- a/src/ChatAgent.Client/wwwroot/css/app.css +++ b/src/ChatAgent.Client/wwwroot/css/app.css @@ -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 { diff --git a/src/ChatAgent.Client/wwwroot/index.html b/src/ChatAgent.Client/wwwroot/index.html index 20bca18..ef846a2 100644 --- a/src/ChatAgent.Client/wwwroot/index.html +++ b/src/ChatAgent.Client/wwwroot/index.html @@ -1,32 +1,40 @@ - - - - - - - ChatAgent.Client - - - - - - - - -
- - - - -
-
- -
- An unhandled error has occurred. - Reload - 🗙 -
- - - - + + + + + + + Chat Agent + + + + + + + + + + + + + +
+ + + + +
+
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + + + + diff --git a/src/ChatAgent.Shared/Models/ChatMessage.cs b/src/ChatAgent.Shared/Models/ChatMessage.cs new file mode 100644 index 0000000..d3969f9 --- /dev/null +++ b/src/ChatAgent.Shared/Models/ChatMessage.cs @@ -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 +{ + /// + /// 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. + /// + public class ChatMessage + { + /// + /// Who sent the message: "user" or "assistant". + /// Uses string rather than enum so it matches the OpenAI API's role format directly. + /// + public string Role { get; set; } = string.Empty; + + /// + /// The text content of the message. + /// + public string Content { get; set; } = string.Empty; + + /// + /// When the message was created (UTC). + /// + public DateTime Timestamp { get; set; } + } +}