Compare commits

18 Commits

Author SHA1 Message Date
local
711df97ce9 feat(01-03): implement echo chat UI with MudBlazor
Add ChatGPT-style chat interface using MudBlazor components. User messages
display on the right (purple), bot echoes "success msg!" on the left.
Includes app bar, centered 768px layout, bottom-anchored messages, and
documented inline comments on every Blazor concept introduced.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 01:51:53 +01:00
local
7dd4243f01 incorporate openspec 2026-04-03 00:18:34 +01:00
local
697e9dce23 fix(01-02): auto-match API URL protocol to client load protocol
Client now detects whether it was loaded over HTTP or HTTPS and
selects the corresponding API base URL, so both protocols work
without manual config changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:00:43 +00:00
local
1fde98ca79 feat(01-02): implement health check round-trip with CORS and tutorial comments
- Add shared HealthResponse DTO in ChatAgent.Shared
- Add HealthController API endpoint with CORS policy for localhost:5200
- Add ChatApiClient typed HttpClient wrapper in WASM client
- Update Home.razor to display health check result on load
- Simplify MainLayout to minimal centered layout
- Add global imports for Services and Shared.Models
- Replace app.css with clean Phase 1 light theme styles
- Remove unused OpenAPI package from API project
- All files include tutorial-style inline comments (CODE-01)
2026-03-27 22:58:19 +00:00
local
4ef27598a0 docs(01-01): complete solution scaffold plan
- SUMMARY.md with execution results and metrics
- STATE.md advanced to plan 2 of 2 (50%)
- ROADMAP.md updated with plan progress
- CODE-02 requirement marked complete

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:53:47 +00:00
local
c6f1225810 feat(01-01): configure predictable dev ports and API base URL
- Client: https://localhost:5200, http://localhost:5100
- API: https://localhost:7100, http://localhost:7000
- Client wwwroot/appsettings.json with ApiBaseUrl pointing to API

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:52:27 +00:00
local
eeaa9de3c5 feat(01-01): create three-project .NET 9 solution scaffold
- ChatAgent.sln at repo root with Client, Api, Shared projects
- Blazor WASM client, ASP.NET Core Web API, shared class library
- Both Client and Api reference Shared project
- Removed template boilerplate (WeatherForecast, Class1)
- Added .NET ignores to .gitignore
- Pinned .NET 9 SDK via global.json

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:51:55 +00:00
local
b958230216 docs(01): add validation strategy for phase 1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:50:43 +00:00
local
4f1176be19 docs(01-architecture-foundation): create phase plan 2026-03-27 01:48:23 +00:00
local
410150eff9 docs(01): research phase domain 2026-03-27 01:38:56 +00:00
local
ccc9b0da91 docs(state): record phase 1 context session 2026-03-27 01:30:56 +00:00
local
98055dfb04 docs(01): capture phase context 2026-03-27 01:30:44 +00:00
local
7962df1094 docs: create roadmap (5 phases) 2026-03-27 01:21:35 +00:00
local
81bcc49bae docs: define v1 requirements 2026-03-27 01:07:09 +00:00
local
d9878dea73 docs: complete project research 2026-03-27 00:59:24 +00:00
local
b45ae0400e chore: add project config 2026-03-27 00:49:08 +00:00
local
6dbb1085b0 docs: initialize project 2026-03-27 00:43:18 +00:00
local
d963902a28 docs: map existing codebase 2026-03-27 00:21:00 +00:00
47 changed files with 2889 additions and 207 deletions

View File

