Antigravity 2.11.0(8月26日)で、HTML アーティファクトがチャットの中にそのまま描かれるようになりました。KaTeX の数式も、Chart.js や Plotly のチャートも、プレビューの中で動きます。
私が最初に書かせたのは、個人開発で続けている壁紙アプリの、カテゴリごとの枚数を棒グラフにするだけの短い一枚です。数千枚のアセットをカテゴリに振り分けていると、「いま nature が何枚で、abstract が何枚か」を目で確かめたくなる場面が定期的にあります。表よりグラフのほうが、偏りが一瞬で分かります。
出来上がったファイルは 499 バイトでした。
グラフを描いている本体は、その中に入っていません。
最小の一枚は 499 バイトで書けてしまいます
まず、実際に書いた一枚をそのまま載せます。
<!doctype html>
<meta charset="utf-8">
<title>Category counts</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<canvas id="c" width="640" height="320"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.js"></script>
<script>
new Chart(document.getElementById('c'), {
type: 'bar',
data: {
labels: ['nature', 'city', 'abstract', 'animal'],
datasets: [{ label: 'assets', data: [1840, 1220, 980, 640] }]
}
});
</script>やっていることは、キャンバスを1つ置いて、Chart.js に棒グラフを描かせるだけです。これで十分にきれいなグラフが出ます。
ただ、このファイルが 499 バイトで済んでいる理由は、軽く書けたからではありません。描画に必要なコードが、このファイルの外にあるからです。<script src="..."> の行は「ここに 200KB 弱のライブラリを持ってきてください」という依頼であって、ライブラリそのものではありません。
チャットのプレビューの中で見ている限り、この区別は表に出てきません。困るのは、その一枚をどこかへ持ち出したときです。
外部参照が残っているかは、20行で確かめられます
目視で https:// を探すのは、ファイルが長くなるほど当てになりません。@import や CSS の url() の中に紛れていることもあります。それで、外部参照を数えるだけの小さなスクリプトを手元に置いています。
// selfcheck.mjs — HTML の外部参照を数える
import { readFileSync } from 'node:fs';
const html = readFileSync(process.argv[2], 'utf8');
const pats = [
/<script[^>]+src=["']([^"']+)["']/gi,
/<link[^>]+href=["']([^"']+)["']/gi,
/<img[^>]+src=["']([^"']+)["']/gi,
/@import\s+(?:url\()?["']([^"']+)["']/gi,
/url\(\s*["']?(https?:\/\/[^"')]+)["']?\s*\)/gi,
];
const refs = new Set();
for (const p of pats) for (const m of html.matchAll(p)) refs.add(m[1]);
const remote = [...refs].filter((u) => /^(https?:)?\/\//.test(u));
console.log(`file=${process.argv[2]} bytes=${Buffer.byteLength(html)} refs=${refs.size} remote=${remote.length}`);
for (const u of remote) console.log(' REMOTE ' + u);
process.exit(remote.length ? 1 : 0);先ほどの一枚にかけると、こう出ます。
$ node selfcheck.mjs cdn.html
file=cdn.html bytes=499 refs=2 remote=2
REMOTE https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.js
REMOTE https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css参照が2件。終了コードは 1 です。
終了コードを返すようにしてあるのは、あとで自動化に載せられるようにするためです。生成した HTML を成果物として保存する工程があるなら、その手前に一度通すだけで「持ち出せない一枚」を保存前に止められます。エージェントの変更を手元で検証する流れ自体は、Antigravity 2.10.0 の埋め込みターミナルで、エージェントの変更を検証するループを組むでも触れました。今回のチェッカーは、そのループに1行足すだけの部品として使えます。
正規表現で拾っているだけなので、動的に URL を組み立てて document.createElement('script') で差し込むような書き方は捕まえられません。そこまで凝った HTML を生成させることは私の用途ではまずないため、割り切って使っています。
参照が引けない環境では、キャンバスだけが残ります
外部参照が引けないとどうなるかを、名前解決を落とした状態で確かめました。
$ curl -sS -o /dev/null --connect-timeout 5 \
--resolve cdn.jsdelivr.net:443:127.0.0.1 \
https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.js
curl: (7) Failed to connect to cdn.jsdelivr.net port 443 after 0 ms: Connection refused接続は 0.0001 秒で拒否されました。タイムアウトを待つことすらありません。
ブラウザで同じことが起きると、<script> が読み込まれないまま次の行に進みます。new Chart(...) は Chart is not defined で止まり、<canvas> は幅 640・高さ 320 の空白として残ります。
厄介なのは、ページそのものは壊れていない点です。タイトルは出ますし、レイアウトも崩れません。受け取った側から見えるのは「グラフが入るはずの場所が白い」という状態だけで、原因を推測する手がかりが画面上にほとんど残りません。
| 開く場所 | 外部参照あり(499 バイト) | 外部参照なし(201,146 バイト) |
|---|---|---|
| チャットのプレビュー | 描画される | 描画される |
| ネットワークのあるブラウザ | 描画される | 描画される |
| 機内・オフライン | 空白のまま | 描画される |
| 外部 CDN を塞いだ社内ネットワーク | 空白のまま | 描画される |
| 数か月後に開き直したとき | CDN のバージョン次第 | そのとき保存した見た目 |
最後の行が、私にとっては一番重い項目です。集計結果をグラフにして残す目的は、あとから見返すことにあります。半年後に開いて空白だったなら、残していなかったのと変わりません。
私自身、App Store に出しているアプリのアセット構成は年単位で少しずつ変わっていきます。半年前のグラフと今日のグラフを並べて初めて見えてくる偏りがあるので、古いほうが開けなくなるのは実際に困ります。
一枚に畳むのは、置換1回で済みます
やることは、<script src> の行をライブラリの中身に差し替えるだけです。
// inline.mjs — CDN 参照をファイルの中身に差し替える
import { readFileSync, writeFileSync } from 'node:fs';
let html = readFileSync('cdn.html', 'utf8');
const js = readFileSync('chart.umd.min.js', 'utf8');
html = html.replace(
/<script src="https:\/\/cdn\.jsdelivr\.net[^"]*"><\/script>/,
'<script>' + js + '</script>'
);
html = html.replace(/<link rel="stylesheet" href="https:\/\/cdn\.jsdelivr\.net[^"]*">\n/, '');
writeFileSync('inline.html', html);数式を使っていないので、KaTeX の CSS はここで落としています。使う分だけを残すほうが、結果的に小さくなります。
畳んだあとにチェッカーをもう一度通すと、こうなります。
$ node selfcheck.mjs inline.html
file=inline.html bytes=201146 refs=0 remote=0外部参照 0 件、終了コード 0 です。ファイルは 499 バイトから 201,146 バイトになりました。増えた分のほとんどは Chart.js の UMD ミニファイ版で、これが 200,807 バイトです。
なお、速度が理由で畳んでいるわけではありません。同じ CDN からの取得を3回計ったところ、0.107 秒・0.117 秒・0.110 秒でした。まっとうな回線があれば、この往復は体感に出ません。
畳む理由は、開ける場所が増えることのほうです。200KB のファイルは、メールに添付しても、リポジトリに置いても、USB メモリに入れても、そのまま開きます。
畳まないほうがよい場面もあります
とはいえ、毎回畳んでいるわけではありません。私は次のように分けています。
| 状況 | 私の選択 | 理由 |
|---|---|---|
| チャットの中で一度見て終わり | そのまま | プレビューでは差が出ない。畳む手間のほうが大きい |
| ライブラリや設定を試している最中 | そのまま | 差し替えるたびに畳み直すのは無駄 |
| 誰かに渡す・共有する | 畳む | 相手の回線とネットワーク制限が分からない |
| リポジトリや記録として残す | 畳む | 数か月後の再現性がライブラリ側の都合に左右されない |
| 移動中や機内で見返す予定がある | 畳む | オフラインで確実に開く |
境目は「この一枚は使い捨てか、残すか」です。使い捨てなら 499 バイトのまま捨ててしまってよく、残すなら畳む。判断の材料はそれだけで足ります。
Generative UI が入ってから、集計をその場でグラフにしてもらう回数が明らかに増えました。手軽に出せるぶん、そのうちの何枚かは残したくなります。残す側に回った一枚だけ、チェッカーを一度通す。それくらいの手数で足りています。
まずは、最後に描かせた HTML を1つ選んで node selfcheck.mjs にかけてみてください。remote=0 で返ってくるか、それとも数字が並ぶか。そこが分かれば、畳むかどうかの判断はすぐに付きます。
最後までお読みいただき、ありがとうございました。