This document is a practical guide for developers adding features. It explains what each file does and where to make changes for common tasks.
The user picks a personality (Chuck Norris, Developer/QA, Kong Harald),
types in a task they need motivation for, and clicks "Get Motivated!". The
frontend sends that as a POST /api/motivate with { task, persona }. The
backend builds a prompt, sends it to Azure OpenAI, and returns a structured
JSON response with a headline, pep talk, facts, and suggestions. The frontend
renders the response as an accordion result card.
In local dev the frontend Vite dev server runs on http://localhost:5173 and
the backend API runs on http://localhost:5099. Vite is configured to
proxy any request whose path starts with /api to the backend, so the
frontend can call /api/motivate without hardcoding a host or dealing with
CORS during development.
The proxy is defined in src/HuMotivatoren.Web/vite.config.ts:
server: {
proxy: {
'/api': 'http://localhost:5099',
},
}The backend configures CORS (Program.cs) to allow the origins listed in the
ALLOWED_ORIGINS environment variable. In development this is set to
http://localhost:5173 in .env.
The multi-stage Dockerfile builds the Vue SPA first (npm run build), then
copies the resulting dist/ folder into the .NET image as static web assets.
ASP.NET Core serves the built files directly via UseStaticFiles(). The API
and the SPA are served from the same origin (port 8080), so there is no CORS
issue and no proxy needed.
Program.cs calls app.MapFallbackToFile("index.html") so that any
non-/api path (e.g. /status) returns the SPA's index.html and Vue
Router takes over client-side.
The only data shape crossing the boundary is defined in two places that must stay in sync:
| Layer | File | What it defines |
|---|---|---|
| Backend | HuMotivatoren.Core/Models/MotivationRequest.cs |
C# records for request + response |
| Frontend | HuMotivatoren.Web/src/api/client.ts |
TypeScript interfaces for the same shapes |
Request (POST /api/motivate body):
{ "task": "read the news", "persona": "chuck-norris" }Response (200 OK body):
{
"headline": "...",
"pep": "...",
"facts": ["...", "..."],
"suggestions": ["...", "..."]
}The backend enforces the response shape via OpenAI's structured-output JSON
Schema (in AzureOpenAiMotivationService.cs). The frontend trusts the shape
and casts directly — there is no runtime validation on the frontend side.
User clicks "Get Motivated!"
│
▼
HomePage.vue: submit()
calls motivate({ task, persona }) from api/client.ts
│
▼
api/client.ts: fetch POST /api/motivate
(proxied to :5099 in dev, same-origin in prod)
│
▼
MotivationEndpoints.cs: POST /api/motivate handler
receives MotivationRequest, calls IMotivationService.MotivateAsync()
│
▼
AzureOpenAiMotivationService.cs: MotivateAsync()
builds messages: SystemChatMessage + UserChatMessage (via MotivationPrompt.cs)
sends to Azure OpenAI with JSON Schema structured-output constraint
│
▼
Azure OpenAI returns JSON string
│
▼
MotivationPrompt.ParseResponse() deserializes → MotivationResponse record
│
▼
MotivationEndpoints.cs: Results.Ok(response) → HTTP 200 JSON
│
▼
api/client.ts: parses response JSON → MotivationResponse TS object
│
▼
HomePage.vue: result.value = response → renders result card
src/
HuMotivatoren.Api/ # ASP.NET Core host — HTTP endpoints, startup
HuMotivatoren.Core/ # Domain: models, services, prompt logic
HuMotivatoren.Tests.Unit/ # xUnit unit tests (no HTTP host)
HuMotivatoren.Tests.Integration/ # xUnit API tests via WebApplicationFactory
HuMotivatoren.Web/ # Vue 3 SPA (frontend)
src/HuMotivatoren.Api/Program.cs
The startup file. Registers all services, configures CORS, mounts endpoint groups, and serves the built SPA as static files. If you add a new service or middleware, this is where you wire it in.
Key things it does:
- Loads
.envin development viaDotNetEnv - Reads Azure OpenAI config from environment variables into
AzureOpenAiOptions - Registers
IMotivationService→AzureOpenAiMotivationServiceas a singleton - Calls
app.MapMotivationEndpoints()andapp.MapStatusEndpoints() - Falls back to
index.htmlfor SPA client-side routing
All endpoints live in src/HuMotivatoren.Api/Endpoints/. Each file is a
static class with an extension method on IEndpointRouteBuilder.
MotivationEndpoints.cs
Mounts the route group /api/motivate.
POST /api/motivate— accepts aMotivationRequestbody, callsIMotivationService.MotivateAsync, returns 200 withMotivationResponse.
To add a new motivation-related endpoint (e.g. GET /api/motivate/history),
add it to this file inside the same group.
StatusEndpoints.cs
Mounts health/status endpoints under /api/status. Used by the frontend
Status page and for ops monitoring.
To add a new endpoint for a different feature area, create a new file like
MyFeatureEndpoints.cs following the same pattern, then call
app.MapMyFeatureEndpoints() in Program.cs.
src/HuMotivatoren.Core/Models/MotivationRequest.cs
Defines the two C# records that travel across the API boundary:
record MotivationRequest(string Task, string? Persona = null);
record MotivationResponse(
string Headline,
string Pep,
IReadOnlyList<string> Facts,
IReadOnlyList<string> Suggestions);If you need to add a field to the request (e.g. Language, Tone) or
response (e.g. ImageUrl), change it here. Remember to also update:
MotivationPrompt.cs— the system prompt andBuildUserContentAzureOpenAiMotivationService.cs— the JSON schema for structured outputsrc/HuMotivatoren.Web/src/api/client.ts— the TypeScript interfaces
src/HuMotivatoren.Core/Services/IMotivationService.cs
interface IMotivationService {
Task<MotivationResponse> MotivateAsync(MotivationRequest request, CancellationToken ct);
}The contract between the API layer and the AI backend. If you want to add a
second implementation (e.g. a mock, a different LLM provider), implement this
interface and swap the registration in Program.cs.
src/HuMotivatoren.Core/Services/MotivationPrompt.cs
Pure static helpers — no dependencies, fully unit-testable.
| Member | Purpose |
|---|---|
SystemPrompt (const) |
The LLM system message. Defines HuMotivatoren's character, language (Norwegian bokmål), and the exact JSON schema it must return. Edit this to change the AI's personality, tone, or output structure. |
BuildUserContent(request) |
Assembles the user message sent to the model: "Oppgave: <task>\nPersona: <persona>". Edit this to change how context is presented to the model. |
ResolveTask(task) |
Falls back to "whatever you're about to do" if task is blank. |
ParseResponse(json) |
Deserializes the model's JSON into a MotivationResponse. Throws on missing fields. |
This is the most important file to edit when:
- Changing the AI's persona or tone
- Adding a new output field
- Changing how the task or persona is described to the model
src/HuMotivatoren.Core/Services/AzureOpenAiMotivationService.cs
Production implementation of IMotivationService. Talks to Azure OpenAI
using the Azure.AI.OpenAI and OpenAI NuGet packages.
- Validates that endpoint/deployment/key are configured at startup
- Sends
SystemChatMessage+UserChatMessageto the chat completion API - Uses structured output (JSON Schema mode) to guarantee the model
returns the expected shape — see
MotivationJsonSchemaat the bottom of the file - Delegates response parsing to
MotivationPrompt.ParseResponse
If you add a field to MotivationResponse, you must also update the
MotivationJsonSchema constant here, or the model will never emit the new
field.
src/HuMotivatoren.Core/Options/AzureOpenAiOptions.cs
Plain POCO holding the four Azure OpenAI config values:
Endpoint, Deployment, ApiVersion, ApiKey.
Populated in Program.cs from environment variables. In development,
values come from .env (copy .env.example → .env).
src/HuMotivatoren.Api/appsettings.json / appsettings.Development.json
Standard ASP.NET Core config. Secrets go in .env, not here.
src/HuMotivatoren.Api/Status/ contains IStatusCheck implementations:
| File | What it checks |
|---|---|
ApiCheck.cs |
API process is running |
DatabaseCheck.cs |
(Placeholder) database connectivity |
MotivationServiceCheck.cs |
Azure OpenAI is reachable / configured |
StartupInfo.cs |
Records when the process started (for uptime) |
src/HuMotivatoren.Core/Status/StatusAggregator.cs rolls them up into the
/api/status response.
To add a new health check, implement IStatusCheck in
HuMotivatoren.Api/Status/, register it as a singleton in Program.cs.
The Vue 3 SPA lives in src/HuMotivatoren.Web/src/.
src/main.ts — Mounts the Vue app, installs the router.
src/App.vue — Root layout: sticky header with navigation links (Home /
Status) and a <RouterView /> for the active page.
Pages are in src/pages/. One component per route.
pages/HomePage.vue — The main feature page.
This is the most important frontend file. It handles:
-
Personality selector — Three cards defined in the
levelsarray at the top of the<script setup>block. Each level has anid, display name, emoji, description, and Tailwind colour tokens.To add a new personality: add an entry to the
levelsarray. Theidvalue is sent to the backend aspersona. -
Task input — A textarea that appears after a personality is selected. Bound to
taskref.maxlength="200". -
Submit — Calls
motivate({ task, persona })from the API client. Setsresulton success. -
Result view — Shows
headline,pep, and accordion sections forfactsandsuggestions.To add a new response field (e.g. an image URL): add an accordion section here after updating the model, API endpoint, and TS types.
pages/StatusPage.vue — Calls getStatus() and renders the health
check table. Useful for ops; rarely needs changing.
src/router/index.ts
Defines the two routes:
| Path | Component |
|---|---|
/ |
HomePage.vue |
/status |
StatusPage.vue |
To add a new page: create src/pages/MyPage.vue, import it here, add a route
entry, and optionally add a <RouterLink> in App.vue.
src/api/client.ts
Single file that owns all communication with the backend. Contains:
| Export | Purpose |
|---|---|
MotivationRequest interface |
{ task: string, persona?: string } |
MotivationResponse interface |
{ headline, pep, facts[], suggestions[] } |
motivate(req) |
POST /api/motivate → MotivationResponse |
StatusCheck, AppStatus interfaces |
Status response shapes |
getStatus() |
GET /api/status → AppStatus |
To add a new API call: add a function and its types here. Keep all
fetch calls in this file — pages import from client.ts, not bare fetch.
src/style.css — Global CSS, Tailwind v4 import.
No tailwind.config.js — Tailwind v4 is configured via the @tailwindcss/vite
plugin. Use utility classes directly in templates; avoid @apply.
src/HuMotivatoren.Tests.Unit/
| File | Tests |
|---|---|
MotivationServiceTests.cs |
Service-level tests |
StatusAggregatorTests.cs |
Status rollup logic |
Run: dotnet test HuMotivatoren.slnx
src/HuMotivatoren.Tests.Integration/
Uses WebApplicationFactory<Program> to spin up the real API in memory.
| File | Tests |
|---|---|
MotivationEndpointsTests.cs |
POST /api/motivate contract tests |
StatusEndpointsTests.cs |
GET /api/status contract tests |
src/HuMotivatoren.Web/src/tests/
Co-located Vitest + @vue/test-utils + happy-dom tests.
| File | Tests |
|---|---|
HomePage.test.ts |
HomePage component behaviour |
StatusPage.test.ts |
StatusPage component behaviour |
Run: cd src/HuMotivatoren.Web && npm test
HomePage.vue— add entry to thelevelsarray with a uniqueidMotivationPrompt.cs— optionally mention the new persona inSystemPromptif it needs special handling- Add a frontend test for the new card in
HomePage.test.ts
MotivationRequest.cs— add to theMotivationResponserecordMotivationPrompt.cs— updateSystemPromptJSON schema comment andParseResponseto map the new fieldAzureOpenAiMotivationService.cs— updateMotivationJsonSchemaconstantclient.ts— add the field to theMotivationResponseTypeScript interfaceHomePage.vue— render the new field in the result view
- Add a handler to the appropriate
*Endpoints.csfile (or create a new one) - Call
app.MapMyEndpoints()inProgram.cs - Add a function to
client.ts - Add integration tests in
HuMotivatoren.Tests.Integration/
- Create
src/pages/MyPage.vue - Add the route to
src/router/index.ts - Add a
<RouterLink>inApp.vueif it needs nav access - Add a test file in
src/tests/