Vasanth6 commited on
Commit
d0649b4
·
1 Parent(s): 25d4f70

initial: RAG Visualizer (Phases 1-4)

Browse files
backend/models/schemas.py CHANGED
@@ -96,7 +96,6 @@ class RetrievedChunk(BaseModel):
96
  end_char: int
97
  parent_id: Optional[str] = None
98
  level: int = 0
99
- text_highlighted: Optional[str] = None
100
 
101
 
102
  class QueryResponse(BaseModel):
@@ -124,61 +123,3 @@ class CompareResponse(BaseModel):
124
  results_a: List[RetrievedChunk]
125
  results_b: List[RetrievedChunk]
126
  hypothetical_answer: Optional[str] = None
127
-
128
-
129
- class JudgeRequest(BaseModel):
130
- search_query: str
131
- chunk_a: str
132
- chunk_b: str
133
-
134
-
135
- class ChunkScore(BaseModel):
136
- query_relevance: int = Field(ge=1, le=10)
137
- answer_completeness: int = Field(ge=1, le=10)
138
- factual_plausibility: int = Field(ge=1, le=10)
139
- clarity: int = Field(ge=1, le=10)
140
- overall: float
141
-
142
- @model_validator(mode="after")
143
- def check_overall(self) -> "ChunkScore":
144
- expected = round(
145
- (
146
- self.query_relevance
147
- + self.answer_completeness
148
- + self.factual_plausibility
149
- + self.clarity
150
- )
151
- / 4,
152
- 2,
153
- )
154
- self.overall = expected
155
- return self
156
-
157
-
158
- class JudgeResponse(BaseModel):
159
- winner: Literal["chunk_a", "chunk_b", "tie"]
160
- confidence: float = Field(ge=0, le=1)
161
-
162
- chunk_a_score: ChunkScore
163
- chunk_b_score: ChunkScore
164
-
165
- winner_reason: str
166
- chunk_a_strengths: List[str]
167
- chunk_b_strengths: List[str]
168
- chunk_a_weaknesses: List[str]
169
- chunk_b_weaknesses: List[str]
170
- deciding_dimension: str
171
-
172
- @model_validator(mode="after")
173
- def check_winner_consistency(self) -> "JudgeResponse":
174
- a = self.chunk_a_score.overall
175
- b = self.chunk_b_score.overall
176
- gap = abs(a - b)
177
-
178
- if gap <= 0.5:
179
- self.winner = "tie"
180
- elif a > b:
181
- self.winner = "chunk_a"
182
- else:
183
- self.winner = "chunk_b"
184
- return self
 
96
  end_char: int
97
  parent_id: Optional[str] = None
98
  level: int = 0
 
99
 
100
 
101
  class QueryResponse(BaseModel):
 
123
  results_a: List[RetrievedChunk]
124
  results_b: List[RetrievedChunk]
125
  hypothetical_answer: Optional[str] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/routers/retrieval_router.py CHANGED
