Skip to content

Latest commit

 

History

History
354 lines (261 loc) · 23 KB

File metadata and controls

354 lines (261 loc) · 23 KB

Gradelytics — Application Architecture Document

Version: 1.0 Classification: Internal Engineering / AMD Hackathon Technical Submission Author: Principal Software Architecture Review Status: Production-Grade Reference Architecture


0. Executive Summary

Gradelytics is a client-native, edge-AI-accelerated educational SaaS platform that automates rubric-based essay assessment for educators. The system is architected as a fully decoupled Single Page Application (SPA) with zero backend server dependency for its core grading workflow: all application state, credential management, and configuration persistence occur client-side, while computationally intensive natural-language inference is offloaded to a serverless edge inference layer — Fireworks AI, running on AMD Instinct GPU infrastructure.

This architectural posture yields three strategic advantages that anchor the platform's B2B SaaS value proposition:

  1. Zero-infrastructure operational cost — no application server, no database tier, and no session management layer to provision, patch, or scale.
  2. Data sovereignty by design — student essays and grading rubrics never transit or persist on a Gradelytics-owned server; the only network hop is teacher-browser → Fireworks AI inference endpoint.
  3. Sub-second time-to-first-byte and infinite horizontal scalability — the entire application is static output, served from a CDN-fronted Nginx edge container with no stateful compute to bottleneck concurrent tenants.

1. High-Level System Architecture

Gradelytics separates concerns into three strictly decoupled layers: a UI View Layer built on React 18 and Tailwind CSS 4, a Local Persistence Layer that repurposes the browser's localStorage as a lightweight client-side configuration state machine, and an Edge AI Inference Infrastructure Layer provided by Fireworks AI's OpenAI-compatible chat completions API, physically executing on AMD GPU silicon.

No proprietary backend, ORM, or relational database exists in this architecture. The browser is the runtime; localStorage is the persistence tier; Fireworks AI is the compute tier. This inversion — pushing state and orchestration to the edge/client while renting inference compute per-call — is what allows Gradelytics to operate as a stateless static bundle in production.

flowchart TB
    subgraph Client["🖥️ Client Runtime — End User Browser"]
        direction TB
        subgraph ViewLayer["UI View Layer"]
            React["React 18 Component Tree<br/>(App.tsx Root)"]
            Tailwind["Tailwind CSS 4<br/>Utility-First Styling Engine"]
            Recharts["Recharts Visualization Module<br/>(Grade Distribution / Analytics)"]
            React --- Tailwind
            React --- Recharts
        end

        subgraph PersistLayer["Local Persistence Layer — Config State Machine"]
            LS[("Browser localStorage<br/>fireworks_api_key<br/>fireworks_model_id<br/>syllabus_data<br/>grading_history")]
        end

        ViewLayer <-->|"Read/Write Credentials<br/>& App Config (sync, no I/O latency)"| PersistLayer
    end

    subgraph Edge["☁️ Edge AI Inference Infrastructure Layer"]
        direction TB
        Gateway["Fireworks AI Inference Gateway<br/>OpenAI-Compatible Chat Completions API<br/>api.fireworks.ai/inference/v1"]
        GPU["AMD Instinct GPU Fleet<br/>Serving Gemma / DeepSeek / Llama<br/>Class LLMs at Low-Latency Throughput"]
        Gateway --> GPU
    end

    ViewLayer -->|"HTTPS POST — Bearer Token Auth<br/>Structured JSON Prompt Payload"| Gateway
    Gateway -->|"Streamed / Completed JSON<br/>Grading Response"| ViewLayer

    classDef clientStyle fill:#eff6ff,stroke:#2563eb,stroke-width:2px,color:#1e3a5f
    classDef edgeStyle fill:#f0fdf4,stroke:#059669,stroke-width:2px,color:#064e3b
    classDef persistStyle fill:#fef9c3,stroke:#ca8a04,stroke-width:2px,color:#713f12

    class ViewLayer clientStyle
    class Edge edgeStyle
    class PersistLayer persistStyle
Loading

1.1 Layer Responsibilities

