Add Tab 3: Cost Analysis with cumulative cost chart and projections
Browse files
web/src/components/tabs/CostAnalysis.tsx
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useState, useMemo } from "react";
|
| 4 |
+
import {
|
| 5 |
+
LineChart, Line, XAxis, YAxis, CartesianGrid,
|
| 6 |
+
Tooltip, ResponsiveContainer, Legend,
|
| 7 |
+
AreaChart, Area,
|
| 8 |
+
} from "recharts";
|
| 9 |
+
|
| 10 |
+
const MODELS = [
|
| 11 |
+
{ id: "claude-sonnet", label: "Claude Sonnet 4", inputPer1k: 0.003, outputPer1k: 0.015 },
|
| 12 |
+
{ id: "claude-haiku", label: "Claude Haiku 4", inputPer1k: 0.00025, outputPer1k: 0.00125 },
|
| 13 |
+
{ id: "gpt-4o-mini", label: "GPT-4o-mini", inputPer1k: 0.00015, outputPer1k: 0.0006 },
|
| 14 |
+
{ id: "gpt-4o", label: "GPT-4o", inputPer1k: 0.0025, outputPer1k: 0.01 },
|
| 15 |
+
];
|
| 16 |
+
|
| 17 |
+
export function CostAnalysis() {
|
| 18 |
+
const [numQueries, setNumQueries] = useState(10000);
|
| 19 |
+
const [modelIdx, setModelIdx] = useState(0);
|
| 20 |
+
const model = MODELS[modelIdx];
|
| 21 |
+
|
| 22 |
+
const baselineAvgTokens = 950;
|
| 23 |
+
const graphragAvgTokens = 2400;
|
| 24 |
+
const baselineCostPerQ = (800 / 1000) * model.inputPer1k + (150 / 1000) * model.outputPer1k;
|
| 25 |
+
const graphragCostPerQ = (2200 / 1000) * model.inputPer1k + (200 / 1000) * model.outputPer1k;
|
| 26 |
+
|
| 27 |
+
const cumulativeData = useMemo(() => {
|
| 28 |
+
const points = [];
|
| 29 |
+
const step = Math.max(Math.floor(numQueries / 50), 1);
|
| 30 |
+
for (let q = 0; q <= numQueries; q += step) {
|
| 31 |
+
points.push({
|
| 32 |
+
queries: q,
|
| 33 |
+
Baseline: +(baselineCostPerQ * q).toFixed(4),
|
| 34 |
+
GraphRAG: +(graphragCostPerQ * q).toFixed(4),
|
| 35 |
+
});
|
| 36 |
+
}
|
| 37 |
+
return points;
|
| 38 |
+
}, [numQueries, baselineCostPerQ, graphragCostPerQ]);
|
| 39 |
+
|
| 40 |
+
return (
|
| 41 |
+
<div>
|
| 42 |
+
{/* Controls */}
|
| 43 |
+
<div className="card mb-6">
|
| 44 |
+
<div className="display-sm mb-4">Cost & Token Projections</div>
|
| 45 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
| 46 |
+
<div>
|
| 47 |
+
<label className="caption">Number of Queries</label>
|
| 48 |
+
<input
|
| 49 |
+
type="range"
|
| 50 |
+
min={100}
|
| 51 |
+
max={100000}
|
| 52 |
+
step={100}
|
| 53 |
+
value={numQueries}
|
| 54 |
+
onChange={(e) => setNumQueries(+e.target.value)}
|
| 55 |
+
className="w-full mt-2 accent-[#FF6B00]"
|
| 56 |
+
/>
|
| 57 |
+
<div className="body-sm font-mono mt-1">{numQueries.toLocaleString()}</div>
|
| 58 |
+
</div>
|
| 59 |
+
<div>
|
| 60 |
+
<label className="caption">LLM Model</label>
|
| 61 |
+
<div className="flex flex-wrap gap-2 mt-2">
|
| 62 |
+
{MODELS.map((m, i) => (
|
| 63 |
+
<button
|
| 64 |
+
key={m.id}
|
| 65 |
+
className={i === modelIdx ? "badge-orange" : "badge-outline cursor-pointer"}
|
| 66 |
+
onClick={() => setModelIdx(i)}
|
| 67 |
+
style={{ fontSize: "0.75rem" }}
|
| 68 |
+
>
|
| 69 |
+
{m.label}
|
| 70 |
+
</button>
|
| 71 |
+
))}
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
<div>
|
| 75 |
+
<label className="caption">Token Ratio</label>
|
| 76 |
+
<div className="metric-value-sm mt-2" style={{ color: "#cc785c" }}>
|
| 77 |
+
{(graphragAvgTokens / baselineAvgTokens).toFixed(1)}x
|
| 78 |
+
</div>
|
| 79 |
+
<div className="metric-label">GraphRAG / Baseline</div>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
|
| 84 |
+
{/* Summary Cards */}
|
| 85 |
+
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-6">
|
| 86 |
+
{[
|
| 87 |
+
{ label: "Cost/Query (Baseline)", value: "$" + baselineCostPerQ.toFixed(6), color: "#0072CE" },
|
| 88 |
+
{ label: "Cost/Query (GraphRAG)", value: "$" + graphragCostPerQ.toFixed(6), color: "#FF6B00" },
|
| 89 |
+
{ label: `Total (${(numQueries / 1000).toFixed(0)}K)`, value: "$" + (baselineCostPerQ * numQueries).toFixed(2), sub: "Baseline", color: "#0072CE" },
|
| 90 |
+
{ label: `Total (${(numQueries / 1000).toFixed(0)}K)`, value: "$" + (graphragCostPerQ * numQueries).toFixed(2), sub: "GraphRAG", color: "#FF6B00" },
|
| 91 |
+
{ label: "Annual (1K qpd)", value: "$" + (graphragCostPerQ * 1000 * 365).toFixed(0), sub: "GraphRAG", color: "#cc785c" },
|
| 92 |
+
].map((m, i) => (
|
| 93 |
+
<div key={i} className="card-cream" style={{ padding: "16px", textAlign: "center" }}>
|
| 94 |
+
<div className="metric-value-sm" style={{ color: m.color, fontSize: "1.125rem" }}>{m.value}</div>
|
| 95 |
+
<div className="metric-label">{m.label}</div>
|
| 96 |
+
</div>
|
| 97 |
+
))}
|
| 98 |
+
</div>
|
| 99 |
+
|
| 100 |
+
{/* Cumulative Cost Chart */}
|
| 101 |
+
<div className="card mb-6">
|
| 102 |
+
<div className="title-md mb-4">
|
| 103 |
+
Cumulative Cost — {model.label}
|
| 104 |
+
</div>
|
| 105 |
+
<ResponsiveContainer width="100%" height={380}>
|
| 106 |
+
<AreaChart data={cumulativeData} margin={{ top: 10, right: 30, left: 10, bottom: 0 }}>
|
| 107 |
+
<defs>
|
| 108 |
+
<linearGradient id="baselineGrad" x1="0" y1="0" x2="0" y2="1">
|
| 109 |
+
<stop offset="5%" stopColor="#0072CE" stopOpacity={0.2} />
|
| 110 |
+
<stop offset="95%" stopColor="#0072CE" stopOpacity={0} />
|
| 111 |
+
</linearGradient>
|
| 112 |
+
<linearGradient id="graphragGrad" x1="0" y1="0" x2="0" y2="1">
|
| 113 |
+
<stop offset="5%" stopColor="#FF6B00" stopOpacity={0.2} />
|
| 114 |
+
<stop offset="95%" stopColor="#FF6B00" stopOpacity={0} />
|
| 115 |
+
</linearGradient>
|
| 116 |
+
</defs>
|
| 117 |
+
<CartesianGrid strokeDasharray="3 3" stroke="#002B49" strokeOpacity={0.06} />
|
| 118 |
+
<XAxis
|
| 119 |
+
dataKey="queries"
|
| 120 |
+
tick={{ fill: "#6c6a64", fontSize: 11 }}
|
| 121 |
+
tickFormatter={(v: number) => (v >= 1000 ? `${v / 1000}K` : v.toString())}
|
| 122 |
+
/>
|
| 123 |
+
<YAxis tick={{ fill: "#6c6a64", fontSize: 11 }} tickFormatter={(v: number) => `$${v}`} />
|
| 124 |
+
<Tooltip
|
| 125 |
+
contentStyle={{ background: "#faf9f5", border: "1px solid #e6dfd8", borderRadius: "8px" }}
|
| 126 |
+
formatter={(v: number) => [`$${v.toFixed(4)}`, undefined]}
|
| 127 |
+
/>
|
| 128 |
+
<Legend />
|
| 129 |
+
<Area type="monotone" dataKey="Baseline" stroke="#0072CE" strokeWidth={2.5} fill="url(#baselineGrad)" />
|
| 130 |
+
<Area type="monotone" dataKey="GraphRAG" stroke="#FF6B00" strokeWidth={2.5} fill="url(#graphragGrad)" />
|
| 131 |
+
</AreaChart>
|
| 132 |
+
</ResponsiveContainer>
|
| 133 |
+
</div>
|
| 134 |
+
|
| 135 |
+
{/* Insight Card */}
|
| 136 |
+
<div className="card-coral">
|
| 137 |
+
<div className="display-sm" style={{ color: "white" }}>💡 Key Insight</div>
|
| 138 |
+
<p className="body-md mt-3" style={{ color: "rgba(255,255,255,0.9)", maxWidth: "640px" }}>
|
| 139 |
+
GraphRAG uses <strong>{(graphragAvgTokens / baselineAvgTokens).toFixed(1)}×</strong> more
|
| 140 |
+
tokens per query, but delivers <strong>+13% higher F1</strong> on complex multi-hop questions.
|
| 141 |
+
The Adaptive Router eliminates this overhead for simple queries by routing them to Baseline RAG —
|
| 142 |
+
achieving the best of both worlds.
|
| 143 |
+
</p>
|
| 144 |
+
<button className="btn btn-on-dark mt-4" onClick={() => document.getElementById("live")?.scrollIntoView({ behavior: "smooth" })}>
|
| 145 |
+
Try Adaptive Routing →
|
| 146 |
+
</button>
|
| 147 |
+
</div>
|
| 148 |
+
</div>
|
| 149 |
+
);
|
| 150 |
+
}
|