Browser Annotation API を使うと、ウェブサイト上でユーザーが選択できる対象、フィードバックに添えるコンテキスト、ChatGPT に注釈を送信する前に変更をプレビューするためのコントロールをカスタマイズできます。
ChatGPT Enterprise および ChatGPT Edu のワークスペースでは、管理者がワークスペースのポリシーでサイトによる注釈のカスタマイズを許可する必要があります。
ブラウザ注釈は、サイトのコードを変更しなくても利用できます。ユーザーはページの一部を選択してコメントを追加し、コンテキストとして Codex または ChatGPT Work に送信できます。
開発者は Browser Annotation API を使って、アプリケーション固有のコンテキストやコントロールを提供できます。たとえば、デザインシステムのプレビュー画面でコンポーネントのバリエーションごとのプレビューを注釈に添えると、開発者はサイトのコンポーネントをどう更新すればよいか確認できます。
Browser Annotation API の仕組みや、ウェブサイトに注釈機能を追加する方法についてサポートが必要な場合は、Annotations Extensibility プラグインをインストールしてください。
Annotations Extensibility プラグインをインストール
このガイドで Browser Annotation API の動作を確認できます。
- ChatGPT のアプリ内ブラウザでこのページを開きます。
- このカードに表示されるおすすめのプロンプトを開いてみます。
- 注釈モードに入り、下の表またはコード例を選択して、あらかじめ用意されたレイアウトやテーマを切り替えます。
- ページ上のどの項目にも注釈を付けられるので、注釈の標準動作も確認できます。
カスタマイズする機能の選択
サイトの目的に合う機能から導入してください。
| 目的 | 導入する機能 |
|---|---|
| カードなど、複数の要素を 1 つのオブジェクトとして選択できるようにする | 選択対象 |
| 語句や文を選択できるようにする | テキスト選択コンテナ |
| 選択対象にコンテキストを追加する | 選択対象のメタデータ |
| サイトのボタンからコメント案付きの注釈を開く | 注釈リクエスト |
| サイトの UI から特定の箇所についてフィードバックを求める | テキスト範囲の注釈リクエスト |
| 注釈を開いたときに詳細コントロールを表示する | エディターのデフォルト設定 |
| アプリケーションのプロパティをプレビューする、またはユーザーの選択を記録する | カスタムコントロール |
| キャンバス内に描画されたオブジェクトを個別に選択する | 注釈サーフェス |
| サイトから注釈モードのオンとオフを切り替える | 注釈モードの制御 |
このガイドは、DevDay 2026 でリリースされたバージョン以降の ChatGPT デスクトップアプリを対象としています。アプリ内ブラウザでは、HTTPS や localhost などの安全なトップレベルページで document.oai.annotation を通じて JavaScript API を利用できます。有効な場合、ブラウザはページのスクリプトを実行する前に API を設定します。古いブラウザや未対応のブラウザでもサイトが動作するよう、各メソッドの有無を確認し、必要な DOM 要素が揃ってから連携機能を初期化してください。準備完了イベントを待ったり、ポーリングしたりする必要はありません。
API メソッドは同期的に戻り、登録ハンドルはすぐに使えます。注釈エディターの読み込みはその後で完了することがあります。サーフェスの hitTest コールバックは Promise を返すこともできます。
選択対象のカスタマイズ
デフォルトでは、注釈モードはテキスト、画像、コントロールなどを優先して、ページの DOM から要素を選択します。より大きなまとまりを選択できるようにするには、その領域に oai-annotation-container を指定し、選択対象にする子孫要素に oai-annotatable を指定します。
次の例では、グラフを含むカードを 1 つのオブジェクトとして選択できます。
<section oai-annotation-container>
<article id="chart-card" oai-annotatable="Q3 Revenue">
<h2>Q3 Revenue</h2>
<p>$120,000 this quarter</p>
<button type="button">More info</button>
</article>
</section>カード内のどこにポインターを合わせても、カード全体が強調表示されます。oai-annotatable に値を指定すると、ユーザーとモデルに示すオブジェクト名を設定できます。周囲のオブジェクトと区別できる名前を付けるか、値を省略してください。
コンテナは、これらの選択ルールを適用する範囲を定めます。oai-annotatable 属性だけを指定しても、選択動作は変わりません。コンテナ内では、ポインター直下の要素を含む選択対象のうち、DOM 上で最も近いものが選ばれます。コンテナが入れ子の場合は、最も近いコンテナが使われます。コンテナ内で選択対象が指定されていない領域を選ぶと、ウェブページに対する注釈が作成されます。すべてのコンテナの外側では標準の選択動作が適用されます。
テキスト選択の有効化
領域に oai-annotation-container-text を追加すると、注釈モードでドラッグしてテキストを選択できます。この属性に値は不要です。
<article oai-annotation-container-text>
<h2>Design guidelines</h2>
<p>Use consistent spacing between related components.</p>
<p>Leave more space between separate groups.</p>
</article>テキストを選択してドラッグを終えると、選択範囲とそのコンテキストを含むテキスト注釈エディターが開きます。テキストを選択せずにクリックしても注釈は作成されません。Escape キーを押すか、ジェスチャーを取り消すと、選択がキャンセルされます。
ジェスチャー開始時の動作は、最も近いテキスト選択コンテナまたは DOM 選択コンテナで決まります。テキスト選択コンテナでは、要素の選択に oai-annotatable は使われません。内側で要素選択に戻すには oai-annotation-container を、テキスト選択に戻すにはテキスト選択コンテナを入れ子にします。同じ要素に両方の属性がある場合は、テキスト選択が優先されます。
テキスト選択にはページの通常の選択ルールが適用され、選択範囲は開始位置のコンテナを越えることがあります。コンテナは選択範囲を切り詰めず、iframe 内での選択を有効にすることもありません。テキストフィールドは引き続き選択できますが、注釈モードの間は通常のページクリックやコントロール本来の操作はブロックされます。明示的な request() 呼び出しの動作は変わりません。
テキスト選択コンテナが機能するのは、サイトの注釈機能とサイトごとの設定が両方とも有効で、注釈モードを使用しているときだけです。
選択対象へのコンテキストの追加
選択対象として指定した要素に oai-annotation-metadata を追加すると、ページに表示されていない情報もコンテキストに含められます。たとえば、次の架空の連絡先の行には、メールの下書きを依頼するときに使うメールアドレスを含めています。
<section oai-annotation-container>
<div
id="contact-row"
oai-annotatable="Alex Morgan"
oai-annotation-metadata='{"email":"alex@example.com"}'
>
<div>Alex Morgan</div>
<div>Project lead</div>
</div>
</section>注釈モードで連絡先を選択すると、コンテキストに含まれるメールアドレスを確認できます。
名前と役職のどちらを選んでも、連絡先の行全体が選択されます。メタデータは注釈とともに表示され、会話にも注釈と一緒に渡されます。ユーザーとモデルの両方に共有したい情報だけを含めてください。実際にメールを送信するには、別途メールツールを接続する必要があります。
メタデータには、次の制限を満たす、入れ子のない小さな JSON オブジェクトを使います。
- プロパティは最大 6 個。値は文字列、有限の数値、真偽値、または
null - キーは最大 64 文字、文字列値は最大 256 文字
- シリアライズ後のオブジェクトは最大 2,048 バイト
キーの先頭には ASCII の英字を使い、キー全体には ASCII の英字、数字、スペース、アンダースコア、ハイフンのみを使ってください。単語の間にはスペースを 1 つ入れます。入れ子のオブジェクトや配列には対応していません。ブラウザは無効なメタデータを無視します。
request() やサーフェスの hitTest の戻り値を通じてメタデータを渡すこともできます。
ウェブサイトからの注釈の起動
ボタンを押すなどのユーザー操作から直接 document.oai.annotation.request(target, options) を呼び出します。対象には、現在のドキュメントに接続され、ページの表示領域内にある HTML 要素、または DOM の Range を指定できます。注釈用の属性は不要です。仮想オブジェクトの ID は指定できません。
サイトからのリクエストには、ユーザーの許可が必要な場合があります。ユーザーは、ブロックした注釈機能をサイトツール > 注釈機能で再度有効にできます。
次の例では、選択用の属性を持たない段落に対して注釈を開きます。両方の要素が作成されてからスクリプトを実行してください。
<p id="delivery-summary">
Standard delivery takes three to five business days.
</p>
<button id="discuss-delivery" type="button" hidden>Explain more</button>const summary = /** @type {HTMLElement} */ (
document.getElementById("delivery-summary")
);
const button = /** @type {HTMLButtonElement} */ (
document.getElementById("discuss-delivery")
);
button.hidden = typeof document.oai?.annotation?.request !== "function";
button.addEventListener("click", () => {
const annotation = document.oai?.annotation;
if (typeof annotation?.request !== "function") return;
annotation.request(summary, {
initialComment: "Explain this delivery estimate.",
});
});Explain more(詳しく説明) を選ぶと、段落を選択した状態で編集可能なコメント付きの注釈を開くようブラウザにリクエストします。ユーザーは注釈を編集、保存し、メッセージに添えて送信できます。注釈を開いただけでは ChatGPT にメッセージは送信されません。送信できるのはユーザーだけです。
通常配送には 3~5 営業日かかります。
ユーザー操作が有効な間に呼び出してください。先にネットワークリクエストの完了を待つと、そのユーザー操作が有効とみなされなくなることがあります。
省略可能な第 2 引数には、次のフィールドを指定できます。
| オプション | 動作 |
|---|---|
mode |
"advanced" を指定すると要素の詳細コントロールが開き、"default" を指定するとエディターの標準動作が適用されます。既定値は "default" です。"default" でもカスタムコントロールが開くことがあります。エディターのデフォルト設定を参照してください。テキスト範囲で指定できるのは "default" のみです。 |
enterAnnotationMode |
true にすると注釈モードに入り、注釈のキャンセル後や送信後もそのモードに留まります。 |
omitScreenshot |
true にすると、この注釈では自動スクリーンショットを省略します。既定値は false で、スクリーンショットは通常どおり扱われます。要素を対象とするリクエストとテキスト範囲を対象とするリクエストの両方に適用されます。 |
metadata |
有効で空でないメタデータを指定すると、このリクエストに限り、対象の HTML メタデータが置き換わります。対象がテキスト範囲の場合や、対象要素がシャドウ DOM 内にある場合、このオプションは無視されます。 |
initialComment |
編集可能なコメントを指定します。長さの上限は UTF-16 コード単位で 240 です。 |
request() は、真偽値の accepted を持つオブジェクトを返します。ブラウザがリクエストを受信して検証できたかどうかは、戻り値のオブジェクト自体ではなく result.accepted で確認してください。この値からは、エディターが開いたか、ユーザーが注釈を保存または送信したかは分かりません。
エディターの読み込み中、ブラウザは受理済みのリクエストを 1 件保持できます。処理待ちのリクエストがある場合、エディターが開いている場合、または ChatGPT がブラウザを操作している場合は、新たなリクエストを拒否することがあります。
テキスト範囲の注釈リクエスト
DOM の Range を渡すと、ブラウザ上のテキスト選択を変更せずに、特定の箇所についてフィードバックを求められます。次の例で選択するのは強調表示された語句だけです。oai-annotation-container-text 属性は必要ありません。
<p>
Your trial includes full access for <mark id="draft-passage">14 days</mark>.
</p>
<button id="discuss-passage" type="button" hidden>
Ask about highlighted text
</button>両方の要素が作成されてから、次のスクリプトを実行してください。
const passage = /** @type {HTMLElement} */ (
document.getElementById("draft-passage")
);
const button = /** @type {HTMLButtonElement} */ (
document.getElementById("discuss-passage")
);
button.hidden = typeof document.oai?.annotation?.request !== "function";
button.addEventListener("click", () => {
const annotation = document.oai?.annotation;
if (typeof annotation?.request !== "function") return;
const range = document.createRange();
range.selectNodeContents(passage);
annotation.request(range, {
initialComment: "Explain when this trial ends.",
});
});試用版ですべての機能を利用できる期間は 14 日間です。
強調表示された語句だけを対象に注釈を開きます。
範囲には現在のドキュメントの空でない可視テキストが含まれ、選択範囲の少なくとも一部がページの表示領域内にある必要があります。長さの上限は UTF-16 コード単位で 20,000 です。開始位置と終了位置が同じ範囲、空白のみのテキスト、非表示のテキストの選択、閉じたシャドウルート内の範囲には対応していません。
テキスト範囲のリクエストでは、標準のテキストエディターだけを使用します。mode: "advanced" を指定したリクエストは拒否され、リクエストのメタデータは無視されます。受理済みのリクエストがエディターの準備を待っている間は、対象テキストを利用可能な状態に保ってください。ブラウザはエディターを開く前に範囲を再確認します。注釈モードでユーザー自身がドラッグしてテキストを選択できるようにするには、別途テキスト選択コンテナを使います。
エディターのデフォルトモードの設定
ブラウザの選択 UI から注釈を開いたときに詳細コントロールをすぐに表示するには、ページの <head> に次のタグを追加します。
<meta name="oai-annotation-editor-default-mode" content="advanced" />サイトの UI から注釈を開く場合は、{ mode: "advanced" } を request() に渡します。エディターの標準動作を使う場合は mode: "default" を指定するか、指定を省略してください。ページの meta タグの設定がこのリクエストオプションを上書きすることはありません。デフォルトモードでも、エディターがコメント専用になるとは限りません。カスタムコントロールに初期値の候補が設定されている場合や、currentValue が省略されている場合は、コントロールエディターが開くことがあります。
手動での調整、折りたたみ、Option キーを押しながらクリックの操作は、Codex または localhost でのみ利用できます。ホスティングされたサイトを ChatGPT で開くと、登録済みのカスタムコントロールが自動的に表示され、調整ボタンや折りたたみボタンは表示されません。この場合も、ページから詳細モードを指定できます。
カスタムコントロールの追加
registerControls() を使って、注釈コントロールを 1 つ以上の DOM 要素に関連付けます。コントロールでは、余白のトークンなどアプリケーションのプロパティをプレビューしたり、メールの文体などユーザーが選んだ項目をリクエストに含めたりできます。
共通の余白トークンのプレビュー
この例では、2 枚のカードで同じ CSS プロパティを使用します。カードに直接コントロールを登録するため、選択用の属性は不要です。
<style>
#component-preview {
--card-padding: 16px;
}
.preview-card {
padding: var(--card-padding);
border: 1px solid #d1d5db;
}
</style>
<section id="component-preview">
<article class="preview-card">Profile card</article>
<article class="preview-card">Summary card</article>
</section>プレビューを作成してから、次のスクリプトを実行します。
const preview = /** @type {HTMLElement} */ (
document.getElementById("component-preview")
);
const annotation = document.oai?.annotation;
/** @param {CustomEvent<{ callback: string; value: number | string }>} event */
function previewSpacing(event) {
const { callback, value } = event.detail;
if (callback === "setCardPadding") {
preview.style.setProperty("--card-padding", `${value}px`);
}
}
/** @type {{ dispose(): void } | undefined} */
let registration;
if (typeof annotation?.registerControls === "function") {
preview.addEventListener("oaiannotationcontrolchange", previewSpacing);
registration = annotation.registerControls({
targets: preview.querySelectorAll(".preview-card"),
controlsHeading: "Card spacing",
controlsMode: "replace",
controls: [
{
type: "range",
label: "Card padding (pixels)",
callback: "setCardPadding",
reference: "--card-padding",
min: 8,
max: 32,
step: 4,
currentValue: 16,
},
],
});
}
function disposeAnnotationControls() {
preview.style.removeProperty("--card-padding");
registration?.dispose();
preview.removeEventListener("oaiannotationcontrolchange", previewSpacing);
}プロフィールカード
概要カード
注釈モードでいずれかのカードを選び、「カードの内側の余白(ピクセル)」を変更します。必要に応じて「調整」を選択してください。両方のカードが同時に更新されます。
いずれかのカードに注釈を付け、カードの内側の余白(ピクセル) を 16 から 24 に変更します。ホスティングされたサイトを ChatGPT で開くと、コントロールが自動的に表示されます。Codex または localhost では、必要に応じて調整を選択してください。両方のカードが更新されます。注釈にはラベル、参照先、変更前後の値が記録されます。プレビューをクリアすると、余白が元に戻ります。コンポーネントを削除するときは disposeAnnotationControls() を呼び出してください。
プレビューを使わない選択内容の記録
この例では、通常の段落にメールの文体を選ぶコントロールを追加します。選択用の属性やメタデータは不要です。段落が作成されてからスクリプトを実行してください。
<p id="email-draft">Draft a follow-up email about the project timeline.</p>const draft = /** @type {HTMLElement} */ (
document.getElementById("email-draft")
);
const registration = document.oai?.annotation?.registerControls?.({
targets: draft,
controlsHeading: "Email options",
controlsMode: "replace",
controls: [
{
type: "select",
label: "Email tone",
callback: "emailTone",
options: [
{ label: "Professional", value: "professional" },
{ label: "Friendly", value: "friendly" },
{ label: "Direct", value: "direct" },
],
defaultValue: "professional",
},
],
});このコントロールはページの変更をプレビューしないため、イベントハンドラーは不要です。currentValue を省略すると、ユーザーが最初の選択肢を変更しなくても、ブラウザは選択中の文体を注釈に含めます。段落を削除するときは registration?.dispose() を呼び出してください。
選択式コントロールのプレビュー用コールバックは option.value(例:"professional")を受け取ります。注釈履歴と ChatGPT に渡されるのは、変更前と変更後のどちらの選択肢についても、画面に表示される option.label(例:"Professional")です。ラベルには各選択肢の意味が分かる文言を使ってください。value の内部 ID が選択肢のテキストとして送信されることはありません。
コントロールと初期値の設定
登録した対象に独自のコントロールだけを表示するには controlsMode: "replace" を、組み込みコントロールと一緒に表示するには "extend" を指定します。省略可能な controlsHeading でパネルに見出しを付けられます。ブラウザは前後の空白を取り除き、1~80 文字の見出しを受け付けます。見出しがない場合、パネルには要素の HTML タグが表示されます。ChatGPT に送信されるコンテキストに見出しは含まれません。
1 件の登録につき最大 12 個のコントロールを指定できます。各コントロールには type、画面に表示する label、識別子 callback が必要です。
コントロールには変更対象を表す名前を付けてください。「ページの背景」「ブランドのメインカラー」「アクセントカラー」など、プロパティやデザイン上の役割を表すラベルを使います。「温かみのあるアイボリー」や「深い緑」といった現在の値はラベルにせず、値が変わっても意味が通じる名前にしてください。現在の値は currentValue に、対応するトークンやプロパティは reference に指定します。
たとえば、次のコントロール定義ではラベルと色を別々に指定しています。
const pageBackgroundControl = {
type: "color",
label: "Page background",
callback: "setPageBackground",
reference: "--color-background",
currentValue: "#f6f3ec",
};注釈モードでプレビューを選び、「ページの背景」を変更します。必要に応じて「調整」を選択してください。色を変えてもラベルは変わりません。
使用できるコントロールの種類は次のとおりです。
| 種類 | 値 | 追加フィールド |
|---|---|---|
color |
16 進数のカラーコード(例:"#2563eb") |
なし |
range |
数値 | min、max、step |
select |
文字列 | options({ label, value } オブジェクトの配列) |
toggle |
真偽値 | なし |
callback には JavaScript 関数ではなく文字列の識別子を指定します。同じ登録内で一意とし、先頭には ASCII の英字を使ってください。使用できる文字は ASCII の英字、数字、アンダースコア、ハイフンです。省略可能な reference は変更対象のプロパティを識別するもので、ラベルと値とともに注釈に含まれます。
currentValue には、未保存の編集内容も反映した、そのプロパティの通常時の有効な値を設定します。プレビュー中の状態や入力途中の値は基準値にしないでください。ブラウザはこの基準値を変更前後の比較やリセットに使います。アプリケーションの状態が変わったら、registration.update({ controls }) で各コントロールの currentValue を更新してください。更新が反映されるのは以降の注釈です。既存の注釈には取得時の値が保持されます。
推奨する初期値は defaultValue に設定します。コントロールの初期状態では、この値が currentValue より優先されます。両方を省略すると、色は白、範囲は最小値、選択式は最初の選択肢、トグルは false で始まります。
提案する値は通常の下書きと分けて管理してください。更新で例外が発生した場合や、リクエストが失敗するか accepted: false を返した場合は、その提案を破棄し、コントロールと提案の状態を元に戻します。リクエストが受理された場合は提案を保持してください。ブラウザはコントロールの状態を取得する前に、リクエストをキューに入れることがあります。
コントロールと登録内容の検証
ブラウザは、登録時と更新時に次の制限を満たしているか検証します。
| フィールドまたはリソース | 制限 |
|---|---|
| コントロール | 1 件の登録につき最大 12 個。callback 識別子は重複できません。その種類のコントロールに定義されているフィールドのみ使用できます。 |
| ラベルと見出し | コントロールのラベル、選択肢のラベル、controlsHeading は、前後の空白を取り除いた後も空でなく、長さが UTF-16 コード単位で 80 以下である必要があります。コントロールのテキストに制御文字や書字方向を変更する文字は使えません。 |
| 識別子 | callback は前述の形式に従い、前後の空白を取り除いた長さが UTF-16 コード単位で 80 以下である必要があります。reference は 1~80 文字で、ASCII の英字、数字、_ . / : @ $ # - が使えます。スペースは使えません。 |
| 選択肢 | 1~12 個。各選択肢の value は互いに異なる文字列で、長さの上限は UTF-16 コード単位で 512 です。currentValue と defaultValue を指定する場合は、それぞれいずれかの選択肢の値と一致する必要があります。 |
| 範囲の値 | min、max、currentValue、defaultValue は −10,000~10,000 の有限の数値である必要があります。min < max を満たし、step は 0.001~10,000 かつ max - min 以下、初期値は範囲内としてください。初期値は step の刻み幅に合わせる必要はありません。 |
| 色とトグル | 色は # の後に 3 桁、4 桁、6 桁、または 8 桁の 16 進数を指定します。トグルの値は true または false に限られます。 |
| 対象 | 登録時の対象は 1~128 件で、すべて現在のドキュメント内の要素である必要があります。更新時に targets: [] を渡すと対象との関連付けを解除できます。1 つのドキュメントにつき、コントロールの登録は最大 64 件、登録と対象の関連付けは最大 1,024 件です。 |
| シリアライズ後のコントロール | controls、controlsHeading、controlsMode を含む JSON ペイロードの長さは、UTF-16 コード単位で 16,384 以下である必要があります。関数、シンボル、BigInt 値を含まない、JSON にエンコードできるデータを使ってください。 |
定義や対象が無効な場合や上限を超えた場合、registerControls() と registration.update() は同期的に例外をスローすることがあります。dispose() の後に更新を試みた場合も例外が発生します。更新が拒否されると、コントロールと対象を含めて、以前の登録内容が保持されます。失敗は呼び出し元で処理し、通常の編集を続けられるようにしてください。管理している登録を再利用または破棄し、上限を超えないようにします。
プレビューとリセットの処理
oaiannotationcontrolchange イベントは選択された要素からバブリングします。イベントの detail には callback、value、action が含まれます。
| アクション | 渡された値の用途 |
|---|---|
preview |
リクエストされた変更を表示します。 |
preview-original |
比較のために元の状態を一時的に表示します。 |
reset |
プレビューがクリアされたときに元の状態に戻します。 |
余白の例と同様に、すべてのアクションで渡された値を適用します。ハンドラーは変更を元に戻せるようにし、繰り返し呼び出しても問題が生じないようにしてください。プレビューイベントは変更の永続化を求めるものではありません。保存にはアプリケーションの通常の保存処理を使います。無関係な編集でプレビューが上書きされないように、通常の下書き、入力途中の値、プレビューを分けて管理してください。同じ設定が編集された場合は、その設定のプレビューを置き換え、以降の注釈に使う基準値を更新します。
更新と後処理のために登録ハンドルを保持してください。update() には targets、controls、controlsHeading、controlsMode を任意の組み合わせで渡せます。省略したフィールドは以前の値を保持します。たとえば、コンポーネントの DOM 要素を置き換えるときは registration.update({ targets: newElement }) を使います。対象のコレクションにはその時点で存在する要素が記録され、その後セレクターに一致した要素が自動で追加されることはありません。
登録と対象の関連付けを解除するには targets: [] を、登録したコントロールをクリアするには controls: [] を渡します。連携機能を削除するときは dispose() を呼び出し、イベントリスナーを削除してください。後処理では通常の表示も復元する必要があります。dispose() はコントロールの登録を削除するだけで、プレビューによる変更は取り消しません。余白の例では、インラインスタイルによる上書きを削除して元の CSS 値に戻しています。どちらのメソッドも同期的に終了し、値は返しません。更新は以降の選択に反映されます。保存済みの注釈には、取得時のコントロールと対象が保持されます。
コントロールイベントの対象は、注釈に記録された要素です。登録の対象を更新しても、既存の注釈の対象は変わりません。削除済みの要素でもリセットイベントが発生することがあり、その場合、以前の親要素に設定したリスナーには届きません。
キャンバス内のオブジェクト選択
注釈サーフェスを使うと、アプリケーションはキャンバス内に描画されたオブジェクトを個別に識別できます。registerSurface() でホスト要素を登録し、hitTest 関数を指定します。この関数は、オブジェクトがある位置では安定した id を持つオブジェクトを、何もない位置では null を返します。
ホストには、安全なトップレベルドキュメントに接続された、シャドウ DOM の外側にある HTML 要素を指定してください。iframe 内でのサーフェス登録には対応していません。
選択位置の判定とオブジェクトの識別
次の例では、売上を表す棒を描画し、hitTest と安定したオブジェクト ID だけで選択できるようにします。スクリプトはキャンバスの後に配置してください。
<canvas id="revenue-canvas" width="480" height="240">
Revenue this quarter: $120,000.
</canvas>const canvas = /** @type {HTMLCanvasElement} */ (
document.getElementById("revenue-canvas")
);
const context = /** @type {CanvasRenderingContext2D} */ (
canvas.getContext("2d")
);
const bar = { x: 40, y: 60, width: 320, height: 100 };
context.fillStyle = "#2563eb";
context.fillRect(bar.x, bar.y, bar.width, bar.height);
const surface = document.oai?.annotation?.registerSurface?.({
element: canvas,
hitTest({ clientX, clientY }) {
const bounds = canvas.getBoundingClientRect();
const scaleX = bounds.width / canvas.width;
const scaleY = bounds.height / canvas.height;
const rect = {
x: bounds.left + bar.x * scaleX,
y: bounds.top + bar.y * scaleY,
width: bar.width * scaleX,
height: bar.height * scaleY,
};
if (
clientX < rect.x ||
clientX > rect.x + rect.width ||
clientY < rect.y ||
clientY > rect.y + rect.height
) {
return null;
}
return { id: "revenue-this-quarter" };
},
});注釈モードに入って図形を選ぶと、注釈でその名前、ID、塗りつぶしの色を確認できます。図形の間の空白部分は選択できません。
コード例では、注釈モードで棒を選ぶと revenue-this-quarter として識別されます。同じサーフェス内では、各オブジェクトの ID を一定に保ってください。この例では、何もない位置に対して null を返します。
hitTest は Promise を返すことができます。新しい判定によって不要になった処理をキャンセルできるよう、AbortSignal を signal として受け取ります。ブラウザは最大 250 ミリ秒待ってから DOM 選択に切り替えます。エラーが発生した場合や結果が無効な場合も DOM 選択に切り替わります。ヒットテストが正常に完了し、該当するオブジェクトがなかった場合は、明示的に null を返してください。
処理をキャンセルした場合も、Promise は必ず解決または拒否してください。ブラウザが同時に実行できる hitTest コールバックは 1 件だけです。キャンセルやタイムアウトの後でも、その Promise が確定するまで次のコールバックは実行できません。選択判定をワーカーで処理する場合は、処理を中止した時点で保留中の Promise を確定させてください。キャンセルしたワーカーの応答を単に破棄すると、以降のキャンバス内の選択ができなくなることがあります。
オブジェクトを移動した後やズームを変更した後は surface?.invalidate() を、連携機能を削除するときは surface?.dispose() を呼び出します。どちらも同期的に終了し、値は返しません。
オブジェクトのコンテキストの追加(任意)
オブジェクトの識別には id だけで十分です。表示名、意味上の役割、画面に表示されないコンテキスト、選択範囲の境界を含めるには、先ほどの hitTest コールバックでオブジェクトが見つかったときの return 文を次のように置き換えます。
return {
id: "revenue-this-quarter",
name: "Revenue this quarter",
role: "chart-bar",
metadata: { metricId: "quarterly-revenue" },
rect,
};省略可能な name はユーザーに表示されます。role にはオブジェクトの役割を簡潔に記述します。メタデータは、画面上で確認できないコンテキストを補う場合にのみ含めてください。省略可能な rect は clientX、clientY と同じく、ページの表示領域を基準とした CSS ピクセル単位で指定します。先ほどの選択判定の例では、この矩形をすでに計算しています。シーン座標から変換する際は、スケール、パン、ズームを考慮してください。
選択状態の表示(任意)
省略可能な renderSelection コールバックを使うと、ポインターを合わせたときや選択したときの表示を独自に描画できます。オブジェクトのコンテキストを指定する任意フィールドは不要です。
上のキャンバスでは、次の関数を定義し、renderSelection: renderRevenueSelection を registerSurface() のオプションに追加します。
/** @param {{ hoveredId: string | null; selectedId: string | null }} selection */
function renderRevenueSelection({ hoveredId, selectedId }) {
context.clearRect(0, 0, canvas.width, canvas.height);
context.fillStyle = "#2563eb";
context.fillRect(bar.x, bar.y, bar.width, bar.height);
if (
hoveredId === "revenue-this-quarter" ||
selectedId === "revenue-this-quarter"
) {
context.strokeStyle = "#111827";
context.lineWidth = 3;
context.strokeRect(bar.x, bar.y, bar.width, bar.height);
}
}このコールバックは、棒にポインターが合っている間や棒が選択されている間、輪郭線を付けて棒を再描画します。両方の ID が null のときは、輪郭線なしで再描画します。
キャンバス内のオブジェクトへのコントロールの追加
サーフェスの DOM 要素にカスタムコントロールを登録します。描画されたオブジェクトは API では仮想オブジェクトと呼ばれ、カスタムコントロールを使用します。組み込みの CSS コントロールやテキストコントロールは適用されません。
オブジェクトが複数ある場合は、renderSelection を使い、selectedId が変わった時点で、ブラウザが注釈の内容を取得する前にホストのコントロールを更新します。コントロールイベントには detail.virtualTarget: { surfaceId, targetId } が含まれます。各イベントの処理先は、現在の選択ではなく、取得済みの識別情報と callback から決定してください。targetId は id(hitTest が返す値)と一致し、surfaceId はブラウザに登録されたサーフェスを識別します。通常の DOM イベントには virtualTarget は含まれません。
別のオブジェクトを選択した後も、プレビュー、比較、リセットの各イベントには元のオブジェクトの識別情報が保持されます。保存した注釈を開き直しても、ヒットテストは再実行されず、取得済みのオブジェクトやメタデータが置き換わることはありません。
注釈モードの制御
モード変更のリクエストには toggle()、確定した状態の取得には isActive() を使います。ドキュメントの oaiannotationmodechange イベントを使うと、UI を実際の状態と同期できます。古いブラウザや未対応のブラウザに配慮し、両方のメソッドの有無を確認してください。次のボタンを追加し、要素が作成されてからスクリプトを実行します。
<button id="toggle-annotations" type="button" hidden>
Enter annotation mode
</button>const button = /** @type {HTMLButtonElement} */ (
document.getElementById("toggle-annotations")
);
const annotation = document.oai?.annotation;
/** @param {boolean} active */
function renderMode(active) {
button.textContent = active
? "Exit annotation mode"
: "Enter annotation mode";
}
/** @param {CustomEvent<{ active: boolean }>} event */
const onModeChange = (event) => renderMode(event.detail.active);
const onClick = () => annotation?.toggle(!annotation.isActive());
if (
typeof annotation?.toggle === "function" &&
typeof annotation?.isActive === "function"
) {
document.addEventListener("oaiannotationmodechange", onModeChange);
button.addEventListener("click", onClick);
renderMode(annotation.isActive());
button.hidden = false;
}
function cleanupAnnotationButton() {
document.removeEventListener("oaiannotationmodechange", onModeChange);
button.removeEventListener("click", onClick);
button.hidden = true;
}toggle() はモードのオンとオフを反転します。オンを指定するには true、オフを指定するには false を渡します。同じ真偽値でリクエストを繰り返しても結果は変わりません。true を渡すと、開いているエディターや進行中の有効化処理が維持されます。false を渡すと、保留中の有効化がキャンセルされ、通常の終了処理が実行されます。
toggle() はユーザー操作が有効な間に呼び出してください。同期的に返される { accepted } で分かるのは、リクエストが受理されたかどうかです。ブラウザが適用条件を確認した結果、モードが変更されない場合があります。実際の状態は例のように isActive() とイベントから取得してください。
isActive() の呼び出しにユーザー操作は不要です。ブラウザはこの値を更新してから oaiannotationmodechange を発行します。イベントの event.detail.active は真偽値です。初期状態を通知するイベントや、明示的に指定した状態が現在の状態と同じ場合の重複イベントは発行されないため、ゲッターで UI を初期化してください。モードが有効な間にアクセス権が取り消されると、最後のイベントで active: false が通知され、保持していたゲッターは false を返します。
注釈モードでは、ページ上のクリックは注釈操作として処理されます。サイトの終了ボタンなどページ上のコントロールを操作するには、スペースキーを押したまま操作します。ブラウザの注釈 UI から終了することもできます。スペースキーを押しているだけなら isActive() は true のままです。API は oai-annotation-ignore など、クリックをページに通すための属性には対応していません。
注釈モードを終了するとエディターが閉じます。保存済みの注釈は保持され、送信されたりコンポーザーに移されたりすることはありません。コンポーネントを削除するときは cleanupAnnotationButton() を呼び出してください。API への参照を取得しておくことで、名前空間がなくなった後でも、後処理でリスナーを削除できます。
連携機能のテスト
デスクトップアプリのアプリ内ブラウザでウェブサイトを開き、追加した機能をテストしてください。
- 注釈モードに入り、オブジェクトとテキストを選択します。強調表示、名前、範囲、メタデータが意図した対象と一致することを確認します。
- サイトのボタンから注釈を開きます。選択された要素またはテキスト範囲、初期コメント、エディターのモードを確認します。
- カスタムコントロールの値を変更し、元の状態と比較してからプレビューをクリアします。アプリケーションが元の状態に戻ることを確認します。
- キャンバスでは、何もない位置の選択、サイズ変更、シーンの変更をテストします。別のオブジェクトに切り替えた後も、コントロールイベントが注釈に記録された対象を更新することを確認します。非同期のヒットテストをキャンセルし、その後もオブジェクトを選択できることを確認します。
- 注釈を保存し、コンポーザーの添付プレビューから開き直して編集します。コントロールと対象が保持されていること、注釈を削除するとプレビューもクリアされることを確認します。
- メッセージに注釈を添えて送信します。選択したコンテンツ、メタデータ、リクエストした値が ChatGPT に届くことを確認します。選択式コントロールに表示されたラベルや、
currentValueを省略したコントロールで変更せずに残した選択肢も確認してください。 - API に対応していないブラウザでサイトを開き、通常の操作が引き続き機能することを確認します。
ChatGPT がサイト上で実行できる操作を公開するには、サイトツール(WebMCP)を追加します。注釈を使うと、ユーザーの選択内容とフィードバックが会話に取り込まれます。サイトツールを使うと、エージェントはそのコンテキストに基づいてアプリケーションの既存の機能を実行できます。