Skip to content

Latest commit

 

History

History
455 lines (318 loc) · 13.9 KB

File metadata and controls

455 lines (318 loc) · 13.9 KB

HuMotivatoren — Codebase Overview

This document is a practical guide for developers adding features. It explains what each file does and where to make changes for common tasks.


How the App Works

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.


Frontend ↔ Backend Interaction

Development (two terminals)

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.

Production / Docker

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 Request/Response Contract

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.

How a Motivate Request Flows End-to-End

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

Project Layout

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)

Backend

Entry Point

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 .env in development via DotNetEnv
  • Reads Azure OpenAI config from environment variables into AzureOpenAiOptions
  • Registers IMotivationServiceAzureOpenAiMotivationService as a singleton
  • Calls app.MapMotivationEndpoints() and app.MapStatusEndpoints()
  • Falls back to index.html for SPA client-side routing

Endpoints

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 a MotivationRequest body, calls IMotivationService.MotivateAsync, returns 200 with MotivationResponse.

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.


Domain — Models

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:

  1. MotivationPrompt.cs — the system prompt and BuildUserContent
  2. AzureOpenAiMotivationService.cs — the JSON schema for structured output
  3. src/HuMotivatoren.Web/src/api/client.ts — the TypeScript interfaces

Domain — Service Interface

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.


Domain — Prompt Logic

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

Domain — Azure OpenAI Service

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 + UserChatMessage to the chat completion API
  • Uses structured output (JSON Schema mode) to guarantee the model returns the expected shape — see MotivationJsonSchema at 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.


Configuration

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.


Status Checks

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.


Frontend

The Vue 3 SPA lives in src/HuMotivatoren.Web/src/.

Entry Point

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

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:

  1. Personality selector — Three cards defined in the levels array at the top of the <script setup> block. Each level has an id, display name, emoji, description, and Tailwind colour tokens.

    To add a new personality: add an entry to the levels array. The id value is sent to the backend as persona.

  2. Task input — A textarea that appears after a personality is selected. Bound to task ref. maxlength="200".

  3. Submit — Calls motivate({ task, persona }) from the API client. Sets result on success.

  4. Result view — Shows headline, pep, and accordion sections for facts and suggestions.

    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.


Router

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.


API Client

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/motivateMotivationResponse
StatusCheck, AppStatus interfaces Status response shapes
getStatus() GET /api/statusAppStatus

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.


Styles

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.


Tests

Backend Unit Tests

src/HuMotivatoren.Tests.Unit/

File Tests
MotivationServiceTests.cs Service-level tests
StatusAggregatorTests.cs Status rollup logic

Run: dotnet test HuMotivatoren.slnx

Backend Integration Tests

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

Frontend 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


Common Feature Addition Checklist

Add a new personality / persona

  1. HomePage.vue — add entry to the levels array with a unique id
  2. MotivationPrompt.cs — optionally mention the new persona in SystemPrompt if it needs special handling
  3. Add a frontend test for the new card in HomePage.test.ts

Add a new field to the AI response

  1. MotivationRequest.cs — add to the MotivationResponse record
  2. MotivationPrompt.cs — update SystemPrompt JSON schema comment and ParseResponse to map the new field
  3. AzureOpenAiMotivationService.cs — update MotivationJsonSchema constant
  4. client.ts — add the field to the MotivationResponse TypeScript interface
  5. HomePage.vue — render the new field in the result view

Add a new API endpoint

  1. Add a handler to the appropriate *Endpoints.cs file (or create a new one)
  2. Call app.MapMyEndpoints() in Program.cs
  3. Add a function to client.ts
  4. Add integration tests in HuMotivatoren.Tests.Integration/

Add a new frontend page

  1. Create src/pages/MyPage.vue
  2. Add the route to src/router/index.ts
  3. Add a <RouterLink> in App.vue if it needs nav access
  4. Add a test file in src/tests/