@@ -0,0 +1,152 @@
---
name: "OPSX: Apply"
description: Implement tasks from an OpenSpec change (Experimental)
category: Workflow
tags: [workflow, artifacts, experimental]
---
Implement tasks from an OpenSpec change.
**Input**: Optionally specify a change name (e.g., `/opsx:apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **Select the change**
If a name is provided, use it. Otherwise:
- Infer from conversation context if the user mentioned a change
- Auto-select if only one active change exists
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
2. **Check status to understand the schema**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand:
- `schemaName`: The workflow being used (e.g., "spec-driven")
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
3. **Get apply instructions**
```bash
openspec instructions apply --change "<name>" --json
```
This returns:
- Context file paths (varies by schema)
- Progress (total, complete, remaining)
- Task list with status
- Dynamic instruction based on current state
**Handle states:**
- If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx:continue`
- If `state: "all_done"`: congratulate, suggest archive
- Otherwise: proceed to implementation
4. **Read context files**
Read the files listed in `contextFiles` from the apply instructions output.
The files depend on the schema being used:
- **spec-driven**: proposal, specs, design, tasks
- Other schemas: follow the contextFiles from CLI output
5. **Show current progress**
Display:
- Schema being used
- Progress: "N/M tasks complete"
- Remaining tasks overview
- Dynamic instruction from CLI
6. **Implement tasks (loop until done or blocked)**
For each pending task:
- Show which task is being worked on
- Make the code changes required
- Keep changes minimal and focused
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
- Continue to next task
**Pause if:**
- Task is unclear → ask for clarification
- Implementation reveals a design issue → suggest updating artifacts
- Error or blocker encountered → report and wait for guidance
- User interrupts
7. **On completion or pause, show status**
Display:
- Tasks completed this session
- Overall progress: "N/M tasks complete"
- If all done: suggest archive
- If paused: explain why and wait for guidance
**Output During Implementation**
```
## Implementing: <change-name> (schema: <schema-name>)
Working on task 3/7: <task description>
[...implementation happening...]
✓ Task complete
Working on task 4/7: <task description>
[...implementation happening...]
✓ Task complete
```
**Output On Completion**
```
## Implementation Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 7/7 tasks complete ✓
### Completed This Session
- [x] Task 1
- [x] Task 2
...
All tasks complete! You can archive this change with `/opsx:archive`.
```
**Output On Pause (Issue Encountered)**
```
## Implementation Paused
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 4/7 tasks complete
### Issue Encountered
<description of the issue>
**Options:**
1. <option 1>
2. <option 2>
3. Other approach
What would you like to do?
```
**Guardrails**
- Keep going through tasks until done or blocked
- Always read context files before starting (from the apply instructions output)
- If task is ambiguous, pause and ask before implementing
- If implementation reveals issues, pause and suggest artifact updates
- Keep code changes minimal and scoped to each task
- Update task checkbox immediately after completing each task
- Pause on errors, blockers, or unclear requirements - don't guess
- Use contextFiles from CLI output, don't assume specific file names
**Fluid Workflow Integration**
This skill supports the "actions on a change" model:
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly

View File

@@ -0,0 +1,157 @@
---
name: "OPSX: Archive"
description: Archive a completed change in the experimental workflow
category: Workflow
tags: [workflow, archive, experimental]
---
Archive a completed change in the experimental workflow.
**Input**: Optionally specify a change name after `/opsx:archive` (e.g., `/opsx:archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **If no change name provided, prompt for selection**
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
Show only active changes (not already archived).
Include the schema used for each change if available.
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Check artifact completion status**
Run `openspec status --change "<name>" --json` to check artifact completion.
Parse the JSON to understand:
- `schemaName`: The workflow being used
- `artifacts`: List of artifacts with their status (`done` or other)
**If any artifacts are not `done`:**
- Display warning listing incomplete artifacts
- Prompt user for confirmation to continue
- Proceed if user confirms
3. **Check task completion status**
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
**If incomplete tasks found:**
- Display warning showing count of incomplete tasks
- Prompt user for confirmation to continue
- Proceed if user confirms
**If no tasks file exists:** Proceed without task-related warning.
4. **Assess delta spec sync state**
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
**If delta specs exist:**
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
- Determine what changes would be applied (adds, modifications, removals, renames)
- Show a combined summary before prompting
**Prompt options:**
- If changes needed: "Sync now (recommended)", "Archive without syncing"
- If already synced: "Archive now", "Sync anyway", "Cancel"
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
5. **Perform the archive**
Create the archive directory if it doesn't exist:
```bash
mkdir -p openspec/changes/archive
```
Generate target name using current date: `YYYY-MM-DD-<change-name>`
**Check if target already exists:**
- If yes: Fail with error, suggest renaming existing archive or using different date
- If no: Move the change directory to archive
```bash
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
```
6. **Display summary**
Show archive completion summary including:
- Change name
- Schema that was used
- Archive location
- Spec sync status (synced / sync skipped / no delta specs)
- Note about any warnings (incomplete artifacts/tasks)
**Output On Success**
```
## Archive Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
**Specs:** ✓ Synced to main specs
All artifacts complete. All tasks complete.
```
**Output On Success (No Delta Specs)**
```
## Archive Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
**Specs:** No delta specs
All artifacts complete. All tasks complete.
```
**Output On Success With Warnings**
```
## Archive Complete (with warnings)
**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
**Specs:** Sync skipped (user chose to skip)
**Warnings:**
- Archived with 2 incomplete artifacts
- Archived with 3 incomplete tasks
- Delta spec sync was skipped (user chose to skip)
Review the archive if this was not intentional.
```
**Output On Error (Archive Exists)**
```
## Archive Failed
**Change:** <change-name>
**Target:** openspec/changes/archive/YYYY-MM-DD-<name>/
Target archive directory already exists.
**Options:**
1. Rename the existing archive
2. Delete the existing archive if it's a duplicate
3. Wait until a different date to archive
```
**Guardrails**
- Always prompt for change selection if not provided
- Use artifact graph (openspec status --json) for completion checking
- Don't block archive on warnings - just inform and confirm
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
- Show clear summary of what happened
- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven)
- If delta specs exist, always run the sync assessment and show the combined summary before prompting

View File

@@ -0,0 +1,173 @@
---
name: "OPSX: Explore"
description: "Enter explore mode - think through ideas, investigate problems, clarify requirements"
category: Workflow
tags: [workflow, explore, experimental, thinking]
---
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
**Input**: The argument after `/opsx:explore` is whatever the user wants to think about. Could be:
- A vague idea: "real-time collaboration"
- A specific problem: "the auth system is getting unwieldy"
- A change name: "add-dark-mode" (to explore in context of that change)
- A comparison: "postgres vs sqlite for this"
- Nothing (just enter explore mode)
---
## The Stance
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
- **Adaptive** - Follow interesting threads, pivot when new information emerges
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
---
## What You Might Do
Depending on what the user brings, you might:
**Explore the problem space**
- Ask clarifying questions that emerge from what they said
- Challenge assumptions
- Reframe the problem
- Find analogies
**Investigate the codebase**
- Map existing architecture relevant to the discussion
- Find integration points
- Identify patterns already in use
- Surface hidden complexity
**Compare options**
- Brainstorm multiple approaches
- Build comparison tables
- Sketch tradeoffs
- Recommend a path (if asked)
**Visualize**
```
┌─────────────────────────────────────────┐
│ Use ASCII diagrams liberally │
├─────────────────────────────────────────┤
│ │
│ ┌────────┐ ┌────────┐ │
│ │ State │────────▶│ State │ │
│ │ A │ │ B │ │
│ └────────┘ └────────┘ │
│ │
│ System diagrams, state machines, │
│ data flows, architecture sketches, │
│ dependency graphs, comparison tables │
│ │
└─────────────────────────────────────────┘
```
**Surface risks and unknowns**
- Identify what could go wrong
- Find gaps in understanding
- Suggest spikes or investigations
---
## OpenSpec Awareness
You have full context of the OpenSpec system. Use it naturally, don't force it.
### Check for context
At the start, quickly check what exists:
```bash
openspec list --json
```
This tells you:
- If there are active changes
- Their names, schemas, and status
- What the user might be working on
If the user mentioned a specific change name, read its artifacts for context.
### When no change exists
Think freely. When insights crystallize, you might offer:
- "This feels solid enough to start a change. Want me to create a proposal?"
- Or keep exploring - no pressure to formalize
### When a change exists
If the user mentions a change or you detect one is relevant:
1. **Read existing artifacts for context**
- `openspec/changes/<name>/proposal.md`
- `openspec/changes/<name>/design.md`
- `openspec/changes/<name>/tasks.md`
- etc.
2. **Reference them naturally in conversation**
- "Your design mentions using Redis, but we just realized SQLite fits better..."
- "The proposal scopes this to premium users, but we're now thinking everyone..."
3. **Offer to capture when decisions are made**
| Insight Type | Where to Capture |
|--------------|------------------|
| New requirement discovered | `specs/<capability>/spec.md` |
| Requirement changed | `specs/<capability>/spec.md` |
| Design decision made | `design.md` |
| Scope changed | `proposal.md` |
| New work identified | `tasks.md` |
| Assumption invalidated | Relevant artifact |
Example offers:
- "That's a design decision. Capture it in design.md?"
- "This is a new requirement. Add it to specs?"
- "This changes scope. Update the proposal?"
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
---
## What You Don't Have To Do
- Follow a script
- Ask the same questions every time
- Produce a specific artifact
- Reach a conclusion
- Stay on topic if a tangent is valuable
- Be brief (this is thinking time)
---
## Ending Discovery
There's no required ending. Discovery might:
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
- **Result in artifact updates**: "Updated design.md with these decisions"
- **Just provide clarity**: User has what they need, moves on
- **Continue later**: "We can pick this up anytime"
When things crystallize, you might offer a summary - but it's optional. Sometimes the thinking IS the value.
---
## Guardrails
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
- **Don't fake understanding** - If something is unclear, dig deeper
- **Don't rush** - Discovery is thinking time, not task time
- **Don't force structure** - Let patterns emerge naturally
- **Don't auto-capture** - Offer to save insights, don't just do it
- **Do visualize** - A good diagram is worth many paragraphs
- **Do explore the codebase** - Ground discussions in reality
- **Do question assumptions** - Including the user's and your own

View File

@@ -0,0 +1,106 @@
---
name: "OPSX: Propose"
description: Propose a new change - create it and generate all artifacts in one step
category: Workflow
tags: [workflow, artifacts, experimental]
---
Propose a new change - create the change and generate all artifacts in one step.
I'll create a change with artifacts:
- proposal.md (what & why)
- design.md (how)
- tasks.md (implementation steps)
When ready to implement, run /opsx:apply
---
**Input**: The argument after `/opsx:propose` is the change name (kebab-case), OR a description of what the user wants to build.
**Steps**
1. **If no input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2. **Create the change directory**
```bash
openspec new change "<name>"
```
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
3. **Get the artifact build order**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to get:
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
- `artifacts`: list of all artifacts with their status and dependencies
4. **Create artifacts in sequence until apply-ready**
Use the **TodoWrite tool** to track progress through the artifacts.
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
a. **For each artifact that is `ready` (dependencies satisfied)**:
- Get instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- The instructions JSON includes:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance for this artifact type
- `outputPath`: Where to write the artifact
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context
- Create the artifact file using `template` as the structure
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "Created <artifact-id>"
b. **Continue until all `applyRequires` artifacts are complete**
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
- Stop when all `applyRequires` artifacts are done
c. **If an artifact requires user input** (unclear context):
- Use **AskUserQuestion tool** to clarify
- Then continue with creation
5. **Show final status**
```bash
openspec status --change "<name>"
```
**Output**
After completing all artifacts, summarize:
- Change name and location
- List of artifacts created with brief descriptions
- What's ready: "All artifacts created! Ready for implementation."
- Prompt: "Run `/opsx:apply` to start implementing."
**Artifact Creation Guidelines**
- Follow the `instruction` field from `openspec instructions` for each artifact type
- The schema defines what each artifact should contain - follow it
- Read dependency artifacts for context before creating new ones
- Use `template` as the structure for your output file - fill in its sections
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
- These guide what you write, but should never appear in the output
**Guardrails**
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
- Always read dependency artifacts before creating a new one
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
- If a change with that name already exists, ask if user wants to continue it or create a new one
- Verify each artifact file exists after writing before proceeding to next

View File

@@ -0,0 +1,156 @@
---
name: openspec-apply-change
description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.2.0"
---
Implement tasks from an OpenSpec change.
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **Select the change**
If a name is provided, use it. Otherwise:
- Infer from conversation context if the user mentioned a change
- Auto-select if only one active change exists
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
2. **Check status to understand the schema**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand:
- `schemaName`: The workflow being used (e.g., "spec-driven")
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
3. **Get apply instructions**
```bash
openspec instructions apply --change "<name>" --json
```
This returns:
- Context file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
- Progress (total, complete, remaining)
- Task list with status
- Dynamic instruction based on current state
**Handle states:**
- If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change
- If `state: "all_done"`: congratulate, suggest archive
- Otherwise: proceed to implementation
4. **Read context files**
Read the files listed in `contextFiles` from the apply instructions output.
The files depend on the schema being used:
- **spec-driven**: proposal, specs, design, tasks
- Other schemas: follow the contextFiles from CLI output
5. **Show current progress**
Display:
- Schema being used
- Progress: "N/M tasks complete"
- Remaining tasks overview
- Dynamic instruction from CLI
6. **Implement tasks (loop until done or blocked)**
For each pending task:
- Show which task is being worked on
- Make the code changes required
- Keep changes minimal and focused
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
- Continue to next task
**Pause if:**
- Task is unclear → ask for clarification
- Implementation reveals a design issue → suggest updating artifacts
- Error or blocker encountered → report and wait for guidance
- User interrupts
7. **On completion or pause, show status**
Display:
- Tasks completed this session
- Overall progress: "N/M tasks complete"
- If all done: suggest archive
- If paused: explain why and wait for guidance
**Output During Implementation**
```
## Implementing: <change-name> (schema: <schema-name>)
Working on task 3/7: <task description>
[...implementation happening...]
✓ Task complete
Working on task 4/7: <task description>
[...implementation happening...]
✓ Task complete
```
**Output On Completion**
```
## Implementation Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 7/7 tasks complete ✓
### Completed This Session
- [x] Task 1
- [x] Task 2
...
All tasks complete! Ready to archive this change.
```
**Output On Pause (Issue Encountered)**
```
## Implementation Paused
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 4/7 tasks complete
### Issue Encountered
<description of the issue>
**Options:**
1. <option 1>
2. <option 2>
3. Other approach
What would you like to do?
```
**Guardrails**
- Keep going through tasks until done or blocked
- Always read context files before starting (from the apply instructions output)
- If task is ambiguous, pause and ask before implementing
- If implementation reveals issues, pause and suggest artifact updates
- Keep code changes minimal and scoped to each task
- Update task checkbox immediately after completing each task
- Pause on errors, blockers, or unclear requirements - don't guess
- Use contextFiles from CLI output, don't assume specific file names
**Fluid Workflow Integration**
This skill supports the "actions on a change" model:
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly

View File

@@ -0,0 +1,114 @@
---
name: openspec-archive-change
description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.2.0"
---
Archive a completed change in the experimental workflow.
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **If no change name provided, prompt for selection**
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
Show only active changes (not already archived).
Include the schema used for each change if available.
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Check artifact completion status**
Run `openspec status --change "<name>" --json` to check artifact completion.
Parse the JSON to understand:
- `schemaName`: The workflow being used
- `artifacts`: List of artifacts with their status (`done` or other)
**If any artifacts are not `done`:**
- Display warning listing incomplete artifacts
- Use **AskUserQuestion tool** to confirm user wants to proceed
- Proceed if user confirms
3. **Check task completion status**
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
**If incomplete tasks found:**
- Display warning showing count of incomplete tasks
- Use **AskUserQuestion tool** to confirm user wants to proceed
- Proceed if user confirms
**If no tasks file exists:** Proceed without task-related warning.
4. **Assess delta spec sync state**
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
**If delta specs exist:**
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
- Determine what changes would be applied (adds, modifications, removals, renames)
- Show a combined summary before prompting
**Prompt options:**
- If changes needed: "Sync now (recommended)", "Archive without syncing"
- If already synced: "Archive now", "Sync anyway", "Cancel"
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
5. **Perform the archive**
Create the archive directory if it doesn't exist:
```bash
mkdir -p openspec/changes/archive
```
Generate target name using current date: `YYYY-MM-DD-<change-name>`
**Check if target already exists:**
- If yes: Fail with error, suggest renaming existing archive or using different date
- If no: Move the change directory to archive
```bash
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
```
6. **Display summary**
Show archive completion summary including:
- Change name
- Schema that was used
- Archive location
- Whether specs were synced (if applicable)
- Note about any warnings (incomplete artifacts/tasks)
**Output On Success**
```
## Archive Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
**Specs:** ✓ Synced to main specs (or "No delta specs" or "Sync skipped")
All artifacts complete. All tasks complete.
```
**Guardrails**
- Always prompt for change selection if not provided
- Use artifact graph (openspec status --json) for completion checking
- Don't block archive on warnings - just inform and confirm
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
- Show clear summary of what happened
- If sync is requested, use openspec-sync-specs approach (agent-driven)
- If delta specs exist, always run the sync assessment and show the combined summary before prompting

View File

@@ -0,0 +1,288 @@
---
name: openspec-explore
description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.2.0"
---
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
---
## The Stance
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
- **Adaptive** - Follow interesting threads, pivot when new information emerges
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
---
## What You Might Do
Depending on what the user brings, you might:
**Explore the problem space**
- Ask clarifying questions that emerge from what they said
- Challenge assumptions
- Reframe the problem
- Find analogies
**Investigate the codebase**
- Map existing architecture relevant to the discussion
- Find integration points
- Identify patterns already in use
- Surface hidden complexity
**Compare options**
- Brainstorm multiple approaches
- Build comparison tables
- Sketch tradeoffs
- Recommend a path (if asked)
**Visualize**
```
┌─────────────────────────────────────────┐
│ Use ASCII diagrams liberally │
├─────────────────────────────────────────┤
│ │
│ ┌────────┐ ┌────────┐ │
│ │ State │────────▶│ State │ │
│ │ A │ │ B │ │
│ └────────┘ └────────┘ │
│ │
│ System diagrams, state machines, │
│ data flows, architecture sketches, │
│ dependency graphs, comparison tables │
│ │
└─────────────────────────────────────────┘
```
**Surface risks and unknowns**
- Identify what could go wrong
- Find gaps in understanding
- Suggest spikes or investigations
---
## OpenSpec Awareness
You have full context of the OpenSpec system. Use it naturally, don't force it.
### Check for context
At the start, quickly check what exists:
```bash
openspec list --json
```
This tells you:
- If there are active changes
- Their names, schemas, and status
- What the user might be working on
### When no change exists
Think freely. When insights crystallize, you might offer:
- "This feels solid enough to start a change. Want me to create a proposal?"
- Or keep exploring - no pressure to formalize
### When a change exists
If the user mentions a change or you detect one is relevant:
1. **Read existing artifacts for context**
- `openspec/changes/<name>/proposal.md`
- `openspec/changes/<name>/design.md`
- `openspec/changes/<name>/tasks.md`
- etc.
2. **Reference them naturally in conversation**
- "Your design mentions using Redis, but we just realized SQLite fits better..."
- "The proposal scopes this to premium users, but we're now thinking everyone..."
3. **Offer to capture when decisions are made**
| Insight Type | Where to Capture |
|--------------|------------------|
| New requirement discovered | `specs/<capability>/spec.md` |
| Requirement changed | `specs/<capability>/spec.md` |
| Design decision made | `design.md` |
| Scope changed | `proposal.md` |
| New work identified | `tasks.md` |
| Assumption invalidated | Relevant artifact |
Example offers:
- "That's a design decision. Capture it in design.md?"
- "This is a new requirement. Add it to specs?"
- "This changes scope. Update the proposal?"
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
---
## What You Don't Have To Do
- Follow a script
- Ask the same questions every time
- Produce a specific artifact
- Reach a conclusion
- Stay on topic if a tangent is valuable
- Be brief (this is thinking time)
---
## Handling Different Entry Points
**User brings a vague idea:**
```
User: I'm thinking about adding real-time collaboration
You: Real-time collab is a big space. Let me think about this...
COLLABORATION SPECTRUM
════════════════════════════════════════════
Awareness Coordination Sync
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Presence│ │Cursors │ │ CRDT │
│ "3 │ │ Multi │ │Conflict│
│online" │ │ select │ │ free │
└────────┘ └────────┘ └────────┘
│ │ │
trivial moderate complex
Where's your head at?
```
**User brings a specific problem:**
```
User: The auth system is a mess
You: [reads codebase]
┌─────────────────────────────────────────────┐
│ CURRENT AUTH FLOW │
└─────────────────────────────────────────────┘
┌─────────────┼─────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Google │ │ GitHub │ │ Email │
│ OAuth │ │ OAuth │ │ Magic │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└─────────────┼─────────────┘
┌───────────┐
│ Session │
└─────┬─────┘
┌───────────┐
│ Perms │
└───────────┘
I see three tangles. Which one's burning?
```
**User is stuck mid-implementation:**
```
User: /opsx:explore add-auth-system
The OAuth integration is more complex than expected
You: [reads change artifacts]
You're on task 4: "Implement OAuth flow"
Let me trace what's involved...
[draws diagram, explores options, suggests paths]
Want to update the design to reflect this?
Or add a spike task to investigate?
```
**User wants to compare options:**
```
User: Should we use Postgres or SQLite?
You: Generic answer is boring. What's the context?
User: A CLI tool that tracks local dev environments
You: That changes everything.
┌─────────────────────────────────────────────────┐
│ CLI TOOL DATA STORAGE │
└─────────────────────────────────────────────────┘
Key constraints:
• No daemon running
• Must work offline
• Single user
SQLite Postgres
Deployment embedded ✓ needs server ✗
Offline yes ✓ no ✗
Single file yes ✓ no ✗
SQLite. Not even close.
Unless... is there a sync component?
```
---
## Ending Discovery
There's no required ending. Discovery might:
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
- **Result in artifact updates**: "Updated design.md with these decisions"
- **Just provide clarity**: User has what they need, moves on
- **Continue later**: "We can pick this up anytime"
When it feels like things are crystallizing, you might summarize:
```
## What We Figured Out
**The problem**: [crystallized understanding]
**The approach**: [if one emerged]
**Open questions**: [if any remain]
**Next steps** (if ready):
- Create a change proposal
- Keep exploring: just keep talking
```
But this summary is optional. Sometimes the thinking IS the value.
---
## Guardrails
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
- **Don't fake understanding** - If something is unclear, dig deeper
- **Don't rush** - Discovery is thinking time, not task time
- **Don't force structure** - Let patterns emerge naturally
- **Don't auto-capture** - Offer to save insights, don't just do it
- **Do visualize** - A good diagram is worth many paragraphs
- **Do explore the codebase** - Ground discussions in reality
- **Do question assumptions** - Including the user's and your own

View File

@@ -0,0 +1,110 @@
---
name: openspec-propose
description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.2.0"
---
Propose a new change - create the change and generate all artifacts in one step.
I'll create a change with artifacts:
- proposal.md (what & why)
- design.md (how)
- tasks.md (implementation steps)
When ready to implement, run /opsx:apply
---
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
**Steps**
1. **If no clear input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2. **Create the change directory**
```bash
openspec new change "<name>"
```
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
3. **Get the artifact build order**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to get:
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
- `artifacts`: list of all artifacts with their status and dependencies
4. **Create artifacts in sequence until apply-ready**
Use the **TodoWrite tool** to track progress through the artifacts.
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
a. **For each artifact that is `ready` (dependencies satisfied)**:
- Get instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- The instructions JSON includes:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance for this artifact type
- `outputPath`: Where to write the artifact
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context
- Create the artifact file using `template` as the structure
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "Created <artifact-id>"
b. **Continue until all `applyRequires` artifacts are complete**
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
- Stop when all `applyRequires` artifacts are done
c. **If an artifact requires user input** (unclear context):
- Use **AskUserQuestion tool** to clarify
- Then continue with creation
5. **Show final status**
```bash
openspec status --change "<name>"
```
**Output**
After completing all artifacts, summarize:
- Change name and location
- List of artifacts created with brief descriptions
- What's ready: "All artifacts created! Ready for implementation."
- Prompt: "Run `/opsx:apply` or ask me to implement to start working on the tasks."
**Artifact Creation Guidelines**
- Follow the `instruction` field from `openspec instructions` for each artifact type
- The schema defines what each artifact should contain - follow it
- Read dependency artifacts for context before creating new ones
- Use `template` as the structure for your output file - fill in its sections
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
- These guide what you write, but should never appear in the output
**Guardrails**
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
- Always read dependency artifacts before creating a new one
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
- If a change with that name already exists, ask if user wants to continue it or create a new one
- Verify each artifact file exists after writing before proceeding to next

251
.gitignore vendored
View File

@@ -1,218 +1,57 @@
# ---> Python # ---> .NET / C#
# Byte-compiled / optimized / DLL files ## Build output
__pycache__/ bin/
*.py[cod] obj/
*$py.class publish/
# C extensions ## NuGet
*.so *.nupkg
**/packages/
*.nuget.props
*.nuget.targets
# Distribution / packaging ## User-specific files
.Python *.user
build/ *.suo
develop-eggs/ *.userosscache
dist/ *.sln.docstates
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller ## IDE
# Usually these files are written by a python script from a template .vs/
# before PyInstaller builds the exe, so as to inject date/other infos into it. .idea/
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# ---> JupyterNotebooks
# gitignore template for Jupyter Notebooks
# website: http://jupyter.org/
.ipynb_checkpoints
*/.ipynb_checkpoints/*
# IPython
profile_default/
ipython_config.py
# Remove previous ipynb_checkpoints
# git rm -r .ipynb_checkpoints/
# ---> VisualStudioCode
.vscode/* .vscode/*
!.vscode/settings.json !.vscode/settings.json
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code ## Rider
.history/ *.sln.iml
# Built Visual Studio Code Extensions ## Blazor WASM publish artifacts
*.vsix **/wwwroot/_framework/
# ---> VirtualEnv ## ReSharper / DotSettings
# Virtualenv *.DotSettings.user
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ _ReSharper*/
.Python
[Bb]in
[Ii]nclude
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
.venv
pip-selfcheck.json
## Test results
TestResults/
*.trx
coverage*.xml
coverage*.json
## Secrets and environment
*.env
appsettings.*.json
!appsettings.json
!appsettings.Development.json
## OS files
.DS_Store
Thumbs.db
*.swp
*~
## Misc
*.log

96
ARCHITECTURE.md Normal file
View 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.

43
CLAUDE.md Normal file
View File

@@ -0,0 +1,43 @@
## Project
**Chat Agent WebApp**
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 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**: 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
## Technology Stack
| 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 |
## Architecture
See [ARCHITECTURE.md](ARCHITECTURE.md) for detailed hosting model discussion, API design decisions, and project structure rationale.
**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
## Conventions
- 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

69
ChatAgent.sln Normal file
View File

@@ -0,0 +1,69 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatAgent.Client", "src\ChatAgent.Client\ChatAgent.Client.csproj", "{600EA0C4-7CDE-4807-BE3C-30A6D2242392}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatAgent.Api", "src\ChatAgent.Api\ChatAgent.Api.csproj", "{467D4550-6F9A-456E-B99C-0ABE94070ECF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatAgent.Shared", "src\ChatAgent.Shared\ChatAgent.Shared.csproj", "{06182E3F-BC78-449B-ADF6-D9EE49E48945}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Debug|Any CPU.Build.0 = Debug|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Debug|x64.ActiveCfg = Debug|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Debug|x64.Build.0 = Debug|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Debug|x86.ActiveCfg = Debug|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Debug|x86.Build.0 = Debug|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Release|Any CPU.ActiveCfg = Release|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Release|Any CPU.Build.0 = Release|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Release|x64.ActiveCfg = Release|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Release|x64.Build.0 = Release|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Release|x86.ActiveCfg = Release|Any CPU
{600EA0C4-7CDE-4807-BE3C-30A6D2242392}.Release|x86.Build.0 = Release|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Debug|x64.ActiveCfg = Debug|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Debug|x64.Build.0 = Debug|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Debug|x86.ActiveCfg = Debug|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Debug|x86.Build.0 = Debug|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Release|Any CPU.Build.0 = Release|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Release|x64.ActiveCfg = Release|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Release|x64.Build.0 = Release|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Release|x86.ActiveCfg = Release|Any CPU
{467D4550-6F9A-456E-B99C-0ABE94070ECF}.Release|x86.Build.0 = Release|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Debug|Any CPU.Build.0 = Debug|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Debug|x64.ActiveCfg = Debug|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Debug|x64.Build.0 = Debug|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Debug|x86.ActiveCfg = Debug|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Debug|x86.Build.0 = Debug|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Release|Any CPU.ActiveCfg = Release|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Release|Any CPU.Build.0 = Release|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Release|x64.ActiveCfg = Release|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Release|x64.Build.0 = Release|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Release|x86.ActiveCfg = Release|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{600EA0C4-7CDE-4807-BE3C-30A6D2242392} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{467D4550-6F9A-456E-B99C-0ABE94070ECF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{06182E3F-BC78-449B-ADF6-D9EE49E48945} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
EndGlobal

View File

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

6
global.json Normal file
View File

@@ -0,0 +1,6 @@
{
"sdk": {
"version": "9.0.312",
"rollForward": "latestPatch"
}
}

20
openspec/config.yaml Normal file
View File

@@ -0,0 +1,20 @@
schema: spec-driven
# Project context (optional)
# This is shown to AI when creating artifacts.
# Add your tech stack, conventions, style guides, domain knowledge, etc.
# Example:
# context: |
# Tech stack: TypeScript, React, Node.js
# We use conventional commits
# Domain: e-commerce platform
# Per-artifact rules (optional)
# Add custom rules for specific artifacts.
# Example:
# rules:
# proposal:
# - Keep proposals under 500 words
# - Always include a "Non-goals" section
# tasks:
# - Break tasks into chunks of max 2 hours

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ChatAgent.Shared\ChatAgent.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,47 @@
// HealthController.cs -- API endpoint that proves the WASM-to-API connection works.
//
// This is the first controller in the project. It exists to verify that:
// 1. The API server starts and responds to HTTP requests
// 2. CORS allows the Blazor WASM client to call across origins
// 3. The shared HealthResponse DTO serializes/deserializes correctly
//
// In ASP.NET Core, a "controller" is a class that handles HTTP requests.
// The framework routes requests to controller methods based on attributes.
using ChatAgent.Shared.Models;
using Microsoft.AspNetCore.Mvc;
namespace ChatAgent.Api.Controllers
{
// [ApiController] enables several conveniences for API development:
// - Automatic HTTP 400 responses for invalid model state
// - Automatic inference of [FromBody] for complex types
// - Problem details responses for error status codes
// It signals "this class handles API requests, not HTML pages."
[ApiController]
// [Route("api/[controller]")] maps this class to the URL path /api/health.
// The [controller] token is replaced with the class name minus "Controller",
// so HealthController becomes "health" -> /api/health.
[Route("api/[controller]")]
public class HealthController : ControllerBase
{
// [HttpGet] maps this method to HTTP GET requests at the controller's route.
// When a client sends GET /api/health, ASP.NET Core calls this method.
// It returns an IActionResult, which gives us control over the HTTP status code.
[HttpGet]
public IActionResult Get()
{
// Return HTTP 200 OK with a HealthResponse body.
// The framework serializes the object to JSON automatically
// because this is an [ApiController].
// DateTime.UtcNow provides a live timestamp so the client can verify
// the response is fresh (not cached).
return Ok(new HealthResponse
{
Status = "healthy",
Timestamp = DateTime.UtcNow
});
}
}
}

View File

@@ -0,0 +1,59 @@
// Program.cs -- ASP.NET Core Web API entry point for ChatAgent.
//
// This is the backend server. In Phase 1, it only serves a health check endpoint.
// In later phases, it will proxy OpenAI API calls (keeping the API key server-side)
// and manage JSON file storage for conversation persistence.
//
// ASP.NET Core uses a "builder pattern": first configure services (DI container),
// then build the app, configure middleware pipeline, and run.
var builder = WebApplication.CreateBuilder(args);
// --- Service Registration (Dependency Injection container) ---
// AddControllers() registers MVC controller services so ASP.NET Core discovers
// classes decorated with [ApiController]. We use Controllers (not Minimal API)
// for explicit structure -- each controller is a separate file with clear routing (D-05).
builder.Services.AddControllers();
// AddCors() registers Cross-Origin Resource Sharing services.
// CORS is REQUIRED because the Blazor WASM client runs on a different origin
// (https://localhost:5200) than this API (https://localhost:7100).
// Browsers block cross-origin HTTP requests by default as a security measure.
// Without this policy, the client's fetch() calls to the API would be rejected.
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowBlazorClient", policy =>
{
policy
// Only allow requests from the Blazor WASM client origin
.WithOrigins("https://localhost:5200")
// Allow any HTTP header (Content-Type, Accept, etc.)
.AllowAnyHeader()
// Allow any HTTP method (GET, POST, PUT, DELETE, etc.)
.AllowAnyMethod();
});
});
var app = builder.Build();
// --- Middleware Pipeline ---
// Middleware order matters in ASP.NET Core -- each middleware runs in the order
// it is registered. CORS must be applied before routing and authorization
// so that preflight (OPTIONS) requests are handled correctly.
// Apply the CORS policy globally -- every response includes the correct
// Access-Control-Allow-Origin header for the Blazor client's origin.
app.UseCors("AllowBlazorClient");
// UseAuthorization() enables the authorization middleware. Even though we have
// no auth in Phase 1, it is included because ASP.NET Core expects it in the
// pipeline when controllers are used. It is a no-op without [Authorize] attributes.
app.UseAuthorization();
// MapControllers() scans the assembly for all classes with [ApiController]
// and maps their routes. This is what connects HealthController's
// [Route("api/[controller]")] to the URL path /api/health.
app.MapControllers();
app.Run();

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:7000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7100;http://localhost:7000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,12 @@
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<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>
<ProjectReference Include="..\ChatAgent.Shared\ChatAgent.Shared.csproj" />
</ItemGroup>
</Project>

View 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();
}
}
}

View 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}";
}
}

View File

@@ -0,0 +1,43 @@
@* MainLayout.razor -- The root layout component for the application.
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 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.
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
@* 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>

View File

@@ -0,0 +1,77 @@
.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;
}
}

View File

@@ -0,0 +1,39 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">ChatAgent.Client</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
<nav class="nav flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
</NavLink>
</div>
</nav>
</div>
@code {
private bool collapseNavMenu = true;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}

View File

@@ -0,0 +1,83 @@
.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;
}
}

View 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;
}

View 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();
}
}

View File

@@ -0,0 +1,18 @@
@page "/counter"
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@@ -0,0 +1,86 @@
@* Home.razor -- The landing 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.
This page demonstrates the health check round-trip:
1. On load, it calls the API's /api/health endpoint via ChatApiClient
2. Displays the server's health status and timestamp
3. Proves CORS is working (WASM on :5200 calling API on :7100)
*@
@* @page directive maps this component to a URL route.
"/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. *@
@using ChatAgent.Client.Services
@using ChatAgent.Shared.Models
@* @inject requests a service from the Dependency Injection (DI) container.
ChatApiClient was registered in Program.cs via AddHttpClient<ChatApiClient>.
Blazor creates one instance per component and injects it here. *@
@inject ChatApiClient ApiClient
<PageTitle>Chat Agent</PageTitle>
<h1>Chat Agent</h1>
@* Conditional rendering: Blazor re-renders the component after OnInitializedAsync completes.
We show different content based on the state of our data fields. *@
@if (_healthResponse is null && _error is null)
{
@* Loading state: OnInitializedAsync has not completed yet *@
<p class="loading">Checking API connection...</p>
}
else if (_healthResponse is not null)
{
@* Success state: API responded with a HealthResponse *@
<div class="health-status">
<p><strong>API Status:</strong> @_healthResponse.Status</p>
<p><strong>Server Time:</strong> @_healthResponse.Timestamp.ToString("yyyy-MM-dd HH:mm:ss UTC")</p>
</div>
}
else if (_error is not null)
{
@* Error state: the API call failed (network error, CORS blocked, server down, etc.) *@
<div class="error-message">
<p><strong>Connection Error:</strong> @_error</p>
</div>
}
@code {
// Private fields to hold the health check result or error message.
// Blazor components use private fields for state that drives the UI.
private HealthResponse? _healthResponse;
private string? _error;
/// <summary>
/// OnInitializedAsync is a Blazor lifecycle method called once when the component
/// is first rendered. It runs after the component receives its initial parameters.
///
/// Because it returns Task, Blazor awaits it and automatically calls
/// StateHasChanged() when it completes, triggering a re-render.
/// This means we do NOT need to call StateHasChanged() manually here.
///
/// The component renders twice: once immediately (showing "Checking..."),
/// and again after this method completes (showing the result or error).
/// </summary>
protected override async Task OnInitializedAsync()
{
try
{
// Call the API health endpoint via our typed HttpClient wrapper.
// This makes an HTTP GET to https://localhost:7100/api/health.
_healthResponse = await ApiClient.GetHealthAsync();
}
catch (Exception ex)
{
// Catch any exception (network error, CORS block, timeout, etc.)
// and store the message for display in the error UI.
_error = $"Failed to reach API: {ex.Message}";
}
}
}

View File

@@ -0,0 +1,57 @@
@page "/weather"
@inject HttpClient Http
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates fetching data from the server.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th aria-label="Temperature in Celsius">Temp. (C)</th>
<th aria-label="Temperature in Farenheit">Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
}
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

View File

@@ -0,0 +1,62 @@
// Program.cs -- Blazor WebAssembly application entry point for ChatAgent.
//
// This is where the WASM client is configured and launched. Unlike a server-side
// ASP.NET Core app, a Blazor WASM app runs entirely in the browser. The
// WebAssemblyHostBuilder configures:
// - Root components (what gets rendered into the HTML page)
// - Services (dependency injection container, similar to server-side DI)
// - Configuration (reads from wwwroot/appsettings.json)
//
// This file will grow as we add more services in later phases (e.g., chat state management).
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using MudBlazor.Services;
using ChatAgent.Client;
using ChatAgent.Client.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
// Add<App>("#app") tells Blazor to render the App component inside the
// <div id="app"> element in wwwroot/index.html. This is the "mount point"
// for the entire Blazor component tree.
builder.RootComponents.Add<App>("#app");
// HeadOutlet allows Razor components to modify <head> elements (e.g., <title>)
// using the <PageTitle> and <HeadContent> components. "head::after" means
// content is appended after existing head elements.
builder.RootComponents.Add<HeadOutlet>("head::after");
// Determine the API base URL by matching the protocol the app was loaded with.
// In Blazor WASM, builder.HostEnvironment.BaseAddress is the URL the browser
// used to load the app (e.g., "http://localhost:5100/" or "https://localhost:5200/").
// We check if the app was loaded over HTTPS and pick the corresponding API URL.
// IMPORTANT: wwwroot/ files are PUBLIC -- they are downloaded to the browser.
// Never put secrets (API keys, passwords) in appsettings.json for a WASM app.
// The API key lives server-side in the ChatAgent.Api project.
var isHttps = builder.HostEnvironment.BaseAddress.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
var apiBaseUrl = isHttps
? builder.Configuration["ApiBaseUrl_Https"] ?? "https://localhost:7100"
: builder.Configuration["ApiBaseUrl_Http"] ?? "http://localhost:7000";
// AddHttpClient<ChatApiClient> registers a typed HttpClient using IHttpClientFactory.
// IHttpClientFactory manages the underlying HttpMessageHandler lifetime to prevent
// socket exhaustion (a common problem with raw HttpClient in long-running apps).
// The lambda configures the client with the API base URL so ChatApiClient
// does not need to know the URL -- it is injected with a pre-configured HttpClient.
// In Blazor WASM, HttpClient uses the browser's Fetch API under the hood.
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();

View File

@@ -0,0 +1,25 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5100",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:5200;http://localhost:5100",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,53 @@
// ChatApiClient.cs -- Typed HttpClient wrapper for communicating with the ChatAgent API.
//
// WHY A TYPED CLIENT? Instead of injecting HttpClient directly into components,
// we wrap it in a dedicated service class. This follows the "typed client" pattern (D-04):
// - Components depend on ChatApiClient, not HttpClient (loose coupling)
// - API URL paths are centralized here, not scattered across components
// - Easy to mock for testing -- swap ChatApiClient, not HttpClient
// - IHttpClientFactory manages the underlying HttpClient lifetime
//
// In Blazor WASM, HttpClient is backed by the browser's Fetch API.
// The base URL is configured in Program.cs via AddHttpClient<ChatApiClient>.
using System.Net.Http.Json;
using ChatAgent.Shared.Models;
namespace ChatAgent.Client.Services
{
/// <summary>
/// Typed HttpClient for the ChatAgent API. Each public method maps to one API endpoint.
/// Constructor injection of HttpClient is provided by IHttpClientFactory,
/// which was configured in Program.cs with the API base URL.
/// </summary>
public class ChatApiClient
{
// The HttpClient instance is injected by the DI container (IHttpClientFactory).
// It is pre-configured with BaseAddress pointing to the API server.
private readonly HttpClient _httpClient;
/// <summary>
/// Constructor receives an HttpClient from IHttpClientFactory DI registration.
/// The client is already configured with the API base URL.
/// </summary>
public ChatApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
/// <summary>
/// Calls GET /api/health on the API server and deserializes the JSON response
/// into a HealthResponse object. Returns null if deserialization fails.
///
/// GetFromJsonAsync is an extension method from System.Net.Http.Json that
/// combines the HTTP GET call and JSON deserialization into a single step.
/// It uses System.Text.Json internally (the built-in .NET JSON serializer).
/// </summary>
public async Task<HealthResponse?> GetHealthAsync()
{
// "api/health" is a relative URL -- it is appended to the BaseAddress
// configured in Program.cs (e.g., https://localhost:7100/api/health).
return await _httpClient.GetFromJsonAsync<HealthResponse>("api/health");
}
}
}

View 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);
}
}

View File

@@ -0,0 +1,20 @@
@* _Imports.razor -- Global using directives for all .razor files in this project.
Any @using directive placed here is automatically available in every .razor file
in the ChatAgent.Client project. This avoids repeating common imports in each component.
It works like a "global usings" file but specifically for Razor components.
*@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using ChatAgent.Client
@using ChatAgent.Client.Layout
@using ChatAgent.Client.Services
@using ChatAgent.Shared.Models
@using MudBlazor

View File

@@ -0,0 +1,4 @@
{
"ApiBaseUrl_Http": "http://localhost:7000",
"ApiBaseUrl_Https": "https://localhost:7100"
}

View File

@@ -0,0 +1,117 @@
/* app.css -- Application styles for ChatAgent (Phase 1).
*
* 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.
*/
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 {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
/* Loading progress indicator -- shown while the WASM runtime downloads.
* This SVG-based progress circle is defined in index.html. */
.loading-progress {
position: relative;
display: block;
width: 8rem;
height: 8rem;
margin: 20vh auto 1rem auto;
}
.loading-progress circle {
fill: none;
stroke: #e0e0e0;
stroke-width: 0.6rem;
transform-origin: 50% 50%;
transform: rotate(-90deg);
}
.loading-progress circle:last-child {
stroke: #1b6ec2;
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
transition: stroke-dasharray 0.05s ease-in-out;
}
.loading-progress-text {
position: absolute;
text-align: center;
font-weight: bold;
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}
.loading-progress-text:after {
content: var(--blazor-load-percentage-text, "Loading");
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ChatAgent.Client</title>
<base href="/" />
<link rel="stylesheet" href="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>
<div id="app">
<svg class="loading-progress">
<circle r="40%" cx="50%" cy="50%" />
<circle r="40%" cx="50%" cy="50%" />
</svg>
<div class="loading-progress-text"></div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
<!-- 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>
</html>

View File

@@ -0,0 +1,27 @@
[
{
"date": "2022-01-06",
"temperatureC": 1,
"summary": "Freezing"
},
{
"date": "2022-01-07",
"temperatureC": 14,
"summary": "Bracing"
},
{
"date": "2022-01-08",
"temperatureC": -13,
"summary": "Freezing"
},
{
"date": "2022-01-09",
"temperatureC": -16,
"summary": "Balmy"
},
{
"date": "2022-01-10",
"temperatureC": -2,
"summary": "Chilly"
}
]

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,32 @@
// HealthResponse.cs -- Shared Data Transfer Object (DTO) for the health check endpoint.
//
// WHY SHARED? This class lives in the ChatAgent.Shared project, which is referenced by
// both the API (ChatAgent.Api) and the client (ChatAgent.Client). By sharing the type:
// - The API serializes a HealthResponse to JSON when responding to GET /api/health
// - The Client deserializes that same JSON back into a HealthResponse object
// - Both sides agree on the shape of the data -- no mismatched property names or types
//
// This is the "shared contract" pattern: one type definition, two consumers.
namespace ChatAgent.Shared.Models
{
/// <summary>
/// Data Transfer Object (DTO) returned by the API health check endpoint.
/// Contains the server's health status and current timestamp to prove
/// the response is live (not cached or stale).
/// </summary>
public class HealthResponse
{
/// <summary>
/// Server health status string (e.g., "healthy").
/// Initialized to empty string to avoid null warnings with nullable enabled.
/// </summary>
public string Status { get; set; } = string.Empty;
/// <summary>
/// Server-side UTC timestamp at the moment the health check ran.
/// The client displays this to confirm the response is fresh.
/// </summary>
public DateTime Timestamp { get; set; }
}
}