Layer Technology Responsibility Trust Boundary
UI View Layer React 18 + Tailwind CSS 4 + Recharts + Lucide Icons Declarative rendering, view-state routing, chart visualization, user interaction capture Fully client-trusted; no secrets embedded at build time
Local Persistence Layer Browser localStorage Durable client-side storage of API credentials (fireworks_api_key), selected inference model (fireworks_model_id), syllabus artifacts, and grading history — acting as a de facto embedded config state machine in lieu of a backend database Trusted only within the single-origin browser sandbox; never transmitted except as an Authorization: Bearer header directly to Fireworks AI
Edge AI Inference Infrastructure Fireworks AI (AMD GPU-backed) Stateless, on-demand LLM inference for rubric-grounded essay evaluation; returns structured JSON grading payloads External, authenticated third-party boundary — the only network egress point in the entire application

This three-tier separation means Gradelytics has no middle tier to compromise: there is no application server holding session tokens, no SQL injection surface, and no server-side credential store to breach. The attack surface collapses to browser-local storage hygiene and TLS-secured calls to a single, well-known third-party endpoint.


2. Detailed Component Hierarchy Tree

The component tree is rooted at App.tsx, which owns global view-routing state and renders a persistent Sidebar alongside a dynamically swapped View Controller. Below the View Controllers sit domain-specific presentational and container components, terminating in atomic UI cards (e.g., MetricCard) and shared utility modules that are imported horizontally across the tree rather than owned by any single view.

flowchart TD
    App["📦 App.tsx<br/><i>Global App Wrapper</i><br/>• Active-view routing state (useState)<br/>• Global theme/design tokens<br/>• Toast / notification host"]

    App --> Sidebar["🧭 Sidebar.tsx<br/><i>Sidebar Layout Component</i><br/>• Collapsible nav state<br/>• Connection Status Node<br/>&nbsp;&nbsp;(reads hasApiKey())<br/>• API Settings Modal trigger"]

    Sidebar --> Modal["⚙️ ApiSettingsModal.tsx<br/>Fireworks API Key + Model ID form<br/>Writes to localStorage via<br/>saveApiKey() / saveModelId()"]

    App --> ViewSwitch{"View Controller<br/>Router"}

    ViewSwitch --> Dashboard["📊 DashboardView.tsx"]
    ViewSwitch --> Syllabus["📐 SyllabusArchitectView.tsx"]
    ViewSwitch --> VibeGrading["🎓 VibeGradingView.tsx<br/><i>(VibeGrading Simulator)</i>"]
    ViewSwitch --> Analytics["📈 AnalyticsView.tsx<br/><i>(Analytics &amp; History)</i>"]

    Dashboard --> MetricCard["🔹 MetricCard.tsx<br/>Atomic KPI Card"]
    Dashboard --> GradeChart["🔹 GradeDistributionChart.tsx<br/>Recharts Histogram/Bar"]

    Analytics --> MetricCard
    Analytics --> GradeChart

    VibeGrading -.->|invokes| ApiClient
    Dashboard -.->|reads| MockData
    Analytics -.->|reads| MockData

    subgraph SharedUtils["🔧 Shared Utility Modules — src/utils/"]
        ApiClient["fireworksClient — utils/api.ts<br/>getApiKey / saveApiKey / hasApiKey<br/>getModelId / saveModelId<br/>callFireworksAI()"]
        ExportUtils["utils/export.ts<br/>exportMarkdown() / exportPdf()<br/>jsPDF-based document generation"]
        PrintUtils["printElement()<br/>html-to-image / html2canvas<br/>bridge for print &amp; snapshot export"]
    end

    VibeGrading --> ExportUtils
    Analytics --> ExportUtils
    Dashboard --> PrintUtils
    Analytics --> PrintUtils

    classDef root fill:#1e293b,stroke:#0f172a,stroke-width:2px,color:#fff
    classDef layout fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f
    classDef view fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d
    classDef atom fill:#fef3c7,stroke:#d97706,stroke-width:1.5px,color:#78350f
    classDef util fill:#f3e8ff,stroke:#9333ea,stroke-width:2px,color:#581c87

    class App root
    class Sidebar,Modal layout
    class Dashboard,Syllabus,VibeGrading,Analytics view
    class MetricCard,GradeChart atom
    class ApiClient,ExportUtils,PrintUtils util
Loading

2.1 Component Contracts

