クライアントセットアップ
フロントエンドにHyperGen iframeをマウント — React、Vue、Svelte、またはバニラJS。テーマ管理、リサイズ処理、ライフサイクル制御。
クライアントセットアップ
HyperGenのクライアントサイドは、サンドボックス化されたiframeをマウントし、ブートストラップドキュメントを注入し、iframeのライフサイクルを管理するコントローラーを提供します。必要なものは1つのファイルhypergen-client.tsにすべて含まれています。
API概要
| エクスポート | 目的 |
|---|---|
mountHyperGen(container, options) | サンドボックス化されたiframeを作成し、HyperGenControllerを返す |
mountHyperGen()
サンドボックス化されたiframeを作成し、ブートストラップドキュメントを読み込み、postMessageリスナーをセットアップし、コントローラーを返します。
import { mountHyperGen } from "./hypergen-client";
const controller = mountHyperGen(document.getElementById("agent-ui")!, {
bootstrapUrl: "/bootstrap",
});オプション
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
bootstrapUrl | string | -- | ブートストラップHTMLを提供するURL(iframeのsrc) |
bootstrapHtml | string | -- | インラインブートストラップHTML(iframeのsrcdoc) |
theme | Record<string, string> | -- | iframe読み込み後に送信される初期CSS変数 |
iframeOrigin | string | "*" | postMessage検証に使用される期待されるiframeオリジン |
autoResize | boolean | true | コンテンツの高さに基づいてiframeを自動リサイズ |
maxHeight | number | 2000 | iframeの最大高さ(ピクセル) |
minHeight | number | 100 | iframeの最小高さ(ピクセル) |
extraSandboxPermissions | string[] | [] | 追加のsandboxパーミッション |
className | string | -- | iframe要素のCSSクラス |
style | Partial<CSSStyleDeclaration> | -- | iframe要素のインラインスタイル |
bootstrapUrlまたはbootstrapHtmlのいずれか一方を指定する必要があります(両方は不可)。
bootstrapUrl — iframeがHTTP経由でブートストラップドキュメントを読み込みます。標準的なアプローチです: iframeのsrc属性にこのURLが設定されます。
bootstrapHtml — ブートストラップHTMLがsrcdoc経由で直接注入されます。サーバーサイドでブートストラップを生成し、追加のHTTPリクエストを避けたい場合に使用します(SSRフレームワークなど)。
// URLアプローチ(追加HTTPリクエストあり、よりシンプル)
const ctrl = mountHyperGen(el, { bootstrapUrl: "/bootstrap" });
// インラインHTMLアプローチ(追加リクエストなし、サーバーサイドレンダリングが必要)
const ctrl = mountHyperGen(el, { bootstrapHtml: serverGeneratedHtml });HyperGenController
mountHyperGen()が返すオブジェクトです。マウント後のiframeを管理するために使用します。
controller.setTheme(vars)
postMessage経由でiframeにCSS変数を送信します。iframeのブリッジスクリプトがそれらを:rootに適用します。
controller.setTheme({
"--hg-surface": "#1a1a2e",
"--hg-text": "#e0e0e0",
"--hg-accent": "#7c3aed",
});テーマを更新するためにいつでも呼び出せます(ダークモードの切り替え時など)。指定された変数のみが更新され、既存の変数は削除されません。
controller.onResize(callback)
コンテンツサイズの変更に対するコールバックを登録します。iframeのResizeObserverが高さの変更を検出し、postMessage経由で通知します。
const unsubscribe = controller.onResize((height, width) => {
console.log(`Content: ${height}px tall, ${width}px wide`);
});
// 後で: リスニングを停止
unsubscribe();autoResizeがtrue(デフォルト)の場合、iframe要素の高さはminHeight/maxHeightの範囲内で自動的に調整されます。コールバックはautoResizeの設定に関係なく発火します。
controller.onNavigate(callback)
iframeからのナビゲーションリクエストに対するコールバックを登録します。エージェントはdata-hg-navigate属性を持つ要素を含めることでナビゲーションをトリガーします。
const unsubscribe = controller.onNavigate((url) => {
// ルーターと統合
router.push(url);
});iframe内で、エージェントは以下を生成できます:
<a data-hg-navigate="/dashboard" href="#">Go to Dashboard</a>クリックすると、iframe内でのナビゲーションの代わりにhg:navigate postMessageが送信されます。
controller.onData(callback)
iframeから送信される任意のデータに対するコールバックを登録します。エージェントはブリッジがホストに転送するカスタムイベントをディスパッチできます。
const unsubscribe = controller.onData((payload) => {
console.log("Data from agent:", payload);
});iframe内で、エージェント生成のJavaScriptがデータを送信できます:
<script>
document.dispatchEvent(new CustomEvent("hg:data", {
detail: { action: "selected", itemId: 42 }
}));
</script>controller.destroy()
iframeを破棄します: SSE接続を閉じるためにhg:destroyを送信し、メッセージリスナーを削除し、すべてのサブスクリプションをクリアし、DOMからiframeを削除します。
controller.destroy();
console.log(controller.destroyed); // trueメモリリークを防ぐため、コンポーネントのアンマウント時には必ずdestroy()を呼び出してください。
controller.iframe
高度なユースケースのために、基になるHTMLIFrameElementに直接アクセスできます。
controller.iframe.style.opacity = "0.5";controller.destroyed
destroy()が呼び出されたかどうかを示す真偽値です。
フレームワーク統合
React
import { useEffect, useRef } from "react";
import { mountHyperGen, type HyperGenController } from "./hypergen-client";
function AgentUI({ theme }: { theme?: Record<string, string> }) {
const containerRef = useRef<HTMLDivElement>(null);
const controllerRef = useRef<HyperGenController | null>(null);
useEffect(() => {
if (!containerRef.current) return;
const controller = mountHyperGen(containerRef.current, {
bootstrapUrl: "/bootstrap",
theme,
});
controllerRef.current = controller;
controller.onNavigate((url) => {
// ルーターを使用: window.location.href = url
});
return () => controller.destroy();
}, []);
// propsが変更されたらテーマを更新
useEffect(() => {
if (theme && controllerRef.current && !controllerRef.current.destroyed) {
controllerRef.current.setTheme(theme);
}
}, [theme]);
return <div ref={containerRef} />;
}Vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from "vue";
import { mountHyperGen, type HyperGenController } from "./hypergen-client";
const props = defineProps<{
theme?: Record<string, string>;
}>();
const container = ref<HTMLElement | null>(null);
let controller: HyperGenController | null = null;
onMounted(() => {
if (!container.value) return;
controller = mountHyperGen(container.value, {
bootstrapUrl: "/bootstrap",
theme: props.theme,
});
controller.onNavigate((url) => {
// useRouter().push(url)
});
});
watch(() => props.theme, (newTheme) => {
if (newTheme && controller && !controller.destroyed) {
controller.setTheme(newTheme);
}
});
onUnmounted(() => {
controller?.destroy();
});
</script>
<template>
<div ref="container" />
</template>Svelte
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { mountHyperGen, type HyperGenController } from "./hypergen-client";
export let theme: Record<string, string> | undefined = undefined;
let container: HTMLElement;
let controller: HyperGenController | null = null;
onMount(() => {
controller = mountHyperGen(container, {
bootstrapUrl: "/bootstrap",
theme,
});
controller.onNavigate((url) => {
// goto(url)
});
});
onDestroy(() => {
controller?.destroy();
});
$: if (theme && controller && !controller.destroyed) {
controller.setTheme(theme);
}
</script>
<div bind:this={container}></div>バニラJS
<div id="agent-ui"></div>
<script type="module">
import { mountHyperGen } from "./hypergen-client.js";
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);
});
// ページアンロード時にクリーンアップ
window.addEventListener("beforeunload", () => {
controller.destroy();
});
</script>自動リサイズ動作
デフォルトでは、iframeはコンテンツの高さに合わせて自動的にリサイズされます。フロー:
- iframeのブートストラップドキュメントに
<body>のResizeObserverが含まれる - コンテンツのサイズが変わると、オブザーバーがホストに
hg:resizeメッセージを送信 mountHyperGen()がメッセージを受信し、iframe.style.heightを調整- 高さは
minHeightとmaxHeightの間にクランプされる
リサイズの遷移はスムーズ(150msイーズアウト)で、視覚的なちらつきを防ぎます。
自動リサイズを無効にして自分で処理する場合:
const controller = mountHyperGen(el, {
bootstrapUrl: "/bootstrap",
autoResize: false,
});
controller.onResize((height) => {
// カスタムリサイズロジック
el.style.height = `${Math.min(height, 800)}px`;
});1ページに複数のiframe
複数のHyperGen iframeをマウントでき、それぞれが独自のコントローラーとSSE接続を持ちます:
const chatCtrl = mountHyperGen(
document.getElementById("chat")!,
{ bootstrapUrl: "/bootstrap?view=chat" },
);
const dashCtrl = mountHyperGen(
document.getElementById("dashboard")!,
{ bootstrapUrl: "/bootstrap?view=dashboard" },
);
// 各コントローラーは独立
chatCtrl.setTheme({ "--hg-accent": "#10b981" });
dashCtrl.setTheme({ "--hg-accent": "#f59e0b" });各iframeは独自のSSE接続、テーマ、ライフサイクルを維持します。mountHyperGen()の実装は、受信postMessageイベントが正しいiframeから発信されていることを検証するため(event.source === iframe.contentWindow)、インスタンス間でメッセージが交差することはありません。
Sandboxパーミッション
デフォルトのsandboxはallow-scripts allow-same-origin allow-forms — HTMXが必要とする最小限です。
セキュリティ
sandboxパーミッションを追加すると、セキュリティ境界が弱くなります。実際に必要なパーミッションのみを追加してください。
const controller = mountHyperGen(el, {
bootstrapUrl: "/bootstrap",
extraSandboxPermissions: ["allow-popups"], // 注意して使用
});完全なセキュリティモデルの理由についてはADR-004を参照してください。