@@ -12,8 +12,6 @@ from backend.engines.reducer import ReducerEngine
12
  from backend.models.schemas import (
13
  CompareRequest,
14
  CompareResponse,
15
- JudgeResponse,
16
- JudgeRequest,
17
  QueryRequest,
18
  QueryResponse,
19
  RetrievedChunk,
@@ -33,18 +31,12 @@ stop_words = set(stopwords.words("english"))
33
  @router.post("/retrieve", response_model=QueryResponse)
34
  async def retrieve(request: QueryRequest):
35
 
36
- search_text = (
37
- await get_hyde_text(request.search_text)
38
- if request.use_hyde
39
- else request.search_text
40
- )
41
-
42
  if request.use_reranking == True:
43
  "" ""
44
 
45
  retrieved_chunks, embeddings = await process_retrieval(
46
  request.embedding_model,
47
- search_text,
48
  request.strategy,
49
  request.top_k,
50
  request.search_text,
@@ -62,67 +54,11 @@ async def retrieve(request: QueryRequest):
62
  }
63
 
64
  if request.use_hyde:
65
- response_kwargs["hypothetical_answer"] = search_text
66
 
67
  return QueryResponse(**response_kwargs)
68
 
69
 
70
- @router.post("/compare", response_model=CompareResponse)
71
- async def compare(request: CompareRequest):
72
- retrieval_text = (
73
- await get_hyde_text(request.search_text)
74
- if request.use_hyde
75
- else request.search_text
76
- )
77
- (
78
- (retrieved_chunks_a, _),
79
- (retrieved_chunks_b, _),
80
- ) = await asyncio.gather(
81
- process_retrieval(
82
- request.model_a,
83
- retrieval_text,
84
- request.strategy_a,
85
- request.top_k,
86
- request.search_text,
87
- ),
88
- process_retrieval(
89
- request.model_b,
90
- retrieval_text,
91
- request.strategy_b,
92
- request.top_k,
93
- request.search_text,
94
- ),
95
- )
96
-
97
- response_kwargs = {
98
- "search_text": request.search_text,
99
- "results_a": retrieved_chunks_a,
100
- "results_b": retrieved_chunks_b,
101
- }
102
- if request.use_hyde:
103
- response_kwargs["hypothetical_answer"] = retrieval_text
104
-
105
- return CompareResponse(**response_kwargs)
106
-
107
-
108
- @router.post("/judge", response_model=JudgeResponse)
109
- async def judge(request: JudgeRequest):
110
- prompt = system_instructions.format(
111
- search_query=request.search_query,
112
- chunk_a=request.chunk_a,
113
- chunk_b=request.chunk_b,
114
- )
115
-
116
- llm_client = OllamaClient()
117
- result = await llm_client.generate(prompt=prompt)
118
-
119
- print(f"res:res: {result}")
120
-
121
- result_dict = json.loads(result)
122
-
123
- return JudgeResponse(**result_dict)
124
-
125
-
126
  async def process_retrieval(
127
  model, search_text, strategy, top_k, original_query: Optional[str] = None
128
  ):
@@ -147,7 +83,7 @@ async def process_retrieval(
147
  metadatas = result["metadatas"][0]
148
 
149
  for i in range(len(ids)):
150
- text_highlighted = get_text_highlights(documents[i], highlight_text)
151
  meta = metadatas[i] or {}
152
  retrieved_chunks.append(
153
  RetrievedChunk(
@@ -158,34 +94,7 @@ async def process_retrieval(
158
  end_char=meta.get("end_char", 0),
159
  parent_id=meta.get("parent_id") or None,
160
  level=meta.get("level", 0),
161
- text_highlighted=text_highlighted,
162
  )
163
  )
164
 
165
  return retrieved_chunks, embeddings
166
-
167
-
168
- def get_text_highlights(original_text, search_text):
169
- query_keywords = get_keywords(search_text)
170
- if not query_keywords:
171
- return original_text
172
-
173
- query_keywords.sort(key=len, reverse=True)
174
-
175
- safe_keywords = [f"{re.escape(w)}(?:'s)?" for w in query_keywords]
176
-
177
- pattern_string = r"\b(" + "|".join(safe_keywords) + r")\b"
178
- pattern = re.compile(pattern_string, re.IGNORECASE)
179
-
180
- return pattern.sub(r"<mark>\1</mark>", original_text)
181
-
182
-
183
- def get_keywords(text: str):
184
- token = tokenizer.tokenize(text.lower())
185
- return [word for word in token if word not in stop_words]
186
-
187
-
188
- async def get_hyde_text(search_text):
189
- llm_client = OllamaClient()
190
- hyde_prompt.format(search_text=search_text)
191
- return await llm_client.generate(hyde_prompt, response_format=None)
 
12
  from backend.models.schemas import (
13
  CompareRequest,
14
  CompareResponse,
 
 
15
  QueryRequest,
16
  QueryResponse,
17
  RetrievedChunk,
 
31
  @router.post("/retrieve", response_model=QueryResponse)
32
  async def retrieve(request: QueryRequest):
33
 
 
 
 
 
 
 
34
  if request.use_reranking == True:
35
  "" ""
36
 
37
  retrieved_chunks, embeddings = await process_retrieval(
38
  request.embedding_model,
39
+ request.search_text,
40
  request.strategy,
41
  request.top_k,
42
  request.search_text,
 
54
  }
55
 
56
  if request.use_hyde:
57
+ response_kwargs["hypothetical_answer"] = request.search_text
58
 
59
  return QueryResponse(**response_kwargs)
60
 
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  async def process_retrieval(
63
  model, search_text, strategy, top_k, original_query: Optional[str] = None
64
  ):
 
83
  metadatas = result["metadatas"][0]
84
 
85
  for i in range(len(ids)):
86
+
87
  meta = metadatas[i] or {}
88
  retrieved_chunks.append(
89
  RetrievedChunk(
 
94
  end_char=meta.get("end_char", 0),
95
  parent_id=meta.get("parent_id") or None,
96
  level=meta.get("level", 0),
 
97
  )
98
  )
99
 
100
  return retrieved_chunks, embeddings
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/app.js CHANGED
@@ -1328,325 +1328,3 @@ if (dom.closeDrawer) {
1328
  // ============================================================
1329
  // TASK 3.5: ARENA COMPARISON LOGIC
1330
  // ============================================================
1331
- const domArena = {
1332
- modal: document.getElementById("arena-modal"),
1333
- btnOpen: document.getElementById("btn-open-arena"),
1334
- btnClose: document.getElementById("btn-close-arena"),
1335
- btnFight: document.getElementById("btn-arena-fight"),
1336
- queryInput: document.getElementById("arena-query-input"),
1337
- modelA: document.getElementById("arena-model-a"),
1338
- strategyA: document.getElementById("arena-strategy-a"),
1339
- resultsA: document.getElementById("arena-results-a"),
1340
- modelB: document.getElementById("arena-model-b"),
1341
- strategyB: document.getElementById("arena-strategy-b"),
1342
- resultsB: document.getElementById("arena-results-b"),
1343
- };
1344
-
1345
- if (domArena.btnOpen) {
1346
- domArena.btnOpen.addEventListener("click", () => {
1347
- domArena.modal.style.display = "flex";
1348
- if (dom.queryInput && dom.queryInput.value) {
1349
- domArena.queryInput.value = dom.queryInput.value;
1350
- }
1351
- });
1352
- }
1353
-
1354
- if (domArena.btnClose) {
1355
- domArena.btnClose.addEventListener("click", () => {
1356
- domArena.modal.style.display = "none";
1357
- });
1358
- }
1359
-
1360
- if (domArena.btnFight) {
1361
- domArena.btnFight.addEventListener("click", async () => {
1362
- const query = domArena.queryInput.value.trim();
1363
- if (!query) return;
1364
-
1365
- domArena.btnFight.disabled = true;
1366
- domArena.btnFight.textContent = "FIGHTING...";
1367
-
1368
- // Reset AI Referee if active
1369
- if (typeof domReferee !== "undefined" && domReferee.panel) {
1370
- domReferee.panel.style.display = "none";
1371
- domReferee.verdictBox.style.display = "none";
1372
- domReferee.loadingBox.style.display = "none";
1373
- domReferee.triggerBox.style.display = "block";
1374
- }
1375
-
1376
- domArena.resultsA.innerHTML =
1377
- '<div class="skeleton-loader"></div><div class="skeleton-loader"></div>';
1378
- domArena.resultsB.innerHTML =
1379
- '<div class="skeleton-loader"></div><div class="skeleton-loader"></div>';
1380
-
1381
- try {
1382
- const res = await fetch("/api/compare", {
1383
- method: "POST",
1384
- headers: { "Content-Type": "application/json" },
1385
- body: JSON.stringify({
1386
- search_text: query,
1387
- top_k: 3,
1388
- model_a: domArena.modelA.value,
1389
- strategy_a: domArena.strategyA.value,
1390
- model_b: domArena.modelB.value,
1391
- strategy_b: domArena.strategyB.value,
1392
- }),
1393
- });
1394
-
1395
- if (!res.ok)
1396
- throw new Error(
1397
- "Comparison failed. Check if server is running and /api/compare exists.",
1398
- );
1399
-
1400
- const data = await res.json();
1401
-
1402
- // Render Column A
1403
- domArena.resultsA.innerHTML = data.results_a
1404
- .map(
1405
- (chunk, i) => `
1406
- <div class="query-result-item" style="border-left-color: var(--warning-color)">
1407
- <div class="query-result-header">
1408
- <span class="rank-badge" style="color: var(--warning-color)">Rank ${i + 1}</span>
1409
- <span class="dist-badge">Dist: ${chunk.score.toFixed(3)}</span>
1410
- </div>
1411
- <div class="query-result-text">${chunk.text_highlighted || escapeHtml(chunk.text)}</div>
1412
- </div>
1413
- `,
1414
- )
1415
- .join("");
1416
-
1417
- // Render Column B
1418
- domArena.resultsB.innerHTML = data.results_b
1419
- .map(
1420
- (chunk, i) => `
1421
- <div class="query-result-item" style="border-left-color: var(--info-color)">
1422
- <div class="query-result-header">
1423
- <span class="rank-badge" style="color: var(--info-color)">Rank ${i + 1}</span>
1424
- <span class="dist-badge">Dist: ${chunk.score.toFixed(3)}</span>
1425
- </div>
1426
- <div class="query-result-text">${chunk.text_highlighted || escapeHtml(chunk.text)}</div>
1427
- </div>
1428
- `,
1429
- )
1430
- .join("");
1431
-
1432
- // Activate AI Referee if both sides retrieved chunks
1433
- if (
1434
- typeof domReferee !== "undefined" &&
1435
- domReferee.panel &&
1436
- data.results_a.length > 0 &&
1437
- data.results_b.length > 0
1438
- ) {
1439
- domReferee.panel.style.display = "block";
1440
- domReferee.btnCall.dataset.query = query;
1441
- domReferee.btnCall.dataset.chunkA = data.results_a[0].text;
1442
- domReferee.btnCall.dataset.chunkB = data.results_b[0].text;
1443
- }
1444
- } catch (err) {
1445
- console.error(err);
1446
- domArena.resultsA.innerHTML = `<div style="color: var(--superman-red); padding: 1rem;">Error: ${err.message}</div>`;
1447
- domArena.resultsB.innerHTML = `<div style="color: var(--superman-red); padding: 1rem;">Error: ${err.message}</div>`;
1448
- } finally {
1449
- domArena.btnFight.disabled = false;
1450
- domArena.btnFight.textContent = "🔥 FIGHT!";
1451
- }
1452
- });
1453
- }
1454
-
1455
- // ============================================================
1456
- // TASK 5.1: AI REFEREE / LLM-AS-A-JUDGE LOGIC
1457
- // ============================================================
1458
- const domReferee = {
1459
- panel: document.getElementById("arena-referee-panel"),
1460
- triggerBox: document.getElementById("referee-trigger-box"),
1461
- btnCall: document.getElementById("btn-call-referee"),
1462
- loadingBox: document.getElementById("referee-loading-box"),
1463
- statusText: document.getElementById("referee-status-text"),
1464
- verdictBox: document.getElementById("referee-verdict-box"),
1465
- };
1466
-
1467
- if (domReferee.btnCall) {
1468
- domReferee.btnCall.addEventListener("click", async () => {
1469
- const query = domReferee.btnCall.dataset.query;
1470
- const chunkA = domReferee.btnCall.dataset.chunkA;
1471
- const chunkB = domReferee.btnCall.dataset.chunkB;
1472
- if (!query || !chunkA || !chunkB) return;
1473
-
1474
- // Capture user-selected model names for clean revealed scorecard display
1475
- const labelA = domArena.modelA.options[domArena.modelA.selectedIndex].text;
1476
- const labelB = domArena.modelB.options[domArena.modelB.selectedIndex].text;
1477
-
1478
- domReferee.triggerBox.style.display = "none";
1479
- domReferee.loadingBox.style.display = "block";
1480
- domReferee.verdictBox.style.display = "none";
1481
-
1482
- const statuses = [
1483
- "⚖️ Summoning Gemma evaluation referee...",
1484
- "🔍 Blinding model names and metadata...",
1485
- "🧠 Auditing Corner A relevance and clarity...",
1486
- "🧠 Auditing Corner B completeness and truthfulness...",
1487
- "📊 Aggregating overall dimensional scores...",
1488
- "✍️ Draft verdict and writing final card...",
1489
- ];
1490
- let statusIdx = 0;
1491
- domReferee.statusText.textContent = statuses[0];
1492
- const statusInterval = setInterval(() => {
1493
- statusIdx = (statusIdx + 1) % statuses.length;
1494
- domReferee.statusText.textContent = statuses[statusIdx];
1495
- }, 1500);
1496
-
1497
- try {
1498
- const res = await fetch("/api/judge", {
1499
- method: "POST",
1500
- headers: { "Content-Type": "application/json" },
1501
- body: JSON.stringify({
1502
- search_query: query,
1503
- chunk_a: chunkA,
1504
- chunk_b: chunkB,
1505
- }),
1506
- });
1507
-
1508
- if (!res.ok)
1509
- throw new Error(
1510
- "Gemma evaluation failed. Check if local Ollama server is running.",
1511
- );
1512
-
1513
- const data = await res.json();
1514
- clearInterval(statusInterval);
1515
-
1516
- renderRefereeScorecard(data, labelA, labelB);
1517
- } catch (err) {
1518
- clearInterval(statusInterval);
1519
- console.error(err);
1520
- domReferee.verdictBox.innerHTML = `
1521
- <div style="color: var(--superman-red); text-align: center; padding: 1.5rem; border: 1px solid var(--superman-red-glow); border-radius: 6px; background-color: var(--superman-red-muted);">
1522
- <strong>Referee Error:</strong> ${err.message}
1523
- </div>
1524
- `;
1525
- domReferee.verdictBox.style.display = "block";
1526
- } finally {
1527
- domReferee.loadingBox.style.display = "none";
1528
- }
1529
- });
1530
- }
1531
-
1532
- function renderRefereeScorecard(data, labelA, labelB) {
1533
- const isA = data.winner === "chunk_a";
1534
- const isB = data.winner === "chunk_b";
1535
- const isTie = data.winner === "tie";
1536
-
1537
- let winnerColor = "var(--superman-yellow)";
1538
- let winnerBanner = "⚖️ IT'S A TIE";
1539
- if (isA) {
1540
- winnerColor = "var(--superman-yellow)";
1541
- winnerBanner = `🏆 ${labelA.toUpperCase()} WINS`;
1542
- } else if (isB) {
1543
- winnerColor = "var(--superman-blue)";
1544
- winnerBanner = `🏆 ${labelB.toUpperCase()} WINS`;
1545
- }
1546
-
1547
- // Dynamically replace blinded chunk labels with actual bolded model names
1548
- const boldLabelA = `<strong>${labelA}</strong>`;
1549
- const boldLabelB = `<strong>${labelB}</strong>`;
1550
-
1551
- let cleanedReason = data.winner_reason.trim();
1552
- // Strip leading/trailing quote characters
1553
- cleanedReason = cleanedReason
1554
- .replace(/^["']/, "")
1555
- .replace(/["']$/, "")
1556
- .trim();
1557
- // Collapse any newlines or multiple whitespace characters to a single space for single-line display
1558
- cleanedReason = cleanedReason.replace(/\r?\n|\r/g, " ").replace(/\s+/g, " ");
1559
-
1560
- cleanedReason = cleanedReason
1561
- .replace(/\b(Chunk A|chunk_a|chunk A)\b/g, boldLabelA)
1562
- .replace(/\b(Chunk B|chunk_b|chunk B)\b/g, boldLabelB);
1563
-
1564
- const scoreCardHtml = `
1565
- <div class="verdict-banner" style="background-color: ${winnerColor}-muted; border-color: ${winnerColor};">
1566
- <div class="verdict-title" style="color: ${winnerColor}">${winnerBanner}</div>
1567
- <div class="verdict-confidence">Confidence: ${(data.confidence * 100).toFixed(0)}%</div>
1568
- </div>
1569
-
1570
- <div class="verdict-reason">
1571
- <strong>Verdict Reason:</strong>
1572
- <p>${cleanedReason}</p>
1573
- <div class="deciding-factor">Deciding Factor: <code>${data.deciding_dimension}</code></div>
1574
- </div>
1575
-
1576
- <!-- Comparative Table of dimensions -->
1577
- <div class="referee-scores-table">
1578
- <div class="scores-table-header">
1579
- <span class="score-col-label">Criterion</span>
1580
- <span class="score-col-val" style="font-size: 0.65rem; color: var(--superman-yellow);">${labelA}</span>
1581
- <span class="score-col-bar">Comparison</span>
1582
- <span class="score-col-val" style="font-size: 0.65rem; color: var(--superman-blue);">${labelB}</span>
1583
- </div>
1584
-
1585
- ${renderScoreRow("Query Relevance", data.chunk_a_score.query_relevance, data.chunk_b_score.query_relevance)}
1586
- ${renderScoreRow("Completeness", data.chunk_a_score.answer_completeness, data.chunk_b_score.answer_completeness)}
1587
- ${renderScoreRow("Plausibility", data.chunk_a_score.factual_plausibility, data.chunk_b_score.factual_plausibility)}
1588
- ${renderScoreRow("Clarity & Style", data.chunk_a_score.clarity, data.chunk_b_score.clarity)}
1589
-
1590
- <div class="scores-table-row overall-row">
1591
- <span class="score-col-label"><strong>Overall Average</strong></span>
1592
- <span class="score-col-val" style="color: var(--superman-yellow)"><strong>${data.chunk_a_score.overall.toFixed(2)}</strong></span>
1593
- <span class="score-col-bar">
1594
- <div class="overall-progress-bar">
1595
- <div class="overall-val-a" style="width: ${data.chunk_a_score.overall * 10}%"></div>
1596
- <div class="overall-val-b" style="width: ${data.chunk_b_score.overall * 10}%"></div>
1597
- </div>
1598
- </span>
1599
- <span class="score-col-val" style="color: var(--superman-blue)"><strong>${data.chunk_b_score.overall.toFixed(2)}</strong></span>
1600
- </div>
1601
- </div>
1602
-
1603
- <!-- Strengths & Weaknesses Column -->
1604
- <div class="verdict-details-grid">
1605
- <div class="verdict-details-col">
1606
- <h4>${labelA} Analysis</h4>
1607
- <div class="verdict-list-title strength-list-title">🟢 Strengths</div>
1608
- <ul>
1609
- ${data.chunk_a_strengths.map((s) => `<li>${escapeHtml(s)}</li>`).join("")}
1610
- </ul>
1611
- <div class="verdict-list-title weakness-list-title">🔴 Weaknesses</div>
1612
- <ul>
1613
- ${data.chunk_a_weaknesses.map((w) => `<li>${escapeHtml(w)}</li>`).join("")}
1614
- </ul>
1615
- </div>
1616
-
1617
- <div class="verdict-details-col">
1618
- <h4>${labelB} Analysis</h4>
1619
- <div class="verdict-list-title strength-list-title">🟢 Strengths</div>
1620
- <ul>
1621
- ${data.chunk_b_strengths.map((s) => `<li>${escapeHtml(s)}</li>`).join("")}
1622
- </ul>
1623
- <div class="verdict-list-title weakness-list-title">🔴 Weaknesses</div>
1624
- <ul>
1625
- ${data.chunk_b_weaknesses.map((w) => `<li>${escapeHtml(w)}</li>`).join("")}
1626
- </ul>
1627
- </div>
1628
- </div>
1629
- `;
1630
-
1631
- domReferee.verdictBox.innerHTML = scoreCardHtml;
1632
- domReferee.verdictBox.style.display = "block";
1633
- }
1634
-
1635
- function renderScoreRow(label, valA, valB) {
1636
- const pctA = valA * 10;
1637
- const pctB = valB * 10;
1638
-
1639
- return `
1640
- <div class="scores-table-row">
1641
- <span class="score-col-label">${label}</span>
1642
- <span class="score-col-val">${valA}/10</span>
1643
- <span class="score-col-bar">
1644
- <div class="score-progress-bar">
1645
- <div class="score-val-a" style="width: ${pctA}%"></div>
1646
- <div class="score-val-b" style="width: ${pctB}%"></div>
1647
- </div>
1648
- </span>
1649
- <span class="score-col-val">${valB}/10</span>
1650
- </div>
1651
- `;
1652
- }
 
1328
  // ============================================================
1329
  // TASK 3.5: ARENA COMPARISON LOGIC
1330
  // ============================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/index.html CHANGED
@@ -240,7 +240,6 @@
240
  <div class="query-simulator-section">
241
  <div class="query-section-title" style="display: flex; justify-content: space-between; align-items: center;">
242
  <span>🔮 Sonar Query Simulator (RAG Retrieval)</span>
243
- <button id="btn-open-arena" class="btn-sm" style="background: var(--superman-red); color: white; border: none; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-weight: bold; font-family: var(--font-sans);">⚔️ Arena Comparison</button>
244
  </div>
245
  <div class="query-input-wrapper">
246
  <input type="text" id="query-input" placeholder="Type a concept query to fetch semantically similar chunks (e.g. 'linear regression')..." />
@@ -296,93 +295,6 @@
296
  </div>
297
  </aside>
298
 
299
- <!-- ===== THE ARENA MODAL ===== -->
300
- <div id="arena-modal" class="arena-modal-overlay" style="display: none;">
301
- <div class="arena-modal-content">
302
- <div class="arena-header">
303
- <h2>⚔️ The Grand Comparison Arena</h2>
304
- <button id="btn-close-arena" class="btn-close-arena">&times;</button>
305
- </div>
306
-
307
- <div class="arena-query-bar">
308
- <input type="text" id="arena-query-input" placeholder="Type a query to compare configurations (e.g., 'What is Clark Kent's weakness?')..." />
309
- <button class="btn-run" id="btn-arena-fight" style="width: 150px; flex-shrink: 0;">🔥 FIGHT!</button>
310
- </div>
311
-
312
- <div class="arena-body">
313
- <div class="arena-columns">
314
- <!-- COLUMN A -->
315
- <div class="arena-col">
316
- <div class="arena-col-header">
317
- <h3>Corner A</h3>
318
- <div class="arena-config-row">
319
- <select id="arena-model-a" class="select-input">
320
- <option value="nomic-embed-text">Nomic Embed</option>
321
- <option value="EmbeddingGemma">Gemma Embed</option>
322
- <option value="qwen3-embedding:0.6b">Qwen3 Embed</option>
323
- </select>
324
- <select id="arena-strategy-a" class="select-input">
325
- <option value="fixed_size">Fixed Size</option>
326
- <option value="sentence">Sentence</option>
327
- <option value="recursive">Recursive</option>
328
- <option value="semantic">Semantic</option>
329
- </select>
330
- </div>
331
- </div>
332
- <div class="arena-results-list" id="arena-results-a">
333
- <div style="text-align: center; color: var(--text-tertiary); padding: 2rem;">Awaiting challengers...</div>
334
- </div>
335
- </div>
336
-
337
- <!-- COLUMN B -->
338
- <div class="arena-col">
339
- <div class="arena-col-header" style="border-bottom-color: var(--info-color);">
340
- <h3>Corner B</h3>
341
- <div class="arena-config-row">
342
- <select id="arena-model-b" class="select-input">
343
- <option value="nomic-embed-text">Nomic Embed</option>
344
- <option value="EmbeddingGemma">Gemma Embed</option>
345
- <option value="qwen3-embedding:0.6b">Qwen3 Embed</option>
346
- </select>
347
- <select id="arena-strategy-b" class="select-input">
348
- <option value="fixed_size">Fixed Size</option>
349
- <option value="sentence">Sentence</option>
350
- <option value="recursive">Recursive</option>
351
- <option value="semantic">Semantic</option>
352
- </select>
353
- </div>
354
- </div>
355
- <div class="arena-results-list" id="arena-results-b">
356
- <div style="text-align: center; color: var(--text-tertiary); padding: 2rem;">Awaiting challengers...</div>
357
- </div>
358
- </div>
359
- </div>
360
-
361
- <!-- THE AI REFEREE PANEL (TASK 5.1) -->
362
- <div class="arena-referee-panel" id="arena-referee-panel" style="display: none;">
363
- <div class="referee-divider">
364
- <span class="referee-badge">⚖️ THE AI REFEREE</span>
365
- </div>
366
-
367
- <div class="referee-trigger-box" id="referee-trigger-box">
368
- <p class="referee-prompt-text">Call upon the local Gemma model to analyze, score, and judge both contexts!</p>
369
- <button class="btn-run" id="btn-call-referee" style="margin: 0 auto; display: block; max-width: 250px;">⚖️ CALL THE REFEREE</button>
370
- </div>
371
-
372
- <!-- Loading scanner status -->
373
- <div class="referee-loading-box" id="referee-loading-box" style="display: none;">
374
- <div class="skeleton-loader" style="max-width: 400px; margin: 0 auto 1rem; border-radius: 6px;"></div>
375
- <div class="referee-status-text" id="referee-status-text">Summoning Gemma to review the case...</div>
376
- </div>
377
-
378
- <!-- Scorecard Verdict Display -->
379
- <div class="referee-verdict-box" id="referee-verdict-box" style="display: none;"></div>
380
- </div>
381
- </div>
382
-
383
- </div>
384
- </div>
385
-
386
  </main>
387
  </div>
388
 
 
240
  <div class="query-simulator-section">
241
  <div class="query-section-title" style="display: flex; justify-content: space-between; align-items: center;">
242
  <span>🔮 Sonar Query Simulator (RAG Retrieval)</span>
 
243
  </div>
244
  <div class="query-input-wrapper">
245
  <input type="text" id="query-input" placeholder="Type a concept query to fetch semantically similar chunks (e.g. 'linear regression')..." />
 
295
  </div>
296
  </aside>
297
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  </main>
299
  </div>
300
 
frontend/styles.css CHANGED
@@ -750,134 +750,6 @@ input[type="range"]::-moz-range-thumb {
750
  cursor: help;
751
  }
752
 
753
-
754
- /* ============================================================
755
- THE GRAND COMPARISON ARENA (TASK 3.5)
756
- ============================================================ */
757
- .arena-modal-overlay {
758
- position: fixed;
759
- inset: 0;
760
- background: rgba(15, 23, 42, 0.9);
761
- backdrop-filter: blur(8px);
762
- z-index: 9999;
763
- display: flex;
764
- align-items: center;
765
- justify-content: center;
766
- }
767
-
768
- .arena-modal-content {
769
- background: var(--bg-surface);
770
- width: 90vw;
771
- height: 90vh;
772
- border-radius: var(--radius-lg);
773
- box-shadow: var(--shadow-xl);
774
- display: flex;
775
- flex-direction: column;
776
- overflow: hidden;
777
- border: 1px solid var(--border-default);
778
- }
779
-
780
- .arena-header {
781
- display: flex;
782
- justify-content: space-between;
783
- align-items: center;
784
- padding: var(--space-md) var(--space-xl);
785
- background: var(--bg-surface-elevated);
786
- border-bottom: 1px solid var(--border-default);
787
- }
788
-
789
- .arena-header h2 {
790
- margin: 0;
791
- font-size: 1.25rem;
792
- color: var(--superman-red);
793
- }
794
-
795
- .btn-close-arena {
796
- background: none;
797
- border: none;
798
- color: var(--text-secondary);
799
- font-size: 1.5rem;
800
- cursor: pointer;
801
- }
802
-
803
- .btn-close-arena:hover {
804
- color: var(--superman-red);
805
- }
806
-
807
- .arena-query-bar {
808
- display: flex;
809
- padding: var(--space-lg) var(--space-xl);
810
- gap: var(--space-md);
811
- background: var(--bg-surface-elevated);
812
- border-bottom: 1px solid var(--border-default);
813
- }
814
-
815
- .arena-query-bar input {
816
- flex: 1;
817
- padding: var(--space-md) var(--space-lg);
818
- border-radius: var(--radius-md);
819
- border: 1px solid var(--border-default);
820
- background: var(--bg-surface);
821
- color: var(--text-primary);
822
- font-size: 1rem;
823
- }
824
-
825
- .arena-columns {
826
- display: flex;
827
- height: 385px;
828
- flex-shrink: 0;
829
- border-bottom: 1px solid var(--border-default);
830
- }
831
-
832
- .arena-body {
833
- flex: 1;
834
- overflow-y: auto;
835
- padding: 0 0 var(--space-xl) 0;
836
- display: flex;
837
- flex-direction: column;
838
- }
839
-
840
- .arena-col {
841
- flex: 1;
842
- display: flex;
843
- flex-direction: column;
844
- border-right: 1px solid var(--border-default);
845
- }
846
- .arena-col:last-child {
847
- border-right: none;
848
- }
849
-
850
- .arena-col-header {
851
- padding: var(--space-md) var(--space-xl);
852
- border-bottom: 2px solid var(--warning-color);
853
- background: rgba(255, 255, 255, 0.02);
854
- }
855
-
856
- .arena-col-header h3 {
857
- margin: 0 0 var(--space-sm) 0;
858
- font-size: 1.1rem;
859
- font-weight: 700;
860
- color: var(--text-primary);
861
- }
862
-
863
- .arena-config-row {
864
- display: flex;
865
- gap: var(--space-sm);
866
- }
867
-
868
- .arena-config-row select {
869
- flex: 1;
870
- }
871
-
872
- .arena-results-list {
873
- flex: 1;
874
- overflow-y: auto;
875
- padding: var(--space-xl);
876
- display: flex;
877
- flex-direction: column;
878
- gap: var(--space-md);
879
- }
880
-
881
  /* Low opacity wash of primary colors, no underlines */
882
  .chunk-highlight[data-color="1"] { background: rgba(37, 99, 235, 0.1); }
883
  .chunk-highlight[data-color="2"] { background: rgba(239, 68, 68, 0.1); }
@@ -1558,310 +1430,6 @@ input[type="range"]::-moz-range-thumb {
1558
  border-right: none;
1559
  border-bottom: 1px solid var(--border-default);
1560
  }
1561
- }
1562
-
1563
- /* ============================================================
1564
- 12. AI REFEREE (TASK 5.1)
1565
- ============================================================ */
1566
- .arena-referee-panel {
1567
- margin-top: 2rem;
1568
- padding: 1.5rem;
1569
- border-radius: 8px;
1570
- background-color: var(--bg-surface-raised);
1571
- border: 1px solid var(--border-default);
1572
- box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.02);
1573
- }
1574
-
1575
- .referee-divider {
1576
- display: flex;
1577
- align-items: center;
1578
- text-align: center;
1579
- margin-bottom: 1.5rem;
1580
- }
1581
-
1582
- .referee-divider::before,
1583
- .referee-divider::after {
1584
- content: '';
1585
- flex: 1;
1586
- border-bottom: 1px dashed var(--border-default);
1587
- }
1588
-
1589
- .referee-badge {
1590
- padding: 0.25rem 0.75rem;
1591
- font-family: 'Roboto Condensed', sans-serif;
1592
- font-weight: 700;
1593
- font-size: 0.85rem;
1594
- color: var(--superman-blue);
1595
- background-color: var(--superman-blue-muted);
1596
- border-radius: 50px;
1597
- border: 1px solid rgba(37, 99, 235, 0.2);
1598
- letter-spacing: 0.05em;
1599
- margin: 0 1rem;
1600
- }
1601
-
1602
- .referee-prompt-text {
1603
- text-align: center;
1604
- color: var(--text-secondary);
1605
- font-size: 0.9rem;
1606
- margin-bottom: 1rem;
1607
- }
1608
-
1609
- .referee-status-text {
1610
- text-align: center;
1611
- font-weight: 500;
1612
- color: var(--superman-blue);
1613
- font-size: 0.9rem;
1614
- animation: pulse 1.5s infinite ease-in-out;
1615
- }
1616
-
1617
- .referee-verdict-box {
1618
- animation: fadeIn 0.4s ease-out;
1619
- }
1620
-
1621
- /* Verdict banner */
1622
- .verdict-banner {
1623
- display: flex;
1624
- align-items: center;
1625
- justify-content: space-between;
1626
- padding: 1rem 1.5rem;
1627
- border-radius: 6px;
1628
- border: 1.5px solid;
1629
- margin-bottom: 1.5rem;
1630
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03);
1631
- }
1632
-
1633
- .verdict-title {
1634
- font-family: 'Roboto Condensed', sans-serif;
1635
- font-weight: 700;
1636
- font-size: 1.2rem;
1637
- letter-spacing: 0.02em;
1638
- }
1639
-
1640
- .verdict-confidence {
1641
- font-size: 0.85rem;
1642
- font-weight: 600;
1643
- background-color: rgba(255, 255, 255, 0.7);
1644
- padding: 0.2rem 0.5rem;
1645
- border-radius: 4px;
1646
- border: 1px solid rgba(0, 0, 0, 0.05);
1647
- }
1648
-
1649
- /* Verdict reason */
1650
- .verdict-reason {
1651
- background-color: var(--bg-surface);
1652
- padding: 1.25rem;
1653
- border-radius: 6px;
1654
- border: 1px solid var(--border-default);
1655
- margin-bottom: 1.5rem;
1656
- }
1657
-
1658
- .verdict-reason > strong {
1659
- display: block;
1660
- font-size: 0.85rem;
1661
- color: var(--text-tertiary);
1662
- text-transform: uppercase;
1663
- letter-spacing: 0.05em;
1664
- margin-bottom: 0.5rem;
1665
- }
1666
-
1667
- .verdict-reason p {
1668
- font-size: 0.95rem;
1669
- color: var(--text-primary);
1670
- line-height: 1.5;
1671
- margin-bottom: 0.75rem;
1672
- font-style: italic;
1673
- }
1674
-
1675
- .deciding-factor {
1676
- font-size: 0.8rem;
1677
- color: var(--text-tertiary);
1678
- display: flex;
1679
- align-items: center;
1680
- gap: 0.5rem;
1681
- }
1682
-
1683
- .deciding-factor code {
1684
- background-color: var(--bg-surface-raised);
1685
- padding: 0.1rem 0.4rem;
1686
- border-radius: 4px;
1687
- font-family: 'JetBrains Mono', monospace;
1688
- color: var(--superman-red);
1689
- font-weight: 600;
1690
- }
1691
-
1692
- /* Scorecard Table */
1693
- .referee-scores-table {
1694
- background-color: var(--bg-surface);
1695
- border: 1px solid var(--border-default);
1696
- border-radius: 6px;
1697
- overflow: hidden;
1698
- margin-bottom: 1.5rem;
1699
- }
1700
-
1701
- .scores-table-header {
1702
- display: grid;
1703
- grid-template-columns: 150px 70px 1fr 70px;
1704
- padding: 0.75rem 1rem;
1705
- background-color: var(--bg-surface-raised);
1706
- border-bottom: 1px solid var(--border-default);
1707
- font-weight: 600;
1708
- font-size: 0.8rem;
1709
- color: var(--text-tertiary);
1710
- text-transform: uppercase;
1711
- letter-spacing: 0.05em;
1712
- }
1713
-
1714
- .scores-table-row {
1715
- display: grid;
1716
- grid-template-columns: 150px 70px 1fr 70px;
1717
- align-items: center;
1718
- padding: 0.75rem 1rem;
1719
- border-bottom: 1px solid var(--border-default);
1720
- font-size: 0.9rem;
1721
- }
1722
-
1723
- .scores-table-row:last-child {
1724
- border-bottom: none;
1725
- }
1726
-
1727
- .overall-row {
1728
- background-color: rgba(37, 99, 235, 0.02);
1729
- border-top: 1.5px solid var(--border-default);
1730
- }
1731
-
1732
- .score-col-label {
1733
- font-weight: 500;
1734
- color: var(--text-primary);
1735
- }
1736
-
1737
- .score-col-val {
1738
- text-align: center;
1739
- font-weight: 600;
1740
- }
1741
-
1742
- .score-col-bar {
1743
- padding: 0 1rem;
1744
- }
1745
-
1746
- .score-progress-bar {
1747
- height: 6px;
1748
- background-color: #e2e8f0;
1749
- border-radius: 10px;
1750
- overflow: hidden;
1751
- display: grid;
1752
- grid-template-columns: 1fr 1fr;
1753
- gap: 2px;
1754
- }
1755
-
1756
- .score-val-a {
1757
- background-color: var(--superman-yellow);
1758
- height: 100%;
1759
- border-radius: 10px 0 0 10px;
1760
- justify-self: end;
1761
- }
1762
-
1763
- .score-val-b {
1764
- background-color: var(--superman-blue);
1765
- height: 100%;
1766
- border-radius: 0 10px 10px 0;
1767
- justify-self: start;
1768
- }
1769
-
1770
- .overall-progress-bar {
1771
- height: 10px;
1772
- background-color: #e2e8f0;
1773
- border-radius: 10px;
1774
- overflow: hidden;
1775
- display: grid;
1776
- grid-template-columns: 1fr 1fr;
1777
- gap: 2px;
1778
- }
1779
-
1780
- .overall-val-a {
1781
- background-color: var(--superman-yellow);
1782
- height: 100%;
1783
- border-radius: 10px 0 0 10px;
1784
- justify-self: end;
1785
- }
1786
-
1787
- .overall-val-b {
1788
- background-color: var(--superman-blue);
1789
- height: 100%;
1790
- border-radius: 0 10px 10px 0;
1791
- justify-self: start;
1792
- }
1793
-
1794
- /* Strengths & Weaknesses Columns */
1795
- .verdict-details-grid {
1796
- display: grid;
1797
- grid-template-columns: 1fr 1fr;
1798
- gap: 1.5rem;
1799
- }
1800
-
1801
- .verdict-details-col {
1802
- background-color: var(--bg-surface);
1803
- border: 1px solid var(--border-default);
1804
- padding: 1.25rem;
1805
- border-radius: 6px;
1806
- }
1807
-
1808
- .verdict-details-col h4 {
1809
- font-family: 'Roboto Condensed', sans-serif;
1810
- font-weight: 700;
1811
- margin-bottom: 1rem;
1812
- font-size: 1rem;
1813
- border-bottom: 1.5px solid var(--border-default);
1814
- padding-bottom: 0.5rem;
1815
- }
1816
-
1817
- .verdict-details-col:first-child h4 {
1818
- color: var(--warning-color);
1819
- border-bottom-color: rgba(245, 158, 11, 0.2);
1820
- }
1821
-
1822
- .verdict-details-col:last-child h4 {
1823
- color: var(--info-color);
1824
- border-bottom-color: rgba(59, 130, 246, 0.2);
1825
- }
1826
-
1827
- .verdict-list-title {
1828
- font-size: 0.75rem;
1829
- font-weight: 700;
1830
- text-transform: uppercase;
1831
- letter-spacing: 0.05em;
1832
- margin-bottom: 0.5rem;
1833
- }
1834
-
1835
- .strength-list-title {
1836
- color: #16a34a;
1837
- }
1838
-
1839
- .weakness-list-title {
1840
- color: #dc2626;
1841
- margin-top: 1rem;
1842
- }
1843
-
1844
- .verdict-details-col ul {
1845
- padding-left: 1.25rem;
1846
- margin: 0;
1847
- }
1848
-
1849
- .verdict-details-col li {
1850
- font-size: 0.85rem;
1851
- color: var(--text-secondary);
1852
- line-height: 1.4;
1853
- margin-bottom: 0.4rem;
1854
- }
1855
-
1856
- @keyframes fadeIn {
1857
- from { opacity: 0; transform: translateY(10px); }
1858
- to { opacity: 1; transform: translateY(0); }
1859
- }
1860
-
1861
-
1862
- /* ============================================================
1863
- RETRIEVAL X-RAY HIGHLIGHTS (TASK 3.4)
1864
- ============================================================ */
1865
  .xray-content.search-active .chunk-highlight {
1866
  opacity: 0.3;
1867
  transition: all 0.3s ease;
 
750
  cursor: help;
751
  }
752
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
753
  /* Low opacity wash of primary colors, no underlines */
754
  .chunk-highlight[data-color="1"] { background: rgba(37, 99, 235, 0.1); }
755
  .chunk-highlight[data-color="2"] { background: rgba(239, 68, 68, 0.1); }
 
1430
  border-right: none;
1431
  border-bottom: 1px solid var(--border-default);
1432
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1433
  .xray-content.search-active .chunk-highlight {
1434
  opacity: 0.3;
1435
  transition: all 0.3s ease;