Skip to content

Instantly share code, notes, and snippets.

@affandhia
Last active March 12, 2026 05:39
Show Gist options
  • Select an option

  • Save affandhia/2d50d683258e9ef656973133159bead8 to your computer and use it in GitHub Desktop.

Select an option

Save affandhia/2d50d683258e9ef656973133159bead8 to your computer and use it in GitHub Desktop.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>navigator.modelContext — Chrome Built-in AI Context API</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family:
system-ui,
-apple-system,
sans-serif;
background: #0f0f0f;
color: #e0e0e0;
min-height: 100vh;
padding: 24px;
}
h1 {
font-size: 1.6rem;
font-weight: 700;
margin-bottom: 4px;
color: #fff;
}
.subtitle {
font-size: 0.9rem;
color: #888;
margin-bottom: 24px;
}
.card {
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
}
.card h2 {
font-size: 1rem;
font-weight: 600;
margin-bottom: 12px;
color: #ccc;
display: flex;
align-items: center;
gap: 8px;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 600;
}
.badge-ok {
background: #1a3a1a;
color: #4ade80;
border: 1px solid #166534;
}
.badge-warn {
background: #3a2a00;
color: #fbbf24;
border: 1px solid #92400e;
}
.badge-err {
background: #3a1a1a;
color: #f87171;
border: 1px solid #991b1b;
}
.badge-info {
background: #1a2a3a;
color: #60a5fa;
border: 1px solid #1e3a5f;
}
label {
display: block;
font-size: 0.8rem;
color: #888;
margin-bottom: 6px;
}
textarea,
input[type='text'] {
width: 100%;
background: #111;
border: 1px solid #333;
border-radius: 8px;
color: #e0e0e0;
padding: 10px 12px;
font-size: 0.9rem;
font-family: inherit;
resize: vertical;
outline: none;
}
textarea:focus,
input[type='text']:focus {
border-color: #555;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
border: none;
transition: opacity 0.15s;
}
.btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.btn-primary {
background: #3b82f6;
color: #fff;
}
.btn-primary:hover:not(:disabled) {
background: #2563eb;
}
.btn-danger {
background: #ef4444;
color: #fff;
}
.btn-danger:hover:not(:disabled) {
background: #dc2626;
}
.btn-ghost {
background: #2a2a2a;
color: #ccc;
border: 1px solid #333;
}
.btn-ghost:hover:not(:disabled) {
background: #333;
}
.output {
background: #111;
border: 1px solid #222;
border-radius: 8px;
padding: 12px;
font-size: 0.85rem;
line-height: 1.6;
min-height: 60px;
white-space: pre-wrap;
word-break: break-word;
color: #a0e0a0;
font-family: ui-monospace, monospace;
}
.output.thinking {
color: #fbbf24;
}
.row {
display: flex;
gap: 8px;
align-items: flex-start;
flex-wrap: wrap;
}
.grid2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid #222;
font-size: 0.85rem;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
color: #888;
}
.info-val {
color: #e0e0e0;
font-family: ui-monospace, monospace;
font-size: 0.8rem;
}
tabs {
display: flex;
gap: 4px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.tab {
padding: 7px 14px;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
background: #1a1a1a;
border: 1px solid #2a2a2a;
color: #888;
transition: all 0.15s;
}
.tab.active {
background: #3b82f6;
border-color: #3b82f6;
color: #fff;
}
.tab:hover:not(.active) {
background: #222;
color: #ccc;
}
.panel {
display: none;
}
.panel.active {
display: block;
}
.progress-bar {
height: 4px;
background: #222;
border-radius: 2px;
overflow: hidden;
margin-top: 8px;
}
.progress-fill {
height: 100%;
background: #3b82f6;
border-radius: 2px;
transition: width 0.3s;
}
select {
background: #111;
border: 1px solid #333;
border-radius: 8px;
color: #e0e0e0;
padding: 8px 12px;
font-size: 0.875rem;
outline: none;
}
select:focus {
border-color: #555;
}
.token-bar {
height: 8px;
background: #222;
border-radius: 4px;
overflow: hidden;
margin-top: 4px;
}
.token-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
border-radius: 4px;
transition: width 0.4s;
}
.history-msg {
padding: 8px 12px;
border-radius: 8px;
margin-bottom: 8px;
font-size: 0.85rem;
line-height: 1.5;
}
.msg-user {
background: #1e3a5f;
color: #93c5fd;
align-self: flex-end;
}
.msg-assistant {
background: #1a2a1a;
color: #86efac;
}
.msg-system {
background: #2a1a3a;
color: #c4b5fd;
font-style: italic;
font-size: 0.8rem;
}
.chat-history {
display: flex;
flex-direction: column;
gap: 6px;
max-height: 300px;
overflow-y: auto;
margin-bottom: 12px;
}
</style>
</head>
<body>
<h1>🤖 navigator.modelContext</h1>
<p class="subtitle">Chrome 146+ — Provide context &amp; tools to the browser's built-in AI assistant</p>
<div id="apiStatus" class="card">
<h2>API Status</h2>
<div id="statusContent"><span class="status-badge badge-info">⏳ Checking...</span></div>
<div style="margin-top: 12px; font-size: 0.82rem; color: #888; line-height: 1.6">
<strong style="color: #ccc">What is this?</strong> <code>navigator.modelContext</code> lets web pages provide
<strong>context</strong> and register <strong>tools</strong> that Chrome's built-in AI assistant (e.g. the AI
sidebar) can use while on your page. It is <em>not</em> the same as <code>LanguageModel</code> — you don't
prompt a model directly here.
</div>
</div>
<tabs>
<div class="tab active" onclick="switchTab('context')">📄 provideContext</div>
<div class="tab" onclick="switchTab('tools')">🔧 registerTool</div>
<div class="tab" onclick="switchTab('combined')">🚀 Combined Demo</div>
<div class="tab" onclick="switchTab('api')">📋 API Explorer</div>
</tabs>
<!-- PROVIDE CONTEXT PANEL -->
<div id="panel-context" class="panel active">
<div class="card">
<h2>📄 provideContext()</h2>
<p style="font-size: 0.83rem; color: #888; margin-bottom: 12px">
Provide structured context to Chrome's AI assistant so it knows what your page is about. The AI sidebar can
use this when the user opens it on your page.
</p>
<label>Page Context (describe what this page is about)</label>
<textarea id="ctxText" rows="3">
This is a product page for the AeroX Pro headphones. Price: $299. Rating: 4.8/5. Key features: Active noise cancellation, 40h battery, Bluetooth 5.3, foldable design.</textarea
>
<br />
<div class="row" style="margin-top: 10px">
<button class="btn btn-primary" onclick="doProvideContext()">📤 provideContext()</button>
<button class="btn btn-danger" onclick="doClearContext()">🗑 clearContext()</button>
</div>
<div class="output" id="ctxOut" style="margin-top: 12px; color: #60a5fa">Status will appear here...</div>
</div>
<div class="card">
<h2>💡 How it works</h2>
<div style="font-size: 0.83rem; color: #888; line-height: 1.8">
<div class="info-row">
<span class="info-label">provideContext(params)</span
><span class="info-val">Sends context + tools to browser AI</span>
</div>
<div class="info-row">
<span class="info-label">params.context</span><span class="info-val">string — describe your page</span>
</div>
<div class="info-row">
<span class="info-label">params.tools</span
><span class="info-val">array — tools AI can call (required)</span>
</div>
<div class="info-row">
<span class="info-label">clearContext()</span><span class="info-val">Removes all provided context</span>
</div>
</div>
</div>
</div>
<!-- REGISTER TOOL PANEL -->
<div id="panel-tools" class="panel">
<div class="card">
<h2>🔧 registerTool() / unregisterTool()</h2>
<p style="font-size: 0.83rem; color: #888; margin-bottom: 12px">
Register JavaScript functions as tools the browser AI can invoke. The AI sidebar can call these when it needs
live data from your page.
</p>
<div class="grid2">
<div>
<label>Tool Name</label>
<input type="text" id="toolName" value="getCurrentPageData" />
</div>
<div>
<label>Description</label>
<input type="text" id="toolDesc" value="Get live data from this page" />
</div>
</div>
<br />
<label>Tool Body (JS — return a JSON string)</label>
<textarea id="toolBody" rows="5">
function execute(params) {
return JSON.stringify({
pageTitle: document.title,
url: location.href,
timestamp: new Date().toISOString(),
params: params
});
}</textarea
>
<br />
<div class="row" style="margin-top: 10px">
<button class="btn btn-primary" onclick="doRegisterTool()">➕ registerTool()</button>
<button class="btn btn-danger" onclick="doUnregisterTool()">➖ unregisterTool()</button>
<button class="btn btn-ghost" onclick="registerPresets()">🎁 Register Presets</button>
</div>
<div class="output" id="toolRegOut" style="margin-top: 12px; color: #fbbf24">Status will appear here...</div>
</div>
<div class="card">
<h2>📋 Registered Tools</h2>
<div id="registeredToolsList" style="font-size: 0.83rem; color: #888">No tools registered yet.</div>
</div>
</div>
<!-- COMBINED DEMO PANEL -->
<div id="panel-combined" class="panel">
<div class="card">
<h2>🚀 Full Demo — Context + Tools Together</h2>
<p style="font-size: 0.83rem; color: #888; margin-bottom: 14px">
This simulates a real-world e-commerce page that provides page context AND registers tools for the browser AI.
Open the Chrome AI sidebar after clicking "Activate" to see it in action.
</p>
<div id="demoStatus" style="font-size: 0.83rem; color: #888; margin-bottom: 12px">Not activated</div>
<div class="row">
<button class="btn btn-primary" id="btnActivate" onclick="activateDemo()">🚀 Activate Page AI</button>
<button class="btn btn-danger" onclick="deactivateDemo()">⏹ Deactivate</button>
</div>
<br />
<div class="output" id="demoLog" style="color: #a0e0a0; min-height: 100px">
Click Activate to set up context and tools...
</div>
</div>
<div class="card" id="demoProduct" style="display: none">
<h2>🎧 AeroX Pro — Mock Product Page</h2>
<div style="display: grid; grid-template-columns: auto 1fr; gap: 16px; align-items: start">
<div
style="
width: 80px;
height: 80px;
background: #2a2a2a;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
"
>
🎧
</div>
<div>
<div style="font-size: 1.1rem; font-weight: 700; color: #fff">AeroX Pro Headphones</div>
<div style="color: #fbbf24; font-size: 0.9rem">★★★★★ 4.8/5 (2,341 reviews)</div>
<div style="font-size: 1.3rem; font-weight: 700; color: #4ade80; margin-top: 4px">$299.00</div>
<div style="font-size: 0.8rem; color: #888; margin-top: 4px">In stock · Free shipping</div>
</div>
</div>
<div style="margin-top: 12px; font-size: 0.83rem; color: #aaa; line-height: 1.7">
<strong style="color: #ccc">Features:</strong> Active noise cancellation · 40h battery · Bluetooth 5.3 ·
Foldable · USB-C charging · Hi-Res Audio certified
</div>
<div style="margin-top: 10px; font-size: 0.78rem; color: #666">
🤖 AI tools registered: getProductDetails, getReviews, checkStock, compareProducts
</div>
</div>
</div>
<!-- API EXPLORER PANEL -->
<div id="panel-api" class="panel">
<div class="card">
<h2>📋 Live API Explorer</h2>
<div id="apiExplorer">
<div class="info-row">
<span class="info-label">navigator.modelContext</span><span id="xMC" class="info-val">—</span>
</div>
<div class="info-row">
<span class="info-label">Prototype</span><span id="xProto" class="info-val">—</span>
</div>
<div class="info-row">
<span class="info-label">Methods</span><span id="xMethods" class="info-val">—</span>
</div>
</div>
<br />
<label>Run custom JS on navigator.modelContext</label>
<textarea id="customCode" rows="5">
// Try any of these:
// await navigator.modelContext.provideContext({ tools: [], context: 'hello' })
// await navigator.modelContext.registerTool({ name: 'test', description: 'test', execute: () => '42' })
// await navigator.modelContext.clearContext()
await navigator.modelContext.provideContext({ tools: [], context: 'Custom context from explorer' })</textarea
>
<button class="btn btn-primary" style="margin-top: 10px" onclick="runCustomCode()">▶ Run</button>
<div class="output" id="customOut" style="margin-top: 10px; color: #c4b5fd">Result will appear here...</div>
</div>
</div>
<script>
const mc = navigator.modelContext;
const registeredTools = new Set();
function switchTab(name) {
document.querySelectorAll('.tab').forEach((t) => t.classList.remove('active'));
document.querySelectorAll('.panel').forEach((p) => p.classList.remove('active'));
event.target.classList.add('active');
document.getElementById('panel-' + name).classList.add('active');
}
async function detectAPI() {
const el = document.getElementById('statusContent');
if (!mc) {
el.innerHTML = '<span class="status-badge badge-err">&#10007; navigator.modelContext not found</span>';
return;
}
const proto = Object.getPrototypeOf(mc).constructor.name;
const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(mc)).filter((k) => k !== 'constructor');
el.innerHTML =
'<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px">' +
'<span class="status-badge badge-ok">&#10003; navigator.modelContext present</span>' +
'<span class="status-badge badge-info">Prototype: ' +
proto +
'</span></div>' +
'<div class="info-row"><span class="info-label">Methods</span><span class="info-val">' +
methods.join(', ') +
'</span></div>' +
'<div class="info-row"><span class="info-label">Own keys</span><span class="info-val">' +
JSON.stringify(Object.keys(mc)) +
'</span></div>';
const xMC = document.getElementById('xMC');
const xProto = document.getElementById('xProto');
const xMethods = document.getElementById('xMethods');
if (xMC) xMC.textContent = '[object ModelContext]';
if (xProto) xProto.textContent = proto;
if (xMethods) xMethods.textContent = methods.join(', ');
}
detectAPI();
async function doProvideContext() {
const out = document.getElementById('ctxOut');
const ctx = document.getElementById('ctxText').value.trim();
out.textContent = 'Calling provideContext()...';
try {
await mc.provideContext({ context: ctx, tools: [] });
out.textContent =
'✅ provideContext() succeeded!\n\nContext sent:\n' +
ctx +
'\n\nNow open the Chrome AI sidebar on this page.';
} catch (e) {
out.textContent = '✗ Error: ' + e.message;
}
}
async function doClearContext() {
const out = document.getElementById('ctxOut');
try {
await mc.clearContext();
out.textContent = '✅ clearContext() succeeded!';
} catch (e) {
out.textContent = '✗ Error: ' + e.message;
}
}
async function doRegisterTool() {
const out = document.getElementById('toolRegOut');
const name = document.getElementById('toolName').value.trim();
const desc = document.getElementById('toolDesc').value.trim();
const body = document.getElementById('toolBody').value.trim();
if (!name || !desc) {
out.textContent = 'Name and description required.';
return;
}
let execFn;
try {
execFn = new Function('return ' + body)();
} catch (e) {
out.textContent = 'JS parse error: ' + e.message;
return;
}
try {
await mc.registerTool({ name, description: desc, execute: execFn });
registeredTools.add(name);
out.textContent = '✅ registerTool("' + name + '") succeeded!';
updateToolsList();
} catch (e) {
out.textContent = '✗ Error: ' + e.message;
}
}
async function doUnregisterTool() {
const out = document.getElementById('toolRegOut');
const name = document.getElementById('toolName').value.trim();
try {
await mc.unregisterTool(name);
registeredTools.delete(name);
out.textContent = '✅ unregisterTool("' + name + '") succeeded!';
updateToolsList();
} catch (e) {
out.textContent = '✗ Error: ' + e.message;
}
}
async function registerPresets() {
const out = document.getElementById('toolRegOut');
const presets = [
{
name: 'getCurrentTime',
description: 'Get the current date and time',
execute: () => JSON.stringify({ time: new Date().toLocaleString(), iso: new Date().toISOString() }),
},
{
name: 'getPageInfo',
description: 'Get info about the current page',
execute: () => JSON.stringify({ title: document.title, url: location.href }),
},
{
name: 'getWeather',
description: 'Get mock weather for a location',
execute: (p) =>
JSON.stringify({ city: (p || {}).city || 'Unknown', weather: 'Sunny 22C', humidity: '60%' }),
},
{
name: 'calculate',
description: 'Evaluate a math expression',
execute: (p) => {
try {
return JSON.stringify({
result: Function(
'return (' + String((p || {}).expression || '0').replace(/[^0-9+\-*/.() ]/g, '') + ')',
)(),
});
} catch (e) {
return JSON.stringify({ error: 'invalid' });
}
},
},
];
const log = [];
for (const p of presets) {
try {
try {
await mc.unregisterTool(p.name);
} catch (_) {}
await mc.registerTool(p);
registeredTools.add(p.name);
log.push('✅ ' + p.name);
} catch (e) {
log.push('✗ ' + p.name + ': ' + e.message);
}
}
out.textContent = log.join('\n');
updateToolsList();
}
function updateToolsList() {
const el = document.getElementById('registeredToolsList');
if (!registeredTools.size) {
el.textContent = 'No tools registered yet.';
return;
}
el.innerHTML = [...registeredTools]
.map(
(n) =>
'<div class="info-row"><span class="info-label" style="color:#4ade80">✓ ' +
n +
'</span>' +
'<button class="btn btn-ghost" style="padding:3px 10px;font-size:0.75rem" onclick="quickUnregister(\'' +
n +
'\')">Remove</button></div>',
)
.join('');
}
async function quickUnregister(name) {
try {
await mc.unregisterTool(name);
registeredTools.delete(name);
updateToolsList();
} catch (e) {}
}
async function activateDemo() {
const log = document.getElementById('demoLog');
const status = document.getElementById('demoStatus');
const btn = document.getElementById('btnActivate');
btn.disabled = true;
const lines = [];
const add = (t) => {
lines.push(t);
log.textContent = lines.join('\n');
};
add('Registering tools...');
const demoTools = [
{
name: 'getProductDetails',
description: 'Get details about the AeroX Pro headphones',
execute: () =>
JSON.stringify({
name: 'AeroX Pro',
price: 299,
rating: 4.8,
reviews: 2341,
features: ['ANC', '40h battery', 'Bluetooth 5.3', 'USB-C', 'Foldable'],
inStock: true,
}),
},
{
name: 'getReviews',
description: 'Get customer reviews for this product',
execute: (p) =>
JSON.stringify({
reviews: [
{ user: 'Alice', rating: 5, text: 'Best headphones I ever owned!' },
{ user: 'Bob', rating: 4, text: 'Great ANC, slightly heavy' },
{ user: 'Carol', rating: 5, text: 'Battery lasts forever' },
].slice(0, (p || {}).limit || 3),
}),
},
{
name: 'checkStock',
description: 'Check stock and shipping info',
execute: (p) =>
JSON.stringify({
color: (p || {}).color || 'black',
inStock: true,
shipsIn: '1-2 days',
freeShipping: true,
}),
},
{
name: 'compareProducts',
description: 'Compare AeroX Pro with competitors',
execute: () =>
JSON.stringify({
comparison: [
{ name: 'AeroX Pro', price: 299, battery: '40h', anc: true },
{ name: 'SonyXM5', price: 349, battery: '30h', anc: true },
{ name: 'AirPodsMax', price: 549, battery: '20h', anc: true },
],
}),
},
];
for (const t of demoTools) {
try {
try {
await mc.unregisterTool(t.name);
} catch (_) {}
await mc.registerTool(t);
registeredTools.add(t.name);
add(' ✅ ' + t.name);
} catch (e) {
add(' ✗ ' + t.name + ': ' + e.message);
}
}
add('\nProviding page context...');
try {
await mc.provideContext({
context:
'This is a product page for AeroX Pro wireless headphones. Price $299. Rating 4.8/5 stars with 2341 reviews. Features: ANC, 40h battery, Bluetooth 5.3, foldable. In stock with free shipping.',
tools: demoTools,
});
add(' ✅ Context provided!');
} catch (e) {
add(' ✗ provideContext: ' + e.message);
}
updateToolsList();
document.getElementById('demoProduct').style.display = 'block';
status.innerHTML = '<span class="status-badge badge-ok">✅ Active — Open Chrome AI sidebar</span>';
add('\n\u2389 Done! Open Chrome AI sidebar and ask:');
add(' • "What are the features of this product?"');
add(' • "Show me some reviews"');
add(' • "Compare to Sony XM5"');
add(' • "Is it in stock?"');
btn.disabled = false;
}
async function deactivateDemo() {
try {
await mc.clearContext();
} catch (e) {}
for (const name of [...registeredTools]) {
try {
await mc.unregisterTool(name);
registeredTools.delete(name);
} catch (e) {}
}
document.getElementById('demoProduct').style.display = 'none';
document.getElementById('demoStatus').textContent = 'Deactivated';
document.getElementById('demoLog').textContent = '✅ Context cleared and all tools unregistered.';
updateToolsList();
}
async function runCustomCode() {
const code = document.getElementById('customCode').value;
const out = document.getElementById('customOut');
out.textContent = 'Running...';
try {
const AsyncFn = Object.getPrototypeOf(async function () {}).constructor;
const result = await new AsyncFn('navigator', code)(navigator);
out.textContent = result === undefined ? '✅ Done (undefined)' : '✅ ' + JSON.stringify(result, null, 2);
} catch (e) {
out.textContent = '✗ ' + e.message;
}
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment