<!DOCTYPE html>
<html>
<head>
<meta name="color-scheme" content="light dark">
<style>
html, body { margin: 0; padding: 0; background: transparent; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px; }
.container { max-width: 500px; }
h2 { font-size: 1.2rem; margin: 0 0 12px 0; }
.subtitle { color: #666; font-size: 0.85rem; margin-bottom: 16px; }
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 16px; }
.stat { background: #f5f5f5; padding: 12px; border-radius: 8px; text-align: center; }
@media (prefers-color-scheme: dark) { .stat { background: #2a2a2a; } }
.stat-label { font-size: 0.7rem; color: #888; text-transform: uppercase; }
.stat-value { font-size: 1.1rem; font-weight: 600; margin-top: 4px; }
.chart { display: flex; flex-direction: column; gap: 8px; }
.bar-row { display: flex; align-items: center; gap: 8px; }
.bar-label { width: 70px; font-size: 0.75rem; color: #666; text-align: right; }
.bar-bg { flex: 1; height: 24px; background: #eee; border-radius: 4px; overflow: hidden; }
@media (prefers-color-scheme: dark) { .bar-bg { background: #333; } }
.bar { height: 100%; border-radius: 4px; display: flex; align-items: center; justify-content: flex-end; padding-right: 6px; transition: width 0.3s; }
.bar span { font-size: 0.65rem; color: white; font-weight: 600; }
.bar.you { background: linear-gradient(90deg, #7c3aed, #a78bfa); }
.bar.regional { background: linear-gradient(90deg, #2563eb, #60a5fa); }
.bar.national { background: linear-gradient(90deg, #059669, #34d399); }
.loading { text-align: center; padding: 40px; color: #888; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 0.7rem; }
.badge.positive { background: rgba(34,197,94,0.15); color: #22c55e; }
.badge.negative { background: rgba(239,68,68,0.15); color: #ef4444; }
</style>
</head>
<body>
<div class="container">
<h2 id="title">Industry Benchmark</h2>
<p class="subtitle" id="subtitle">Loading...</p>
<div class="stats" id="stats"></div>
<div class="chart" id="chart"></div>
</div>
<script type="module">
// =========================================================================
// Universal MCP Apps Widget - Works with Claude AND ChatGPT
// =========================================================================
let dataRendered = false;
function fmt(v, m) {
if (m === 'margin') return v.toFixed(1) + '%';
if (v >= 1e6) return '$' + (v/1e6).toFixed(1) + 'M';
if (v >= 1e3) return '$' + (v/1e3).toFixed(0) + 'K';
return '$' + v.toFixed(0);
}
function render(d) {
if (dataRendered) return; // Prevent duplicate renders
dataRendered = true;
console.log('[Widget] Rendering data:', d);
document.getElementById('title').textContent = d.industry_name + ' Benchmark';
document.getElementById('subtitle').textContent = d.metric.charAt(0).toUpperCase() + d.metric.slice(1) + ' comparison for ' + d.region;
const diff = ((d.user_value - d.regional_average) / d.regional_average * 100);
const cls = diff > 5 ? 'positive' : diff < -5 ? 'negative' : 'neutral';
const txt = diff > 5 ? '+' + Math.abs(diff).toFixed(0) + '%' : diff < -5 ? '-' + Math.abs(diff).toFixed(0) + '%' : '~0%';
document.getElementById('stats').innerHTML = `
<div class="stat"><div class="stat-label">Your ${d.metric}</div><div class="stat-value">${fmt(d.user_value, d.metric)}</div></div>
<div class="stat"><div class="stat-label">Regional</div><div class="stat-value">${fmt(d.regional_average, d.metric)}</div></div>
<div class="stat"><div class="stat-label">vs Avg</div><div class="stat-value"><span class="badge ${cls}">${txt}</span></div></div>
`;
const maxVal = Math.max(d.user_value, d.regional_average, d.national_average) * 1.1;
const bars = [
{ label: 'You', value: d.user_value, cls: 'you' },
{ label: 'Regional', value: d.regional_average, cls: 'regional' },
{ label: 'National', value: d.national_average, cls: 'national' },
];
document.getElementById('chart').innerHTML = bars.map(b => `
<div class="bar-row">
<div class="bar-label">${b.label}</div>
<div class="bar-bg">
<div class="bar ${b.cls}" style="width: ${(b.value / maxVal * 100).toFixed(1)}%">
<span>${fmt(b.value, d.metric)}</span>
</div>
</div>
</div>
`).join('');
}
// Try to extract data from various sources (structuredContent or text JSON)
function extractData(source) {
if (!source) return null;
// If it has our expected fields, use directly (structuredContent)
if (source.industry_name && source.user_value !== undefined) {
console.log('[Widget] ✓ Found structuredContent format');
return source;
}
// If it's a tool result with content array, parse text
if (source.content) {
const textItem = source.content.find(c => c.type === 'text');
if (textItem?.text) {
try {
console.log('[Widget] Trying to parse text content as JSON');
return JSON.parse(textItem.text);
} catch {}
}
}
return null;
}
// =========================================================================
// SOURCE 1: ChatGPT - window.openai.toolOutput (check immediately & poll)
// =========================================================================
function checkOpenAI() {
const win = window;
// Check structuredContent first (preferred)
if (win.openai?.toolOutput?.structuredContent) {
console.log('[Widget] ChatGPT: Found window.openai.toolOutput.structuredContent');
const data = extractData(win.openai.toolOutput.structuredContent);
if (data) { render(data); return true; }
}
// Check toolOutput directly
if (win.openai?.toolOutput) {
console.log('[Widget] ChatGPT: Found window.openai.toolOutput');
const data = extractData(win.openai.toolOutput);
if (data) { render(data); return true; }
}
return false;
}
// Check immediately
if (checkOpenAI()) {
console.log('[Widget] ✓ Data loaded from window.openai.toolOutput');
} else {
// Poll for ChatGPT (it may set toolOutput asynchronously)
let attempts = 0;
const pollInterval = setInterval(() => {
attempts++;
if (checkOpenAI() || dataRendered) {
clearInterval(pollInterval);
} else if (attempts > 30) { // 3 seconds
clearInterval(pollInterval);
}
}, 100);
}
// =========================================================================
// SOURCE 2: MCP Apps standard bridge (Claude, etc.) - ext-apps SDK
// =========================================================================
try {
const { App } = await import("https://unpkg.com/@modelcontextprotocol/ext-apps@0.4.0/app-with-deps");
const app = new App({ name: "Benchmark View", version: "1.0.0" });
// Handle tool result via MCP Apps standard
app.ontoolresult = (result) => {
console.log('[Widget] MCP Apps: Received tool result');
console.log('[Widget] MCP Apps: result.structuredContent =', result.structuredContent);
// Try structuredContent first (MCP standard)
let data = extractData(result.structuredContent);
if (!data) {
console.log('[Widget] MCP Apps: structuredContent not found, trying content array');
// Fallback to parsing text content
data = extractData(result);
}
if (data) render(data);
};
// Handle host context changes
app.onhostcontextchanged = (ctx) => {
if (ctx?.safeAreaInsets) {
document.body.style.paddingTop = ctx.safeAreaInsets.top + 'px';
document.body.style.paddingBottom = ctx.safeAreaInsets.bottom + 'px';
}
};
// Connect to host
await app.connect();
console.log('[Widget] ✓ MCP Apps SDK connected');
// Apply initial context
const ctx = app.getHostContext();
if (ctx?.safeAreaInsets) {
document.body.style.paddingTop = ctx.safeAreaInsets.top + 'px';
document.body.style.paddingBottom = ctx.safeAreaInsets.bottom + 'px';
}
} catch (e) {
console.log('[Widget] MCP Apps SDK not available (ChatGPT-only mode):', e.message);
}
// =========================================================================
// SOURCE 3: postMessage fallback (generic MCP hosts)
// =========================================================================
window.addEventListener('message', (event) => {
const msg = event.data;
if (!msg || typeof msg !== 'object') return;
console.log('[Widget] Received postMessage:', msg);
// Check for structuredContent in message
const data = extractData(msg.structuredContent) ||
extractData(msg.params?.structuredContent) ||
extractData(msg.result?.structuredContent) ||
extractData(msg);
if (data) {
console.log('[Widget] ✓ Data loaded from postMessage');
render(data);
}
});
</script>
</body>
</html>