HyperGen

はじめに

HyperGenを5分で動かす。2つのファイルをコピーし、SSEエンドポイントを設定し、iframeをマウントして、初めてのAI生成UIをストリーミング。

はじめに

HyperGenは、AIエージェントがHTMX属性付きのHTMLフラグメントをServer-Sent Events (SSE) 経由でサンドボックス化されたiframeにストリーミングすることで、インタラクティブなユーザーインターフェースを生成できるオープンプロトコルです。インストールするSDKも、学ぶべきJSONスキーマも、フレームワークへのロックインもありません。HTML文字列を出力できるシステムなら何でもHyperGenを使えます。

前提条件

  • ランタイム: Node.js 20+、Deno 2+、またはBun 1+
  • HTTPフレームワーク: 標準Response APIをサポートするもの(Hono、Express、Next.js Route Handlers、Deno.serveなど)
  • フロントエンド: 任意のフレームワーク(React、Vue、Svelte)またはバニラHTML/JS

ステップ 1: ドロップインファイルをコピー

HyperGenはコードオーナーシップモデルに従います。ソースファイルをプロジェクトにコピーして、自分のものにします。npm installは不要です。

packages/dropin/src/からこれらのファイルをコピーしてください:

ファイル配置場所目的
hypergen-server.tsサーバーSSEストリーミング、ブートストラップHTML生成
hypergen-client.tsフロントエンドiframeマウント、テーマ注入、ライフサイクル
hypergen-theme.tsどちらでも(任意)CSS変数のTypeScript型定義

両ファイルとも外部依存関係ゼロ — 標準Web APIのみを使用します。

ステップ 2: サーバーをセットアップ

2つのHTTPエンドポイントが必要です:

  1. ブートストラップエンドポイント — iframe内に配置されるHTMLドキュメントを提供
  2. SSEエンドポイント — iframeにHTMLフラグメントをストリーミング
// server.ts
import { bootstrapHtml, createSSEStream, HyperGenResponse } from "./hypergen-server";

// iframeブートストラップドキュメントを提供
app.get("/bootstrap", () => {
  return new Response(
    bootstrapHtml({ sseEndpoint: "/api/stream" }),
    { headers: { "Content-Type": "text/html" } },
  );
});

// SSE経由でHTMLフラグメントをストリーミング
app.get("/api/stream", () => {
  const stream = createSSEStream(async function* () {
    yield "<h1>Hello from HyperGen!</h1>";
    yield "<p>This HTML was streamed via SSE.</p>";
  });

  return HyperGenResponse(stream);
});

bootstrapHtml()は、CDNからHTMXを読み込み、SSE拡張を接続し、ホスト通信用のpostMessageブリッジと自動サイズ調整用のResizeObserverを含む完全なHTMLドキュメントを生成します。

createSSEStream()は、HTML文字列の非同期ジェネレーターを受け取り、適切なSSEフォーマットとキープアライブpingを含むReadableStreamを返します。

HyperGenResponse()は、ストリームを正しいSSEヘッダー付きのResponseでラップします。

ステップ 3: フロントエンドにiframeをマウント

// client.ts
import { mountHyperGen } from "./hypergen-client";

const controller = mountHyperGen(
  document.getElementById("agent-ui")!,
  { bootstrapUrl: "/bootstrap" },
);

// オプション: アプリのテーマを注入
controller.setTheme({
  "--hg-accent": "#7c3aed",
  "--hg-surface": "#ffffff",
});

// オプション: コンテンツサイズの変更に反応
controller.onResize((height) => {
  console.log("Content height:", height);
});

mountHyperGen()は、sandbox="allow-scripts allow-same-origin allow-forms"のサンドボックス化された<iframe>を作成し、ブートストラップドキュメントを読み込み、iframeのライフサイクルを管理するHyperGenControllerを返します。

ステップ 4: エージェントからHTMLを生成

サーバーサイドの非同期ジェネレーターが、エージェントがUIを生成する場所です。各yieldは、HTMXがiframeにレンダリングするHTMLフラグメントを含む1つのSSEイベントを送信します。

app.get("/api/stream", () => {
  const stream = createSSEStream(async function* () {
    // AIエージェントを呼び出す
    const response = await agent.chat("Show me a dashboard");

    // 生成されたHTMLをストリーミング
    yield response.html;

    // または構造化データからHTMLを構築
    yield `
      <div style="padding: 16px; background: var(--hg-surface-elevated); border-radius: var(--hg-radius);">
        <h2 style="color: var(--hg-text);">Dashboard</h2>
        <button
          hx-post="/api/action"
          hx-vals='{"action": "refresh"}'
          hx-target="closest div"
          hx-swap="outerHTML"
          style="background: var(--hg-accent); color: var(--hg-accent-fg); border: none; padding: 8px 16px; border-radius: var(--hg-radius-sm); cursor: pointer;">
          Refresh
        </button>
      </div>
    `;
  });

  return HyperGenResponse(stream);
});

// HTMXから送信されるユーザーインタラクションを処理
app.post("/api/action", async (c) => {
  const body = await c.req.parseBody();
  // HTMXがスワップする新しいHTMLを返す
  return c.html("<div>Updated at " + new Date().toLocaleTimeString() + "</div>");
});

重要なポイント: エージェントの出力がそのままUIです。JSON変換もコンポーネントカタログの参照もありません — HTMX属性によるインタラクティビティと--hg-* CSS変数によるテーマ設定が施されたHTMLだけです。

完全な最小限の例

Honoを使った完全に動作する例です(任意のフレームワークに置き換え可能):

import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { bootstrapHtml, createSSEStream, HyperGenResponse } from "./hypergen-server";

const app = new Hono();

app.get("/", (c) => c.html(`<!DOCTYPE html>
<html><head><title>HyperGen Demo</title></head>
<body>
  <h1>My App</h1>
  <div id="agent-ui"></div>
  <script type="module">
    import { mountHyperGen } from "/hypergen-client.js";
    mountHyperGen(document.getElementById("agent-ui"), {
      bootstrapUrl: "/bootstrap",
    });
  </script>
</body></html>`));

app.get("/bootstrap", (c) => c.html(
  bootstrapHtml({ sseEndpoint: "/api/stream", swapStrategy: "beforeend" })
));

app.get("/api/stream", () => HyperGenResponse(
  createSSEStream(async function* () {
    yield '<p style="color: var(--hg-text);">Loading dashboard...</p>';
    await new Promise((r) => setTimeout(r, 1000));
    yield `<div style="background: var(--hg-surface-elevated); padding: 16px; border-radius: var(--hg-radius); border: 1px solid var(--hg-border);">
      <h2>Welcome</h2>
      <p>This UI was streamed by an AI agent.</p>
      <button hx-post="/api/action" hx-swap="outerHTML"
              style="background: var(--hg-accent); color: var(--hg-accent-fg); border: none; padding: 8px 16px; border-radius: var(--hg-radius-sm); cursor: pointer;">
        Click me
      </button>
    </div>`;
  })
));

app.post("/api/action", (c) => c.html(
  '<div style="color: var(--hg-accent); font-weight: bold;">Button clicked!</div>'
));

serve({ fetch: app.fetch, port: 3000 });

次のステップ

目次