App.tsx — Global App Wrapper. Owns the top-level activeView state enum (dashboard | syllabus | vibe-grading | analytics), instantiates global theme context, and hosts the toast/notification surface consumed by descendant views for asynchronous success/error feedback (e.g., "Grading complete", "API key saved").

Sidebar.tsx — Sidebar Layout Component. Manages its own isCollapsed boolean for responsive real-estate optimization, renders a live Connection Status Node that reactively reflects hasApiKey() from the shared fireworksClient, and exposes the trigger that mounts ApiSettingsModal.tsx as an overlay for credential configuration.

View Controllers. Each of DashboardView, SyllabusArchitectView, VibeGradingView, and AnalyticsView is a self-contained container component responsible for its own local UI state (loading/error/success), while delegating all network I/O to the shared fireworksClient and all document generation to utils/export.ts. This enforces a strict single-responsibility boundary: views orchestrate; utilities execute cross-cutting concerns.

Atomic UI Cards. MetricCard.tsx and GradeDistributionChart.tsx are pure, stateless, prop-driven presentational components reused across DashboardView and AnalyticsView, ensuring visual and behavioral consistency for KPI display and grade distribution rendering without duplicated logic.

Shared Utilities. fireworksClient (utils/api.ts) centralizes all outbound HTTP to the inference gateway and all localStorage credential access behind a narrow function-level API (getApiKey, saveApiKey, hasApiKey, getModelId, saveModelId, callFireworksAI) — no component touches window.localStorage or fetch directly. printElement and exportMarkdown/PDF export (utils/export.ts, built on jsPDF and html-to-image/html2canvas) provide document-generation and print-snapshot capabilities consumed identically by both VibeGradingView and AnalyticsView.


3. Event-Driven Data Flow Pipeline

The centerpiece of Gradelytics' functional value is the VibeGrading Simulator Execution Pipeline — a fully client-orchestrated sequence that transforms a teacher-authored rubric and a raw student essay into a structured, AI-generated grading result, rendered as reactive dashboard state.

The pipeline is intentionally stateless and idempotent per invocation: no grading request depends on server-side session context, and every request carries its full instruction payload (rubric + essay + output-format constraint) in a single HTTP call.

