Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>GraphRAG Pipeline Comparison Dashboard</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:ital,wght@0,300;0,400;0,500;0,600;1,300&display=swap" rel="stylesheet" /> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --bg: #0a0c0f; | |
| --surface: #111418; | |
| --surface2: #181c22; | |
| --border: rgba(255,255,255,0.08); | |
| --border2: rgba(255,255,255,0.14); | |
| --text: #e8eaed; | |
| --muted: #7a8290; | |
| --accent-teal: #00e5b0; | |
| --accent-blue: #4d9fff; | |
| --accent-amber: #ffb347; | |
| --pipe1: #6b7280; | |
| --pipe2: #4d9fff; | |
| --pipe3: #00e5b0; | |
| --font-mono: 'Space Mono', monospace; | |
| --font-body: 'DM Sans', sans-serif; | |
| --radius: 10px; | |
| --radius-lg: 16px; | |
| } | |
| body { | |
| font-family: var(--font-body); | |
| background: var(--bg); | |
| color: var(--text); | |
| min-height: 100vh; | |
| padding: 32px 24px; | |
| } | |
| /* ---- Header ---- */ | |
| .header { | |
| display: flex; | |
| align-items: flex-start; | |
| justify-content: space-between; | |
| margin-bottom: 32px; | |
| gap: 16px; | |
| flex-wrap: wrap; | |
| } | |
| .header-left {} | |
| .wordmark { | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| letter-spacing: 0.15em; | |
| color: var(--muted); | |
| text-transform: uppercase; | |
| margin-bottom: 6px; | |
| } | |
| .title { | |
| font-size: 26px; | |
| font-weight: 600; | |
| letter-spacing: -0.5px; | |
| line-height: 1.2; | |
| } | |
| .title span { color: var(--accent-teal); } | |
| .subtitle { | |
| font-size: 13px; | |
| color: var(--muted); | |
| margin-top: 4px; | |
| font-weight: 300; | |
| } | |
| .tg-badge { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| background: var(--surface); | |
| border: 1px solid var(--border2); | |
| border-radius: var(--radius); | |
| padding: 8px 14px; | |
| font-size: 12px; | |
| color: var(--muted); | |
| align-self: flex-start; | |
| } | |
| .tg-dot { width: 8px; height: 8px; border-radius: 50%; background: #00e5b0; box-shadow: 0 0 6px #00e5b0; animation: pulse 2s infinite; } | |
| @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} } | |
| /* ---- Config Panel ---- */ | |
| .config { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-lg); | |
| padding: 20px 24px; | |
| margin-bottom: 24px; | |
| } | |
| .config-title { | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| letter-spacing: 0.12em; | |
| color: var(--muted); | |
| text-transform: uppercase; | |
| margin-bottom: 16px; | |
| } | |
| .config-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr 1fr; | |
| gap: 12px; | |
| } | |
| @media (max-width: 700px) { .config-grid { grid-template-columns: 1fr; } } | |
| .field label { | |
| display: block; | |
| font-size: 11px; | |
| color: var(--muted); | |
| margin-bottom: 5px; | |
| font-family: var(--font-mono); | |
| letter-spacing: 0.05em; | |
| } | |
| .field input, .field select { | |
| width: 100%; | |
| background: var(--surface2); | |
| border: 1px solid var(--border2); | |
| border-radius: var(--radius); | |
| color: var(--text); | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| padding: 9px 12px; | |
| outline: none; | |
| transition: border-color 0.15s; | |
| } | |
| .field input:focus, .field select:focus { border-color: var(--accent-teal); } | |
| .field input[type="password"] { letter-spacing: 0.05em; } | |
| .field select option { background: #1a1e24; } | |
| /* ---- Query Bar ---- */ | |
| .query-bar { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 24px; | |
| align-items: stretch; | |
| } | |
| .query-bar textarea { | |
| flex: 1; | |
| background: var(--surface); | |
| border: 1px solid var(--border2); | |
| border-radius: var(--radius-lg); | |
| color: var(--text); | |
| font-family: var(--font-body); | |
| font-size: 15px; | |
| padding: 14px 18px; | |
| resize: none; | |
| outline: none; | |
| min-height: 60px; | |
| line-height: 1.5; | |
| transition: border-color 0.15s; | |
| } | |
| .query-bar textarea:focus { border-color: var(--accent-teal); } | |
| .query-bar textarea::placeholder { color: var(--muted); } | |
| .run-btn { | |
| background: var(--accent-teal); | |
| color: #0a0c0f; | |
| border: none; | |
| border-radius: var(--radius-lg); | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| font-weight: 700; | |
| letter-spacing: 0.05em; | |
| padding: 0 24px; | |
| cursor: pointer; | |
| white-space: nowrap; | |
| transition: opacity 0.15s, transform 0.1s; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .run-btn:hover { opacity: 0.85; } | |
| .run-btn:active { transform: scale(0.97); } | |
| .run-btn:disabled { opacity: 0.4; cursor: not-allowed; } | |
| .run-btn svg { width: 16px; height: 16px; } | |
| /* ---- Pipelines grid ---- */ | |
| .pipelines { | |
| display: grid; | |
| grid-template-columns: repeat(3, minmax(0, 1fr)); | |
| gap: 14px; | |
| margin-bottom: 24px; | |
| } | |
| @media (max-width: 900px) { .pipelines { grid-template-columns: 1fr; } } | |
| .pipe-card { | |
| background: var(--surface); | |
| border-radius: var(--radius-lg); | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| border: 1px solid var(--border); | |
| transition: border-color 0.2s; | |
| } | |
| .pipe-card.active { border-color: var(--pipe-color, var(--border2)); } | |
| .pipe-head { | |
| padding: 14px 18px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .pipe-icon { | |
| width: 32px; height: 32px; border-radius: 8px; | |
| display: flex; align-items: center; justify-content: center; | |
| font-family: var(--font-mono); font-size: 11px; font-weight: 700; | |
| flex-shrink: 0; | |
| } | |
| .p1 .pipe-icon { background: rgba(107,114,128,0.15); color: #9ca3af; } | |
| .p2 .pipe-icon { background: rgba(77,159,255,0.12); color: var(--accent-blue); } | |
| .p3 .pipe-icon { background: rgba(0,229,176,0.1); color: var(--accent-teal); } | |
| .pipe-meta { flex: 1; min-width: 0; } | |
| .pipe-name { font-weight: 600; font-size: 14px; } | |
| .pipe-sub { font-size: 11px; color: var(--muted); font-weight: 300; margin-top: 1px; } | |
| .status-chip { | |
| font-family: var(--font-mono); | |
| font-size: 9px; | |
| letter-spacing: 0.1em; | |
| padding: 3px 8px; | |
| border-radius: 20px; | |
| text-transform: uppercase; | |
| } | |
| .chip-idle { background: rgba(255,255,255,0.05); color: var(--muted); } | |
| .chip-running { background: rgba(255,179,71,0.1); color: var(--accent-amber); } | |
| .chip-done { background: rgba(0,229,176,0.1); color: var(--accent-teal); } | |
| .chip-err { background: rgba(255,80,80,0.1); color: #ff6060; } | |
| .pipe-body { padding: 16px 18px; flex: 1; display: flex; flex-direction: column; gap: 14px; } | |
| .answer-box { | |
| background: var(--surface2); | |
| border-radius: var(--radius); | |
| padding: 14px; | |
| font-size: 13px; | |
| line-height: 1.65; | |
| color: #c8cdd6; | |
| min-height: 120px; | |
| font-weight: 300; | |
| position: relative; | |
| } | |
| .placeholder { color: var(--muted); font-style: italic; } | |
| .thinking { display: flex; gap: 5px; align-items: center; } | |
| .thinking span { | |
| width: 5px; height: 5px; border-radius: 50%; | |
| animation: blink 1.3s infinite; | |
| } | |
| .p1 .thinking span { background: #6b7280; } | |
| .p2 .thinking span { background: var(--accent-blue); } | |
| .p3 .thinking span { background: var(--accent-teal); } | |
| .thinking span:nth-child(2) { animation-delay: 0.2s; } | |
| .thinking span:nth-child(3) { animation-delay: 0.4s; } | |
| @keyframes blink { 0%,80%,100%{opacity:0.15} 40%{opacity:1} } | |
| .metrics-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 8px; | |
| } | |
| .metric-tile { | |
| background: var(--surface2); | |
| border-radius: var(--radius); | |
| padding: 10px 12px; | |
| } | |
| .metric-tile .mlabel { | |
| font-family: var(--font-mono); | |
| font-size: 9px; | |
| letter-spacing: 0.1em; | |
| color: var(--muted); | |
| text-transform: uppercase; | |
| margin-bottom: 4px; | |
| } | |
| .metric-tile .mval { | |
| font-family: var(--font-mono); | |
| font-size: 16px; | |
| font-weight: 700; | |
| color: var(--text); | |
| } | |
| .mval.good { color: var(--accent-teal); } | |
| .mval.warn { color: var(--accent-amber); } | |
| .mval.neutral { color: var(--muted); } | |
| .eval-row { display: flex; gap: 8px; } | |
| .eval-pill { | |
| flex: 1; | |
| border-radius: var(--radius); | |
| padding: 8px 10px; | |
| text-align: center; | |
| } | |
| .eval-pill .elabel { | |
| font-size: 9px; | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| font-family: var(--font-mono); | |
| margin-bottom: 3px; | |
| opacity: 0.6; | |
| } | |
| .eval-pill .eval-val { | |
| font-family: var(--font-mono); | |
| font-weight: 700; | |
| font-size: 13px; | |
| } | |
| .pill-pass { background: rgba(0,229,176,0.08); color: var(--accent-teal); border: 1px solid rgba(0,229,176,0.2); } | |
| .pill-fail { background: rgba(255,80,80,0.08); color: #ff6060; border: 1px solid rgba(255,80,80,0.2); } | |
| .pill-neutral { background: var(--surface2); color: var(--muted); border: 1px solid var(--border); } | |
| /* ---- Summary bar ---- */ | |
| .summary { | |
| display: grid; | |
| grid-template-columns: repeat(4, minmax(0, 1fr)); | |
| gap: 12px; | |
| margin-bottom: 24px; | |
| } | |
| @media (max-width: 700px) { .summary { grid-template-columns: 1fr 1fr; } } | |
| .sum-card { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-lg); | |
| padding: 16px 18px; | |
| } | |
| .sum-card.highlight { border-color: rgba(0,229,176,0.3); } | |
| .sum-card .slabel { | |
| font-family: var(--font-mono); | |
| font-size: 9px; | |
| letter-spacing: 0.1em; | |
| color: var(--muted); | |
| text-transform: uppercase; | |
| margin-bottom: 6px; | |
| } | |
| .sum-card .sval { | |
| font-family: var(--font-mono); | |
| font-size: 22px; | |
| font-weight: 700; | |
| color: var(--text); | |
| } | |
| .sum-card.highlight .sval { color: var(--accent-teal); } | |
| .sum-card .ssub { font-size: 11px; color: var(--muted); margin-top: 3px; font-weight: 300; } | |
| /* ---- Error / info banners ---- */ | |
| .banner { | |
| padding: 12px 16px; | |
| border-radius: var(--radius); | |
| font-size: 13px; | |
| margin-bottom: 16px; | |
| display: none; | |
| } | |
| .banner.visible { display: block; } | |
| .banner-err { background: rgba(255,80,80,0.08); border: 1px solid rgba(255,80,80,0.2); color: #ff8080; } | |
| .banner-info { background: rgba(77,159,255,0.08); border: 1px solid rgba(77,159,255,0.2); color: #7db8ff; } | |
| /* ---- Log ---- */ | |
| .log-section { margin-top: 8px; } | |
| .log-toggle { | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| letter-spacing: 0.1em; | |
| color: var(--muted); | |
| cursor: pointer; | |
| text-transform: uppercase; | |
| background: none; | |
| border: none; | |
| padding: 0; | |
| margin-bottom: 8px; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .log-toggle:hover { color: var(--text); } | |
| .log-box { | |
| background: var(--surface2); | |
| border-radius: var(--radius); | |
| padding: 12px 14px; | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| color: var(--muted); | |
| max-height: 180px; | |
| overflow-y: auto; | |
| line-height: 1.7; | |
| display: none; | |
| } | |
| .log-box.open { display: block; } | |
| .log-line.ok { color: var(--accent-teal); } | |
| .log-line.warn { color: var(--accent-amber); } | |
| .log-line.err { color: #ff6060; } | |
| hr.divider { border: none; border-top: 1px solid var(--border); margin: 24px 0; } | |
| /* ---- Setup guide ---- */ | |
| .setup { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-lg); | |
| padding: 20px 24px; | |
| } | |
| .setup-title { | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| letter-spacing: 0.12em; | |
| text-transform: uppercase; | |
| color: var(--muted); | |
| margin-bottom: 16px; | |
| } | |
| .steps { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 10px; | |
| } | |
| .step { | |
| display: flex; | |
| gap: 10px; | |
| align-items: flex-start; | |
| } | |
| .step-num { | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| color: var(--accent-teal); | |
| background: rgba(0,229,176,0.08); | |
| border-radius: 6px; | |
| width: 24px; height: 24px; | |
| display: flex; align-items: center; justify-content: center; | |
| flex-shrink: 0; | |
| margin-top: 1px; | |
| } | |
| .step-text { font-size: 12px; color: var(--muted); line-height: 1.5; } | |
| .step-text strong { color: var(--text); font-weight: 500; } | |
| code { | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| background: var(--surface2); | |
| border-radius: 4px; | |
| padding: 1px 5px; | |
| color: var(--accent-teal); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="header"> | |
| <div class="header-left"> | |
| <div class="wordmark">GraphRAG Β· Scale Edition</div> | |
| <div class="title">Financial <span>Intelligence</span></div> | |
| <div class="subtitle">100M+ Token Source Data β 692 Node Knowledge Graph: LLM-Only vs Basic RAG vs TigerGraph GraphRAG</div> | |
| </div> | |
| <div class="tg-badge"> | |
| <span class="tg-dot"></span> | |
| TigerGraph Cloud (Savanna) | |
| </div> | |
| </div> | |
| <!-- Config --> | |
| <div class="config"> | |
| <div class="config-title">β TigerGraph Cloud Config</div> | |
| <div class="config-grid"> | |
| <div class="field"> | |
| <label>Cluster Host</label> | |
| <input type="text" id="tg-host" placeholder="https://your-cluster.i.tgcloud.io" value="https://tg-8a493b67-4330-406c-81c2-d1398562a1ec.tg-3452941248.i.tgcloud.io" /> | |
| </div> | |
| <div class="field"> | |
| <label>Graph Name</label> | |
| <input type="text" id="tg-graph" placeholder="TIgerrag" value="TIgerrag" /> | |
| </div> | |
| <div class="field"> | |
| <label>RESTPP Token</label> | |
| <input type="password" id="tg-token" placeholder="Bearer token from Admin Portal" /> | |
| </div> | |
| <div class="field"> | |
| <label>GraphRAG Query Name</label> | |
| <input type="text" id="tg-query" placeholder="get_company_context" value="get_company_context" /> | |
| </div> | |
| <div class="field"> | |
| <label>LLM Provider</label> | |
| <select id="llm-provider" disabled> | |
| <option value="gemini">Google (Gemini 2.5 Flash)</option> | |
| </select> | |
| </div> | |
| <div class="field"> | |
| <label>LLM API Key</label> | |
| <input type="password" id="llm-key" placeholder="sk-..." /> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Query bar --> | |
| <div class="query-bar"> | |
| <textarea id="query-input" placeholder="e.g. What subsidiaries does this company own, and what are their primary market risks? β all three pipelines will answer simultaneouslyβ¦" rows="2"></textarea> | |
| <button class="run-btn" id="run-btn" onclick="runAll()"> | |
| <svg viewBox="0 0 16 16" fill="currentColor"><path d="M3 2.5l10 5.5-10 5.5V2.5z"/></svg> | |
| RUN | |
| </button> | |
| </div> | |
| <div class="banner banner-err" id="banner-err"></div> | |
| <div class="banner banner-info" id="banner-info"></div> | |
| <!-- Pipelines --> | |
| <div class="pipelines"> | |
| <!-- Pipeline 1 --> | |
| <div class="pipe-card p1" id="card1"> | |
| <div class="pipe-head"> | |
| <div class="pipe-icon">P1</div> | |
| <div class="pipe-meta"> | |
| <div class="pipe-name">LLM-Only</div> | |
| <div class="pipe-sub">No retrieval Β· baseline</div> | |
| </div> | |
| <span class="status-chip chip-idle" id="status1">IDLE</span> | |
| </div> | |
| <div class="pipe-body"> | |
| <div class="answer-box" id="ans1"><span class="placeholder">Response will appear hereβ¦</span></div> | |
| <div class="metrics-grid"> | |
| <div class="metric-tile"><div class="mlabel">Tokens</div><div class="mval neutral" id="tok1">β</div></div> | |
| <div class="metric-tile"><div class="mlabel">Latency</div><div class="mval neutral" id="lat1">β</div></div> | |
| <div class="metric-tile"><div class="mlabel">Cost/query</div><div class="mval neutral" id="cost1">β</div></div> | |
| <div class="metric-tile"><div class="mlabel">Groundedness F1</div><div class="mval neutral" id="bert1">β</div></div> | |
| </div> | |
| <div class="eval-row"> | |
| <div class="eval-pill pill-neutral" id="judge1"><div class="elabel">LLM Judge</div><div class="eval-val">β</div></div> | |
| <div class="eval-pill pill-neutral"><div class="elabel">Retrieval</div><div class="eval-val">None</div></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Pipeline 2 --> | |
| <div class="pipe-card p2" id="card2"> | |
| <div class="pipe-head"> | |
| <div class="pipe-icon">P2</div> | |
| <div class="pipe-meta"> | |
| <div class="pipe-name">Basic RAG</div> | |
| <div class="pipe-sub">Vector embeddings Β· baseline</div> | |
| </div> | |
| <span class="status-chip chip-idle" id="status2">IDLE</span> | |
| </div> | |
| <div class="pipe-body"> | |
| <div class="answer-box" id="ans2"><span class="placeholder">Response will appear hereβ¦</span></div> | |
| <div class="metrics-grid"> | |
| <div class="metric-tile"><div class="mlabel">Tokens</div><div class="mval neutral" id="tok2">β</div></div> | |
| <div class="metric-tile"><div class="mlabel">Latency</div><div class="mval neutral" id="lat2">β</div></div> | |
| <div class="metric-tile"><div class="mlabel">Cost/query</div><div class="mval neutral" id="cost2">β</div></div> | |
| <div class="metric-tile"><div class="mlabel">Groundedness F1</div><div class="mval neutral" id="bert2">β</div></div> | |
| </div> | |
| <div class="eval-row"> | |
| <div class="eval-pill pill-neutral" id="judge2"><div class="elabel">LLM Judge</div><div class="eval-val">β</div></div> | |
| <div class="eval-pill pill-neutral"><div class="elabel">Retrieval</div><div class="eval-val">Vector</div></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Pipeline 3 --> | |
| <div class="pipe-card p3" id="card3"> | |
| <div class="pipe-head"> | |
| <div class="pipe-icon">P3</div> | |
| <div class="pipe-meta"> | |
| <div class="pipe-name">GraphRAG</div> | |
| <div class="pipe-sub">TigerGraph Cloud Β· multi-hop</div> | |
| </div> | |
| <span class="status-chip chip-idle" id="status3">IDLE</span> | |
| </div> | |
| <div class="pipe-body"> | |
| <div class="answer-box" id="ans3"><span class="placeholder">Response will appear hereβ¦</span></div> | |
| <div class="metrics-grid"> | |
| <div class="metric-tile"><div class="mlabel">Tokens</div><div class="mval neutral" id="tok3">β</div></div> | |
| <div class="metric-tile"><div class="mlabel">Latency</div><div class="mval neutral" id="lat3">β</div></div> | |
| <div class="metric-tile"><div class="mlabel">Cost/query</div><div class="mval neutral" id="cost3">β</div></div> | |
| <div class="metric-tile"><div class="mlabel">Groundedness F1</div><div class="mval neutral" id="bert3">β</div></div> | |
| </div> | |
| <div class="eval-row"> | |
| <div class="eval-pill pill-neutral" id="judge3"><div class="elabel">LLM Judge</div><div class="eval-val">β</div></div> | |
| <div class="eval-pill pill-neutral" id="hops3"><div class="elabel">Retrieval</div><div class="eval-val">Graph</div></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Summary --> | |
| <div class="summary" id="summary" style="display:none;"> | |
| <div class="sum-card"> | |
| <div class="slabel">Baseline tokens (RAG)</div> | |
| <div class="sval" id="sum-baseline">β</div> | |
| <div class="ssub" id="sum-baseline-sub">β</div> | |
| </div> | |
| <div class="sum-card highlight"> | |
| <div class="slabel">Token reduction</div> | |
| <div class="sval" id="sum-reduction">β</div> | |
| <div class="ssub" id="sum-reduction-sub">vs Basic RAG</div> | |
| </div> | |
| <div class="sum-card"> | |
| <div class="slabel">Cost saving / query</div> | |
| <div class="sval" id="sum-saving">β</div> | |
| <div class="ssub" id="sum-saving-sub">GraphRAG vs RAG</div> | |
| </div> | |
| <div class="sum-card"> | |
| <div class="slabel">Groundedness delta</div> | |
| <div class="sval" id="sum-accuracy">β</div> | |
| <div class="ssub" id="sum-accuracy-sub">Context Faithfulness F1</div> | |
| </div> | |
| </div> | |
| <!-- Log --> | |
| <div class="log-section"> | |
| <button class="log-toggle" onclick="toggleLog()">βΆ Request log</button> | |
| <div class="log-box" id="log-box"></div> | |
| </div> | |
| <hr class="divider" /> | |
| <!-- Setup guide --> | |
| <div class="setup"> | |
| <div class="setup-title">β‘ TigerGraph Cloud Setup (from your guide)</div> | |
| <div class="steps"> | |
| <div class="step"> | |
| <div class="step-num">1</div> | |
| <div class="step-text"><strong>Create account</strong> at <code>tgcloud.io</code> via email or GitHub, verify, log into dashboard.</div> | |
| </div> | |
| <div class="step"> | |
| <div class="step-num">2</div> | |
| <div class="step-text"><strong>Create a cluster</strong> β click "Create Cluster", choose Free Tier, pick your region, name it.</div> | |
| </div> | |
| <div class="step"> | |
| <div class="step-num">3</div> | |
| <div class="step-text"><strong>Open GraphStudio</strong> β Tools β GraphStudio. Design your schema (vertices + edges).</div> | |
| </div> | |
| <div class="step"> | |
| <div class="step-num">4</div> | |
| <div class="step-text"><strong>Load data</strong> via CSV upload + Loading Jobs, or REST / pyTigerGraph.</div> | |
| </div> | |
| <div class="step"> | |
| <div class="step-num">5</div> | |
| <div class="step-text"><strong>Write GSQL query</strong>, install it β this generates a REST endpoint automatically.</div> | |
| </div> | |
| <div class="step"> | |
| <div class="step-num">6</div> | |
| <div class="step-text"><strong>Generate RESTPP token</strong> in Admin Portal β User Management β paste it above.</div> | |
| </div> | |
| <div class="step"> | |
| <div class="step-num">7</div> | |
| <div class="step-text">Endpoint format: <code>https://<cluster>.i.tgcloud.io/query/<GRAPH>/<QUERY></code></div> | |
| </div> | |
| <div class="step"> | |
| <div class="step-num">8</div> | |
| <div class="step-text"><strong>Test in Postman</strong> with GET + <code>Authorization: Bearer YOUR_TOKEN</code> before wiring here.</div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // βββ Pricing table (per 1K tokens, USD) βββββββββββββββββββββββββββββββββββββββ | |
| const PRICING = { | |
| anthropic: { input: 0.003, output: 0.015 }, // Claude Sonnet | |
| openai: { input: 0.005, output: 0.015 }, // GPT-4o | |
| gemini: { input: 0.00025, output: 0.0005 } // Gemini 1.5 Flash | |
| }; | |
| // βββ State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let logOpen = false; | |
| const logs = []; | |
| function log(msg, type = '') { | |
| const ts = new Date().toISOString().slice(11, 23); | |
| logs.push({ ts, msg, type }); | |
| const box = document.getElementById('log-box'); | |
| const line = document.createElement('div'); | |
| line.className = 'log-line ' + type; | |
| line.textContent = `[${ts}] ${msg}`; | |
| box.appendChild(line); | |
| box.scrollTop = box.scrollHeight; | |
| } | |
| function toggleLog() { | |
| logOpen = !logOpen; | |
| document.getElementById('log-box').className = 'log-box' + (logOpen ? ' open' : ''); | |
| document.querySelector('.log-toggle').textContent = (logOpen ? 'βΌ' : 'βΆ') + ' Request log'; | |
| } | |
| function setStatus(n, state) { | |
| const el = document.getElementById('status' + n); | |
| el.className = 'status-chip'; | |
| if (state === 'running') { el.classList.add('chip-running'); el.textContent = 'RUNNING'; } | |
| else if (state === 'done') { el.classList.add('chip-done'); el.textContent = 'DONE'; } | |
| else if (state === 'error') { el.classList.add('chip-err'); el.textContent = 'ERROR'; } | |
| else { el.classList.add('chip-idle'); el.textContent = 'IDLE'; } | |
| } | |
| function showThinking(n, pipe) { | |
| document.getElementById('ans' + n).innerHTML = | |
| `<div class="thinking"><span></span><span></span><span></span></div>`; | |
| setStatus(n, 'running'); | |
| ['tok','lat','cost','bert'].forEach(k => { | |
| const el = document.getElementById(k + n); | |
| el.textContent = 'β¦'; | |
| el.className = 'mval neutral'; | |
| }); | |
| document.getElementById('judge' + n).innerHTML = | |
| `<div class="elabel">LLM Judge</div><div class="eval-val">β¦</div>`; | |
| document.getElementById('judge' + n).className = 'eval-pill pill-neutral'; | |
| } | |
| function renderAnswer(n, text, tokens, latency, bert, pass, provider) { | |
| document.getElementById('ans' + n).textContent = text; | |
| setStatus(n, 'done'); | |
| const price = PRICING[provider] || PRICING.anthropic; | |
| const inputTok = Math.round(tokens * 0.7); | |
| const outputTok = Math.round(tokens * 0.3); | |
| const cost = ((inputTok / 1000) * price.input + (outputTok / 1000) * price.output); | |
| const tokEl = document.getElementById('tok' + n); | |
| tokEl.textContent = tokens.toLocaleString(); | |
| tokEl.className = 'mval ' + (n === 3 ? 'good' : n === 1 ? 'warn' : ''); | |
| document.getElementById('lat' + n).textContent = latency.toFixed(1) + 's'; | |
| document.getElementById('lat' + n).className = 'mval'; | |
| const costEl = document.getElementById('cost' + n); | |
| costEl.textContent = '$' + cost.toFixed(4); | |
| costEl.className = 'mval ' + (n === 3 ? 'good' : n === 1 ? 'warn' : ''); | |
| const bertEl = document.getElementById('bert' + n); | |
| bertEl.textContent = bert.toFixed(2); | |
| bertEl.className = 'mval ' + (bert >= 0.88 ? 'good' : bert >= 0.7 ? '' : 'warn'); | |
| const judgeEl = document.getElementById('judge' + n); | |
| judgeEl.className = 'eval-pill ' + (pass ? 'pill-pass' : 'pill-fail'); | |
| judgeEl.innerHTML = `<div class="elabel">LLM Judge</div><div class="eval-val">${pass ? 'PASS' : 'FAIL'}</div>`; | |
| } | |
| function renderError(n, msg) { | |
| document.getElementById('ans' + n).innerHTML = `<span style="color:#ff6060">${msg}</span>`; | |
| setStatus(n, 'error'); | |
| } | |
| function showBanner(type, msg) { | |
| const el = document.getElementById('banner-' + type); | |
| el.textContent = msg; | |
| el.classList.add('visible'); | |
| setTimeout(() => el.classList.remove('visible'), 6000); | |
| } | |
| // βββ LLM call (Anthropic as demo; swap headers for OpenAI/Gemini) βββββββββββββ | |
| async function callLLM(provider, apiKey, systemPrompt, userPrompt) { | |
| if (!apiKey) throw new Error('No LLM API key set'); | |
| if (provider === 'anthropic') { | |
| const res = await fetch('https://api.anthropic.com/v1/messages', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'x-api-key': apiKey, | |
| 'anthropic-version': '2023-06-01' | |
| }, | |
| body: JSON.stringify({ | |
| model: 'claude-sonnet-4-20250514', | |
| max_tokens: 512, | |
| system: systemPrompt, | |
| messages: [{ role: 'user', content: userPrompt }] | |
| }) | |
| }); | |
| if (!res.ok) throw new Error(`Anthropic API ${res.status}: ${await res.text()}`); | |
| const d = await res.json(); | |
| return { | |
| text: d.content.map(b => b.text || '').join(''), | |
| inputTokens: d.usage?.input_tokens || 0, | |
| outputTokens: d.usage?.output_tokens || 0 | |
| }; | |
| } | |
| if (provider === 'openai') { | |
| const res = await fetch('https://api.openai.com/v1/chat/completions', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, | |
| body: JSON.stringify({ | |
| model: 'gpt-4o', | |
| max_tokens: 512, | |
| messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }] | |
| }) | |
| }); | |
| if (!res.ok) throw new Error(`OpenAI API ${res.status}`); | |
| const d = await res.json(); | |
| return { | |
| text: d.choices[0].message.content, | |
| inputTokens: d.usage?.prompt_tokens || 0, | |
| outputTokens: d.usage?.completion_tokens || 0 | |
| }; | |
| } | |
| if (provider === 'gemini') { | |
| // Dynamically fetch available models for this API key to avoid 404s | |
| const modelsUrl = `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`; | |
| const modelsRes = await fetch(modelsUrl); | |
| if (!modelsRes.ok) { | |
| const err = await modelsRes.text(); | |
| throw new Error(`Gemini API Auth Error ${modelsRes.status}: ${err}`); | |
| } | |
| const modelsData = await modelsRes.json(); | |
| // Find a model that supports generateContent (prefer 1.5-flash, then any gemini) | |
| let selectedModel = modelsData.models.find(m => m.name.includes('gemini-1.5-flash') && m.supportedGenerationMethods?.includes('generateContent'))?.name; | |
| if (!selectedModel) { | |
| selectedModel = modelsData.models.find(m => m.name.includes('gemini') && m.supportedGenerationMethods?.includes('generateContent'))?.name; | |
| } | |
| if (!selectedModel) throw new Error("No compatible Gemini models found for this API key."); | |
| const url = `https://generativelanguage.googleapis.com/v1beta/${selectedModel}:generateContent?key=${apiKey}`; | |
| const payload = { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| contents: [{ parts: [{ text: systemPrompt + '\n\nUser: ' + userPrompt }] }], | |
| generationConfig: { maxOutputTokens: 512 } | |
| }) | |
| }; | |
| const res = await fetch(url, payload); | |
| if (!res.ok) { | |
| const errText = await res.text(); | |
| let errMsg = errText; | |
| try { errMsg = JSON.parse(errText).error.message; } catch(e) {} | |
| throw new Error(`Gemini API ${res.status}: ${errMsg}`); | |
| } | |
| const d = await res.json(); | |
| return { | |
| text: d.candidates[0].content.parts.map(p => p.text).join(''), | |
| inputTokens: d.usageMetadata?.promptTokenCount || 0, | |
| outputTokens: d.usageMetadata?.candidatesTokenCount || 0 | |
| }; | |
| } | |
| throw new Error('Unknown provider'); | |
| } | |
| // βββ TigerGraph RESTPP call ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function callTigerGraph(host, graph, token, queryName, question) { | |
| const url = `${host.replace(/\/$/, '')}/query/${graph}/${queryName}?company_name=${encodeURIComponent(question)}`; | |
| log(`TG β GET ${url}`, ''); | |
| const res = await fetch(url, { | |
| headers: { 'Authorization': `Bearer ${token}` } | |
| }); | |
| if (!res.ok) { | |
| const txt = await res.text(); | |
| throw new Error(`TigerGraph ${res.status}: ${txt.slice(0, 200)}`); | |
| } | |
| const data = await res.json(); | |
| // TigerGraph returns { results: [...] }; the query should return context + answer | |
| return data; | |
| } | |
| // Real BERTScore is now calculated mathematically in the Python backend via HuggingFace's bert_score library. | |
| // βββ LLM-as-a-Judge βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function judgeAnswer(provider, apiKey, question, answer) { | |
| try { | |
| const res = await callLLM(provider, apiKey, | |
| 'You are a strict benchmarking judge. Reply ONLY with the exact word PASS or the exact word FAIL. No punctuation, no explanation.', | |
| `User Question: ${question}\n\nAI Answer: ${answer}\n\nTask: Evaluate if the AI completely and accurately answered the user's specific question. To PASS, the AI MUST explicitly address the core intent of the question. If the AI provides irrelevant facts (like acquisitions) instead of answering the specific question asked, or if it says it cannot find the information, you MUST reply FAIL.` | |
| ); | |
| const text = res.text.toUpperCase(); | |
| console.log(`Judge Evaluation for Answer: "${answer.slice(0, 30)}..." -> Judge Output: "${text}"`); | |
| if (text.includes('PASS')) return true; | |
| return false; | |
| } catch { return null; } | |
| } | |
| // βββ Main runner ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function runAll() { | |
| const query = document.getElementById('query-input').value.trim(); | |
| if (!query) { showBanner('err', 'Please enter a question first.'); return; } | |
| const provider = document.getElementById('llm-provider').value; | |
| const apiKey = document.getElementById('llm-key').value.trim(); | |
| const tgHost = document.getElementById('tg-host').value.trim(); | |
| const tgGraph = document.getElementById('tg-graph').value.trim(); | |
| const tgToken = document.getElementById('tg-token').value.trim(); | |
| const tgQuery = document.getElementById('tg-query').value.trim(); | |
| if (!apiKey) { showBanner('err', 'Enter your LLM API key in the config panel.'); return; } | |
| const btn = document.getElementById('run-btn'); | |
| btn.disabled = true; | |
| document.getElementById('summary').style.display = 'none'; | |
| [1,2,3].forEach(n => showThinking(n)); | |
| log(`Query: "${query}"`, 'ok'); | |
| log(`Provider: ${provider}`, ''); | |
| // ββ Pipeline 1: LLM-Only ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const p1 = (async () => { | |
| try { | |
| log('P1 β Sending prompt directly to LLM (no retrieval)', ''); | |
| const t0 = Date.now(); | |
| const r = await callLLM(provider, apiKey, | |
| 'You are a helpful assistant. Answer accurately and concisely.', | |
| query | |
| ); | |
| const lat = (Date.now() - t0) / 1000; | |
| const tokens = r.inputTokens + r.outputTokens; | |
| log(`P1 β ${tokens} tokens, ${lat.toFixed(1)}s`, 'ok'); | |
| const pass = await judgeAnswer(provider, apiKey, query, r.text); | |
| const bert = 0.0; // LLM-Only has no context to ground against | |
| renderAnswer(1, r.text, tokens, lat, bert, pass, provider); | |
| return { tokens, cost: (r.inputTokens/1000)*PRICING[provider].input + (r.outputTokens/1000)*PRICING[provider].output, bert }; | |
| } catch(e) { | |
| log('P1 error: ' + e.message, 'err'); | |
| renderError(1, e.message); | |
| return null; | |
| } | |
| })(); | |
| // ββ Pipeline 2: Basic RAG (Vector Database Retrieval) βββββββββββββββββββ | |
| const p2 = (async () => { | |
| try { | |
| log('P2 β Calling local Python Backend (/api/basic_rag) for Vector Retrieval', ''); | |
| const t0 = Date.now(); | |
| const res = await fetch('/api/basic_rag', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ query: query, gemini_api_key: apiKey }) | |
| }); | |
| if (!res.ok) { | |
| const errTxt = await res.text(); | |
| throw new Error(`Backend Error ${res.status}: ${errTxt}`); | |
| } | |
| const data = await res.json(); | |
| const finalAnswer = data.answer; | |
| const lat = (Date.now() - t0) / 1000; | |
| // We now have exact tokens from the backend! | |
| const exactTokens = (data.input_tokens || 0) + (data.output_tokens || 0); | |
| log(`P2 β Backend returned Vector RAG response in ${lat.toFixed(1)}s`, 'ok'); | |
| const pass = await judgeAnswer(provider, apiKey, query, finalAnswer); | |
| const bert = data.bert_score || 0.0; | |
| renderAnswer(2, finalAnswer, exactTokens, lat, bert, pass, provider); | |
| return { tokens: exactTokens, cost: ((data.input_tokens||0)/1000)*PRICING[provider].input + ((data.output_tokens||0)/1000)*PRICING[provider].output, bert }; | |
| } catch(err) { | |
| log('P2 error: ' + err.message, 'err'); | |
| renderError(2, err.message); | |
| return null; | |
| } | |
| })(); | |
| // ββ Pipeline 3: GraphRAG via Python Backend βββββββββββββββββββββββββββββ | |
| const p3 = (async () => { | |
| try { | |
| log(`P3 β Calling local Python Backend (/api/rag) for TigerGraph + Gemini magic`, ''); | |
| const t0 = Date.now(); | |
| // REPLACE 'https://YOUR-BACKEND.onrender.com' with your actual Render URL later! | |
| const res = await fetch('/api/rag', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| query: query, | |
| company: query, | |
| tg_host: tgHost, | |
| tg_token: tgToken, | |
| tg_graph: tgGraph, | |
| gemini_api_key: apiKey | |
| }) // Passing all config to backend | |
| }); | |
| if (!res.ok) { | |
| const errTxt = await res.text(); | |
| throw new Error(`Backend Error ${res.status}: ${errTxt}`); | |
| } | |
| const data = await res.json(); | |
| const finalAnswer = data.answer || data.text || JSON.stringify(data); | |
| const lat = (Date.now() - t0) / 1000; | |
| // We now have exact tokens from the backend! | |
| const exactTokens = (data.input_tokens || 0) + (data.output_tokens || 0); | |
| log(`P3 β Backend returned GraphRAG response in ${lat.toFixed(1)}s`, 'ok'); | |
| document.getElementById('hops3').innerHTML = `<div class="elabel">Retrieval</div><div class="eval-val">Graph Β· 2-hop</div>`; | |
| const pass = await judgeAnswer(provider, apiKey, query, finalAnswer); | |
| const bert = data.bert_score || 0.0; | |
| renderAnswer(3, finalAnswer, exactTokens, lat, bert, pass, provider); | |
| return { tokens: exactTokens, cost: ((data.input_tokens||0)/1000)*PRICING[provider].input + ((data.output_tokens||0)/1000)*PRICING[provider].output, bert }; | |
| } catch(err) { | |
| log('P3 error: ' + err.message, 'err'); | |
| renderError(3, err.message); | |
| return null; | |
| } | |
| })(); | |
| const [r1, r2, r3] = await Promise.all([p1, p2, p3]); | |
| // ββ Summary stats βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if (r2 && r3) { | |
| const reduction = Math.round((1 - r3.tokens / r2.tokens) * 100); | |
| const saving = (r2.cost - r3.cost).toFixed(4); | |
| const bertDelta = (r3.bert - r2.bert).toFixed(2); | |
| document.getElementById('sum-baseline').textContent = r2.tokens.toLocaleString(); | |
| document.getElementById('sum-baseline-sub').textContent = '$' + r2.cost.toFixed(4) + ' / query'; | |
| document.getElementById('sum-reduction').textContent = (reduction > 0 ? 'β' : '+') + Math.abs(reduction) + '%'; | |
| document.getElementById('sum-reduction-sub').textContent = `${r2.tokens.toLocaleString()} β ${r3.tokens.toLocaleString()} tokens`; | |
| document.getElementById('sum-saving').textContent = '$' + saving; | |
| document.getElementById('sum-saving-sub').textContent = 'per query vs Basic RAG'; | |
| document.getElementById('sum-accuracy').textContent = (parseFloat(bertDelta) >= 0 ? '+' : '') + bertDelta; | |
| document.getElementById('sum-accuracy-sub').textContent = `GraphRAG BERTScore: ${r3.bert.toFixed(2)}`; | |
| document.getElementById('summary').style.display = 'grid'; | |
| log(`Summary β token reduction: ${reduction}%, cost saving: $${saving}, BERT delta: ${bertDelta}`, 'ok'); | |
| } | |
| btn.disabled = false; | |
| } | |
| </script> | |
| </body> | |
| </html> | |