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:
local
2026-04-04 01:24:40 +01:00
parent a462b7dbc7
commit 1614a61617
27 changed files with 819 additions and 375 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-04

View File

@@ -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": "<delta>"}\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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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