Development instance — data may be reset at any time

Custom (non-Chat) frontends

Build a bespoke UI instead of the built-in Chat. defineFrontend gives you state, emit, and gameHistory — render whatever you want from them. Derive views (e.g. per-character threads) from gameHistory.recentBlocks rather than duplicating transcripts in state, type sub-components from the SDK's exported building blocks, and theme with the host site's CSS variables.

The built-in Chat component is one option, not a requirement. defineFrontend hands your component three things — state, emit, and gameHistory — and you render whatever the game needs: a board, a cast sidebar, a map, tabbed interview threads. A custom frontend looks native because it theme-matches the host site through CSS variables.

The shape

defineFrontend takes a single component. Switch on phase and delegate to sub-components:

tsx
// frontend.tsx
import { defineFrontend } from '@aichatgames/sdk'
import type { InputEvent, OutputEvent, State } from './types.js'
import { Investigation } from './components/Investigation.js'
import { Resolution } from './components/Resolution.js'

defineFrontend<InputEvent, OutputEvent, State>(({ state, emit, gameHistory }) => {
  if (state.phase === 'resolved') return <Resolution state={state} />
  if (state.phase === 'setup') return <div className="dp-loading">Setting the scene…</div>
  return <Investigation state={state} emit={emit} gameHistory={gameHistory} />
})

Typing sub-components

FrontendProps is not exported — only the defineFrontend callback receives it. Type each sub-component's props directly from the SDK's exported building blocks plus your own event/state types:

tsx
// components/Investigation.tsx
import type { GameHistory } from '@aichatgames/sdk'
import type { InputEvent, OutputEvent, State } from '../types.js'

export function Investigation({ state, emit, gameHistory }: {
  state: State
  emit: (e: InputEvent) => void
  gameHistory: GameHistory<InputEvent, OutputEvent, State>
}) { /* … */ }

Derive views from blocks — don't duplicate transcripts in state

gameHistory.recentBlocks is the conversation. Each block carries the input event that produced it and the output events the backend emitted. Reconstruct views from blocks rather than keeping a parallel transcript in state (which would be two sources of truth for the same thing). The open next block has no input yet (block.input is undefined), so guard it before reading block.input.*. Here a per-guest interview thread is filtered straight out of the history:

ts
// components/thread.ts
import type { GameBlock } from '@aichatgames/sdk'
import type { InputEvent, OutputEvent, State, GuestSpeech } from '../types.js'

type Block = GameBlock<InputEvent, OutputEvent, State>
export interface Line { who: 'player' | 'guest'; text: string }

export function guestThread(blocks: Block[], id: string): Line[] {
  const lines: Line[] = []
  for (const b of blocks) {
    if (!b.input) continue // open `next` block — no input event yet
    if (b.input.type === 'ask-question' && b.input.guestId === id) {
      lines.push({ who: 'player', text: b.input.content })
      const speech = b.output.find((o): o is GuestSpeech => o.type === 'guest-speech' && o.guestId === id)
      if (speech) lines.push({ who: 'guest', text: speech.text })
    }
  }
  return lines
}

State holds only true game state (the scenario, portraits, phase, verdict); the dialogue lives in blocks. Use gameHistory.streaming to gate input and show a "thinking…" indicator while a turn is in flight.

Theming with the SDK CSS variables

Any .css file in the game source loads automatically — don't import it from TypeScript (the platform injects it via <link>). The host site exposes CSS custom properties on :root that track its light/dark scheme, so styling with them makes the game look native on both sites and in both modes:

css
.dp-cast-card {
  background: var(--bg-input);
  border: 1px solid var(--border);
  color: var(--text);
}
.dp-cast-card.dp-selected {
  border-color: var(--accent);
  background: var(--accent-subtle);
}
.dp-line.dp-player p {
  background: var(--accent);
  color: var(--text-on-accent);
}

Available variables: --bg, --bg-subtle, --bg-input, --border, --text, --text-muted, --accent, --accent-hover, --accent-subtle, --text-on-accent. Use them for every color so the game inherits the site's theme — never hard-code colors that should follow light/dark.

Notes

  • emit is your only channel to the backend. A click, a form submit, an accusation — all become input events: emit({ type: 'ask-question', guestId, content }).
  • state is post-latest-block (or initial state before any block). Render from it for the current snapshot; read recentBlocks for the history/transcript.
  • Keep components micro. One component per file (sidebar, conversation, resolution) — the same small-files discipline as the backend.
  • For the chat-style default instead, use the built-in Chat component (see the SDK reference).