Spaces:
Sleeping
Sleeping
MadhuChitikela
Initial commit: Multi-source RAG with FastAPI backend and lightweight frontend
05faae4 | <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Multi‑Source RAG | Financial & E‑Commerce AI Assistant</title> | |
| <!-- Google Fonts --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet"> | |
| <!-- Lucide Icons --> | |
| <script src="https://unpkg.com/lucide@latest"></script> | |
| <style> | |
| :root { | |
| --bg-base: #f8fafc; | |
| --bg-card: #ffffff; | |
| --bg-input: #ffffff; | |
| --border-color: #e2e8f0; | |
| --border-hover: #cbd5e1; | |
| --text-primary: #0f172a; | |
| --text-secondary: #475569; | |
| --text-muted: #94a3b8; | |
| /* Namespace Colors (Optimized for light mode contrast) */ | |
| --color-products: #047857; /* Emerald-700 */ | |
| --color-stocks: #0369a1; /* Sky-700 */ | |
| --color-deals: #b45309; /* Amber-700 */ | |
| --color-documents: #6d28d9;/* Violet-700 */ | |
| --color-news: #be123c; /* Rose-700 */ | |
| --font-display: 'Outfit', sans-serif; | |
| --font-body: 'Inter', sans-serif; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: var(--font-body); | |
| background-color: var(--bg-base); | |
| color: var(--text-primary); | |
| min-height: 100vh; | |
| padding: 2.5rem 1.5rem; | |
| line-height: 1.5; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| /* Header Styling */ | |
| header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 2rem; | |
| padding-bottom: 1rem; | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| .logo-area { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .logo-icon { | |
| background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); | |
| width: 2.25rem; | |
| height: 2.25rem; | |
| border-radius: 0.5rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .logo-text h1 { | |
| font-family: var(--font-display); | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| letter-spacing: -0.01em; | |
| } | |
| .logo-text p { | |
| font-size: 0.8rem; | |
| color: var(--text-secondary); | |
| margin-top: -0.1rem; | |
| } | |
| .status-badge { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.4rem; | |
| background: #f0fdf4; | |
| border: 1px solid #dcfce7; | |
| color: #15803d; | |
| padding: 0.35rem 0.75rem; | |
| border-radius: 2rem; | |
| font-size: 0.75rem; | |
| font-weight: 500; | |
| } | |
| .status-dot { | |
| width: 6px; | |
| height: 6px; | |
| background-color: #16a34a; | |
| border-radius: 50%; | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { transform: scale(0.9); opacity: 0.6; } | |
| 50% { transform: scale(1.2); opacity: 1; } | |
| 100% { transform: scale(0.9); opacity: 0.6; } | |
| } | |
| /* Control Card */ | |
| .controls-card { | |
| background: var(--bg-card); | |
| border-radius: 1rem; | |
| border: 1px solid var(--border-color); | |
| padding: 1.5rem; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.05); | |
| margin-bottom: 2rem; | |
| } | |
| .filter-section-title { | |
| font-family: var(--font-display); | |
| font-size: 0.9rem; | |
| font-weight: 600; | |
| color: var(--text-secondary); | |
| margin-bottom: 0.75rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.4rem; | |
| } | |
| /* Namespace Grid checkboxes */ | |
| .namespace-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); | |
| gap: 0.75rem; | |
| margin-bottom: 1.5rem; | |
| } | |
| .namespace-label { | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| background: #f8fafc; | |
| border: 1px solid var(--border-color); | |
| border-radius: 0.75rem; | |
| padding: 0.75rem 1rem; | |
| cursor: pointer; | |
| user-select: none; | |
| transition: all 0.2s ease; | |
| } | |
| .namespace-label:hover { | |
| border-color: var(--border-hover); | |
| background: #f1f5f9; | |
| } | |
| .namespace-content { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| font-weight: 500; | |
| font-size: 0.85rem; | |
| } | |
| .namespace-label input[type="checkbox"] { | |
| position: absolute; | |
| opacity: 0; | |
| width: 0; | |
| height: 0; | |
| } | |
| .ns-badge { | |
| width: 6px; | |
| height: 6px; | |
| border-radius: 50%; | |
| } | |
| .namespace-label[data-ns="products"] .ns-badge { background-color: var(--color-products); } | |
| .namespace-label[data-ns="stocks"] .ns-badge { background-color: var(--color-stocks); } | |
| .namespace-label[data-ns="deals"] .ns-badge { background-color: var(--color-deals); } | |
| .namespace-label[data-ns="documents"] .ns-badge { background-color: var(--color-documents); } | |
| .namespace-label[data-ns="news"] .ns-badge { background-color: var(--color-news); } | |
| .checkbox-custom { | |
| width: 1.1rem; | |
| height: 1.1rem; | |
| border: 2px solid var(--text-muted); | |
| border-radius: 0.2rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.2s ease; | |
| } | |
| /* Active styles when selected */ | |
| .namespace-label.selected[data-ns="products"] { | |
| border-color: rgba(16, 185, 129, 0.4); | |
| background: rgba(16, 185, 129, 0.04); | |
| color: var(--color-products); | |
| } | |
| .namespace-label.selected[data-ns="products"] .checkbox-custom { | |
| border-color: var(--color-products); | |
| background-color: var(--color-products); | |
| } | |
| .namespace-label.selected[data-ns="stocks"] { | |
| border-color: rgba(14, 116, 144, 0.4); | |
| background: rgba(14, 116, 144, 0.04); | |
| color: var(--color-stocks); | |
| } | |
| .namespace-label.selected[data-ns="stocks"] .checkbox-custom { | |
| border-color: var(--color-stocks); | |
| background-color: var(--color-stocks); | |
| } | |
| .namespace-label.selected[data-ns="deals"] { | |
| border-color: rgba(180, 83, 9, 0.4); | |
| background: rgba(180, 83, 9, 0.04); | |
| color: var(--color-deals); | |
| } | |
| .namespace-label.selected[data-ns="deals"] .checkbox-custom { | |
| border-color: var(--color-deals); | |
| background-color: var(--color-deals); | |
| } | |
| .namespace-label.selected[data-ns="documents"] { | |
| border-color: rgba(109, 40, 217, 0.4); | |
| background: rgba(109, 40, 217, 0.04); | |
| color: var(--color-documents); | |
| } | |
| .namespace-label.selected[data-ns="documents"] .checkbox-custom { | |
| border-color: var(--color-documents); | |
| background-color: var(--color-documents); | |
| } | |
| .namespace-label.selected[data-ns="news"] { | |
| border-color: rgba(190, 18, 60, 0.4); | |
| background: rgba(190, 18, 60, 0.04); | |
| color: var(--color-news); | |
| } | |
| .namespace-label.selected[data-ns="news"] .checkbox-custom { | |
| border-color: var(--color-news); | |
| background-color: var(--color-news); | |
| } | |
| /* Search input bar */ | |
| .query-area { | |
| display: flex; | |
| gap: 0.75rem; | |
| background: #ffffff; | |
| border: 1px solid var(--border-color); | |
| border-radius: 0.75rem; | |
| padding: 0.35rem; | |
| transition: all 0.2s; | |
| } | |
| .query-area:focus-within { | |
| border-color: #4f46e5; | |
| box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); | |
| } | |
| .search-icon-wrapper { | |
| display: flex; | |
| align-items: center; | |
| padding-left: 0.75rem; | |
| color: var(--text-secondary); | |
| } | |
| input[type="text"] { | |
| flex: 1; | |
| background: transparent; | |
| border: none; | |
| padding: 0.75rem 0.25rem; | |
| color: var(--text-primary); | |
| font-size: 0.95rem; | |
| outline: none; | |
| font-family: var(--font-body); | |
| } | |
| input[type="text"]::placeholder { | |
| color: var(--text-muted); | |
| } | |
| button.search-btn { | |
| background: #4f46e5; | |
| border: none; | |
| border-radius: 0.5rem; | |
| padding: 0 1.5rem; | |
| font-weight: 600; | |
| font-size: 0.9rem; | |
| font-family: var(--font-display); | |
| color: white; | |
| cursor: pointer; | |
| transition: all 0.15s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.4rem; | |
| } | |
| button.search-btn:hover { | |
| background: #4338ca; | |
| } | |
| button.search-btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| /* Results Layout Grid */ | |
| .results-grid { | |
| display: grid; | |
| grid-template-columns: 1.2fr 0.8fr; | |
| gap: 1.5rem; | |
| opacity: 0; | |
| transform: translateY(10px); | |
| transition: all 0.4s ease-out; | |
| } | |
| .results-grid.visible { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| .answer-panel, .sources-panel { | |
| background: var(--bg-card); | |
| border-radius: 1rem; | |
| padding: 1.5rem; | |
| border: 1px solid var(--border-color); | |
| box-shadow: 0 1px 2px rgba(0,0,0,0.03); | |
| } | |
| .panel-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| font-family: var(--font-display); | |
| font-size: 1.05rem; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| margin-bottom: 1rem; | |
| border-bottom: 1px solid var(--border-color); | |
| padding-bottom: 0.5rem; | |
| } | |
| .panel-header i { | |
| color: #4f46e5; | |
| } | |
| /* Answer content formatting */ | |
| .answer-text { | |
| line-height: 1.6; | |
| font-size: 0.95rem; | |
| color: var(--text-secondary); | |
| white-space: pre-wrap; | |
| } | |
| .answer-text p { | |
| margin-bottom: 0.75rem; | |
| } | |
| .answer-text ul, .answer-text ol { | |
| margin-left: 1.25rem; | |
| margin-bottom: 0.75rem; | |
| } | |
| .answer-text li { | |
| margin-bottom: 0.35rem; | |
| } | |
| /* Clickable Citations styling */ | |
| .citation-badge { | |
| background: #f1f5f9; | |
| color: #4f46e5; | |
| border: 1px solid #e2e8f0; | |
| border-radius: 0.25rem; | |
| padding: 0.05rem 0.25rem; | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| margin: 0 0.1rem; | |
| transition: all 0.15s ease; | |
| display: inline-block; | |
| } | |
| .citation-badge:hover { | |
| background: #4f46e5; | |
| color: white; | |
| border-color: #4f46e5; | |
| transform: translateY(-1px); | |
| } | |
| /* Source Card items */ | |
| .sources-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| max-height: 580px; | |
| overflow-y: auto; | |
| padding-right: 0.25rem; | |
| } | |
| .sources-list::-webkit-scrollbar { | |
| width: 4px; | |
| } | |
| .sources-list::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| .sources-list::-webkit-scrollbar-thumb { | |
| background: #cbd5e1; | |
| border-radius: 2px; | |
| } | |
| .source-card { | |
| background: #f8fafc; | |
| border-radius: 0.75rem; | |
| padding: 1rem; | |
| border: 1px solid var(--border-color); | |
| border-left: 3px solid var(--color-products); | |
| transition: all 0.2s ease; | |
| } | |
| .source-card:hover { | |
| border-color: var(--border-hover); | |
| background: #f1f5f9; | |
| } | |
| .source-card[data-ns="products"] { border-left-color: var(--color-products); } | |
| .source-card[data-ns="stocks"] { border-left-color: var(--color-stocks); } | |
| .source-card[data-ns="deals"] { border-left-color: var(--color-deals); } | |
| .source-card[data-ns="documents"] { border-left-color: var(--color-documents); } | |
| .source-card[data-ns="news"] { border-left-color: var(--color-news); } | |
| .source-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 0.5rem; | |
| font-size: 0.75rem; | |
| } | |
| .source-index { | |
| background: #ffffff; | |
| border: 1px solid var(--border-color); | |
| color: var(--text-primary); | |
| width: 1.25rem; | |
| height: 1.25rem; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: 600; | |
| } | |
| .source-ns-tag { | |
| text-transform: uppercase; | |
| letter-spacing: 0.02em; | |
| font-weight: 700; | |
| padding: 0.15rem 0.4rem; | |
| border-radius: 0.25rem; | |
| font-size: 0.65rem; | |
| } | |
| .source-card[data-ns="products"] .source-ns-tag { background: #e6f4ea; color: var(--color-products); } | |
| .source-card[data-ns="stocks"] .source-ns-tag { background: #e0f7fa; color: var(--color-stocks); } | |
| .source-card[data-ns="deals"] .source-ns-tag { background: #fff3e0; color: var(--color-deals); } | |
| .source-card[data-ns="documents"] .source-ns-tag { background: #f3e5f5; color: var(--color-documents); } | |
| .source-card[data-ns="news"] .source-ns-tag { background: #ffebee; color: var(--color-news); } | |
| .source-score { | |
| font-weight: 600; | |
| color: #16a34a; | |
| } | |
| /* Detail table inside sources */ | |
| .source-metadata-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 0.7rem; | |
| margin-bottom: 0.5rem; | |
| background: #ffffff; | |
| border-radius: 0.35rem; | |
| overflow: hidden; | |
| border: 1px solid var(--border-color); | |
| } | |
| .source-metadata-table td { | |
| padding: 0.25rem 0.5rem; | |
| border-bottom: 1px solid rgba(0,0,0,0.02); | |
| } | |
| .source-metadata-table td.meta-key { | |
| color: var(--text-secondary); | |
| font-weight: 600; | |
| width: 30%; | |
| } | |
| .source-metadata-table td.meta-val { | |
| color: var(--text-primary); | |
| word-break: break-all; | |
| } | |
| .source-text-block { | |
| font-size: 0.8rem; | |
| color: var(--text-secondary); | |
| line-height: 1.5; | |
| background: #ffffff; | |
| border-radius: 0.35rem; | |
| padding: 0.5rem; | |
| border: 1px solid var(--border-color); | |
| max-height: 110px; | |
| overflow-y: auto; | |
| white-space: pre-wrap; | |
| } | |
| .source-text-block::-webkit-scrollbar { | |
| width: 3px; | |
| } | |
| .source-text-block::-webkit-scrollbar-thumb { | |
| background: #cbd5e1; | |
| border-radius: 2px; | |
| } | |
| /* Flash animation for citation targeting */ | |
| @keyframes flash-highlight { | |
| 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(79, 70, 229, 0); } | |
| 50% { transform: scale(1.02); box-shadow: 0 0 10px rgba(79, 70, 229, 0.2); border-color: #4f46e5; } | |
| 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(79, 70, 229, 0); } | |
| } | |
| .source-card.highlighted { | |
| animation: flash-highlight 1.2s ease-out; | |
| } | |
| /* Loading Skeleton Shimmer state (Light Mode) */ | |
| .shimmer-container { | |
| display: grid; | |
| grid-template-columns: 1.2fr 0.8fr; | |
| gap: 1.5rem; | |
| margin-top: 1.5rem; | |
| } | |
| .shimmer-panel { | |
| background: #ffffff; | |
| border-radius: 1rem; | |
| padding: 1.5rem; | |
| border: 1px solid var(--border-color); | |
| } | |
| .shimmer-block { | |
| background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%); | |
| background-size: 200% 100%; | |
| animation: shimmer 1.5s infinite; | |
| border-radius: 0.35rem; | |
| } | |
| @keyframes shimmer { | |
| 0% { background-position: 200% 0; } | |
| 100% { background-position: -200% 0; } | |
| } | |
| .shimmer-title { | |
| width: 35%; | |
| height: 1.2rem; | |
| margin-bottom: 1.5rem; | |
| } | |
| .shimmer-line { | |
| width: 100%; | |
| height: 0.75rem; | |
| margin-bottom: 0.5rem; | |
| } | |
| .shimmer-line.half { | |
| width: 60%; | |
| } | |
| .shimmer-card { | |
| height: 100px; | |
| margin-bottom: 0.75rem; | |
| border-radius: 0.75rem; | |
| } | |
| @media (max-width: 768px) { | |
| .results-grid, .shimmer-container { | |
| grid-template-columns: 1fr; | |
| } | |
| body { | |
| padding: 1.5rem 1rem; | |
| } | |
| header { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| gap: 0.75rem; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <!-- Header --> | |
| <header> | |
| <div class="logo-area"> | |
| <div class="logo-icon"> | |
| <i data-lucide="brain-circuit" style="color: white; width: 1.25rem; height: 1.25rem;"></i> | |
| </div> | |
| <div class="logo-text"> | |
| <h1>Multi‑Source RAG</h1> | |
| <p>Financial & E‑Commerce Knowledge Assistant</p> | |
| </div> | |
| </div> | |
| <div class="status-badge"> | |
| <div class="status-dot"></div> | |
| <span>Active: 10,625 vectors</span> | |
| </div> | |
| </header> | |
| <!-- Controls Panel --> | |
| <div class="controls-card"> | |
| <div class="filter-section-title"> | |
| <i data-lucide="layers" style="width: 0.9rem; height: 0.9rem; color: #4f46e5;"></i> | |
| <span>Select Target Namespaces</span> | |
| </div> | |
| <div class="namespace-grid" id="namespace-grid"> | |
| <!-- Populated via Javascript --> | |
| </div> | |
| <div class="query-area"> | |
| <div class="search-icon-wrapper"> | |
| <i data-lucide="search" style="width: 1.1rem; height: 1.1rem;"></i> | |
| </div> | |
| <input type="text" id="question" placeholder="Ask anything... e.g., 'What are the top electronics products on Amazon?'" autocomplete="off" /> | |
| <button id="search-btn" class="search-btn"> | |
| <span>Run Query</span> | |
| <i data-lucide="arrow-right" style="width: 1rem; height: 1rem;"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Shimmer Loader (Hidden initially) --> | |
| <div id="shimmer-loader" class="shimmer-container" style="display: none;"> | |
| <div class="shimmer-panel"> | |
| <div class="shimmer-block shimmer-title"></div> | |
| <div class="shimmer-block shimmer-line"></div> | |
| <div class="shimmer-block shimmer-line"></div> | |
| <div class="shimmer-block shimmer-line"></div> | |
| <div class="shimmer-block shimmer-line half"></div> | |
| </div> | |
| <div class="shimmer-panel"> | |
| <div class="shimmer-block shimmer-title" style="width: 30%;"></div> | |
| <div class="shimmer-block shimmer-card"></div> | |
| <div class="shimmer-block shimmer-card"></div> | |
| </div> | |
| </div> | |
| <!-- Results Panel (Hidden initially) --> | |
| <div id="results" class="results-grid"> | |
| <div class="answer-panel"> | |
| <div class="panel-header"> | |
| <i data-lucide="sparkles"></i> | |
| <span>Generated Synthesis</span> | |
| </div> | |
| <div class="answer-text" id="answer-text"></div> | |
| </div> | |
| <div class="sources-panel"> | |
| <div class="panel-header"> | |
| <i data-lucide="database"></i> | |
| <span>Retrieved Context Sources</span> | |
| </div> | |
| <div class="sources-list" id="sources-list"> | |
| <!-- Populated via Javascript --> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Initialize icons | |
| lucide.createIcons(); | |
| const namespaces = [ | |
| { id: "products", label: "Products", icon: "shopping-bag" }, | |
| { id: "stocks", label: "Stocks (SPY)", icon: "line-chart" }, | |
| { id: "deals", label: "Merchant Deals", icon: "tag" }, | |
| { id: "documents", label: "Manuals & POs", icon: "file-text" }, | |
| { id: "news", label: "Financial News", icon: "newspaper" } | |
| ]; | |
| let selectedNamespaces = namespaces.map(ns => ns.id); // Default select all | |
| function renderNamespaceFilters() { | |
| const grid = document.getElementById('namespace-grid'); | |
| grid.innerHTML = ''; | |
| namespaces.forEach(ns => { | |
| const label = document.createElement('label'); | |
| label.className = `namespace-label ${selectedNamespaces.includes(ns.id) ? 'selected' : ''}`; | |
| label.setAttribute('data-ns', ns.id); | |
| const checkbox = document.createElement('input'); | |
| checkbox.type = 'checkbox'; | |
| checkbox.value = ns.id; | |
| checkbox.checked = selectedNamespaces.includes(ns.id); | |
| checkbox.addEventListener('change', (e) => { | |
| if (checkbox.checked) { | |
| if (!selectedNamespaces.includes(ns.id)) selectedNamespaces.push(ns.id); | |
| label.classList.add('selected'); | |
| } else { | |
| selectedNamespaces = selectedNamespaces.filter(id => id !== ns.id); | |
| label.classList.remove('selected'); | |
| } | |
| }); | |
| const content = document.createElement('span'); | |
| content.className = 'namespace-content'; | |
| const badge = document.createElement('span'); | |
| badge.className = 'ns-badge'; | |
| const icon = document.createElement('i'); | |
| icon.setAttribute('data-lucide', ns.icon); | |
| icon.style.width = '0.9rem'; | |
| icon.style.height = '0.9rem'; | |
| icon.style.color = `var(--color-${ns.id})`; | |
| const text = document.createTextNode(ns.label); | |
| const customCheckbox = document.createElement('span'); | |
| customCheckbox.className = 'checkbox-custom'; | |
| content.appendChild(badge); | |
| content.appendChild(icon); | |
| content.appendChild(text); | |
| label.appendChild(checkbox); | |
| label.appendChild(content); | |
| label.appendChild(customCheckbox); | |
| grid.appendChild(label); | |
| }); | |
| lucide.createIcons(); | |
| } | |
| // Process answer text to highlight citations and parse basic bolding | |
| function formatAnswer(text) { | |
| if (!text) return ""; | |
| // Escape HTML to prevent injection | |
| let escaped = escapeHtml(text); | |
| // Match bracketed citations e.g. [1], [2], [10] | |
| escaped = escaped.replace(/\[([1-8])\]/g, (match, idx) => { | |
| return `<span class="citation-badge" onclick="highlightSource(${idx})">[Source ${idx}]</span>`; | |
| }); | |
| // Replace **text** with bold tags | |
| escaped = escaped.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>'); | |
| // Replace list elements or markdown items | |
| escaped = escaped.replace(/^\s*[-*]\s+(.*?)$/gm, '<li>$1</li>'); | |
| escaped = escaped.replace(/(<li>.*?<\/li>)/gs, '<ul>$1</ul>'); | |
| // Fix multiple double nested lists | |
| escaped = escaped.replace(/<\/ul>\s*<ul>/g, ''); | |
| return escaped; | |
| } | |
| function escapeHtml(str) { | |
| return str.replace(/[&<>]/g, function(m) { | |
| if (m === '&') return '&'; | |
| if (m === '<') return '<'; | |
| if (m === '>') return '>'; | |
| return m; | |
| }); | |
| } | |
| // Scroll to and highlight a specific source card | |
| function highlightSource(index) { | |
| const card = document.getElementById(`source-card-${index}`); | |
| if (card) { | |
| card.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| card.classList.remove('highlighted'); | |
| void card.offsetWidth; | |
| card.classList.add('highlighted'); | |
| } | |
| } | |
| async function performQuery() { | |
| const question = document.getElementById('question').value.trim(); | |
| if (!question) return; | |
| if (selectedNamespaces.length === 0) { | |
| alert("Please select at least one source namespace to query."); | |
| return; | |
| } | |
| const searchBtn = document.getElementById('search-btn'); | |
| const shimmer = document.getElementById('shimmer-loader'); | |
| const results = document.getElementById('results'); | |
| searchBtn.disabled = true; | |
| shimmer.style.display = 'grid'; | |
| results.classList.remove('visible'); | |
| try { | |
| const response = await fetch('/api/query', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| question: question, | |
| namespaces: selectedNamespaces | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (!response.ok) throw new Error(data.detail || 'Server error occurred'); | |
| // Render Answer | |
| document.getElementById('answer-text').innerHTML = formatAnswer(data.answer); | |
| // Render Sources | |
| const listContainer = document.getElementById('sources-list'); | |
| listContainer.innerHTML = ''; | |
| if (data.sources.length === 0) { | |
| listContainer.innerHTML = ` | |
| <div style="text-align: center; color: var(--text-muted); padding: 2rem; font-size: 0.9rem;"> | |
| No source documents retrieved. | |
| </div> | |
| `; | |
| } else { | |
| data.sources.forEach((src, idx) => { | |
| const cardIndex = idx + 1; | |
| const card = document.createElement('div'); | |
| card.className = 'source-card'; | |
| card.setAttribute('data-ns', src.namespace); | |
| card.id = `source-card-${cardIndex}`; | |
| let metaRows = ""; | |
| for (const [key, val] of Object.entries(src.metadata)) { | |
| if (key !== "text" && val !== null && val !== undefined) { | |
| metaRows += ` | |
| <tr> | |
| <td class="meta-key">${key}</td> | |
| <td class="meta-val">${val}</td> | |
| </tr> | |
| `; | |
| } | |
| } | |
| card.innerHTML = ` | |
| <div class="source-header"> | |
| <div style="display: flex; align-items: center; gap: 0.4rem;"> | |
| <span class="source-index">${cardIndex}</span> | |
| <span class="source-ns-tag">${src.namespace}</span> | |
| </div> | |
| <span class="source-score"><i data-lucide="trending-up" style="width: 0.75rem; height: 0.75rem; display: inline-block; vertical-align: middle; margin-right: 0.15rem;"></i>${src.score.toFixed(3)}</span> | |
| </div> | |
| <table class="source-metadata-table"> | |
| ${metaRows ? metaRows : '<tr><td colspan="2" class="meta-val">No additional metadata</td></tr>'} | |
| </table> | |
| <div class="source-text-block">${escapeHtml(src.text)}</div> | |
| `; | |
| listContainer.appendChild(card); | |
| }); | |
| lucide.createIcons(); | |
| } | |
| shimmer.style.display = 'none'; | |
| results.classList.add('visible'); | |
| } catch (err) { | |
| alert('Query Error: ' + err.message); | |
| shimmer.style.display = 'none'; | |
| } finally { | |
| searchBtn.disabled = false; | |
| } | |
| } | |
| document.getElementById('search-btn').addEventListener('click', performQuery); | |
| document.getElementById('question').addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') performQuery(); | |
| }); | |
| renderNamespaceFilters(); | |
| </script> | |
| </body> | |
| </html> | |