import React, { useEffect, useState } from "react";
import styled from "styled-components";
import type { BenchmarkData } from "../types/widget-types";
// ============================================================================
// STYLED COMPONENTS
// ============================================================================
const Container = styled.div`
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
padding: 16px;
max-width: 500px;
margin: 0 auto;
background: transparent;
`;
const Title = styled.h2`
font-size: 1.2rem;
margin: 0 0 4px 0;
color: inherit;
`;
const Subtitle = styled.p`
color: #666;
font-size: 0.85rem;
margin: 0 0 16px 0;
@media (prefers-color-scheme: dark) {
color: #999;
}
`;
const StatsGrid = styled.div`
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-bottom: 16px;
`;
const StatCard = styled.div`
background: #f5f5f5;
padding: 12px;
border-radius: 8px;
text-align: center;
@media (prefers-color-scheme: dark) {
background: #2a2a2a;
}
`;
const StatLabel = styled.div`
font-size: 0.7rem;
color: #888;
text-transform: uppercase;
letter-spacing: 0.5px;
`;
const StatValue = styled.div`
font-size: 1.1rem;
font-weight: 600;
margin-top: 4px;
`;
const Badge = styled.span<{ $variant: "positive" | "negative" | "neutral" }>`
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 0.7rem;
font-weight: 500;
${({ $variant }) => {
switch ($variant) {
case "positive":
return `background: rgba(34, 197, 94, 0.15); color: #22c55e;`;
case "negative":
return `background: rgba(239, 68, 68, 0.15); color: #ef4444;`;
default:
return `background: rgba(156, 163, 175, 0.15); color: #9ca3af;`;
}
}}
`;
const ChartContainer = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
`;
const BarRow = styled.div`
display: flex;
align-items: center;
gap: 8px;
`;
const BarLabel = styled.div`
width: 70px;
font-size: 0.75rem;
color: #666;
text-align: right;
@media (prefers-color-scheme: dark) {
color: #999;
}
`;
const BarBackground = styled.div`
flex: 1;
height: 24px;
background: #eee;
border-radius: 4px;
overflow: hidden;
@media (prefers-color-scheme: dark) {
background: #333;
}
`;
const BarFill = styled.div<{ $width: number; $variant: "you" | "regional" | "national" }>`
height: 100%;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 6px;
transition: width 0.3s ease;
width: ${({ $width }) => $width}%;
${({ $variant }) => {
switch ($variant) {
case "you":
return `background: linear-gradient(90deg, #7c3aed, #a78bfa);`;
case "regional":
return `background: linear-gradient(90deg, #2563eb, #60a5fa);`;
case "national":
return `background: linear-gradient(90deg, #059669, #34d399);`;
}
}}
`;
const BarValue = styled.span`
font-size: 0.65rem;
color: white;
font-weight: 600;
`;
const Loading = styled.div`
text-align: center;
padding: 40px;
color: #888;
`;
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
function formatValue(value: number, metric: string): string {
if (metric === "margin") return `${value.toFixed(1)}%`;
if (value >= 1_000_000) return `$${(value / 1_000_000).toFixed(1)}M`;
if (value >= 1_000) return `$${(value / 1_000).toFixed(0)}K`;
return `$${value.toFixed(0)}`;
}
function extractData(source: unknown): BenchmarkData | null {
if (!source || typeof source !== "object") return null;
const obj = source as Record<string, unknown>;
// Check if it has required fields
if (obj.industry_name && obj.user_value !== undefined) {
console.log("[Widget] ✓ Found structuredContent format");
return obj as unknown as BenchmarkData;
}
// Try to extract from content array (text fallback)
if (Array.isArray(obj.content)) {
const textItem = obj.content.find(
(c: unknown) => (c as { type?: string })?.type === "text"
) as { text?: string } | undefined;
if (textItem?.text) {
try {
console.log("[Widget] Trying to parse text content as JSON");
return JSON.parse(textItem.text) as BenchmarkData;
} catch {
// Ignore parse errors
}
}
}
return null;
}
// ============================================================================
// MAIN COMPONENT
// ============================================================================
const BenchmarkingWidget: React.FC = () => {
const [data, setData] = useState<BenchmarkData | null>(null);
useEffect(() => {
console.log("[Widget] Initializing...");
// =========================================================================
// SOURCE 1: ChatGPT - window.openai.toolOutput
// =========================================================================
const checkOpenAI = (): boolean => {
if (window.openai?.toolOutput?.structuredContent) {
console.log("[Widget] ChatGPT: Found window.openai.toolOutput.structuredContent");
const extracted = extractData(window.openai.toolOutput.structuredContent);
if (extracted) {
setData(extracted);
return true;
}
}
if (window.openai?.toolOutput) {
console.log("[Widget] ChatGPT: Found window.openai.toolOutput");
const extracted = extractData(window.openai.toolOutput);
if (extracted) {
setData(extracted);
return true;
}
}
return false;
};
// Check immediately
if (checkOpenAI()) return;
// Poll for ChatGPT (async loading)
let attempts = 0;
const pollInterval = setInterval(() => {
attempts++;
if (checkOpenAI() || data) {
clearInterval(pollInterval);
} else if (attempts > 30) {
clearInterval(pollInterval);
}
}, 100);
// =========================================================================
// SOURCE 2: MCP Apps standard bridge (Claude)
// =========================================================================
const initMcpApps = async () => {
try {
// Import from installed package
const { App } = await import("@modelcontextprotocol/ext-apps");
const app = new App({ name: "Benchmark Widget", version: "1.0.0" });
app.ontoolresult = (result: { structuredContent?: unknown; content?: unknown[] }) => {
console.log("[Widget] MCP Apps: Received tool result");
console.log("[Widget] MCP Apps: result.structuredContent =", result.structuredContent);
let extracted = extractData(result.structuredContent);
if (!extracted) {
console.log("[Widget] MCP Apps: Falling back to content array");
extracted = extractData(result);
}
if (extracted) setData(extracted);
};
app.onhostcontextchanged = (ctx: { safeAreaInsets?: { top: number; bottom: number } }) => {
if (ctx?.safeAreaInsets) {
document.body.style.paddingTop = `${ctx.safeAreaInsets.top}px`;
document.body.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`;
}
};
await app.connect();
console.log("[Widget] ✓ MCP Apps SDK connected");
} catch (e) {
console.log("[Widget] MCP Apps SDK not available:", (e as Error).message);
}
};
initMcpApps();
// =========================================================================
// SOURCE 3: postMessage fallback
// =========================================================================
const handleMessage = (event: MessageEvent) => {
const msg = event.data;
if (!msg || typeof msg !== "object") return;
console.log("[Widget] Received postMessage");
const extracted =
extractData((msg as { structuredContent?: unknown }).structuredContent) ||
extractData((msg as { params?: { structuredContent?: unknown } }).params?.structuredContent) ||
extractData((msg as { result?: { structuredContent?: unknown } }).result?.structuredContent) ||
extractData(msg);
if (extracted) {
console.log("[Widget] ✓ Data loaded from postMessage");
setData(extracted);
}
};
window.addEventListener("message", handleMessage);
return () => {
clearInterval(pollInterval);
window.removeEventListener("message", handleMessage);
};
}, [data]);
// =========================================================================
// RENDER
// =========================================================================
if (!data) {
return (
<Container>
<Loading>Loading benchmark data...</Loading>
</Container>
);
}
const diff = ((data.user_value - data.regional_average) / data.regional_average) * 100;
const badgeVariant = diff > 5 ? "positive" : diff < -5 ? "negative" : "neutral";
const badgeText =
diff > 5
? `+${Math.abs(diff).toFixed(0)}%`
: diff < -5
? `-${Math.abs(diff).toFixed(0)}%`
: "~0%";
const maxVal = Math.max(data.user_value, data.regional_average, data.national_average) * 1.1;
const bars: Array<{ label: string; value: number; variant: "you" | "regional" | "national" }> = [
{ label: "You", value: data.user_value, variant: "you" },
{ label: "Regional", value: data.regional_average, variant: "regional" },
{ label: "National", value: data.national_average, variant: "national" },
];
return (
<Container>
<Title>{data.industry_name} Benchmark</Title>
<Subtitle>
{data.metric.charAt(0).toUpperCase() + data.metric.slice(1)} comparison for {data.region}
</Subtitle>
<StatsGrid>
<StatCard>
<StatLabel>Your {data.metric}</StatLabel>
<StatValue>{formatValue(data.user_value, data.metric)}</StatValue>
</StatCard>
<StatCard>
<StatLabel>Regional</StatLabel>
<StatValue>{formatValue(data.regional_average, data.metric)}</StatValue>
</StatCard>
<StatCard>
<StatLabel>vs Avg</StatLabel>
<StatValue>
<Badge $variant={badgeVariant}>{badgeText}</Badge>
</StatValue>
</StatCard>
</StatsGrid>
<ChartContainer>
{bars.map((bar) => (
<BarRow key={bar.variant}>
<BarLabel>{bar.label}</BarLabel>
<BarBackground>
<BarFill $width={(bar.value / maxVal) * 100} $variant={bar.variant}>
<BarValue>{formatValue(bar.value, data.metric)}</BarValue>
</BarFill>
</BarBackground>
</BarRow>
))}
</ChartContainer>
</Container>
);
};
export default BenchmarkingWidget;