Spaces:
Sleeping
Sleeping
Download frontend/src/api.ts from misack/war_gaming: direct link, hf CLI and curl.
- Browser
- Download file 16.3 kB
-
https://huggingface.co/spaces/misack/war_gaming/resolve/main/frontend/src/api.ts
- Command line
-
hf download hf://spaces/misack/war_gaming/frontend/src/api.ts
-
curl -L -o api.ts https://huggingface.co/spaces/misack/war_gaming/resolve/main/frontend/src/api.ts
16.3 kB
| // Minimal typed client for the AI War Gaming FastAPI backend. | |
| const BASE = (import.meta as any).env?.VITE_API_BASE ?? '/api'; | |
| // Trial password gate: backend may require an X-Trial-Password header. | |
| // We cache the password in localStorage and re-prompt on a 401. | |
| // Local dev has no gate so this is a no-op there. | |
| const PASSWORD_KEY = 'wargame_trial_password'; | |
| function getPassword(): string | null { | |
| try { return localStorage.getItem(PASSWORD_KEY); } catch { return null; } | |
| } | |
| function setPassword(value: string | null): void { | |
| try { | |
| if (value === null) localStorage.removeItem(PASSWORD_KEY); | |
| else localStorage.setItem(PASSWORD_KEY, value); | |
| } catch { /* private mode etc. — swallow */ } | |
| } | |
| function promptForPassword(): string | null { | |
| if (typeof window === 'undefined' || typeof window.prompt !== 'function') return null; | |
| const value = window.prompt('Enter trial access password'); | |
| return value && value.length > 0 ? value : null; | |
| } | |
| async function doFetch(path: string, init: RequestInit | undefined, password: string | null): Promise<Response> { | |
| const headers: Record<string, string> = { | |
| 'Content-Type': 'application/json', | |
| ...((init?.headers as Record<string, string> | undefined) || {}), | |
| }; | |
| if (password) headers['X-Trial-Password'] = password; | |
| return fetch(`${BASE}${path}`, { ...init, headers }); | |
| } | |
| async function req<T>(path: string, init?: RequestInit): Promise<T> { | |
| let res = await doFetch(path, init, getPassword()); | |
| if (res.status === 401) { | |
| setPassword(null); | |
| const next = promptForPassword(); | |
| if (next) { | |
| setPassword(next); | |
| res = await doFetch(path, init, next); | |
| } | |
| } | |
| if (!res.ok) { | |
| const body = await res.text().catch(() => ''); | |
| throw Object.assign(new Error(`${res.status} ${res.statusText}: ${body}`), { status: res.status }); | |
| } | |
| return res.status === 204 ? (undefined as any) : res.json(); | |
| } | |
| export const api = { | |
| health: () => req<{ status: string }>('/healthz'), | |
| listScenarios: () => req<{ scenarios: Scenario[] }>('/scenarios'), | |
| listGames: () => req<{ games: GameSummary[] }>('/games'), | |
| listModels: () => req<ModelsResponse>('/models'), | |
| listAgents: (scenario_id: string) => | |
| req<AgentsResponse>(`/scenarios/${scenario_id}/agents`), | |
| createGame: ( | |
| scenario_id: string, | |
| model?: string, | |
| role_model_overrides?: Record<string, string>, | |
| language?: 'en' | 'de', | |
| ) => { | |
| const body: Record<string, unknown> = { scenario_id }; | |
| if (model) body.model = model; | |
| if (role_model_overrides && Object.keys(role_model_overrides).length > 0) { | |
| body.role_model_overrides = role_model_overrides; | |
| } | |
| // ---- i18n (round 6) ---- | |
| if (language) body.language = language; | |
| return req<{ game_id: string; turn: number; briefing: Briefing }>(`/games`, { | |
| method: 'POST', | |
| body: JSON.stringify(body), | |
| }); | |
| }, | |
| getState: (id: string) => req<StateResponse>(`/games/${id}/state`), | |
| getBriefing: (id: string) => req<BriefingResponse>(`/games/${id}/briefing`), | |
| recon: (id: string, agent_id: string, question: string) => | |
| req<ReconResponseEnvelope>(`/games/${id}/recon`, { | |
| method: 'POST', body: JSON.stringify({ agent_id, question }), | |
| }), | |
| submitAction: (id: string, action: ActionRequest) => | |
| req<ActionAcceptedResponse>(`/games/${id}/actions`, { | |
| method: 'POST', body: JSON.stringify(action), | |
| }), | |
| advance: (id: string) => req<AdvanceResponse>(`/games/${id}/advance`, { method: 'POST' }), | |
| debrief: (id: string) => req<DebriefResponse>(`/games/${id}/debrief`), | |
| metrics: (id: string) => req<MetricsTimelineResponse>(`/games/${id}/metrics/timeline`), | |
| setupStart: (scenario_id: string, initial_message: string) => | |
| req<SetupSession>(`/scenarios/setup`, { | |
| method: 'POST', body: JSON.stringify({ scenario_id, initial_message }), | |
| }), | |
| setupGet: (session_id: string) => req<SetupSession>(`/scenarios/setup/${session_id}`), | |
| setupAnswer: (session_id: string, message: string) => | |
| req<SetupSession>(`/scenarios/setup/${session_id}/answer`, { | |
| method: 'POST', body: JSON.stringify({ message }), | |
| }), | |
| setupCommit: (session_id: string, force = false) => | |
| req<ScaffoldResponse>(`/scenarios/setup/${session_id}/commit`, { | |
| method: 'POST', body: JSON.stringify({ force }), | |
| }), | |
| fork: (game_id: string, from_turn: number) => | |
| req<ForkResponse>(`/games/${game_id}/fork`, { | |
| method: 'POST', body: JSON.stringify({ from_turn }), | |
| }), | |
| orchestrate: (game_id: string, opts?: { branches?: number; turns?: number }) => | |
| req<OrchestrateAccepted>(`/games/${game_id}/orchestrate`, { | |
| method: 'POST', | |
| body: JSON.stringify({ branches: opts?.branches ?? 3, turns: opts?.turns ?? 1 }), | |
| }), | |
| orchestrateStatus: (game_id: string) => | |
| req<OrchestrateStatus>(`/games/${game_id}/orchestrate/status`), | |
| orchestrateResult: (game_id: string) => | |
| req<ScenarioMap>(`/games/${game_id}/orchestrate/result`), | |
| // Action Coach — conversational action input that drafts a 3-anchor | |
| // ActionSubmission and submits it through the same engine path. | |
| actionCoachStart: (game_id: string, initial_prose: string) => | |
| req<ActionCoachSession>(`/games/${game_id}/actions/coach`, { | |
| method: 'POST', | |
| body: JSON.stringify({ initial_prose }), | |
| }), | |
| actionCoachLoad: (game_id: string, session_id: string) => | |
| req<ActionCoachSession>(`/games/${game_id}/actions/coach/${session_id}`), | |
| actionCoachAnswer: (game_id: string, session_id: string, message: string) => | |
| req<ActionCoachSession>(`/games/${game_id}/actions/coach/${session_id}/answer`, { | |
| method: 'POST', | |
| body: JSON.stringify({ message }), | |
| }), | |
| actionCoachCommit: (game_id: string, session_id: string) => | |
| req<ActionAcceptedResponse>(`/games/${game_id}/actions/coach/${session_id}/commit`, { | |
| method: 'POST', | |
| }), | |
| // ---- forms (5b) ---- | |
| addConstraint: (game_id: string, body: HardConstraintInput) => | |
| req<HardConstraintListResponse>(`/games/${game_id}/constraints`, { | |
| method: 'POST', | |
| body: JSON.stringify(body), | |
| }), | |
| checkAction: (game_id: string, action: ActionRequest) => | |
| req<ConstraintCheckResponse>(`/games/${game_id}/check_action`, { | |
| method: 'POST', | |
| body: JSON.stringify(action), | |
| }), | |
| deleteConstraint: (game_id: string, constraint_id: string, force = false) => | |
| req<HardConstraintListResponse>( | |
| `/games/${game_id}/constraints/${encodeURIComponent(constraint_id)}${force ? '?force=true' : ''}`, | |
| { method: 'DELETE' }, | |
| ), | |
| deleteDocument: (session_id: string, filename: string) => | |
| req<void>( | |
| `/scenarios/setup/${session_id}/documents/${encodeURIComponent(filename)}`, | |
| { method: 'DELETE' }, | |
| ), | |
| listConstraints: (game_id: string) => | |
| req<HardConstraintListResponse>(`/games/${game_id}/constraints`), | |
| listDocuments: (session_id: string) => | |
| req<DocumentListResponse>(`/scenarios/setup/${session_id}/documents`), | |
| uploadDocument: (session_id: string, file: File) => { | |
| const fd = new FormData(); | |
| fd.append('file', file); | |
| // NOTE: don't set Content-Type — the browser fills in the multipart | |
| // boundary. `req()` defaults to JSON, so we call fetch directly here. | |
| return fetch(`${BASE}/scenarios/setup/${session_id}/documents`, { | |
| method: 'POST', | |
| body: fd, | |
| }).then(async (res) => { | |
| if (!res.ok) { | |
| const body = await res.text().catch(() => ''); | |
| throw Object.assign( | |
| new Error(`${res.status} ${res.statusText}: ${body}`), | |
| { status: res.status }, | |
| ); | |
| } | |
| return res.json() as Promise<UploadedDocument>; | |
| }); | |
| }, | |
| // ---- live (5b) ---- | |
| pauseGame: (id: string) => | |
| req<StateResponse>(`/games/${id}/pause`, { method: 'POST' }), | |
| resumeGame: (id: string) => | |
| req<AdvanceResponse>(`/games/${id}/resume`, { method: 'POST' }), | |
| // EventSource can't attach custom headers, so the trial password rides in | |
| // a `?token=` query param instead. The backend middleware accepts either. | |
| streamUrl: (id: string) => { | |
| const pw = getPassword(); | |
| const qs = pw ? `?token=${encodeURIComponent(pw)}` : ''; | |
| return `${BASE}/games/${id}/stream${qs}`; | |
| }, | |
| }; | |
| export type AnchorKind = 'prior_analogue' | 'stated_capability' | 'resource_committed'; | |
| export interface Anchor { kind: AnchorKind; detail: string } | |
| export interface ActionRequest { intent: string; justification: string; anchors: Anchor[] } | |
| export interface ActionAcceptedResponse { game_id: string; turn: number; action: any } | |
| export interface Scenario { scenario_id: string; company_name: string; rival_name: string; industry: string; player_role: string; agent_count: number } | |
| export interface GameSummary { id: string; scenario_id: string; turn: number; max_turns: number; status: 'active' | 'complete'; updated_at: string; llm_model?: string | null } | |
| export interface ModelInfo { id: string; display_name: string; provider: string } | |
| export interface ModelsResponse { models: ModelInfo[]; default: string; roles?: string[] } | |
| export interface AgentInfo { id: string; label: string; kind: string; posture: string } | |
| export interface AgentsResponse { scenario_id: string; agents: AgentInfo[] } | |
| export interface BriefingSection { heading: string; body: string; fog_of_war?: string | null } | |
| export interface DecisionPoint { title: string; context: string; example_actions: string[] } | |
| export interface Briefing { turn: number; headline: string; exec_summary: string; sections: BriefingSection[]; decision_points: DecisionPoint[] } | |
| export interface BriefingResponse { game_id: string; turn: number; briefing: Briefing; recon_budget_remaining: number } | |
| export interface ReconResponseEnvelope { game_id: string; recon_budget_remaining: number; response: { answered_by: string; question: string; answer: string; reveals_to_rival: boolean } } | |
| export interface AdvanceResponse { game_id: string; status: string; turn_resolved: number; next_turn?: number; briefing?: Briefing } | |
| export interface DebriefResponse { game_id: string; status: string; markdown: string } | |
| export interface MetricsTimelineResponse { game_id: string; points: { turn: number; metrics: { key: string; label: string; unit: string; value: number; delta: number }[] }[] } | |
| export interface StateResponse { id: string; scenario_id: string; status: string; world: any; recon_budget_remaining: number; current_phase: string; turns: any[]; language?: 'en' | 'de' } | |
| export interface SetupMessage { role: 'user' | 'setup_agent'; content: string; at: string } | |
| export interface SetupSession { | |
| session_id: string; | |
| scenario_id: string; | |
| status: 'gathering' | 'ready' | 'committed'; | |
| last_agent_message: string; | |
| synthesized_brief: string | null; | |
| committed_scenario_id: string | null; | |
| messages: SetupMessage[]; | |
| doc_filenames?: string[]; | |
| } | |
| export interface ScaffoldResponse { | |
| scenario_id: string; | |
| target_dir: string; | |
| mode: 'blank' | 'generated'; | |
| summary: Scenario | null; | |
| } | |
| export interface ForkResponse { | |
| game_id: string; | |
| forked_from: string; | |
| forked_at_turn: number; | |
| turn: number; | |
| briefing: Briefing; | |
| } | |
| export type OrchestratorStatus = 'pending' | 'running' | 'complete' | 'failed'; | |
| export interface StrategyVector { | |
| id: string; | |
| label: string; | |
| description: string; | |
| play_style_note: string; | |
| likely_stumbles: string[]; | |
| priority_metrics: string[]; | |
| } | |
| export interface OrchestrateAccepted { map_id: string; status: string } | |
| export interface OrchestrateStatus { | |
| map_id: string; | |
| status: OrchestratorStatus; | |
| branches_complete: number; | |
| branches_total: number; | |
| error: string | null; | |
| } | |
| export interface ScenarioMap { | |
| id: string; | |
| source_game_id: string; | |
| scenario_id: string; | |
| created_at: string; | |
| updated_at: string; | |
| vectors: StrategyVector[]; | |
| branches: Array<{ | |
| vector_id: string; | |
| branch_game_id: string; | |
| proxy_temperature: number; | |
| narrative_after?: string; | |
| skipped?: boolean; | |
| skip_reason?: string | null; | |
| turns_played?: number; | |
| }>; | |
| synthesis_markdown: string; | |
| branches_requested: number; | |
| turns_requested: number; | |
| status: OrchestratorStatus; | |
| branches_complete: number; | |
| error: string | null; | |
| } | |
| // Action Coach — conversational action input. | |
| export interface ActionDraft { | |
| intent: string; | |
| justification: string; | |
| anchors: Anchor[]; | |
| } | |
| export interface CoachMessage { | |
| role: 'user' | 'coach'; | |
| content: string; | |
| at: string; | |
| } | |
| export type ActionCoachStatus = 'gathering' | 'ready' | 'committed'; | |
| export interface ActionCoachSession { | |
| session_id: string; | |
| game_id: string; | |
| turn: number; | |
| status: ActionCoachStatus; | |
| rounds_used: number; | |
| last_coach_message: string; | |
| synthesized_draft: ActionDraft | null; | |
| committed: boolean; | |
| messages: CoachMessage[]; | |
| } | |
| // ---- forms (5b) ---- | |
| // Hard constraints + uploaded documents + check-action shapes (Round 4 backend). | |
| export type ConstraintSeverity = 'hard' | 'soft'; | |
| export type ConstraintSource = 'scenario' | 'document' | 'manual'; | |
| export interface HardConstraint { | |
| id: string; | |
| description: string; | |
| rationale: string; | |
| test_question: string; | |
| severity: ConstraintSeverity; | |
| source: ConstraintSource; | |
| source_ref?: string | null; | |
| } | |
| export interface HardConstraintInput { | |
| description: string; | |
| rationale: string; | |
| test_question: string; | |
| severity?: ConstraintSeverity; | |
| source_ref?: string | null; | |
| } | |
| export interface HardConstraintListResponse { | |
| game_id: string; | |
| constraints: HardConstraint[]; | |
| } | |
| export interface ConstraintViolation { | |
| constraint_id: string; | |
| constraint_description: string; | |
| reason: string; | |
| } | |
| export interface ConstraintCheckResult { | |
| passes: boolean; | |
| violations: ConstraintViolation[]; | |
| } | |
| export interface ConstraintCheckResponse { | |
| game_id: string; | |
| result: ConstraintCheckResult; | |
| } | |
| export interface UploadedDocument { | |
| filename: string; | |
| bytes_stored: number; | |
| } | |
| export interface DocumentListResponse { | |
| session_id: string; | |
| documents: UploadedDocument[]; | |
| } | |
| // ---- live (5b) ---- | |
| // PhaseEvent — server-sent events that fire on the engine bus during an | |
| // advance. Discriminated by `kind`. Wire format documented in CONTRACT.md. | |
| export type PhaseEventKind = | |
| | 'phase_started' | |
| | 'phase_completed' | |
| | 'deliberation_complete' | |
| | 'critic_complete' | |
| | 'premortem_complete' | |
| | 'verdict_complete' | |
| | 'evaluator_complete' | |
| | 'world_updated' | |
| | 'briefing_ready' | |
| | 'usage_tick' | |
| | 'paused' | |
| | 'resumed' | |
| | 'error'; | |
| export type VerdictOutcome = | |
| | 'effect_major' | |
| | 'effect_minor' | |
| | 'no_effect' | |
| | 'backfires'; | |
| interface PhaseEventBase { | |
| game_id: string; | |
| turn: number; | |
| label: string; | |
| at: string; | |
| payload: Record<string, unknown> & { dropped?: number }; | |
| } | |
| export type PhaseEvent = | |
| | (PhaseEventBase & { kind: 'phase_started'; payload: { dropped?: number } }) | |
| | (PhaseEventBase & { kind: 'phase_completed'; payload: { dropped?: number } }) | |
| | (PhaseEventBase & { | |
| kind: 'deliberation_complete'; | |
| payload: { agent_id: string; expects: string; fears: string; dropped?: number }; | |
| }) | |
| | (PhaseEventBase & { | |
| kind: 'critic_complete'; | |
| payload: { count: number; issue_count: number; dropped?: number }; | |
| }) | |
| | (PhaseEventBase & { | |
| kind: 'premortem_complete'; | |
| payload: { count: number; dropped?: number }; | |
| }) | |
| | (PhaseEventBase & { | |
| kind: 'verdict_complete'; | |
| payload: { | |
| actor: string; | |
| plausibility: number; | |
| outcome: VerdictOutcome; | |
| dropped?: number; | |
| }; | |
| }) | |
| | (PhaseEventBase & { | |
| kind: 'evaluator_complete'; | |
| payload: { scored: number; dropped?: number }; | |
| }) | |
| | (PhaseEventBase & { | |
| kind: 'world_updated'; | |
| payload: { media_tone: string; public_narrative: string; dropped?: number }; | |
| }) | |
| | (PhaseEventBase & { | |
| kind: 'briefing_ready'; | |
| payload: { turn: number; dropped?: number }; | |
| }) | |
| | (PhaseEventBase & { | |
| kind: 'usage_tick'; | |
| payload: { | |
| cost_usd_so_far: number; | |
| input_tokens_so_far: number; | |
| output_tokens_so_far: number; | |
| dropped?: number; | |
| }; | |
| }) | |
| | (PhaseEventBase & { | |
| kind: 'paused'; | |
| payload: { turn: number; dropped?: number }; | |
| }) | |
| | (PhaseEventBase & { | |
| kind: 'resumed'; | |
| payload: { dropped?: number }; | |
| }) | |
| | (PhaseEventBase & { | |
| kind: 'error'; | |
| payload: Record<string, unknown> & { dropped?: number }; | |
| }); | |