sequenceDiagram
    autonumber
    actor Teacher as Teacher (User)
    participant UI as VibeGradingView.tsx
    participant LS as localStorage<br/>(Config State Machine)
    participant Client as fireworksClient<br/>(utils/api.ts)
    participant Edge as Fireworks AI<br/>Edge Gateway (AMD GPU)
    participant State as Global Dashboard State<br/>(React state / Recharts)

    Teacher->>UI: Enter Rubric Criteria (JSON metrics array)
    Teacher->>UI: Paste raw Student Essay (string)
    Teacher->>UI: Click "Run VibeGrading"

    activate UI
    UI->>UI: Guard: validate non-empty rubric + essay
    UI->>UI: Flip state → isGrading = true<br/>Render animated "Analyzing via Gemma 4 / DeepSeek…" pulse

    UI->>LS: getApiKey() / getModelId()
    activate LS
    LS-->>UI: fireworks_api_key, fireworks_model_id
    deactivate LS

    alt No API key configured
        UI-->>Teacher: Toast error — "Configure Fireworks API key"
        UI->>UI: isGrading = false
    else Key present
        UI->>Client: callFireworksAI(messages, options)
        activate Client
        Client->>Client: Serialize rubric[] + essay into<br/>structured Prompt Schema<br/>(system: "return JSON only" + schema hint)
        Client->>Edge: HTTPS POST /inference/v1/chat/completions<br/>Authorization: Bearer {apiKey}<br/>body: { model, messages, temperature, max_tokens }
        activate Edge
        Note over Edge: AMD GPU-accelerated LLM inference<br/>(Gemma / DeepSeek / Llama-class model)<br/>generates rubric-grounded JSON verdict
        Edge-->>Client: 200 OK — choices[0].message.content<br/>(JSON string: scores, feedback, rationale)
        deactivate Edge
        Client->>Client: extractJsonFromResponse()<br/>strip ```json fences, JSON.parse()
        Client-->>UI: Deterministic GradingResult object
        deactivate Client

        UI->>State: setGradingResult(parsedResult)
        activate State
        State->>State: Recompute derived metrics<br/>(overall score, per-criterion breakdown)
        State-->>UI: Trigger re-render
        deactivate State

        UI->>UI: isGrading = false
        UI-->>Teacher: Render MetricCard grid +<br/>GradeDistributionChart (Recharts) +<br/>Typography feedback layout
    end
    deactivate UI
Loading

3.1 Pipeline Stage Detail

  1. User Inputs. The teacher populates a RubricCriterion[] array (each entry: { label, weight, description }) and supplies a raw essay string via a controlled textarea. Submission is gated behind a client-side guard requiring both fields non-empty before the pipeline may trigger.

  2. Guard & State Trigger. On trigger, the view synchronously flips isGrading to true, which mounts an animated pulse/skeleton state communicating active inference (e.g., "Analyzing via Gemma 4 / DeepSeek…"). Concurrently, the pipeline calls getApiKey() and getModelId() against localStorage — a synchronous, zero-latency read with no network round-trip, reinforcing the "config state machine" role of the persistence layer.

  3. API Payload Construction. fireworksClient.callFireworksAI() serializes the rubric metrics and essay text into a structured Prompt Schema: a system instruction constraining the model to emit valid JSON matching a fixed grading schema (per-criterion scores, weighted total, qualitative feedback), paired with a user message carrying the rubric and essay content. This schema-forcing approach is what makes the downstream parsing step deterministic rather than probabilistic.

  4. Serverless Edge Gateway. The client issues an asynchronous fetch() POST to https://api.fireworks.ai/inference/v1/chat/completions, authenticated via a Bearer token drawn from localStorage. This request is routed to Fireworks AI's inference fleet, executing on AMD Instinct GPU infrastructure, which serves the configured model (Gemma, DeepSeek, or Llama-class, selectable via fireworks_model_id).

  5. Deterministic Response Parsing. On receipt, extractJsonFromResponse() strips any Markdown code-fence wrapping (json … ) the model may emit, and JSON.parse()s the remainder into a strongly-typed GradingResult. This object is committed to the view's local React state, which cascades into re-renders of MetricCard KPI tiles, the GradeDistributionChart Recharts visualization, and the qualitative feedback typography block — completing the closed loop from raw text input to structured, visualized pedagogical insight, with isGrading reset to false to restore the interactive UI.


4. Compliance & Containerization Model (Docker)

4.1 Static Hosting Runtime Architecture

Because Gradelytics' entire runtime is a client-executed SPA with no server-side rendering and no backend API of its own, the production deployment artifact is build-time-compiled static assets (HTML, hashed JS/CSS bundles, and public assets) — nothing more. This has direct compliance and cost implications:

  • No PII/FERPA-scoped data ever touches Gradelytics infrastructure. Student essays and grading rubrics flow directly from the teacher's browser to Fireworks AI over TLS; the hosting layer serves only immutable, non-personalized static files and never sees request bodies.
  • The container is compute-idle by design. Nginx's role is exclusively to serve pre-built files and handle SPA client-side routing fallback — it performs no template rendering, no session handling, and no database queries, which minimizes the compliance surface for SOC 2 / FERPA-adjacent review.
  • Horizontal scaling is trivial and stateless. Because every container instance is byte-identical and holds no session affinity, the runtime can be replicated behind any load balancer or CDN edge network without sticky sessions.

The deployment model follows a two-stage build/runtime separation: a transient Node.js Alpine builder compiles the Vite/React/TypeScript source into a static dist/ bundle, which is then copied — and only the compiled output, never node_modules or source — into a minimal Nginx Alpine image for production serving. This keeps the shipped image free of build toolchains, npm registries, and dev dependencies, reducing both image size and attack surface.

flowchart LR
    subgraph Stage1["Stage 1 — Build (ephemeral, discarded)"]
        direction TB
        Node["node:20-alpine<br/>npm ci<br/>vite build"]
        Src["Source: src/, index.html,<br/>vite.config.ts, tsconfig.json"]
        Dist["Output: /app/dist<br/>(hashed static bundle)"]
        Src --> Node --> Dist
    end

    subgraph Stage2["Stage 2 — Production Delivery (shipped image)"]
        direction TB
        Nginx["nginx:alpine<br/>~7MB base image"]
        Conf["Custom nginx.conf<br/>SPA fallback: try_files → index.html<br/>gzip, cache-control headers"]
        Serve["Serves static assets on :80<br/>No compute, no state, no DB"]
        Nginx --> Conf --> Serve
    end

    Dist -->|"COPY --from=builder<br/>/app/dist → /usr/share/nginx/html"| Nginx

    classDef build fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f
    classDef prod fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d
    class Stage1 build
    class Stage2 prod
Loading

4.2 Multi-Stage Dockerfile Blueprint

# ─────────────────────────────────────────────────────────────
# Stage 1: Build — compiles the Vite/React/TypeScript SPA
# ─────────────────────────────────────────────────────────────
FROM node:20-alpine AS builder

WORKDIR /app

# Leverage Docker layer caching: install deps before copying source
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund

# Copy source and compile production bundle
COPY . .
RUN npm run build
# → outputs static assets to /app/dist

# ─────────────────────────────────────────────────────────────
# Stage 2: Production Delivery — ultra-lightweight Nginx runtime
# ─────────────────────────────────────────────────────────────
FROM nginx:1.27-alpine AS production

# Remove default Nginx welcome config; replace with SPA-safe routing
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf

# Copy only the compiled static bundle — no source, no node_modules
COPY --from=builder /app/dist /usr/share/nginx/html

# Run as non-root for defense-in-depth
RUN addgroup -g 1001 -S appgroup \
    && adduser -S appuser -u 1001 -G appgroup \
    && chown -R appuser:appgroup /usr/share/nginx/html \
    && chown -R appuser:appgroup /var/cache/nginx /var/run \
    && touch /var/run/nginx.pid \
    && chown appuser:appgroup /var/run/nginx.pid

USER appuser

EXPOSE 80

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
    CMD wget --quiet --tries=1 --spider http://localhost:80/ || exit 1

CMD ["nginx", "-g", "daemon off;"]

Companion nginx.conf — required to safely handle client-side routing for the React Router-style view switching used by App.tsx, ensuring deep-linked or refreshed routes resolve to index.html rather than a 404:

server {
    listen 80;
    server_name _;
    root /usr/share/nginx/html;
    index index.html;

    # Gzip static compression for JS/CSS bundle delivery
    gzip on;
    gzip_types text/plain text/css application/javascript application/json image/svg+xml;
    gzip_min_length 1024;

    # Aggressive caching for hashed, immutable Vite build assets
    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # SPA fallback — critical for client-side view routing
    location / {
        try_files $uri $uri/ /index.html;
        add_header Cache-Control "no-cache";
    }

    # Security headers — defense-in-depth for a public-facing static app
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}

4.3 Operational Notes

  • Image footprint. The final production image inherits only from nginx:alpine (~7 MB base), plus the compiled static bundle — typically well under 25 MB total, enabling fast pulls and rapid autoscaling.
  • No secrets baked into the image. Because the Fireworks AI API key is supplied per-teacher at runtime via the ApiSettingsModal and persisted client-side in localStorage, the Docker image itself carries zero embedded credentials — a meaningful compliance win, since the container registry never becomes a secrets-leak vector.
  • CI/CD alignment. This blueprint is directly compatible with any container-native deployment target (AMD-accelerated Kubernetes clusters, ECS/Fargate, Cloud Run, or a simple docker run), since the runtime stage has no external service dependencies beyond outbound HTTPS to api.fireworks.ai.

5. Architectural Summary

Concern Design Decision Rationale
State management localStorage as config state machine (no Redux/backend DB) Eliminates infra tier; instant reads; scoped to browser origin
AI compute Fireworks AI on AMD GPU infrastructure, called client-side Pay-per-inference economics; no self-hosted model-serving burden
Data residency Essay/rubric data never touches Gradelytics servers Simplifies FERPA/PII compliance posture by design, not policy
Deployment unit Static bundle behind multi-stage Docker + Nginx Alpine Stateless, horizontally scalable, minimal attack surface
Component boundary View Controllers delegate I/O to shared utilities (fireworksClient, export.ts) Single-responsibility, testable, no duplicated network/export logic

This architecture positions Gradelytics as a lean, defensible reference implementation of an edge-AI-native educational SaaS product: every architectural choice — from the localStorage-as-state-machine pattern to the AMD GPU-backed inference call to the two-stage Nginx delivery model — is optimized for near-zero operational overhead while preserving a scalable, security-conscious foundation suitable for B2B expansion.