spa-api-provider
v2.2.0
Published
React Context API Provider for handling game data
Readme
API Provider (spa-api-provider)
A React context provider for embedded CAPTRS SPAs (survey-builder, mini-games, etc.): configuration, game data, scores, events, and typed helpers that call the game-platform HTTP APIs (via axios).
Installation
npm install spa-api-providerUse ^2.2.0 (or >=2.2.0) for async report jobs (submitReportJob / getReportJob). For App Custom Signals (emitAppEvent), use ^2.1.0. Use ^2.0.1 (or >=2.0.1) so the AI chat discriminator serializes as lineage (Jackson / Kotlin defaults). 2.0.0 used modelLineage, which does not bind server-side — see 2.0.1 in CHANGELOG.md. Team comms: RELEASE.md.
Usage
Import and wrap your app
import React from 'react';
import { ApiProvider } from 'spa-api-provider';
const App = () => {
return (
// Optional: <ApiProvider apiUrl="https://api.example.com" token="your-token">
<ApiProvider>
<YourComponent />
</ApiProvider>
);
};
export default App;Using the API context
import { useApi } from 'spa-api-provider';
const YourComponent = () => {
const { getGameScores, saveGameScore, platformContext } = useApi();
const fetchScores = async () => {
const scores = await getGameScores();
console.log(scores);
};
console.log(platformContext?.exerciseId);
console.log(platformContext?.organizationId);
return (
<div>
<button onClick={fetchScores}>Get Scores</button>
</div>
);
};AI chat (POST /api/ai/chat)
From useApi(), aiChat(params) posts JSON matching the platform AIChatRequestDTO: include lineage (PLATFORM_MANAGED or CLIENT_OVERRIDE) — the JSON name matches the Kotlin property under Jackson defaults. Type exports: AiModelLineage, AIChatRequestDTO (discriminated union), optional assertAIChatRequestRuntime for dynamic input.
Platform-managed — omit models (do not send an empty array):
import { useApi, AiModelLineage } from 'spa-api-provider';
const { aiChat } = useApi();
await aiChat({
lineage: AiModelLineage.PLATFORM_MANAGED,
messages: [{ role: 'user', content: 'Hello' }],
});Client override — models is required (non-empty at runtime):
import { useApi, AiModelLineage } from 'spa-api-provider';
const { aiChat } = useApi();
await aiChat({
lineage: AiModelLineage.CLIENT_OVERRIDE,
models: ['gpt-4.1-mini'],
messages: [{ role: 'user', content: 'Hello' }],
});See CHANGELOG.md (v2.0.1) for the lineage wire key. If you are on 2.0.0, replace modelLineage with lineage.
Report jobs (POST/GET /api/report/jobs)
From ^2.2.0, submit and poll OpenRouter chat-style report jobs on the existing platform endpoints (no rename). Type exports: SubmitReportJobDTO, ReportJobAcceptedDTO, ReportJobStatusDTO, ReportJobErrorDTO.
import { useApi } from 'spa-api-provider';
const { submitReportJob, getReportJob } = useApi();
const accepted = await submitReportJob({
messages: [
{ role: 'system', content: 'Generate report' },
{ role: 'user', content: 'Analyze survey results' },
],
maxTokens: 1200,
idempotencyKey: 'idem-1',
});
// Poll until status is complete or failed (caller-owned cadence).
const status = await getReportJob(accepted.jobId);This posts POST /api/report/jobs and polls GET /api/report/jobs/{jobId} with Authorization: Bearer ${token}. Out of scope here: a shared polling helper, /api/ai/job rename, SURVEY_RESULTS contract, and report-engine jobs.
App Custom Signals (emitAppEvent)
From ^2.1.0, emit first-party app telemetry without a new platform enum value for every signal (ADR-020). Prefer app-prefixed names:
import { useApi } from 'spa-api-provider';
const { emitAppEvent } = useApi();
await emitAppEvent('c3c.mini_game_completed', { sessionId, scenarioId });
// optional version (default 1):
await emitAppEvent('c3c.mini_game_replay', undefined, 1);This posts POST /api/events with type: APP_CUSTOM and envelope { name, version, payload? }. Keep first-class types (APP_START, APP_FINISH, …) via addEvent when the platform owns the semantics.
Platform context (optional)
useApi() exposes an optional platformContext object:
exerciseId?: stringappInstanceId?: stringgameConfigId?: string(explicit value if provided, otherwise derived fromappInstanceId)organizationId?: stringrawConfig?: Record<string, unknown>
All fields are optional. Apps that only use providerConfig (apiUrl and token) keep working as before.
The provider normalizes these from incoming CONFIG messages in this order:
- camelCase top-level fields
- snake_case top-level fields
context.*fallbacks
