GemmaPoddocs
Quickstart

Embed in React (headless)

Drop the runtime-only IIFE into a React app and render your own transcript + composer.

When to use this

You're building inside an existing React shell — a CopilotKit-shaped host, an AI SDK chat surface, your own design system — and you want GemmaPod's runtime (transport, events, state, chat) without its default Preact widget.

Runnable version in the repo →

The two IIFEs

The shim ships two builds from the same source tree:

FileBundles
gemmapod-shim.iife.jsFull — runtime + Preact chat widget + boot + signing helpers (~678 KB before gzip).
gemmapod-runtime.iife.jsRuntime + transports only — no Preact, no boot, no signing helpers (~656 KB before gzip).

For a headless React embed you want the runtime-only build.

Set up

npm i @gemmapod/embed

Then in index.html of your Vite/Next.js app:

<script src="https://cdn.jsdelivr.net/npm/@gemmapod/embed@0.1.0/dist/gemmapod-runtime.iife.js"></script>

(Or import "@gemmapod/embed/runtime" from a bundler — the IIFE side-effects window.GemmaPod either way.)

A minimal React component

import { useEffect, useRef, useState } from "react";

export function PodChat() {
  const [text, setText] = useState("");
  const [lines, setLines] = useState<Array<{ who: string; text: string }>>([]);
  const runtimeRef = useRef<any>(null);

  useEffect(() => {
    let mounted: any;
    let unsub: Array<() => void> = [];

    (async () => {
      mounted = await window.GemmaPod.mountPod(null, config, {
        ui: "none",                 // no Preact widget
        fallbackUi: "default",      // shim builds the fallback panel for us
      });
      runtimeRef.current = mounted.runtime;

      unsub.push(
        mounted.runtime.events.on("ui.event", ({ event }) => {
          if (event.type === "TEXT_MESSAGE_CONTENT") {
            setLines((cur) => appendAssistantDelta(cur, event.delta));
          }
        }),
      );
    })();

    return () => {
      unsub.forEach((u) => u());
      mounted?.destroy();
    };
  }, []);

  async function send() {
    const t = text.trim();
    if (!t || !runtimeRef.current) return;
    setLines((cur) => [...cur, { who: "user", text: t }]);
    setText("");
    for await (const _chunk of runtimeRef.current.chat.stream(t)) {
      // Assistant text is rendered through ui.event above; just drain.
    }
  }

  return (
    <div>
      {lines.map((l, i) => (
        <div key={i}>
          <strong>{l.who}:</strong> {l.text}
        </div>
      ))}
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={send}>Send</button>
    </div>
  );
}

const config = {
  name: "Headless demo",
  persona: "Demo agent.",
  systemPrompt: "Be terse.",
  model: "gemma4:e4b",
  transport: {
    webrtc: { signalUrl: "wss://signal.gemmapod.com/signal", podId: "demo" },
    fallback: { model: "onnx-community/gemma-4-E2B-it-ONNX" },
  },
};

function appendAssistantDelta(
  cur: Array<{ who: string; text: string }>,
  delta?: string,
) {
  if (!delta) return cur;
  const last = cur[cur.length - 1];
  if (last && last.who === "assistant") {
    return [...cur.slice(0, -1), { who: "assistant", text: last.text + delta }];
  }
  return [...cur, { who: "assistant", text: delta }];
}

Pattern: mount the fallback panel into your layout

Pass a React ref as fallbackMountParent. The shim will inject the WebGPU prepare panel there instead of document.body:

const fallbackHost = useRef<HTMLDivElement | null>(null);
// …
await window.GemmaPod.mountPod(null, config, {
  ui: "none",
  fallbackUi: "default",
  fallbackMountParent: fallbackHost.current ?? undefined,
});
// …
return <div ref={fallbackHost} />;

Pattern: AG-UI-shaped events

If your host already speaks AG-UI (CopilotKit, AI SDK consumers), map DARTC events to AG-UI in one call:

runtime.events.on("ui.event", ({ event }) => {
  const aguiEvent = window.GemmaPod.mapDartcUiEventToAgUi(event);
  copilot.dispatch(aguiEvent); // or your host's equivalent
});

See AG-UI bridge for the full mapping.

What's next