shak3008 commited on
Commit
4e1dadc
Β·
1 Parent(s): 27570ec

added delete benchmark(s) endpoints and exposed them in frontend

Browse files
DocPilot/backend/app/core/dependencies.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from fastapi import Depends, HTTPException
2
 
3
  from fastapi.security import OAuth2PasswordBearer
 
1
+ print("USING DOCPILOT DEPENDENCIES")
2
  from fastapi import Depends, HTTPException
3
 
4
  from fastapi.security import OAuth2PasswordBearer
GaugePilot/backend/app/api/benchmark.py CHANGED
@@ -1,7 +1,7 @@
1
  from fastapi import APIRouter, Depends
2
 
3
  from sqlalchemy.orm import Session
4
-
5
  import json
6
 
7
  from GaugePilot.backend.app.core.dependencies import (
@@ -32,6 +32,12 @@ from pilotcore.runtime.experiment_config import (
32
  ExperimentConfig,
33
  )
34
 
 
 
 
 
 
 
35
  router = APIRouter()
36
 
37
 
@@ -82,9 +88,23 @@ def run_benchmark_endpoint(
82
 
83
  leaderboard = generate_leaderboard(results)
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  run = BenchmarkRun(
86
  owner_id=current_user.id,
87
- name="Benchmark Run",
88
  leaderboard_json=json.dumps(leaderboard),
89
  )
90
 
@@ -111,3 +131,59 @@ def get_runs(
111
  )
112
 
113
  return runs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from fastapi import APIRouter, Depends
2
 
3
  from sqlalchemy.orm import Session
4
+ from fastapi import HTTPException
5
  import json
6
 
7
  from GaugePilot.backend.app.core.dependencies import (
 
32
  ExperimentConfig,
33
  )
34
 
35
+ from sqlalchemy.orm import Session
36
+
37
+ from GaugePilot.backend.app.db.session import get_db
38
+
39
+ from GaugePilot.backend.app.models.benchmark_run import BenchmarkRun
40
+
41
  router = APIRouter()
42
 
43
 
 
88
 
89
  leaderboard = generate_leaderboard(results)
90
 
91
+ # Use the most recently uploaded document name (from DocPilot) as the benchmark run name.
92
+ from DocPilot.backend.app.models.document import Document as PilotDocument
93
+
94
+ uploaded_document = (
95
+ db.query(PilotDocument)
96
+ .filter(PilotDocument.owner_id == current_user.id)
97
+ .order_by(PilotDocument.created_at.desc())
98
+ .first()
99
+ )
100
+
101
+ benchmark_name = (
102
+ uploaded_document.filename if uploaded_document else "Benchmark Run"
103
+ )
104
+
105
  run = BenchmarkRun(
106
  owner_id=current_user.id,
107
+ name=benchmark_name,
108
  leaderboard_json=json.dumps(leaderboard),
109
  )
110
 
 
131
  )
132
 
133
  return runs
134
+
135
+
136
+ @router.get("/runs")
137
+ def get_benchmark_runs(
138
+ db: Session = Depends(get_db),
139
+ current_user=Depends(get_current_user),
140
+ ):
141
+ runs = (
142
+ db.query(BenchmarkRun)
143
+ .filter(BenchmarkRun.owner_id == current_user.id)
144
+ .order_by(BenchmarkRun.created_at.desc())
145
+ .all()
146
+ )
147
+
148
+ return runs
149
+
150
+
151
+ @router.delete("/runs/reset")
152
+ def reset_benchmark_runs(
153
+ db: Session = Depends(get_db),
154
+ current_user=Depends(get_current_user),
155
+ ):
156
+ (db.query(BenchmarkRun).filter(BenchmarkRun.owner_id == current_user.id).delete())
157
+
158
+ db.commit()
159
+
160
+ return {"message": "All benchmark runs deleted"}
161
+
162
+
163
+ @router.delete("/runs/{run_id}")
164
+ def delete_benchmark_run(
165
+ run_id: int,
166
+ db: Session = Depends(get_db),
167
+ current_user=Depends(get_current_user),
168
+ ):
169
+ run = (
170
+ db.query(BenchmarkRun)
171
+ .filter(
172
+ BenchmarkRun.id == run_id,
173
+ BenchmarkRun.owner_id == current_user.id,
174
+ )
175
+ .first()
176
+ )
177
+
178
+ if not run:
179
+ raise HTTPException(
180
+ status_code=404,
181
+ detail="Benchmark run not found",
182
+ )
183
+
184
+ db.delete(run)
185
+ db.commit()
186
+
187
+ return {
188
+ "message": "Benchmark deleted",
189
+ }
GaugePilot/backend/app/api/documents.py CHANGED
@@ -21,12 +21,11 @@ from pilotcore.retrieval.vector_store import (
21
  rebuild_index_without_document,
22
  )
23
 
24
- from DocPilot.backend.app.core.dependencies import (
25
  get_current_user,
26
  )
27
 
28
- from DocPilot.backend.app.db.session import get_db
29
-
30
  from DocPilot.backend.app.models.document import Document
31
 
32
  from GaugePilot.backend.app.schemas.document import (
 
21
  rebuild_index_without_document,
22
  )
23
 
24
+ from GaugePilot.backend.app.core.dependencies import (
25
  get_current_user,
26
  )
27
 
28
+ from GaugePilot.backend.app.db.session import get_db
 
29
  from DocPilot.backend.app.models.document import Document
30
 
31
  from GaugePilot.backend.app.schemas.document import (
GaugePilot/backend/app/core/dependencies.py CHANGED
@@ -1,5 +1,47 @@
1
- from fastapi import Depends
 
 
2
 
3
- # Reuse DocPilot's auth/db wiring if available in this monorepo.
4
- # This keeps GaugePilot thin while preserving existing behavior.
5
- from DocPilot.backend.app.core.dependencies import get_current_user # noqa: F401
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ print("USING GAUGEPILOT DEPENDENCIES")
2
+ from fastapi import Depends, HTTPException
3
+ from fastapi.security import OAuth2PasswordBearer
4
 
5
+ from jose import JWTError, jwt
6
+ from sqlalchemy.orm import Session
7
+
8
+ from DocPilot.backend.app.db.session import get_db
9
+ from DocPilot.backend.app.models.user import User
10
+ from DocPilot.backend.app.core.security import (
11
+ SECRET_KEY,
12
+ ALGORITHM,
13
+ )
14
+
15
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/docpilot/auth/login")
16
+
17
+
18
+ def get_current_user(
19
+ token: str = Depends(oauth2_scheme),
20
+ db: Session = Depends(get_db),
21
+ ):
22
+ credentials_exception = HTTPException(
23
+ status_code=401,
24
+ detail="Could not validate credentials",
25
+ )
26
+
27
+ try:
28
+ payload = jwt.decode(
29
+ token,
30
+ SECRET_KEY,
31
+ algorithms=[ALGORITHM],
32
+ )
33
+
34
+ email = payload.get("sub")
35
+
36
+ if email is None:
37
+ raise credentials_exception
38
+
39
+ except JWTError:
40
+ raise credentials_exception
41
+
42
+ user = db.query(User).filter(User.email == email).first()
43
+
44
+ if user is None:
45
+ raise credentials_exception
46
+
47
+ return user
frontend/src/gaugepilot/GaugePilot.jsx CHANGED
@@ -33,75 +33,81 @@ export default function GaugePilot({ onHome }) {
33
  const [isMobileOpen, setIsMobileOpen] = useState(false);
34
  const [hoveredItem, setHoveredItem] = useState(null);
35
  const [isMobile, setIsMobile] = useState(false);
36
- const [isTablet, setIsTablet] = useState(false);
37
  const observersRef = useRef([]);
38
- const mainRef = useRef(null);
39
- const scrollToSection = (id) => {
40
- document
41
- .getElementById(id)
42
- ?.scrollIntoView({
43
- behavior: "smooth",
44
- block: "start",
45
- });
46
- };
47
 
48
  // ── Responsive breakpoints ─────────────────────────────────────────────────
49
  useEffect(() => {
50
  const check = () => {
51
  setIsMobile(window.innerWidth < 640);
52
- setIsTablet(window.innerWidth >= 640 && window.innerWidth < 1024);
53
  };
54
  check();
55
  window.addEventListener("resize", check);
56
  return () => window.removeEventListener("resize", check);
57
  }, []);
58
 
59
- // ── Scroll spy ─────────────────────────────────────────────────────────────
 
 
 
60
  useEffect(() => {
61
- observersRef.current.forEach((o) => o.disconnect());
62
- observersRef.current = [];
63
-
64
- const observers = SECTION_IDS.map((id) => {
65
- const el = document.getElementById(id);
66
- if (!el) return null;
67
- const obs = new IntersectionObserver(
68
- ([entry]) => { if (entry.isIntersecting) setActiveSection(id); },
69
- { rootMargin: "-30% 0px -60% 0px", threshold: 0 }
70
- );
71
- obs.observe(el);
72
- observersRef.current.push(obs);
73
- return obs;
74
- });
75
-
76
- return () => observers.forEach((o) => o?.disconnect());
77
- }, []);
78
 
79
- // ── Navigation ─────────────────────────────────────────────────────────────
80
- const navigateTo = (sectionId) => {
81
- if (isMobile) setIsMobileOpen(false);
 
 
 
 
 
 
 
 
 
82
 
83
- if (!sectionId) {
84
- onHome?.();
85
- return;
86
- }
87
 
88
- const element = document.getElementById(sectionId);
 
 
 
 
 
89
 
90
- if (element && mainRef.current) {
91
- const top =
92
- element.offsetTop - 20;
 
 
 
93
 
94
- mainRef.current.scrollTo({
95
- top,
96
- behavior: "smooth",
97
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
- setActiveSection(sectionId);
100
- }
101
- };
102
  // ── Design tokens ──────────────────────────────────────────────────────────
103
- const accent = "#4f6ef7";
104
- const sidebarW = isCollapsed ? "72px" : "240px";
105
 
106
  const sidebarStyle = {
107
  width: sidebarW,
@@ -111,9 +117,9 @@ const navigateTo = (sectionId) => {
111
  top: 0,
112
  display: "flex",
113
  flexDirection: "column",
114
- background: "linear-gradient(180deg, rgba(10,14,35,0.98) 0%, rgba(8,12,28,0.99) 100%)",
115
- borderRight: "1px solid rgba(255,255,255,0.07)",
116
- boxShadow: "4px 0 24px rgba(0,0,0,0.3)",
117
  transition: "width 0.25s cubic-bezier(0.4,0,0.2,1), min-width 0.25s cubic-bezier(0.4,0,0.2,1)",
118
  zIndex: 50,
119
  overflowX: "hidden",
@@ -136,7 +142,7 @@ const navigateTo = (sectionId) => {
136
  {/* Branding */}
137
  <div style={{
138
  padding: isCollapsed ? "24px 0" : "28px 20px 20px",
139
- borderBottom: "1px solid rgba(255,255,255,0.06)",
140
  display: "flex", alignItems: "center",
141
  justifyContent: isCollapsed ? "center" : "space-between",
142
  gap: "10px", flexShrink: 0,
@@ -150,7 +156,7 @@ const navigateTo = (sectionId) => {
150
  }}>GaugePilot</h1>
151
  <p style={{
152
  margin: "7px 0 0", fontFamily: "'Courier New', monospace",
153
- fontSize: "10px", fontWeight: 700, color: "rgba(255,255,255,0.35)",
154
  letterSpacing: "0.05em", textTransform: "uppercase", whiteSpace: "nowrap",
155
  }}>benchmark & analysis</p>
156
  </div>
@@ -164,13 +170,20 @@ const navigateTo = (sectionId) => {
164
  <button
165
  onClick={() => setIsCollapsed((c) => !c)}
166
  style={{
167
- background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.08)",
168
- borderRadius: "8px", color: "rgba(255,255,255,0.5)", cursor: "pointer",
 
169
  padding: "5px 7px", fontSize: "12px", lineHeight: 1, flexShrink: 0,
170
  transition: "all 0.15s",
171
  }}
172
- onMouseEnter={(e) => { e.currentTarget.style.background = "rgba(255,255,255,0.1)"; e.currentTarget.style.color = "white"; }}
173
- onMouseLeave={(e) => { e.currentTarget.style.background = "rgba(255,255,255,0.05)"; e.currentTarget.style.color = "rgba(255,255,255,0.5)"; }}
 
 
 
 
 
 
174
  >
175
  {isCollapsed ? "β†’" : "←"}
176
  </button>
@@ -178,7 +191,10 @@ const navigateTo = (sectionId) => {
178
  </div>
179
 
180
  {/* Nav groups */}
181
- <nav style={{ flex: 1, overflowY: "auto", overflowX: "hidden", padding: "12px 8px", scrollbarWidth: "none" }}>
 
 
 
182
  {NAV_GROUPS.map((group) => (
183
  <div key={group.label} style={{ marginBottom: "4px" }}>
184
 
@@ -186,7 +202,7 @@ const navigateTo = (sectionId) => {
186
  {!isCollapsed && (
187
  <div style={{
188
  fontSize: "10px", fontWeight: 700, letterSpacing: "0.12em",
189
- textTransform: "uppercase", color: "rgba(255,255,255,0.22)",
190
  padding: "12px 10px 6px", userSelect: "none",
191
  }}>
192
  {group.label}
@@ -215,19 +231,23 @@ const navigateTo = (sectionId) => {
215
  marginBottom: "2px",
216
  justifyContent: isCollapsed ? "center" : "flex-start",
217
  background: isActive
218
- ? "rgba(79,110,247,0.18)"
219
  : isHover
220
- ? "rgba(255,255,255,0.05)"
221
  : "transparent",
222
  border: "none",
223
  borderRadius: "10px",
224
  cursor: "pointer",
225
- color: isActive ? "#8babff" : isHover ? "rgba(255,255,255,0.85)" : "rgba(255,255,255,0.5)",
 
 
 
 
226
  fontSize: "14px",
227
  fontWeight: isActive ? 600 : 400,
228
  textAlign: "left",
229
  transition: "all 0.15s ease",
230
- boxShadow: isActive ? "0 0 16px rgba(79,110,247,0.15)" : "none",
231
  whiteSpace: "nowrap",
232
  overflow: "hidden",
233
  }}
@@ -238,14 +258,14 @@ const navigateTo = (sectionId) => {
238
  position: "absolute", left: 0, top: "20%", bottom: "20%",
239
  width: "3px", borderRadius: "0 3px 3px 0",
240
  background: "linear-gradient(180deg, #4f6ef7, #a78bfa)",
241
- boxShadow: "0 0 8px rgba(79,110,247,0.7)",
242
  }} />
243
  )}
244
 
245
  {/* Icon */}
246
  <span style={{
247
  fontSize: "16px", lineHeight: 1, flexShrink: 0,
248
- filter: isActive ? "drop-shadow(0 0 4px rgba(79,110,247,0.8))" : "none",
249
  transition: "filter 0.15s",
250
  }}>
251
  {icon}
@@ -253,7 +273,9 @@ const navigateTo = (sectionId) => {
253
 
254
  {/* Label */}
255
  {!isCollapsed && (
256
- <span style={{ overflow: "hidden", textOverflow: "ellipsis" }}>{label}</span>
 
 
257
  )}
258
  </button>
259
  );
@@ -265,18 +287,21 @@ const navigateTo = (sectionId) => {
265
  {/* Footer */}
266
  {!isCollapsed && (
267
  <div style={{
268
- padding: "14px 16px", borderTop: "1px solid rgba(255,255,255,0.05)", flexShrink: 0,
 
 
269
  }}>
270
  <div style={{
271
  display: "flex", alignItems: "center", gap: "8px",
272
  padding: "8px 10px", borderRadius: "10px",
273
- background: "rgba(79,110,247,0.08)", border: "1px solid rgba(79,110,247,0.15)",
 
274
  }}>
275
  <span style={{
276
  width: 7, height: 7, borderRadius: "50%", background: "#22c55e",
277
  boxShadow: "0 0 6px #22c55e", display: "inline-block", flexShrink: 0,
278
  }} />
279
- <span style={{ fontSize: "11px", color: "rgba(255,255,255,0.72)", fontWeight: 500 }}>
280
  System ready
281
  </span>
282
  </div>
@@ -287,71 +312,61 @@ const navigateTo = (sectionId) => {
287
 
288
  // ── Render ─────────────────────────────────────────────────────────────────
289
  return (
290
- <div
291
- style={{
292
  display: "flex",
293
  height: "100vh",
294
  overflow: "hidden",
295
  background: "var(--bg-primary, #0a0e23)",
296
- }}
297
- >
298
 
299
- {/* Mobile overlay */}
300
- {isMobile && isMobileOpen && (
301
- <div
302
- onClick={() => setIsMobileOpen(false)}
303
- style={{
304
- position: "fixed",
305
- inset: 0,
306
- background: "rgba(0,0,0,0.6)",
307
- zIndex: 199,
308
- backdropFilter: "blur(2px)",
309
- }}
310
- />
311
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
 
313
- {/* Mobile hamburger */}
314
- {isMobile && (
315
- <button
316
- onClick={() => setIsMobileOpen((o) => !o)}
 
 
 
 
317
  style={{
318
- position: "fixed",
319
- top: "16px",
320
- left: "16px",
321
- zIndex: 300,
322
- background: "rgba(10,14,35,0.95)",
323
- border: "1px solid rgba(255,255,255,0.12)",
324
- borderRadius: "10px",
325
- color: "white",
326
- fontSize: "18px",
327
- padding: "8px 11px",
328
- cursor: "pointer",
329
- boxShadow: "0 4px 16px rgba(0,0,0,0.4)",
330
  }}
331
  >
332
- {isMobileOpen ? "βœ•" : "☰"}
333
- </button>
334
- )}
335
-
336
- {/* Sidebar */}
337
- <aside style={isMobile ? mobileSidebarStyle : sidebarStyle}>
338
- <SidebarContent />
339
- </aside>
340
-
341
- {/* Main content */}
342
- <main
343
- ref={mainRef}
344
- style={{
345
- flex: 1,
346
- minWidth: 0,
347
- height: "100vh",
348
- overflowY: "auto",
349
- paddingTop: isMobile ? "56px" : 0,
350
- }}
351
- >
352
- <ExperimentSetup />
353
- </main>
354
- </div>
355
-
356
  );
357
  }
 
33
  const [isMobileOpen, setIsMobileOpen] = useState(false);
34
  const [hoveredItem, setHoveredItem] = useState(null);
35
  const [isMobile, setIsMobile] = useState(false);
 
36
  const observersRef = useRef([]);
37
+ const mainRef = useRef(null);
 
 
 
 
 
 
 
 
38
 
39
  // ── Responsive breakpoints ─────────────────────────────────────────────────
40
  useEffect(() => {
41
  const check = () => {
42
  setIsMobile(window.innerWidth < 640);
 
43
  };
44
  check();
45
  window.addEventListener("resize", check);
46
  return () => window.removeEventListener("resize", check);
47
  }, []);
48
 
49
+ // ── Scroll spy (bidirectional) ─────────────────────────────────────────────
50
+ // Uses scroll position on the main container to determine which section is
51
+ // currently "in view" by comparing scrollTop against each section's offsetTop.
52
+ // This avoids IntersectionObserver's one-directional rootMargin bias.
53
  useEffect(() => {
54
+ const mainEl = mainRef.current;
55
+ if (!mainEl) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
+ const handleScroll = () => {
58
+ const scrollTop = mainEl.scrollTop;
59
+ const viewportH = mainEl.clientHeight;
60
+
61
+ // Collect all sections that exist in the DOM
62
+ const sections = SECTION_IDS
63
+ .map((id) => {
64
+ const el = document.getElementById(id);
65
+ return el ? { id, top: el.offsetTop } : null;
66
+ })
67
+ .filter(Boolean)
68
+ .sort((a, b) => a.top - b.top);
69
 
70
+ if (!sections.length) return;
 
 
 
71
 
72
+ // Pick the last section whose top edge is within the upper 40% of viewport
73
+ const threshold = scrollTop + viewportH * 0.4;
74
+ let current = sections[0].id;
75
+ for (const s of sections) {
76
+ if (s.top <= threshold) current = s.id;
77
+ }
78
 
79
+ setActiveSection(current);
80
+ };
81
+
82
+ mainEl.addEventListener("scroll", handleScroll, { passive: true });
83
+ // Run once on mount so initial state is correct
84
+ handleScroll();
85
 
86
+ return () => mainEl.removeEventListener("scroll", handleScroll);
87
+ }, []);
88
+
89
+ // ── Navigation ─────────────────────────────────────────────────────────────
90
+ const navigateTo = (sectionId) => {
91
+ if (isMobile) setIsMobileOpen(false);
92
+
93
+ if (!sectionId) {
94
+ onHome?.();
95
+ return;
96
+ }
97
+
98
+ const element = document.getElementById(sectionId);
99
+ if (element && mainRef.current) {
100
+ mainRef.current.scrollTo({
101
+ top: element.offsetTop - 20,
102
+ behavior: "smooth",
103
+ });
104
+ setActiveSection(sectionId);
105
+ }
106
+ };
107
 
 
 
 
108
  // ── Design tokens ──────────────────────────────────────────────────────────
109
+ const accent = "#4f6ef7";
110
+ const sidebarW = isCollapsed ? "72px" : "240px";
111
 
112
  const sidebarStyle = {
113
  width: sidebarW,
 
117
  top: 0,
118
  display: "flex",
119
  flexDirection: "column",
120
+ background: "linear-gradient(180deg, rgba(12,17,42,0.99) 0%, rgba(9,13,32,1) 100%)",
121
+ borderRight: "1px solid rgba(255,255,255,0.1)",
122
+ boxShadow: "4px 0 24px rgba(0,0,0,0.35)",
123
  transition: "width 0.25s cubic-bezier(0.4,0,0.2,1), min-width 0.25s cubic-bezier(0.4,0,0.2,1)",
124
  zIndex: 50,
125
  overflowX: "hidden",
 
142
  {/* Branding */}
143
  <div style={{
144
  padding: isCollapsed ? "24px 0" : "28px 20px 20px",
145
+ borderBottom: "1px solid rgba(255,255,255,0.09)",
146
  display: "flex", alignItems: "center",
147
  justifyContent: isCollapsed ? "center" : "space-between",
148
  gap: "10px", flexShrink: 0,
 
156
  }}>GaugePilot</h1>
157
  <p style={{
158
  margin: "7px 0 0", fontFamily: "'Courier New', monospace",
159
+ fontSize: "10px", fontWeight: 700, color: "rgba(255,255,255,0.5)",
160
  letterSpacing: "0.05em", textTransform: "uppercase", whiteSpace: "nowrap",
161
  }}>benchmark & analysis</p>
162
  </div>
 
170
  <button
171
  onClick={() => setIsCollapsed((c) => !c)}
172
  style={{
173
+ background: "rgba(255,255,255,0.07)",
174
+ border: "1px solid rgba(255,255,255,0.12)",
175
+ borderRadius: "8px", color: "rgba(255,255,255,0.65)", cursor: "pointer",
176
  padding: "5px 7px", fontSize: "12px", lineHeight: 1, flexShrink: 0,
177
  transition: "all 0.15s",
178
  }}
179
+ onMouseEnter={(e) => {
180
+ e.currentTarget.style.background = "rgba(255,255,255,0.14)";
181
+ e.currentTarget.style.color = "white";
182
+ }}
183
+ onMouseLeave={(e) => {
184
+ e.currentTarget.style.background = "rgba(255,255,255,0.07)";
185
+ e.currentTarget.style.color = "rgba(255,255,255,0.65)";
186
+ }}
187
  >
188
  {isCollapsed ? "β†’" : "←"}
189
  </button>
 
191
  </div>
192
 
193
  {/* Nav groups */}
194
+ <nav style={{
195
+ flex: 1, overflowY: "auto", overflowX: "hidden",
196
+ padding: "12px 8px", scrollbarWidth: "none",
197
+ }}>
198
  {NAV_GROUPS.map((group) => (
199
  <div key={group.label} style={{ marginBottom: "4px" }}>
200
 
 
202
  {!isCollapsed && (
203
  <div style={{
204
  fontSize: "10px", fontWeight: 700, letterSpacing: "0.12em",
205
+ textTransform: "uppercase", color: "rgba(255,255,255,0.38)",
206
  padding: "12px 10px 6px", userSelect: "none",
207
  }}>
208
  {group.label}
 
231
  marginBottom: "2px",
232
  justifyContent: isCollapsed ? "center" : "flex-start",
233
  background: isActive
234
+ ? "rgba(79,110,247,0.22)"
235
  : isHover
236
+ ? "rgba(255,255,255,0.07)"
237
  : "transparent",
238
  border: "none",
239
  borderRadius: "10px",
240
  cursor: "pointer",
241
+ color: isActive
242
+ ? "#a0baff"
243
+ : isHover
244
+ ? "rgba(255,255,255,0.9)"
245
+ : "rgba(255,255,255,0.6)",
246
  fontSize: "14px",
247
  fontWeight: isActive ? 600 : 400,
248
  textAlign: "left",
249
  transition: "all 0.15s ease",
250
+ boxShadow: isActive ? "0 0 16px rgba(79,110,247,0.18)" : "none",
251
  whiteSpace: "nowrap",
252
  overflow: "hidden",
253
  }}
 
258
  position: "absolute", left: 0, top: "20%", bottom: "20%",
259
  width: "3px", borderRadius: "0 3px 3px 0",
260
  background: "linear-gradient(180deg, #4f6ef7, #a78bfa)",
261
+ boxShadow: "0 0 8px rgba(79,110,247,0.8)",
262
  }} />
263
  )}
264
 
265
  {/* Icon */}
266
  <span style={{
267
  fontSize: "16px", lineHeight: 1, flexShrink: 0,
268
+ filter: isActive ? "drop-shadow(0 0 4px rgba(79,110,247,0.9))" : "none",
269
  transition: "filter 0.15s",
270
  }}>
271
  {icon}
 
273
 
274
  {/* Label */}
275
  {!isCollapsed && (
276
+ <span style={{ overflow: "hidden", textOverflow: "ellipsis" }}>
277
+ {label}
278
+ </span>
279
  )}
280
  </button>
281
  );
 
287
  {/* Footer */}
288
  {!isCollapsed && (
289
  <div style={{
290
+ padding: "14px 16px",
291
+ borderTop: "1px solid rgba(255,255,255,0.08)",
292
+ flexShrink: 0,
293
  }}>
294
  <div style={{
295
  display: "flex", alignItems: "center", gap: "8px",
296
  padding: "8px 10px", borderRadius: "10px",
297
+ background: "rgba(79,110,247,0.1)",
298
+ border: "1px solid rgba(79,110,247,0.2)",
299
  }}>
300
  <span style={{
301
  width: 7, height: 7, borderRadius: "50%", background: "#22c55e",
302
  boxShadow: "0 0 6px #22c55e", display: "inline-block", flexShrink: 0,
303
  }} />
304
+ <span style={{ fontSize: "11px", color: "rgba(255,255,255,0.8)", fontWeight: 500 }}>
305
  System ready
306
  </span>
307
  </div>
 
312
 
313
  // ── Render ─────────────────────────────────────────────────────────────────
314
  return (
315
+ <div style={{
 
316
  display: "flex",
317
  height: "100vh",
318
  overflow: "hidden",
319
  background: "var(--bg-primary, #0a0e23)",
320
+ }}>
 
321
 
322
+ {/* Mobile overlay */}
323
+ {isMobile && isMobileOpen && (
324
+ <div
325
+ onClick={() => setIsMobileOpen(false)}
326
+ style={{
327
+ position: "fixed", inset: 0,
328
+ background: "rgba(0,0,0,0.6)",
329
+ zIndex: 199,
330
+ backdropFilter: "blur(2px)",
331
+ }}
332
+ />
333
+ )}
334
+
335
+ {/* Mobile hamburger */}
336
+ {isMobile && (
337
+ <button
338
+ onClick={() => setIsMobileOpen((o) => !o)}
339
+ style={{
340
+ position: "fixed", top: "16px", left: "16px", zIndex: 300,
341
+ background: "rgba(10,14,35,0.95)",
342
+ border: "1px solid rgba(255,255,255,0.15)",
343
+ borderRadius: "10px", color: "white",
344
+ fontSize: "18px", padding: "8px 11px",
345
+ cursor: "pointer", boxShadow: "0 4px 16px rgba(0,0,0,0.4)",
346
+ }}
347
+ >
348
+ {isMobileOpen ? "βœ•" : "☰"}
349
+ </button>
350
+ )}
351
 
352
+ {/* Sidebar */}
353
+ <aside style={isMobile ? mobileSidebarStyle : sidebarStyle}>
354
+ <SidebarContent />
355
+ </aside>
356
+
357
+ {/* Main content */}
358
+ <main
359
+ ref={mainRef}
360
  style={{
361
+ flex: 1,
362
+ minWidth: 0,
363
+ height: "100vh",
364
+ overflowY: "auto",
365
+ paddingTop: isMobile ? "56px" : 0,
 
 
 
 
 
 
 
366
  }}
367
  >
368
+ <ExperimentSetup />
369
+ </main>
370
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  );
372
  }
frontend/src/gaugepilot/api.js CHANGED
@@ -36,4 +36,71 @@ export async function uploadDocument(file, token) {
36
  );
37
 
38
  return response.data;
39
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  );
37
 
38
  return response.data;
39
+ }
40
+ export async function getBenchmarkRuns(token) {
41
+ const response = await API.get(
42
+ "/benchmark/runs",
43
+ {
44
+ headers: {
45
+ Authorization: `Bearer ${token}`,
46
+ },
47
+ }
48
+ );
49
+
50
+ return response.data;
51
+ }
52
+ export async function getDocuments(token) {
53
+ const response = await API.get(
54
+ "/docs/",
55
+ {
56
+ headers: {
57
+ Authorization: `Bearer ${token}`,
58
+ },
59
+ }
60
+ );
61
+
62
+ return response.data;
63
+ }
64
+ export async function deleteBenchmarkRun(
65
+ runId,
66
+ token,
67
+ ) {
68
+ const response = await API.delete(
69
+ `/benchmark/runs/${runId}`,
70
+ {
71
+ headers: {
72
+ Authorization: `Bearer ${token}`,
73
+ },
74
+ }
75
+ );
76
+
77
+ return response.data;
78
+ }
79
+ export async function resetBenchmarkRuns(
80
+ token,
81
+ ) {
82
+ const response = await API.delete(
83
+ "/benchmark/runs/reset",
84
+ {
85
+ headers: {
86
+ Authorization: `Bearer ${token}`,
87
+ },
88
+ }
89
+ );
90
+
91
+ return response.data;
92
+ }
93
+ export async function resetDocuments(
94
+ token,
95
+ ) {
96
+ const response = await API.delete(
97
+ "/docs/reset",
98
+ {
99
+ headers: {
100
+ Authorization: `Bearer ${token}`,
101
+ },
102
+ }
103
+ );
104
+
105
+ return response.data;
106
+ }
frontend/src/gaugepilot/hooks/useBenchmark.js CHANGED
@@ -1,8 +1,9 @@
1
-
2
- import { useState } from "react";
3
-
4
- import { runBenchmark } from "../api";
5
 
 
 
 
 
6
  export function useBenchmark() {
7
  const [loading, setLoading] = useState(false);
8
 
@@ -10,6 +11,40 @@ export function useBenchmark() {
10
 
11
  const [error, setError] = useState(null);
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  const executeBenchmark = async (
14
  payload,
15
  token,
@@ -47,4 +82,4 @@ export function useBenchmark() {
47
  error,
48
  executeBenchmark,
49
  };
50
- }
 
1
+ import { useState, useEffect } from "react";
 
 
 
2
 
3
+ import {
4
+ runBenchmark,
5
+ getBenchmarkRuns,
6
+ } from "../api";
7
  export function useBenchmark() {
8
  const [loading, setLoading] = useState(false);
9
 
 
11
 
12
  const [error, setError] = useState(null);
13
 
14
+ useEffect(() => {
15
+ const loadLatestRun = async () => {
16
+ try {
17
+ const token =
18
+ localStorage.getItem("token");
19
+
20
+ if (!token) return;
21
+
22
+ const runs =
23
+ await getBenchmarkRuns(token);
24
+
25
+ if (!runs.length) return;
26
+
27
+ const latest =
28
+ runs.sort(
29
+ (a, b) =>
30
+ new Date(b.created_at) -
31
+ new Date(a.created_at)
32
+ )[0];
33
+
34
+ setResults({
35
+ leaderboard:
36
+ JSON.parse(
37
+ latest.leaderboard_json
38
+ ),
39
+ });
40
+
41
+ } catch (err) {
42
+ console.error(err);
43
+ }
44
+ };
45
+
46
+ loadLatestRun();
47
+ }, []);
48
  const executeBenchmark = async (
49
  payload,
50
  token,
 
82
  error,
83
  executeBenchmark,
84
  };
85
+ }
frontend/src/gaugepilot/pages/ExperimentSetup.jsx CHANGED
@@ -1,18 +1,16 @@
1
- import { useState, useRef } from "react";
2
  import { useBenchmark } from "../hooks/useBenchmark";
3
  import Leaderboards from "./Leaderboards";
4
  import ExperimentSelector from "../components/ExperimentSelector";
5
  import {
6
  runBenchmark,
7
  uploadDocument,
 
 
 
 
 
8
  } from "../api";
9
- /*const SAMPLE_QUESTIONS = [
10
- "What is the main contribution of this paper?",
11
- "How does the proposed method compare to baselines?",
12
- "What datasets were used for evaluation?",
13
- "What are the limitations acknowledged by the authors?",
14
- "What future work do the authors suggest?",
15
- ];*/
16
 
17
  export default function ExperimentSetup() {
18
  const { loading, results, error, executeBenchmark } = useBenchmark();
@@ -28,9 +26,54 @@ export default function ExperimentSetup() {
28
  const [uploading, setUploading] = useState(false);
29
  const [benchmarkRuns, setBenchmarkRuns] = useState(0);
30
  const [bestScore, setBestScore] = useState(null);
 
 
 
31
  const fileInputRef = useRef(null);
32
 
33
- const questionList = questions.split("\n").map((q) => q.trim()).filter(Boolean);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  const canRun = questionList.length > 0 && !loading;
35
 
36
  const handleRun = async () => {
@@ -49,157 +92,358 @@ export default function ExperimentSetup() {
49
  const s = res.leaderboard.overall[0].avg_rank;
50
  return prev === null ? s : Math.min(prev, s);
51
  });
 
 
 
 
 
 
 
 
 
 
 
 
52
  }
53
  };
54
 
55
  const handleFileChange = async (file) => {
56
- if (!file) return;
57
-
58
- try {
59
- setUploading(true);
 
 
 
 
 
 
 
 
60
 
 
 
61
  const token = localStorage.getItem("token");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
- await uploadDocument(
64
- file,
65
- token
66
- );
67
-
68
- setUploadedFile(file);
69
-
70
- console.log(
71
- "GaugePilot upload successful"
72
- );
73
- } catch (err) {
74
- console.error(
75
- "GaugePilot upload failed",
76
- err
77
- );
78
- } finally {
79
- setUploading(false);
80
- }
81
- };
82
-
83
 
 
 
 
 
 
 
 
 
 
84
 
85
  const handleDrop = (e) => {
86
- e.preventDefault(); setIsDragging(false);
 
87
  handleFileChange(e.dataTransfer.files[0]);
88
  };
89
 
90
  const labelMap = {
91
- "llama-3.1-8b": "Llama 3.1 8B", hybrid: "Hybrid", vector: "Vector",
92
- lexical: "BM25", minilm: "MiniLM", default: "Default",
 
 
 
 
93
  };
94
 
95
- // ── Design tokens ────────────────────────────────────────────────────────────
96
  const accent = "#4f6ef7";
97
  const accentPurple = "#6a4ff7";
98
  const green = "#22c55e";
99
 
100
  const card = {
101
- background: "rgba(255,255,255,0.03)",
102
- border: "1px solid rgba(255,255,255,0.08)",
103
  borderRadius: "20px",
104
  padding: "24px",
105
  };
106
 
107
  const sectionLabel = {
108
- fontSize: "11px", fontWeight: 700, letterSpacing: "0.1em",
109
- textTransform: "uppercase", color: "rgba(255,255,255,0.3)", marginBottom: "14px",
 
 
 
 
110
  };
111
 
112
  const chip = (color = accent) => ({
113
- display: "inline-flex", alignItems: "center", gap: "6px",
114
- padding: "3px 10px", borderRadius: "999px",
115
- background: `${color}22`, border: `1px solid ${color}44`,
116
- color, fontSize: "11px", fontWeight: 600, letterSpacing: "0.04em",
 
 
 
 
 
 
 
117
  });
118
 
119
- // ── KPI Stats ─────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  const stats = [
121
  { label: "Questions Added", value: questionList.length, icon: "❓", color: accent },
122
  { label: "Uploaded Files", value: uploadedFile ? 1 : 0, icon: "πŸ“„", color: "#a78bfa" },
123
  { label: "Benchmark Runs", value: benchmarkRuns, icon: "πŸš€", color: green },
124
- { label: "Best Score", value: bestScore !== null ? bestScore.toFixed(2) : "β€”", icon: "πŸ†", color: "#f59e0b" },
 
 
 
 
 
125
  ];
126
 
127
  return (
128
- <div id="experiment-setup" style={{ maxWidth: "1240px", margin: "0 auto", padding: "28px 24px", fontFamily: "inherit" }}>
129
-
130
- {/* ── Page Header ──────────────────────────────────────────────────────── */}
131
- <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", flexWrap: "wrap", gap: "12px", marginBottom: "24px" }}>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  <div>
133
- <div style={{ display: "flex", alignItems: "center", gap: "12px", marginBottom: "8px" }}>
 
 
 
 
 
 
 
134
  <span style={{ fontSize: "30px", lineHeight: 1 }}>πŸ§ͺ</span>
135
- <h1 style={{ margin: 0, fontSize: "34px", fontWeight: 700, letterSpacing: "-0.5px", color: "white" }}>
 
 
 
 
 
 
 
 
136
  Experiment Setup
137
  </h1>
138
  </div>
139
- <p style={{ margin: 0, fontSize: "13px", color: "rgba(255,255,255,0.72)", lineHeight: 1.6 }}>
140
- Configure your RAG pipeline, upload a source document, and define evaluation questions.
 
 
 
 
 
 
 
 
141
  </p>
142
  </div>
143
  <div style={chip(results ? green : accent)}>
144
- <span style={{
145
- width: 7, height: 7, borderRadius: "50%",
146
- background: results ? green : accent,
147
- boxShadow: `0 0 6px ${results ? green : accent}`,
148
- display: "inline-block",
149
- }} />
 
 
 
 
150
  {loading ? "Running…" : results ? "Complete" : "Ready"}
151
  </div>
152
  </div>
153
 
154
- {/* ── KPI Row ──────────────────────────────────────────────────────────── */}
155
- <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: "12px", marginBottom: "20px" }}>
 
 
 
 
 
 
 
156
  {stats.map(({ label, value, icon, color }) => (
157
- <div key={label} style={{
158
- ...card, padding: "16px 20px",
159
- display: "flex", alignItems: "center", gap: "14px",
160
- }}>
161
- <div style={{
162
- width: 40, height: 40, borderRadius: "10px", flexShrink: 0,
163
- background: `${color}18`, display: "flex", alignItems: "center",
164
- justifyContent: "center", fontSize: "18px",
165
- }}>{icon}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  <div>
167
- <div style={{ fontSize: "22px", fontWeight: 700, color: "white", lineHeight: 1 }}>{value}</div>
168
- <div style={{ fontSize: "11px", color: "rgba(255,255,255,0.35)", marginTop: "3px" }}>{label}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  </div>
170
  </div>
171
  ))}
172
  </div>
173
 
174
- {/* ── Configuration Panel ──────────────────────────────────────────────── */}
175
  <div style={{ ...card, marginBottom: "16px" }}>
176
  <p style={sectionLabel}>Pipeline Configuration</p>
177
  <div style={{ display: "flex", gap: "32px", flexWrap: "wrap" }}>
178
- <ExperimentSelector label="Active Model" value={model} onChange={setModel}
179
- options={[{ value: "llama-3.1-8b", label: "Llama 3.1 8B", description: "Fast & Efficient" }]} />
180
- <ExperimentSelector label="Retrieval Strategy" value={retrievalMethod} onChange={setRetrievalMethod}
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  options={[
182
  { value: "hybrid", label: "Hybrid", description: "Vector + BM25" },
183
- { value: "vector", label: "Vector", description: "Embedding Search" },
 
 
 
 
184
  { value: "lexical", label: "BM25", description: "Keyword Search" },
185
- ]} />
186
- <ExperimentSelector label="Reranker" value={reranker} onChange={setReranker}
187
- options={[{ value: "minilm", label: "MiniLM", description: "Fast balanced baseline" }]} />
188
- <ExperimentSelector label="Enhancements" value={enhancement} onChange={setEnhancement}
189
- options={[{ value: "default", label: "Default", description: "Standard pipeline" }]} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  </div>
191
  </div>
192
 
193
- {/* ── Main Grid ────────────────────────────────────────────────────────── */}
194
- <div style={{ display: "grid", gridTemplateColumns: "420px 1fr", gap: "16px", alignItems: "start" }}>
195
-
196
- {/* ── Left Column ──────────────────────────────────────────────────── */}
 
 
 
 
 
 
197
  <div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
198
 
199
  {/* Upload Card */}
200
  <div
201
  onClick={() => fileInputRef.current?.click()}
202
- onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
 
 
 
203
  onDragLeave={() => setIsDragging(false)}
204
  onDrop={handleDrop}
205
  onMouseEnter={() => setIsHoveringUpload(true)}
@@ -207,201 +451,243 @@ export default function ExperimentSetup() {
207
  style={{
208
  borderRadius: "20px",
209
  minHeight: "240px",
210
- display: "flex", flexDirection: "column",
211
- alignItems: "center", justifyContent: "center", gap: "14px",
 
 
 
212
  cursor: "pointer",
213
- position: "relative", overflow: "hidden",
 
214
  transition: "all 0.25s ease",
215
  background: isDragging
216
- ? `rgba(79,110,247,0.1)`
217
  : uploadedFile
218
- ? `rgba(34,197,94,0.06)`
219
- : `rgba(255,255,255,0.025)`,
220
  border: `1.5px ${isDragging ? "solid" : "dashed"} ${
221
- isDragging ? accent
222
- : uploadedFile ? `${green}66`
223
- : isHoveringUpload ? "rgba(79,110,247,0.5)"
224
- : "rgba(255,255,255,0.1)"
 
 
 
225
  }`,
226
  boxShadow: isDragging
227
  ? `0 0 32px rgba(79,110,247,0.2), inset 0 0 32px rgba(79,110,247,0.05)`
228
  : isHoveringUpload
229
  ? `0 8px 32px rgba(0,0,0,0.3), 0 0 20px rgba(79,110,247,0.1)`
230
  : uploadedFile
231
- ? `0 0 20px rgba(34,197,94,0.1)`
232
  : "none",
233
- transform: isHoveringUpload && !isDragging ? "translateY(-2px)" : "none",
 
234
  }}
235
  >
236
- <input ref={fileInputRef} type="file" style={{ display: "none" }}
237
- onChange={(e) => handleFileChange(e.target.files[0])} />
 
 
 
 
238
 
239
  {/* Ambient glow blob */}
240
- <div style={{
241
- position: "absolute", width: "200px", height: "200px",
242
- borderRadius: "50%", top: "50%", left: "50%",
243
- transform: "translate(-50%, -50%)",
244
- background: uploadedFile
245
- ? `radial-gradient(circle, rgba(34,197,94,0.08) 0%, transparent 70%)`
246
- : `radial-gradient(circle, rgba(79,110,247,0.08) 0%, transparent 70%)`,
247
- pointerEvents: "none",
248
- }} />
 
 
 
 
 
 
249
 
250
  {uploading ? (
251
- <>
252
- <div
253
- style={{
254
- width: 60,
255
- height: 60,
256
- borderRadius: "16px",
257
- background: "rgba(79,110,247,0.15)",
258
- display: "flex",
259
- alignItems: "center",
260
- justifyContent: "center",
261
- fontSize: "28px",
262
- boxShadow: "0 0 20px rgba(79,110,247,0.2)",
263
- }}
264
- >
265
- ⏳
266
- </div>
267
-
268
- <div style={{ textAlign: "center", zIndex: 1 }}>
269
- <p
270
- style={{
271
- margin: 0,
272
- fontWeight: 700,
273
- fontSize: "15px",
274
- color: "#4f6ef7",
275
- }}
276
- >
277
- Uploading & Indexing...
278
- </p>
279
-
280
- <p
281
- style={{
282
- margin: "4px 0 0",
283
- fontSize: "12px",
284
- color: "rgba(255,255,255,0.35)",
285
- }}
286
- >
287
- Processing document
288
- </p>
289
- </div>
290
- </>
291
- ) : uploadedFile ? (
292
- <>
293
- <div
294
- style={{
295
- width: 60,
296
- height: 60,
297
- borderRadius: "16px",
298
- background: "rgba(34,197,94,0.15)",
299
- display: "flex",
300
- alignItems: "center",
301
- justifyContent: "center",
302
- fontSize: "28px",
303
- boxShadow: "0 0 20px rgba(34,197,94,0.2)",
304
- }}
305
- >
306
- βœ…
307
- </div>
308
-
309
- <div style={{ textAlign: "center", zIndex: 1 }}>
310
- <p
311
- style={{
312
- margin: 0,
313
- fontWeight: 700,
314
- fontSize: "15px",
315
- color: green,
316
- }}
317
- >
318
- {uploadedFile.name}
319
- </p>
320
-
321
- <p
322
- style={{
323
- margin: "4px 0 0",
324
- fontSize: "12px",
325
- color: "rgba(255,255,255,0.35)",
326
- }}
327
- >
328
- {(uploadedFile.size / 1024).toFixed(1)} KB Β· Click to replace
329
- </p>
330
- </div>
331
- </>
332
- ) : (
333
- <>
334
- <div
335
- style={{
336
- width: 64,
337
- height: 64,
338
- borderRadius: "18px",
339
- background: isDragging
340
- ? `rgba(79,110,247,0.2)`
341
- : "rgba(79,110,247,0.1)",
342
- display: "flex",
343
- alignItems: "center",
344
- justifyContent: "center",
345
- fontSize: "28px",
346
- zIndex: 1,
347
- boxShadow: isDragging
348
- ? `0 0 24px rgba(79,110,247,0.3)`
349
- : "none",
350
- transition: "all 0.2s ease",
351
- }}
352
- >
353
- ⬆️
354
- </div>
355
-
356
- <div style={{ textAlign: "center", zIndex: 1 }}>
357
- <p
358
- style={{
359
- margin: 0,
360
- fontWeight: 700,
361
- fontSize: "16px",
362
- color: "white",
363
- }}
364
- >
365
- {isDragging
366
- ? "Drop to upload"
367
- : "Upload Document"}
368
- </p>
369
-
370
- <p
371
- style={{
372
- margin: "5px 0 0",
373
- fontSize: "12px",
374
- color: "rgba(255,255,255,0.35)",
375
- lineHeight: 1.6,
376
- }}
377
- >
378
- Drag & drop or click to browse
379
- <br />
380
-
381
- </p>
382
- </div>
383
- </>
384
- )}
385
  </div>
386
 
387
  {/* Benchmark Summary */}
388
  <div style={{ ...card, padding: "16px 20px" }}>
389
- <p style={{ ...sectionLabel, marginBottom: "10px" }}>Benchmark Summary</p>
 
 
390
  <div style={{ display: "flex", flexDirection: "column", gap: "0px" }}>
391
  {[
392
  { label: "Model", value: labelMap[model] ?? model },
393
- { label: "Retrieval", value: labelMap[retrievalMethod] ?? retrievalMethod },
 
 
 
394
  { label: "Reranker", value: labelMap[reranker] ?? reranker },
395
- { label: "Enhancement", value: labelMap[enhancement] ?? enhancement },
396
- { label: "Questions", value: questionList.length === 0 ? "None" : `${questionList.length}` },
 
 
 
 
 
 
 
 
 
397
  ].map(({ label, value }, i, arr) => (
398
- <div key={label} style={{
399
- display: "flex", justifyContent: "space-between", alignItems: "center",
400
- padding: "7px 0",
401
- borderBottom: i < arr.length - 1 ? "1px solid rgba(255,255,255,0.04)" : "none",
402
- }}>
403
- <span style={{ fontSize: "12px", color: "rgba(255,255,255,0.3)" }}>{label}</span>
404
- <span style={{ fontSize: "12px", fontWeight: 600, color: "rgba(255,255,255,0.75)" }}>{value}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  </div>
406
  ))}
407
  </div>
@@ -414,14 +700,22 @@ export default function ExperimentSetup() {
414
  onMouseEnter={() => setIsHoveringRun(true)}
415
  onMouseLeave={() => setIsHoveringRun(false)}
416
  style={{
417
- width: "100%", padding: "18px", borderRadius: "16px", border: "none",
 
 
 
418
  background: canRun
419
  ? `linear-gradient(135deg, ${accent} 0%, ${accentPurple} 100%)`
420
- : "rgba(255,255,255,0.05)",
421
- color: canRun ? "white" : "rgba(255,255,255,0.2)",
422
- fontSize: "16px", fontWeight: 700, letterSpacing: "0.03em",
 
 
423
  cursor: canRun ? "pointer" : "not-allowed",
424
- display: "flex", alignItems: "center", justifyContent: "center", gap: "10px",
 
 
 
425
  boxShadow: canRun
426
  ? isHoveringRun
427
  ? `0 0 40px rgba(79,110,247,0.6), 0 8px 24px rgba(0,0,0,0.4)`
@@ -433,68 +727,148 @@ export default function ExperimentSetup() {
433
  >
434
  {loading ? (
435
  <>
436
- <span style={{
437
- width: 15, height: 15,
438
- border: "2px solid rgba(255,255,255,0.3)",
439
- borderTop: "2px solid white",
440
- borderRadius: "50%",
441
- display: "inline-block",
442
- animation: "spin 0.7s linear infinite",
443
- }} />
 
 
 
444
  Running Benchmark…
445
  </>
446
- ) : <>β–Ά Run Benchmark</>}
 
 
447
  </button>
448
 
449
  <style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
450
 
451
  {!canRun && !loading && (
452
- <p style={{ margin: "-4px 0 0", fontSize: "12px", color: "rgba(255,255,255,0.25)", textAlign: "center" }}>
 
 
 
 
 
 
 
453
  Add at least one question to run
454
  </p>
455
  )}
456
  </div>
457
 
458
- {/* ── Right Column: Questions ─────────────────────────────────────── */}
459
- <div style={{ ...card, display: "flex", flexDirection: "column", gap: "14px" }}>
460
-
 
 
 
 
 
 
461
  {/* Questions header */}
462
- <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
 
 
 
 
 
 
463
  <div>
464
- <p style={{ ...sectionLabel, marginBottom: "3px", color: "rgba(255,255,255,0.72)" }}>Evaluation Questions</p>
465
- <p style={{ margin: 0, fontSize: "12px", color: "rgba(255,255,255,0.72)" }}>One question per line</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
  </div>
467
- <div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
 
 
468
  {questionList.length > 0 && (
469
  <>
470
- <div style={chip(green)}>βœ“ {questionList.length} question{questionList.length !== 1 ? "s" : ""}</div>
 
 
 
471
  <button
472
  onClick={() => setQuestions("")}
473
  style={{
474
- background: "rgba(239,68,68,0.1)", border: "1px solid rgba(239,68,68,0.2)",
475
- borderRadius: "8px", color: "#f87171", fontSize: "12px", fontWeight: 600,
476
- padding: "4px 10px", cursor: "pointer",
 
 
 
 
 
477
  }}
478
- >Clear</button>
 
 
479
  </>
480
  )}
481
- <span style={{ fontSize: "11px", color: "rgba(255,255,255,0.2)" }}>{questions.length} chars</span>
 
 
 
 
 
 
 
482
  </div>
483
  </div>
484
 
485
-
486
-
487
-
488
-
489
- <div style={{ position: "relative", display: "flex", borderRadius: "14px", overflow: "hidden", border: "1px solid rgba(255,255,255,0.08)" }}>
 
 
 
 
490
  {/* Line numbers */}
491
- <div style={{
492
- background: "rgba(0,0,0,0.25)", padding: "18px 10px",
493
- display: "flex", flexDirection: "column", alignItems: "flex-end",
494
- gap: 0, userSelect: "none", minWidth: "36px", borderRight: "1px solid rgba(255,255,255,0.05)",
495
- }}>
 
 
 
 
 
 
 
 
496
  {(questions || " ").split("\n").map((_, i) => (
497
- <div key={i} style={{ fontSize: "12px", color: "rgba(255,255,255,0.18)", lineHeight: "1.8", fontFamily: "'Courier New', monospace", height: "21.6px" }}>
 
 
 
 
 
 
 
 
 
498
  {i + 1}
499
  </div>
500
  ))}
@@ -505,11 +879,18 @@ export default function ExperimentSetup() {
505
  value={questions}
506
  onChange={(e) => setQuestions(e.target.value)}
507
  style={{
508
- flex: 1, minHeight: "300px", padding: "18px 16px",
509
- border: "none", background: "rgba(0,0,0,0.2)",
510
- color: "white", fontSize: "13px", resize: "vertical",
511
- lineHeight: "1.8", fontFamily: "'Courier New', Courier, monospace",
512
- outline: "none", boxSizing: "border-box",
 
 
 
 
 
 
 
513
  }}
514
  />
515
  </div>
@@ -517,19 +898,133 @@ export default function ExperimentSetup() {
517
  </div>
518
 
519
  {error && (
520
- <div style={{
521
- marginTop: "14px", padding: "13px 18px", borderRadius: "12px",
522
- background: "rgba(239,68,68,0.08)", border: "1px solid rgba(239,68,68,0.25)",
523
- color: "#f87171", fontSize: "13px",
524
- }}>⚠️ {error}</div>
 
 
 
 
 
 
 
 
525
  )}
526
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
  <div style={{ marginTop: "24px" }}>
528
  <Leaderboards
529
- leaderboard={results?.leaderboard || {
530
- overall: [], faithfulness: [], grounding: [],
531
- retrieval_quality: [], query_coverage: [], latency: [],
532
- }}
 
 
 
 
 
 
533
  />
534
  </div>
535
  </div>
 
1
+ import { useState, useRef, useEffect } from "react";
2
  import { useBenchmark } from "../hooks/useBenchmark";
3
  import Leaderboards from "./Leaderboards";
4
  import ExperimentSelector from "../components/ExperimentSelector";
5
  import {
6
  runBenchmark,
7
  uploadDocument,
8
+ getBenchmarkRuns,
9
+ deleteBenchmarkRun,
10
+ resetBenchmarkRuns,
11
+ getDocuments,
12
+ resetDocuments,
13
  } from "../api";
 
 
 
 
 
 
 
14
 
15
  export default function ExperimentSetup() {
16
  const { loading, results, error, executeBenchmark } = useBenchmark();
 
26
  const [uploading, setUploading] = useState(false);
27
  const [benchmarkRuns, setBenchmarkRuns] = useState(0);
28
  const [bestScore, setBestScore] = useState(null);
29
+ const [allRuns, setAllRuns] = useState([]);
30
+ const [selectedRun, setSelectedRun] = useState(null);
31
+
32
  const fileInputRef = useRef(null);
33
 
34
+ useEffect(() => {
35
+ const loadDashboardData = async () => {
36
+ try {
37
+ const token = localStorage.getItem("token");
38
+ if (!token) return;
39
+
40
+ const runs = await getBenchmarkRuns(token);
41
+ const docs = await getDocuments(token);
42
+
43
+ setBenchmarkRuns(runs.length);
44
+ setAllRuns(runs);
45
+
46
+ const sorted = [...runs].sort(
47
+ (a, b) => new Date(b.created_at) - new Date(a.created_at)
48
+ );
49
+
50
+ if (sorted.length) {
51
+ setSelectedRun(sorted[0]);
52
+ const leaderboard = JSON.parse(sorted[0].leaderboard_json);
53
+ const score =
54
+ leaderboard.overall?.[0]?.average_rank ??
55
+ leaderboard.overall?.[0]?.avg_rank;
56
+ if (score != null) setBestScore(score);
57
+ }
58
+
59
+ if (docs.length) {
60
+ setUploadedFile({
61
+ name: docs[0].filename,
62
+ size: docs[0].file_size || 0,
63
+ });
64
+ }
65
+ } catch (err) {
66
+ console.error(err);
67
+ }
68
+ };
69
+
70
+ loadDashboardData();
71
+ }, []);
72
+
73
+ const questionList = questions
74
+ .split("\n")
75
+ .map((q) => q.trim())
76
+ .filter(Boolean);
77
  const canRun = questionList.length > 0 && !loading;
78
 
79
  const handleRun = async () => {
 
92
  const s = res.leaderboard.overall[0].avg_rank;
93
  return prev === null ? s : Math.min(prev, s);
94
  });
95
+ // Refresh runs list after a new run completes
96
+ try {
97
+ const updatedRuns = await getBenchmarkRuns(token);
98
+ const sorted = [...updatedRuns].sort(
99
+ (a, b) => new Date(b.created_at) - new Date(a.created_at)
100
+ );
101
+ setAllRuns(updatedRuns);
102
+ setBenchmarkRuns(updatedRuns.length);
103
+ if (sorted.length) setSelectedRun(sorted[0]);
104
+ } catch (err) {
105
+ console.error("Failed to refresh runs", err);
106
+ }
107
  }
108
  };
109
 
110
  const handleFileChange = async (file) => {
111
+ if (!file) return;
112
+ try {
113
+ setUploading(true);
114
+ const token = localStorage.getItem("token");
115
+ await uploadDocument(file, token);
116
+ setUploadedFile(file);
117
+ } catch (err) {
118
+ console.error("Upload failed", err);
119
+ } finally {
120
+ setUploading(false);
121
+ }
122
+ };
123
 
124
+ const handleDeleteRun = async () => {
125
+ if (!selectedRun) return;
126
  const token = localStorage.getItem("token");
127
+ try {
128
+ await deleteBenchmarkRun(selectedRun.id, token);
129
+ const updated = allRuns.filter((r) => r.id !== selectedRun.id);
130
+ const sorted = [...updated].sort(
131
+ (a, b) => new Date(b.created_at) - new Date(a.created_at)
132
+ );
133
+ setAllRuns(updated);
134
+ setBenchmarkRuns(updated.length);
135
+ setSelectedRun(sorted.length ? sorted[0] : null);
136
+ if (!sorted.length) setBestScore(null);
137
+ } catch (err) {
138
+ console.error("Delete run failed", err);
139
+ }
140
+ };
141
 
142
+ const handleResetRuns = async () => {
143
+ const token = localStorage.getItem("token");
144
+ try {
145
+ await resetBenchmarkRuns(token);
146
+ setAllRuns([]);
147
+ setBenchmarkRuns(0);
148
+ setSelectedRun(null);
149
+ setBestScore(null);
150
+ } catch (err) {
151
+ console.error("Reset runs failed", err);
152
+ }
153
+ };
 
 
 
 
 
 
 
 
154
 
155
+ const handleResetDocuments = async () => {
156
+ const token = localStorage.getItem("token");
157
+ try {
158
+ await resetDocuments(token);
159
+ setUploadedFile(null);
160
+ } catch (err) {
161
+ console.error("Reset documents failed", err);
162
+ }
163
+ };
164
 
165
  const handleDrop = (e) => {
166
+ e.preventDefault();
167
+ setIsDragging(false);
168
  handleFileChange(e.dataTransfer.files[0]);
169
  };
170
 
171
  const labelMap = {
172
+ "llama-3.1-8b": "Llama 3.1 8B",
173
+ hybrid: "Hybrid",
174
+ vector: "Vector",
175
+ lexical: "BM25",
176
+ minilm: "MiniLM",
177
+ default: "Default",
178
  };
179
 
180
+ // ── Design tokens ──────────────────────────────────────────────────────────
181
  const accent = "#4f6ef7";
182
  const accentPurple = "#6a4ff7";
183
  const green = "#22c55e";
184
 
185
  const card = {
186
+ background: "rgba(255,255,255,0.07)",
187
+ border: "1px solid rgba(255,255,255,0.15)",
188
  borderRadius: "20px",
189
  padding: "24px",
190
  };
191
 
192
  const sectionLabel = {
193
+ fontSize: "11px",
194
+ fontWeight: 700,
195
+ letterSpacing: "0.1em",
196
+ textTransform: "uppercase",
197
+ color: "rgba(255,255,255,0.6)",
198
+ marginBottom: "14px",
199
  };
200
 
201
  const chip = (color = accent) => ({
202
+ display: "inline-flex",
203
+ alignItems: "center",
204
+ gap: "6px",
205
+ padding: "3px 10px",
206
+ borderRadius: "999px",
207
+ background: `${color}33`,
208
+ border: `1px solid ${color}66`,
209
+ color,
210
+ fontSize: "11px",
211
+ fontWeight: 600,
212
+ letterSpacing: "0.04em",
213
  });
214
 
215
+ const dangerBtn = {
216
+ padding: "10px 18px",
217
+ borderRadius: "10px",
218
+ border: "1px solid rgba(239,68,68,0.45)",
219
+ background: "rgba(239,68,68,0.14)",
220
+ color: "#fca5a5",
221
+ fontSize: "13px",
222
+ fontWeight: 700,
223
+ cursor: "pointer",
224
+ transition: "all 0.2s ease",
225
+ letterSpacing: "0.02em",
226
+ };
227
+
228
+ // ── KPI Stats ──────────────────────────────────────────────────────────────
229
  const stats = [
230
  { label: "Questions Added", value: questionList.length, icon: "❓", color: accent },
231
  { label: "Uploaded Files", value: uploadedFile ? 1 : 0, icon: "πŸ“„", color: "#a78bfa" },
232
  { label: "Benchmark Runs", value: benchmarkRuns, icon: "πŸš€", color: green },
233
+ {
234
+ label: "Best Score",
235
+ value: bestScore !== null ? bestScore.toFixed(2) : "β€”",
236
+ icon: "πŸ†",
237
+ color: "#f59e0b",
238
+ },
239
  ];
240
 
241
  return (
242
+ <div
243
+ id="experiment-setup"
244
+ style={{
245
+ maxWidth: "1240px",
246
+ margin: "0 auto",
247
+ padding: "28px 24px",
248
+ fontFamily: "inherit",
249
+ }}
250
+ >
251
+ {/* ── Page Header ───────────────────────────────────────────────────── */}
252
+ <div
253
+ style={{
254
+ display: "flex",
255
+ alignItems: "flex-start",
256
+ justifyContent: "space-between",
257
+ flexWrap: "wrap",
258
+ gap: "12px",
259
+ marginBottom: "24px",
260
+ }}
261
+ >
262
  <div>
263
+ <div
264
+ style={{
265
+ display: "flex",
266
+ alignItems: "center",
267
+ gap: "12px",
268
+ marginBottom: "8px",
269
+ }}
270
+ >
271
  <span style={{ fontSize: "30px", lineHeight: 1 }}>πŸ§ͺ</span>
272
+ <h1
273
+ style={{
274
+ margin: 0,
275
+ fontSize: "34px",
276
+ fontWeight: 700,
277
+ letterSpacing: "-0.5px",
278
+ color: "white",
279
+ }}
280
+ >
281
  Experiment Setup
282
  </h1>
283
  </div>
284
+ <p
285
+ style={{
286
+ margin: 0,
287
+ fontSize: "13px",
288
+ color: "rgba(255,255,255,0.85)",
289
+ lineHeight: 1.6,
290
+ }}
291
+ >
292
+ Configure your RAG pipeline, upload a source document, and define
293
+ evaluation questions.
294
  </p>
295
  </div>
296
  <div style={chip(results ? green : accent)}>
297
+ <span
298
+ style={{
299
+ width: 7,
300
+ height: 7,
301
+ borderRadius: "50%",
302
+ background: results ? green : accent,
303
+ boxShadow: `0 0 6px ${results ? green : accent}`,
304
+ display: "inline-block",
305
+ }}
306
+ />
307
  {loading ? "Running…" : results ? "Complete" : "Ready"}
308
  </div>
309
  </div>
310
 
311
+ {/* ── KPI Row ───────────────────────────────────────────────────────── */}
312
+ <div
313
+ style={{
314
+ display: "grid",
315
+ gridTemplateColumns: "repeat(4, 1fr)",
316
+ gap: "12px",
317
+ marginBottom: "20px",
318
+ }}
319
+ >
320
  {stats.map(({ label, value, icon, color }) => (
321
+ <div
322
+ key={label}
323
+ style={{
324
+ ...card,
325
+ padding: "16px 20px",
326
+ display: "flex",
327
+ alignItems: "center",
328
+ gap: "14px",
329
+ }}
330
+ >
331
+ <div
332
+ style={{
333
+ width: 40,
334
+ height: 40,
335
+ borderRadius: "10px",
336
+ flexShrink: 0,
337
+ background: `${color}28`,
338
+ display: "flex",
339
+ alignItems: "center",
340
+ justifyContent: "center",
341
+ fontSize: "18px",
342
+ }}
343
+ >
344
+ {icon}
345
+ </div>
346
  <div>
347
+ <div
348
+ style={{
349
+ fontSize: "22px",
350
+ fontWeight: 700,
351
+ color: "white",
352
+ lineHeight: 1,
353
+ }}
354
+ >
355
+ {value}
356
+ </div>
357
+ <div
358
+ style={{
359
+ fontSize: "11px",
360
+ color: "rgba(255,255,255,0.55)",
361
+ marginTop: "3px",
362
+ }}
363
+ >
364
+ {label}
365
+ </div>
366
  </div>
367
  </div>
368
  ))}
369
  </div>
370
 
371
+ {/* ── Configuration Panel ───────────────────────────────────────────── */}
372
  <div style={{ ...card, marginBottom: "16px" }}>
373
  <p style={sectionLabel}>Pipeline Configuration</p>
374
  <div style={{ display: "flex", gap: "32px", flexWrap: "wrap" }}>
375
+ <ExperimentSelector
376
+ label="Active Model"
377
+ value={model}
378
+ onChange={setModel}
379
+ options={[
380
+ {
381
+ value: "llama-3.1-8b",
382
+ label: "Llama 3.1 8B",
383
+ description: "Fast & Efficient",
384
+ },
385
+ ]}
386
+ />
387
+ <ExperimentSelector
388
+ label="Retrieval Strategy"
389
+ value={retrievalMethod}
390
+ onChange={setRetrievalMethod}
391
  options={[
392
  { value: "hybrid", label: "Hybrid", description: "Vector + BM25" },
393
+ {
394
+ value: "vector",
395
+ label: "Vector",
396
+ description: "Embedding Search",
397
+ },
398
  { value: "lexical", label: "BM25", description: "Keyword Search" },
399
+ ]}
400
+ />
401
+ <ExperimentSelector
402
+ label="Reranker"
403
+ value={reranker}
404
+ onChange={setReranker}
405
+ options={[
406
+ {
407
+ value: "minilm",
408
+ label: "MiniLM",
409
+ description: "Fast balanced baseline",
410
+ },
411
+ ]}
412
+ />
413
+ <ExperimentSelector
414
+ label="Enhancements"
415
+ value={enhancement}
416
+ onChange={setEnhancement}
417
+ options={[
418
+ {
419
+ value: "default",
420
+ label: "Default",
421
+ description: "Standard pipeline",
422
+ },
423
+ ]}
424
+ />
425
  </div>
426
  </div>
427
 
428
+ {/* ── Main Grid ─────────────────────────────────────────────────────── */}
429
+ <div
430
+ style={{
431
+ display: "grid",
432
+ gridTemplateColumns: "420px 1fr",
433
+ gap: "16px",
434
+ alignItems: "start",
435
+ }}
436
+ >
437
+ {/* ── Left Column ─────────────────────────────────────────────── */}
438
  <div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
439
 
440
  {/* Upload Card */}
441
  <div
442
  onClick={() => fileInputRef.current?.click()}
443
+ onDragOver={(e) => {
444
+ e.preventDefault();
445
+ setIsDragging(true);
446
+ }}
447
  onDragLeave={() => setIsDragging(false)}
448
  onDrop={handleDrop}
449
  onMouseEnter={() => setIsHoveringUpload(true)}
 
451
  style={{
452
  borderRadius: "20px",
453
  minHeight: "240px",
454
+ display: "flex",
455
+ flexDirection: "column",
456
+ alignItems: "center",
457
+ justifyContent: "center",
458
+ gap: "14px",
459
  cursor: "pointer",
460
+ position: "relative",
461
+ overflow: "hidden",
462
  transition: "all 0.25s ease",
463
  background: isDragging
464
+ ? `rgba(79,110,247,0.12)`
465
  : uploadedFile
466
+ ? `rgba(34,197,94,0.08)`
467
+ : `rgba(255,255,255,0.05)`,
468
  border: `1.5px ${isDragging ? "solid" : "dashed"} ${
469
+ isDragging
470
+ ? accent
471
+ : uploadedFile
472
+ ? `${green}88`
473
+ : isHoveringUpload
474
+ ? "rgba(79,110,247,0.6)"
475
+ : "rgba(255,255,255,0.2)"
476
  }`,
477
  boxShadow: isDragging
478
  ? `0 0 32px rgba(79,110,247,0.2), inset 0 0 32px rgba(79,110,247,0.05)`
479
  : isHoveringUpload
480
  ? `0 8px 32px rgba(0,0,0,0.3), 0 0 20px rgba(79,110,247,0.1)`
481
  : uploadedFile
482
+ ? `0 0 20px rgba(34,197,94,0.12)`
483
  : "none",
484
+ transform:
485
+ isHoveringUpload && !isDragging ? "translateY(-2px)" : "none",
486
  }}
487
  >
488
+ <input
489
+ ref={fileInputRef}
490
+ type="file"
491
+ style={{ display: "none" }}
492
+ onChange={(e) => handleFileChange(e.target.files[0])}
493
+ />
494
 
495
  {/* Ambient glow blob */}
496
+ <div
497
+ style={{
498
+ position: "absolute",
499
+ width: "200px",
500
+ height: "200px",
501
+ borderRadius: "50%",
502
+ top: "50%",
503
+ left: "50%",
504
+ transform: "translate(-50%, -50%)",
505
+ background: uploadedFile
506
+ ? `radial-gradient(circle, rgba(34,197,94,0.1) 0%, transparent 70%)`
507
+ : `radial-gradient(circle, rgba(79,110,247,0.1) 0%, transparent 70%)`,
508
+ pointerEvents: "none",
509
+ }}
510
+ />
511
 
512
  {uploading ? (
513
+ <>
514
+ <div
515
+ style={{
516
+ width: 60,
517
+ height: 60,
518
+ borderRadius: "16px",
519
+ background: "rgba(79,110,247,0.18)",
520
+ display: "flex",
521
+ alignItems: "center",
522
+ justifyContent: "center",
523
+ fontSize: "28px",
524
+ boxShadow: "0 0 20px rgba(79,110,247,0.25)",
525
+ }}
526
+ >
527
+ ⏳
528
+ </div>
529
+ <div style={{ textAlign: "center", zIndex: 1 }}>
530
+ <p
531
+ style={{
532
+ margin: 0,
533
+ fontWeight: 700,
534
+ fontSize: "15px",
535
+ color: "#7b96ff",
536
+ }}
537
+ >
538
+ Uploading & Indexing...
539
+ </p>
540
+ <p
541
+ style={{
542
+ margin: "4px 0 0",
543
+ fontSize: "12px",
544
+ color: "rgba(255,255,255,0.5)",
545
+ }}
546
+ >
547
+ Processing document
548
+ </p>
549
+ </div>
550
+ </>
551
+ ) : uploadedFile ? (
552
+ <>
553
+ <div
554
+ style={{
555
+ width: 60,
556
+ height: 60,
557
+ borderRadius: "16px",
558
+ background: "rgba(34,197,94,0.18)",
559
+ display: "flex",
560
+ alignItems: "center",
561
+ justifyContent: "center",
562
+ fontSize: "28px",
563
+ boxShadow: "0 0 20px rgba(34,197,94,0.25)",
564
+ }}
565
+ >
566
+ βœ…
567
+ </div>
568
+ <div style={{ textAlign: "center", zIndex: 1 }}>
569
+ <p
570
+ style={{
571
+ margin: 0,
572
+ fontWeight: 700,
573
+ fontSize: "15px",
574
+ color: green,
575
+ }}
576
+ >
577
+ {uploadedFile.name}
578
+ </p>
579
+ <p
580
+ style={{
581
+ margin: "4px 0 0",
582
+ fontSize: "12px",
583
+ color: "rgba(255,255,255,0.5)",
584
+ }}
585
+ >
586
+ {(uploadedFile.size / 1024).toFixed(1)} KB Β· Click to replace
587
+ </p>
588
+ </div>
589
+ </>
590
+ ) : (
591
+ <>
592
+ <div
593
+ style={{
594
+ width: 64,
595
+ height: 64,
596
+ borderRadius: "18px",
597
+ background: isDragging
598
+ ? `rgba(79,110,247,0.25)`
599
+ : "rgba(79,110,247,0.14)",
600
+ display: "flex",
601
+ alignItems: "center",
602
+ justifyContent: "center",
603
+ fontSize: "28px",
604
+ zIndex: 1,
605
+ boxShadow: isDragging
606
+ ? `0 0 24px rgba(79,110,247,0.35)`
607
+ : "none",
608
+ transition: "all 0.2s ease",
609
+ }}
610
+ >
611
+ ⬆️
612
+ </div>
613
+ <div style={{ textAlign: "center", zIndex: 1 }}>
614
+ <p
615
+ style={{
616
+ margin: 0,
617
+ fontWeight: 700,
618
+ fontSize: "16px",
619
+ color: "white",
620
+ }}
621
+ >
622
+ {isDragging ? "Drop to upload" : "Upload Document"}
623
+ </p>
624
+ <p
625
+ style={{
626
+ margin: "5px 0 0",
627
+ fontSize: "12px",
628
+ color: "rgba(255,255,255,0.5)",
629
+ lineHeight: 1.6,
630
+ }}
631
+ >
632
+ Drag & drop or click to browse
633
+ </p>
634
+ </div>
635
+ </>
636
+ )}
 
 
 
 
 
 
 
 
 
 
637
  </div>
638
 
639
  {/* Benchmark Summary */}
640
  <div style={{ ...card, padding: "16px 20px" }}>
641
+ <p style={{ ...sectionLabel, marginBottom: "10px" }}>
642
+ Benchmark Summary
643
+ </p>
644
  <div style={{ display: "flex", flexDirection: "column", gap: "0px" }}>
645
  {[
646
  { label: "Model", value: labelMap[model] ?? model },
647
+ {
648
+ label: "Retrieval",
649
+ value: labelMap[retrievalMethod] ?? retrievalMethod,
650
+ },
651
  { label: "Reranker", value: labelMap[reranker] ?? reranker },
652
+ {
653
+ label: "Enhancement",
654
+ value: labelMap[enhancement] ?? enhancement,
655
+ },
656
+ {
657
+ label: "Questions",
658
+ value:
659
+ questionList.length === 0
660
+ ? "None"
661
+ : `${questionList.length}`,
662
+ },
663
  ].map(({ label, value }, i, arr) => (
664
+ <div
665
+ key={label}
666
+ style={{
667
+ display: "flex",
668
+ justifyContent: "space-between",
669
+ alignItems: "center",
670
+ padding: "7px 0",
671
+ borderBottom:
672
+ i < arr.length - 1
673
+ ? "1px solid rgba(255,255,255,0.07)"
674
+ : "none",
675
+ }}
676
+ >
677
+ <span
678
+ style={{ fontSize: "12px", color: "rgba(255,255,255,0.5)" }}
679
+ >
680
+ {label}
681
+ </span>
682
+ <span
683
+ style={{
684
+ fontSize: "12px",
685
+ fontWeight: 600,
686
+ color: "rgba(255,255,255,0.9)",
687
+ }}
688
+ >
689
+ {value}
690
+ </span>
691
  </div>
692
  ))}
693
  </div>
 
700
  onMouseEnter={() => setIsHoveringRun(true)}
701
  onMouseLeave={() => setIsHoveringRun(false)}
702
  style={{
703
+ width: "100%",
704
+ padding: "18px",
705
+ borderRadius: "16px",
706
+ border: "none",
707
  background: canRun
708
  ? `linear-gradient(135deg, ${accent} 0%, ${accentPurple} 100%)`
709
+ : "rgba(255,255,255,0.07)",
710
+ color: canRun ? "white" : "rgba(255,255,255,0.3)",
711
+ fontSize: "16px",
712
+ fontWeight: 700,
713
+ letterSpacing: "0.03em",
714
  cursor: canRun ? "pointer" : "not-allowed",
715
+ display: "flex",
716
+ alignItems: "center",
717
+ justifyContent: "center",
718
+ gap: "10px",
719
  boxShadow: canRun
720
  ? isHoveringRun
721
  ? `0 0 40px rgba(79,110,247,0.6), 0 8px 24px rgba(0,0,0,0.4)`
 
727
  >
728
  {loading ? (
729
  <>
730
+ <span
731
+ style={{
732
+ width: 15,
733
+ height: 15,
734
+ border: "2px solid rgba(255,255,255,0.3)",
735
+ borderTop: "2px solid white",
736
+ borderRadius: "50%",
737
+ display: "inline-block",
738
+ animation: "spin 0.7s linear infinite",
739
+ }}
740
+ />
741
  Running Benchmark…
742
  </>
743
+ ) : (
744
+ <>β–Ά Run Benchmark</>
745
+ )}
746
  </button>
747
 
748
  <style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
749
 
750
  {!canRun && !loading && (
751
+ <p
752
+ style={{
753
+ margin: "-4px 0 0",
754
+ fontSize: "12px",
755
+ color: "rgba(255,255,255,0.4)",
756
+ textAlign: "center",
757
+ }}
758
+ >
759
  Add at least one question to run
760
  </p>
761
  )}
762
  </div>
763
 
764
+ {/* ── Right Column: Questions ───────────────────────────────────── */}
765
+ <div
766
+ style={{
767
+ ...card,
768
+ display: "flex",
769
+ flexDirection: "column",
770
+ gap: "14px",
771
+ }}
772
+ >
773
  {/* Questions header */}
774
+ <div
775
+ style={{
776
+ display: "flex",
777
+ justifyContent: "space-between",
778
+ alignItems: "flex-start",
779
+ }}
780
+ >
781
  <div>
782
+ <p
783
+ style={{
784
+ ...sectionLabel,
785
+ marginBottom: "3px",
786
+ color: "rgba(255,255,255,0.85)",
787
+ }}
788
+ >
789
+ Evaluation Questions
790
+ </p>
791
+ <p
792
+ style={{
793
+ margin: 0,
794
+ fontSize: "12px",
795
+ color: "rgba(255,255,255,0.6)",
796
+ }}
797
+ >
798
+ One question per line
799
+ </p>
800
  </div>
801
+ <div
802
+ style={{ display: "flex", gap: "8px", alignItems: "center" }}
803
+ >
804
  {questionList.length > 0 && (
805
  <>
806
+ <div style={chip(green)}>
807
+ βœ“ {questionList.length} question
808
+ {questionList.length !== 1 ? "s" : ""}
809
+ </div>
810
  <button
811
  onClick={() => setQuestions("")}
812
  style={{
813
+ background: "rgba(239,68,68,0.12)",
814
+ border: "1px solid rgba(239,68,68,0.3)",
815
+ borderRadius: "8px",
816
+ color: "#fca5a5",
817
+ fontSize: "12px",
818
+ fontWeight: 600,
819
+ padding: "4px 10px",
820
+ cursor: "pointer",
821
  }}
822
+ >
823
+ Clear
824
+ </button>
825
  </>
826
  )}
827
+ <span
828
+ style={{
829
+ fontSize: "11px",
830
+ color: "rgba(255,255,255,0.35)",
831
+ }}
832
+ >
833
+ {questions.length} chars
834
+ </span>
835
  </div>
836
  </div>
837
 
838
+ <div
839
+ style={{
840
+ position: "relative",
841
+ display: "flex",
842
+ borderRadius: "14px",
843
+ overflow: "hidden",
844
+ border: "1px solid rgba(255,255,255,0.12)",
845
+ }}
846
+ >
847
  {/* Line numbers */}
848
+ <div
849
+ style={{
850
+ background: "rgba(0,0,0,0.25)",
851
+ padding: "18px 10px",
852
+ display: "flex",
853
+ flexDirection: "column",
854
+ alignItems: "flex-end",
855
+ gap: 0,
856
+ userSelect: "none",
857
+ minWidth: "36px",
858
+ borderRight: "1px solid rgba(255,255,255,0.07)",
859
+ }}
860
+ >
861
  {(questions || " ").split("\n").map((_, i) => (
862
+ <div
863
+ key={i}
864
+ style={{
865
+ fontSize: "12px",
866
+ color: "rgba(255,255,255,0.3)",
867
+ lineHeight: "1.8",
868
+ fontFamily: "'Courier New', monospace",
869
+ height: "21.6px",
870
+ }}
871
+ >
872
  {i + 1}
873
  </div>
874
  ))}
 
879
  value={questions}
880
  onChange={(e) => setQuestions(e.target.value)}
881
  style={{
882
+ flex: 1,
883
+ minHeight: "300px",
884
+ padding: "18px 16px",
885
+ border: "none",
886
+ background: "rgba(0,0,0,0.2)",
887
+ color: "white",
888
+ fontSize: "13px",
889
+ resize: "vertical",
890
+ lineHeight: "1.8",
891
+ fontFamily: "'Courier New', Courier, monospace",
892
+ outline: "none",
893
+ boxSizing: "border-box",
894
  }}
895
  />
896
  </div>
 
898
  </div>
899
 
900
  {error && (
901
+ <div
902
+ style={{
903
+ marginTop: "14px",
904
+ padding: "13px 18px",
905
+ borderRadius: "12px",
906
+ background: "rgba(239,68,68,0.1)",
907
+ border: "1px solid rgba(239,68,68,0.3)",
908
+ color: "#fca5a5",
909
+ fontSize: "13px",
910
+ }}
911
+ >
912
+ ⚠️ {error}
913
+ </div>
914
  )}
915
 
916
+ {/* ── Benchmark History ─────────────────────────────────────────────── */}
917
+ <div style={{ ...card, marginTop: "16px" }}>
918
+ <p style={sectionLabel}>Benchmark History</p>
919
+
920
+ {allRuns.length === 0 ? (
921
+ <p
922
+ style={{
923
+ margin: "0 0 16px",
924
+ fontSize: "13px",
925
+ color: "rgba(255,255,255,0.5)",
926
+ }}
927
+ >
928
+ No benchmark runs yet. Run your first benchmark above.
929
+ </p>
930
+ ) : (
931
+ <div
932
+ style={{
933
+ display: "flex",
934
+ alignItems: "center",
935
+ gap: "10px",
936
+ flexWrap: "wrap",
937
+ marginBottom: "16px",
938
+ }}
939
+ >
940
+ {/* Run selector dropdown */}
941
+ <select
942
+ value={selectedRun?.id ?? ""}
943
+ onChange={(e) =>
944
+ setSelectedRun(
945
+ allRuns.find((r) => String(r.id) === e.target.value) ?? null
946
+ )
947
+ }
948
+ style={{
949
+ flex: "1 1 260px",
950
+ padding: "10px 14px",
951
+ borderRadius: "10px",
952
+ background: "rgba(255,255,255,0.08)",
953
+ border: "1px solid rgba(255,255,255,0.2)",
954
+ color: "white",
955
+ fontSize: "13px",
956
+ fontWeight: 600,
957
+ outline: "none",
958
+ cursor: "pointer",
959
+ appearance: "auto",
960
+ }}
961
+ >
962
+ {[...allRuns]
963
+ .sort(
964
+ (a, b) =>
965
+ new Date(b.created_at) - new Date(a.created_at)
966
+ )
967
+ .map((run) => (
968
+ <option
969
+ key={run.id}
970
+ value={run.id}
971
+ style={{ background: "#1a1a2e", color: "white" }}
972
+ >
973
+ {run.name}
974
+ </option>
975
+ ))}
976
+ </select>
977
+
978
+ {/* Delete selected run */}
979
+ <button
980
+ onClick={handleDeleteRun}
981
+ disabled={!selectedRun}
982
+ title="Delete selected run"
983
+ style={{
984
+ ...dangerBtn,
985
+ opacity: selectedRun ? 1 : 0.4,
986
+ cursor: selectedRun ? "pointer" : "not-allowed",
987
+ }}
988
+ >
989
+ ❌ Delete Selected
990
+ </button>
991
+ </div>
992
+ )}
993
+
994
+ {/* Destructive actions row */}
995
+ <div
996
+ style={{
997
+ display: "flex",
998
+ gap: "10px",
999
+ flexWrap: "wrap",
1000
+ paddingTop: allRuns.length > 0 ? "14px" : "0",
1001
+ borderTop:
1002
+ allRuns.length > 0
1003
+ ? "1px solid rgba(255,255,255,0.08)"
1004
+ : "none",
1005
+ }}
1006
+ >
1007
+ <button onClick={handleResetRuns} style={dangerBtn}>
1008
+ πŸ—‘ Delete All Benchmarks
1009
+ </button>
1010
+ <button onClick={handleResetDocuments} style={dangerBtn}>
1011
+ πŸ“‚ Delete Documents &amp; Vector Store
1012
+ </button>
1013
+ </div>
1014
+ </div>
1015
+
1016
  <div style={{ marginTop: "24px" }}>
1017
  <Leaderboards
1018
+ leaderboard={
1019
+ results?.leaderboard || {
1020
+ overall: [],
1021
+ faithfulness: [],
1022
+ grounding: [],
1023
+ retrieval_quality: [],
1024
+ query_coverage: [],
1025
+ latency: [],
1026
+ }
1027
+ }
1028
  />
1029
  </div>
1030
  </div>