Add Tab 1: Live Query Comparison with side-by-side pipeline display
Browse files
web/src/components/tabs/LiveCompare.tsx
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useState } from "react";
|
| 4 |
+
import {
|
| 5 |
+
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip,
|
| 6 |
+
ResponsiveContainer, Legend,
|
| 7 |
+
} from "recharts";
|
| 8 |
+
|
| 9 |
+
interface PipelineResult {
|
| 10 |
+
answer: string;
|
| 11 |
+
tokens: number;
|
| 12 |
+
latencyMs: number;
|
| 13 |
+
costUsd: number;
|
| 14 |
+
entities: string[];
|
| 15 |
+
relations: string[];
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
interface ComparisonState {
|
| 19 |
+
loading: boolean;
|
| 20 |
+
query: string;
|
| 21 |
+
baseline: PipelineResult | null;
|
| 22 |
+
graphrag: PipelineResult | null;
|
| 23 |
+
complexity: number;
|
| 24 |
+
queryType: string;
|
| 25 |
+
recommended: string;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
const EXAMPLES = [
|
| 29 |
+
"Were Scott Derrickson and Ed Wood of the same nationality?",
|
| 30 |
+
"What government position was held by the woman who portrayed Nora Batty?",
|
| 31 |
+
"Which magazine was started first, Arthur's Magazine or First for Women?",
|
| 32 |
+
"Who was born first, Arthur Conan Doyle or Agatha Christie?",
|
| 33 |
+
"What is the capital of the country where the Eiffel Tower is located?",
|
| 34 |
+
];
|
| 35 |
+
|
| 36 |
+
export function LiveCompare() {
|
| 37 |
+
const [state, setState] = useState<ComparisonState>({
|
| 38 |
+
loading: false,
|
| 39 |
+
query: "",
|
| 40 |
+
baseline: null,
|
| 41 |
+
graphrag: null,
|
| 42 |
+
complexity: 0,
|
| 43 |
+
queryType: "",
|
| 44 |
+
recommended: "",
|
| 45 |
+
});
|
| 46 |
+
|
| 47 |
+
const [adaptiveRouting, setAdaptiveRouting] = useState(true);
|
| 48 |
+
const [showContexts, setShowContexts] = useState(false);
|
| 49 |
+
|
| 50 |
+
const runComparison = async () => {
|
| 51 |
+
if (!state.query.trim()) return;
|
| 52 |
+
setState((s) => ({ ...s, loading: true }));
|
| 53 |
+
|
| 54 |
+
try {
|
| 55 |
+
const res = await fetch("/api/compare", {
|
| 56 |
+
method: "POST",
|
| 57 |
+
headers: { "Content-Type": "application/json" },
|
| 58 |
+
body: JSON.stringify({
|
| 59 |
+
query: state.query,
|
| 60 |
+
adaptiveRouting,
|
| 61 |
+
}),
|
| 62 |
+
});
|
| 63 |
+
const data = await res.json();
|
| 64 |
+
setState((s) => ({
|
| 65 |
+
...s,
|
| 66 |
+
loading: false,
|
| 67 |
+
baseline: data.baseline,
|
| 68 |
+
graphrag: data.graphrag,
|
| 69 |
+
complexity: data.complexity ?? 0,
|
| 70 |
+
queryType: data.queryType ?? "",
|
| 71 |
+
recommended: data.recommended ?? "",
|
| 72 |
+
}));
|
| 73 |
+
} catch {
|
| 74 |
+
// Demo fallback with mock data
|
| 75 |
+
setState((s) => ({
|
| 76 |
+
...s,
|
| 77 |
+
loading: false,
|
| 78 |
+
baseline: {
|
| 79 |
+
answer: "Based on the context, both Scott Derrickson and Ed Wood were American, so yes, they shared the same nationality.",
|
| 80 |
+
tokens: 847,
|
| 81 |
+
latencyMs: 1240,
|
| 82 |
+
costUsd: 0.000203,
|
| 83 |
+
entities: [],
|
| 84 |
+
relations: [],
|
| 85 |
+
},
|
| 86 |
+
graphrag: {
|
| 87 |
+
answer: "Yes. Scott Derrickson (born in Denver, Colorado, USA) and Ed Wood (born in Poughkeepsie, New York, USA) were both American. Following the NATIONALITY relationships in the knowledge graph confirms they share the same nationality.",
|
| 88 |
+
tokens: 2134,
|
| 89 |
+
latencyMs: 3820,
|
| 90 |
+
costUsd: 0.000518,
|
| 91 |
+
entities: ["Scott Derrickson", "Ed Wood", "United States", "Denver", "Poughkeepsie"],
|
| 92 |
+
relations: [
|
| 93 |
+
"Scott Derrickson -[BORN_IN]-> Denver, Colorado",
|
| 94 |
+
"Denver -[LOCATED_IN]-> United States",
|
| 95 |
+
"Ed Wood -[BORN_IN]-> Poughkeepsie, New York",
|
| 96 |
+
"Poughkeepsie -[LOCATED_IN]-> United States",
|
| 97 |
+
],
|
| 98 |
+
},
|
| 99 |
+
complexity: 0.72,
|
| 100 |
+
queryType: "comparison",
|
| 101 |
+
recommended: "graphrag",
|
| 102 |
+
}));
|
| 103 |
+
}
|
| 104 |
+
};
|
| 105 |
+
|
| 106 |
+
const chartData = state.baseline && state.graphrag ? [
|
| 107 |
+
{ name: "Tokens", Baseline: state.baseline.tokens, GraphRAG: state.graphrag.tokens },
|
| 108 |
+
{ name: "Latency (ms)", Baseline: state.baseline.latencyMs, GraphRAG: state.graphrag.latencyMs },
|
| 109 |
+
] : [];
|
| 110 |
+
|
| 111 |
+
return (
|
| 112 |
+
<div>
|
| 113 |
+
{/* Query Input */}
|
| 114 |
+
<div className="card mb-6">
|
| 115 |
+
<div className="display-sm mb-4">Ask a question</div>
|
| 116 |
+
<div className="flex flex-col lg:flex-row gap-4">
|
| 117 |
+
<div className="flex-1">
|
| 118 |
+
<textarea
|
| 119 |
+
className="input textarea"
|
| 120 |
+
placeholder="e.g., Were Scott Derrickson and Ed Wood of the same nationality?"
|
| 121 |
+
value={state.query}
|
| 122 |
+
onChange={(e) => setState((s) => ({ ...s, query: e.target.value }))}
|
| 123 |
+
onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); runComparison(); } }}
|
| 124 |
+
rows={3}
|
| 125 |
+
/>
|
| 126 |
+
</div>
|
| 127 |
+
<div className="flex flex-col gap-3 lg:w-48">
|
| 128 |
+
<label className="flex items-center gap-2 cursor-pointer caption">
|
| 129 |
+
<input
|
| 130 |
+
type="checkbox"
|
| 131 |
+
checked={adaptiveRouting}
|
| 132 |
+
onChange={(e) => setAdaptiveRouting(e.target.checked)}
|
| 133 |
+
className="w-4 h-4 accent-[#FF6B00]"
|
| 134 |
+
/>
|
| 135 |
+
🧠 Adaptive Routing
|
| 136 |
+
</label>
|
| 137 |
+
<button
|
| 138 |
+
className="btn btn-primary btn-lg w-full"
|
| 139 |
+
onClick={runComparison}
|
| 140 |
+
disabled={state.loading || !state.query.trim()}
|
| 141 |
+
>
|
| 142 |
+
{state.loading ? (
|
| 143 |
+
<span className="flex items-center gap-2">
|
| 144 |
+
<span className="animate-spin inline-block w-4 h-4 border-2 border-white border-t-transparent rounded-full" />
|
| 145 |
+
Running…
|
| 146 |
+
</span>
|
| 147 |
+
) : (
|
| 148 |
+
"▶ Compare"
|
| 149 |
+
)}
|
| 150 |
+
</button>
|
| 151 |
+
</div>
|
| 152 |
+
</div>
|
| 153 |
+
|
| 154 |
+
{/* Example queries */}
|
| 155 |
+
<div className="flex flex-wrap gap-2 mt-4">
|
| 156 |
+
<span className="caption">Try:</span>
|
| 157 |
+
{EXAMPLES.slice(0, 3).map((q, i) => (
|
| 158 |
+
<button
|
| 159 |
+
key={i}
|
| 160 |
+
className="badge-outline cursor-pointer hover:bg-surface-soft transition-colors"
|
| 161 |
+
style={{ fontSize: "0.75rem" }}
|
| 162 |
+
onClick={() => setState((s) => ({ ...s, query: q }))}
|
| 163 |
+
>
|
| 164 |
+
{q.slice(0, 50)}…
|
| 165 |
+
</button>
|
| 166 |
+
))}
|
| 167 |
+
</div>
|
| 168 |
+
</div>
|
| 169 |
+
|
| 170 |
+
{/* Adaptive Routing Badge */}
|
| 171 |
+
{state.recommended && (
|
| 172 |
+
<div className="card-cream mb-6 flex items-center gap-4 flex-wrap">
|
| 173 |
+
<span className="badge-orange">🧠 Adaptive Router</span>
|
| 174 |
+
<span className="body-sm">
|
| 175 |
+
Complexity: <strong>{state.complexity.toFixed(2)}</strong> · Type:{" "}
|
| 176 |
+
<strong>{state.queryType}</strong> · Recommended:{" "}
|
| 177 |
+
<strong style={{ color: state.recommended === "graphrag" ? "#FF6B00" : "#0072CE" }}>
|
| 178 |
+
{state.recommended === "graphrag" ? "GraphRAG" : "Baseline RAG"}
|
| 179 |
+
</strong>
|
| 180 |
+
</span>
|
| 181 |
+
</div>
|
| 182 |
+
)}
|
| 183 |
+
|
| 184 |
+
{/* Results */}
|
| 185 |
+
{state.baseline && state.graphrag && (
|
| 186 |
+
<>
|
| 187 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
| 188 |
+
{/* Baseline Card */}
|
| 189 |
+
<div className="card pipeline-baseline">
|
| 190 |
+
<div className="flex items-center gap-2 mb-4">
|
| 191 |
+
<div className="w-3 h-3 rounded-full" style={{ background: "#0072CE" }} />
|
| 192 |
+
<span className="title-md">Baseline RAG</span>
|
| 193 |
+
<span className="badge-blue ml-auto">Pipeline A</span>
|
| 194 |
+
</div>
|
| 195 |
+
<p className="body-md mb-4" style={{ minHeight: "80px" }}>
|
| 196 |
+
{state.baseline.answer}
|
| 197 |
+
</p>
|
| 198 |
+
<div className="grid grid-cols-3 gap-4 pt-4" style={{ borderTop: "1px solid var(--color-hairline)" }}>
|
| 199 |
+
<div>
|
| 200 |
+
<div className="metric-value-sm" style={{ color: "#0072CE", fontFamily: "var(--font-mono)" }}>
|
| 201 |
+
{state.baseline.tokens.toLocaleString()}
|
| 202 |
+
</div>
|
| 203 |
+
<div className="metric-label">Tokens</div>
|
| 204 |
+
</div>
|
| 205 |
+
<div>
|
| 206 |
+
<div className="metric-value-sm" style={{ color: "#0072CE", fontFamily: "var(--font-mono)" }}>
|
| 207 |
+
{state.baseline.latencyMs.toFixed(0)}ms
|
| 208 |
+
</div>
|
| 209 |
+
<div className="metric-label">Latency</div>
|
| 210 |
+
</div>
|
| 211 |
+
<div>
|
| 212 |
+
<div className="metric-value-sm" style={{ color: "#0072CE", fontFamily: "var(--font-mono)" }}>
|
| 213 |
+
${state.baseline.costUsd.toFixed(6)}
|
| 214 |
+
</div>
|
| 215 |
+
<div className="metric-label">Cost</div>
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
</div>
|
| 219 |
+
|
| 220 |
+
{/* GraphRAG Card */}
|
| 221 |
+
<div className="card pipeline-graphrag">
|
| 222 |
+
<div className="flex items-center gap-2 mb-4">
|
| 223 |
+
<div className="w-3 h-3 rounded-full" style={{ background: "#FF6B00" }} />
|
| 224 |
+
<span className="title-md">GraphRAG</span>
|
| 225 |
+
<span className="badge-orange ml-auto">Pipeline B</span>
|
| 226 |
+
</div>
|
| 227 |
+
<p className="body-md mb-4" style={{ minHeight: "80px" }}>
|
| 228 |
+
{state.graphrag.answer}
|
| 229 |
+
</p>
|
| 230 |
+
<div className="grid grid-cols-3 gap-4 pt-4" style={{ borderTop: "1px solid var(--color-hairline)" }}>
|
| 231 |
+
<div>
|
| 232 |
+
<div className="metric-value-sm" style={{ color: "#FF6B00", fontFamily: "var(--font-mono)" }}>
|
| 233 |
+
{state.graphrag.tokens.toLocaleString()}
|
| 234 |
+
</div>
|
| 235 |
+
<div className="metric-label">Tokens</div>
|
| 236 |
+
</div>
|
| 237 |
+
<div>
|
| 238 |
+
<div className="metric-value-sm" style={{ color: "#FF6B00", fontFamily: "var(--font-mono)" }}>
|
| 239 |
+
{state.graphrag.latencyMs.toFixed(0)}ms
|
| 240 |
+
</div>
|
| 241 |
+
<div className="metric-label">Latency</div>
|
| 242 |
+
</div>
|
| 243 |
+
<div>
|
| 244 |
+
<div className="metric-value-sm" style={{ color: "#FF6B00", fontFamily: "var(--font-mono)" }}>
|
| 245 |
+
${state.graphrag.costUsd.toFixed(6)}
|
| 246 |
+
</div>
|
| 247 |
+
<div className="metric-label">Cost</div>
|
| 248 |
+
</div>
|
| 249 |
+
</div>
|
| 250 |
+
|
| 251 |
+
{/* Entities & Relations */}
|
| 252 |
+
{state.graphrag.entities.length > 0 && (
|
| 253 |
+
<div className="mt-4 pt-4" style={{ borderTop: "1px solid var(--color-hairline)" }}>
|
| 254 |
+
<div className="caption mb-2">Entities Found:</div>
|
| 255 |
+
<div className="flex flex-wrap gap-1">
|
| 256 |
+
{state.graphrag.entities.map((e, i) => (
|
| 257 |
+
<span key={i} className="badge-outline" style={{ fontSize: "0.6875rem" }}>{e}</span>
|
| 258 |
+
))}
|
| 259 |
+
</div>
|
| 260 |
+
{state.graphrag.relations.length > 0 && (
|
| 261 |
+
<div className="mt-3">
|
| 262 |
+
<div className="caption mb-1">Reasoning Path:</div>
|
| 263 |
+
{state.graphrag.relations.map((r, i) => (
|
| 264 |
+
<div key={i} className="body-sm" style={{ color: "#6c6a64", fontFamily: "var(--font-mono)", fontSize: "0.75rem" }}>
|
| 265 |
+
🔗 {r}
|
| 266 |
+
</div>
|
| 267 |
+
))}
|
| 268 |
+
</div>
|
| 269 |
+
)}
|
| 270 |
+
</div>
|
| 271 |
+
)}
|
| 272 |
+
</div>
|
| 273 |
+
</div>
|
| 274 |
+
|
| 275 |
+
{/* Comparison Chart */}
|
| 276 |
+
<div className="card">
|
| 277 |
+
<div className="title-md mb-4">Metrics Comparison</div>
|
| 278 |
+
<ResponsiveContainer width="100%" height={280}>
|
| 279 |
+
<BarChart data={chartData} margin={{ top: 20, right: 30, left: 0, bottom: 0 }}>
|
| 280 |
+
<CartesianGrid strokeDasharray="3 3" stroke="#002B49" strokeOpacity={0.08} />
|
| 281 |
+
<XAxis dataKey="name" tick={{ fill: "#6c6a64", fontSize: 13 }} />
|
| 282 |
+
<YAxis tick={{ fill: "#6c6a64", fontSize: 12 }} />
|
| 283 |
+
<Tooltip
|
| 284 |
+
contentStyle={{
|
| 285 |
+
background: "#faf9f5",
|
| 286 |
+
border: "1px solid #e6dfd8",
|
| 287 |
+
borderRadius: "8px",
|
| 288 |
+
fontFamily: "var(--font-sans)",
|
| 289 |
+
}}
|
| 290 |
+
/>
|
| 291 |
+
<Legend />
|
| 292 |
+
<Bar dataKey="Baseline" fill="#0072CE" radius={[4, 4, 0, 0]} />
|
| 293 |
+
<Bar dataKey="GraphRAG" fill="#FF6B00" radius={[4, 4, 0, 0]} />
|
| 294 |
+
</BarChart>
|
| 295 |
+
</ResponsiveContainer>
|
| 296 |
+
</div>
|
| 297 |
+
</>
|
| 298 |
+
)}
|
| 299 |
+
</div>
|
| 300 |
+
);
|
| 301 |
+
}
|