| import os |
| import time |
| import json |
| import hashlib |
| import requests |
| import pandas as pd |
| import plotly.express as px |
| import plotly.graph_objects as go |
| import streamlit as st |
| import extra_streamlit_components as stx |
| from datetime import datetime, timedelta |
|
|
| RAG_API_URL = os.getenv("RAG_API_URL", "").rstrip("/") |
| RAG_API_KEY = os.getenv("RAG_API_KEY", "") |
| APP_USERNAME = os.getenv("USERNAME", "admin") |
| APP_PASSWORD = os.getenv("PASSWORD", "") |
|
|
| SESSION_DURATION_DAYS = 30 |
| COOKIE_NAME = "nexus_auth_token" |
|
|
| st.set_page_config( |
| page_title="Nexus RAG", |
| page_icon="⬡", |
| layout="wide", |
| initial_sidebar_state="expanded", |
| ) |
|
|
| st.markdown(""" |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); |
| * { font-family: 'Inter', sans-serif !important; } |
| #MainMenu, footer, header { visibility: hidden; } |
| html, body, [data-testid="stAppViewContainer"] { background: #0a0a0f; color: #e2e8f0; } |
| [data-testid="stSidebar"] { background: #0d0d18 !important; border-right: 1px solid #1e1e2e; } |
| [data-testid="stMain"] { background: #0a0a0f; } |
| .login-title { font-size: 1.8rem; font-weight: 800; color: #e94560; text-align: center; margin-bottom: 0.3rem; letter-spacing: 3px; } |
| .login-subtitle { font-size: 0.8rem; color: #4a4a6a; text-align: center; margin-bottom: 2rem; letter-spacing: 1px; text-transform: uppercase; } |
| .login-error { background: rgba(233,69,96,0.1); border: 1px solid rgba(233,69,96,0.3); color: #e94560; padding: 0.8rem 1rem; border-radius: 8px; font-size: 0.85rem; margin-bottom: 1rem; text-align: center; } |
| .nexus-brand { display: flex; align-items: center; gap: 0.6rem; padding: 1.2rem 0.5rem; border-bottom: 1px solid #1e1e2e; margin-bottom: 1.5rem; } |
| .nexus-brand-title { font-size: 1rem; font-weight: 800; color: #e94560; letter-spacing: 2px; } |
| .nexus-brand-sub { font-size: 0.7rem; color: #4a4a6a; letter-spacing: 1px; } |
| .sidebar-status-box { background: #0a0a0f; border: 1px solid #1e1e2e; border-radius: 8px; padding: 0.8rem; margin-bottom: 1rem; font-size: 0.78rem; color: #6b7280; } |
| .sidebar-status-row { display: flex; justify-content: space-between; align-items: center; padding: 0.2rem 0; } |
| .sidebar-status-val { color: #e2e8f0; font-weight: 500; } |
| .page-title { font-size: 1.6rem; font-weight: 800; color: #e2e8f0; margin-bottom: 0.3rem; } |
| .page-subtitle { font-size: 0.85rem; color: #4a4a6a; margin-bottom: 1.5rem; } |
| .card { background: #0d0d18; border: 1px solid #1e1e2e; border-radius: 12px; padding: 1.2rem; margin-bottom: 1rem; } |
| .card:hover { border-color: #2e2e4e; } |
| .metric-val { font-size: 2rem; font-weight: 800; color: #e94560; line-height: 1; margin-bottom: 0.3rem; } |
| .metric-lbl { font-size: 0.72rem; color: #4a4a6a; text-transform: uppercase; letter-spacing: 1px; font-weight: 600; } |
| .metric-sub { font-size: 0.78rem; color: #6b7280; margin-top: 0.2rem; } |
| .table-header { display: flex; padding: 0.5rem 0.8rem; border-bottom: 1px solid #1e1e2e; font-size: 0.72rem; font-weight: 600; color: #4a4a6a; text-transform: uppercase; letter-spacing: 1px; } |
| .table-row { display: flex; padding: 0.7rem 0.8rem; border-bottom: 1px solid #0f0f1a; font-size: 0.85rem; color: #a0aec0; align-items: center; } |
| .badge { display: inline-block; padding: 0.2rem 0.6rem; border-radius: 20px; font-size: 0.72rem; font-weight: 600; } |
| .badge-red { background: rgba(233,69,96,0.15); color: #e94560; border: 1px solid rgba(233,69,96,0.2); } |
| .badge-green { background: rgba(16,185,129,0.15); color: #10b981; border: 1px solid rgba(16,185,129,0.2); } |
| .badge-blue { background: rgba(59,130,246,0.15); color: #3b82f6; border: 1px solid rgba(59,130,246,0.2); } |
| .badge-gray { background: rgba(107,114,128,0.15); color: #9ca3af; border: 1px solid rgba(107,114,128,0.2); } |
| .result-block { background: #0a0a0f; border-left: 3px solid #e94560; border-radius: 0 8px 8px 0; padding: 0.8rem 1rem; margin-bottom: 0.6rem; } |
| .score-high { color: #10b981; font-weight: 700; } |
| .score-med { color: #f59e0b; font-weight: 700; } |
| .score-low { color: #e94560; font-weight: 700; } |
| .alert-success { background: rgba(16,185,129,0.08); border: 1px solid rgba(16,185,129,0.2); color: #10b981; padding: 0.8rem 1rem; border-radius: 8px; font-size: 0.85rem; margin-bottom: 0.5rem; } |
| .alert-error { background: rgba(233,69,96,0.08); border: 1px solid rgba(233,69,96,0.2); color: #e94560; padding: 0.8rem 1rem; border-radius: 8px; font-size: 0.85rem; margin-bottom: 0.5rem; } |
| .alert-info { background: rgba(59,130,246,0.08); border: 1px solid rgba(59,130,246,0.2); color: #3b82f6; padding: 0.8rem 1rem; border-radius: 8px; font-size: 0.85rem; margin-bottom: 0.5rem; } |
| div[data-testid="stTextInput"] input { background: #0a0a0f !important; border: 1px solid #1e1e2e !important; border-radius: 8px !important; color: #e2e8f0 !important; } |
| div[data-testid="stTextInput"] input:focus { border-color: #e94560 !important; box-shadow: 0 0 0 2px rgba(233,69,96,0.15) !important; } |
| div[data-testid="stSelectbox"] > div > div { background: #0a0a0f !important; border: 1px solid #1e1e2e !important; border-radius: 8px !important; color: #e2e8f0 !important; } |
| div[data-testid="stTextArea"] textarea { background: #0a0a0f !important; border: 1px solid #1e1e2e !important; border-radius: 8px !important; color: #e2e8f0 !important; } |
| div[data-testid="stTextArea"] textarea:focus { border-color: #e94560 !important; box-shadow: 0 0 0 2px rgba(233,69,96,0.15) !important; } |
| div[data-testid="stTextInput"] label, div[data-testid="stTextArea"] label, div[data-testid="stSelectbox"] label, div[data-testid="stSlider"] label, div[data-testid="stCheckbox"] label, div[data-testid="stFileUploader"] label, div[data-testid="stNumberInput"] label { color: #6b7280 !important; font-size: 0.8rem !important; font-weight: 500 !important; text-transform: uppercase !important; letter-spacing: 0.5px !important; } |
| .stButton > button { background: #e94560 !important; color: white !important; border: none !important; border-radius: 8px !important; font-weight: 600 !important; font-size: 0.875rem !important; padding: 0.55rem 1.5rem !important; width: 100% !important; transition: opacity 0.2s !important; } |
| .stButton > button:hover { opacity: 0.85 !important; color: white !important; } |
| div[data-testid="stTabs"] button { color: #4a4a6a !important; font-weight: 500 !important; font-size: 0.85rem !important; } |
| div[data-testid="stTabs"] button[aria-selected="true"] { color: #e94560 !important; border-bottom-color: #e94560 !important; } |
| hr { border-color: #1e1e2e !important; margin: 1rem 0 !important; } |
| div[data-testid="stExpander"] { background: #0d0d18 !important; border: 1px solid #1e1e2e !important; border-radius: 8px !important; } |
| [data-testid="stFileUploader"] { background: #0a0a0f !important; border: 2px dashed #1e1e2e !important; border-radius: 8px !important; } |
| .logout-btn > button { background: transparent !important; border: 1px solid #1e1e2e !important; color: #6b7280 !important; font-size: 0.78rem !important; } |
| .logout-btn > button:hover { border-color: #e94560 !important; color: #e94560 !important; opacity: 1 !important; } |
| ::-webkit-scrollbar { width: 4px; height: 4px; } |
| ::-webkit-scrollbar-track { background: #0a0a0f; } |
| ::-webkit-scrollbar-thumb { background: #1e1e2e; border-radius: 2px; } |
| ::-webkit-scrollbar-thumb:hover { background: #e94560; } |
| </style> |
| """, unsafe_allow_html=True) |
|
|
| LUCIDE_ICONS = { |
| "lock": """<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#e94560" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>""", |
| } |
|
|
| PLOTLY_LAYOUT = dict( |
| paper_bgcolor="#0d0d18", |
| plot_bgcolor="#0a0a0f", |
| font=dict(color="#6b7280", family="Inter", size=12), |
| margin=dict(t=30, b=30, l=10, r=10), |
| xaxis=dict(gridcolor="#1e1e2e", zerolinecolor="#1e1e2e"), |
| yaxis=dict(gridcolor="#1e1e2e", zerolinecolor="#1e1e2e"), |
| legend=dict(bgcolor="rgba(0,0,0,0)", font=dict(color="#6b7280")), |
| ) |
|
|
| COLOR_PRIMARY = "#e94560" |
| COLOR_SECONDARY = "#3b82f6" |
| COLOR_SUCCESS = "#10b981" |
| COLOR_WARNING = "#f59e0b" |
|
|
|
|
| def get_cookie_manager(): |
| if "cookie_manager" not in st.session_state: |
| st.session_state.cookie_manager = stx.CookieManager(key="nexus_cookie_mgr") |
| return st.session_state.cookie_manager |
|
|
|
|
| def generate_token(username: str) -> str: |
| secret = APP_PASSWORD + username + "nexus_salt_2024" |
| return hashlib.sha256(secret.encode()).hexdigest() |
|
|
|
|
| def verify_token(token: str) -> bool: |
| return token == generate_token(APP_USERNAME) |
|
|
|
|
| def check_auth() -> bool: |
| try: |
| cm = get_cookie_manager() |
| token = cm.get(COOKIE_NAME) |
| if token and verify_token(str(token)): |
| return True |
| except Exception: |
| pass |
| return False |
|
|
|
|
| def do_login(username: str, password: str) -> bool: |
| if username.strip() == APP_USERNAME and password == APP_PASSWORD: |
| token = generate_token(username) |
| expiry = datetime.now() + timedelta(days=SESSION_DURATION_DAYS) |
| try: |
| cm = get_cookie_manager() |
| cm.set(COOKIE_NAME, token, expires_at=expiry, key="cookie_set_login") |
| except Exception: |
| pass |
| return True |
| return False |
|
|
|
|
| def do_logout(): |
| try: |
| cm = get_cookie_manager() |
| cm.delete(COOKIE_NAME, key="cookie_del_logout") |
| except Exception: |
| pass |
| st.session_state.authenticated = False |
| st.session_state.current_page = "Overview" |
| st.rerun() |
|
|
|
|
| def api_get(endpoint: str) -> tuple[bool, dict]: |
| try: |
| r = requests.get( |
| f"{RAG_API_URL}{endpoint}", |
| headers={"Authorization": f"Bearer {RAG_API_KEY}"}, |
| timeout=15, |
| ) |
| return r.status_code == 200, r.json() |
| except Exception as e: |
| return False, {"error": str(e)} |
|
|
|
|
| def api_post(endpoint: str, json_data: dict = None, files=None, data=None) -> tuple[bool, dict]: |
| try: |
| headers = {"Authorization": f"Bearer {RAG_API_KEY}"} |
| if json_data is not None: |
| headers["Content-Type"] = "application/json" |
| r = requests.post(f"{RAG_API_URL}{endpoint}", headers=headers, json=json_data, timeout=120) |
| else: |
| r = requests.post(f"{RAG_API_URL}{endpoint}", headers=headers, files=files, data=data, timeout=120) |
| return r.status_code == 200, r.json() |
| except Exception as e: |
| return False, {"error": str(e)} |
|
|
|
|
| def api_delete(endpoint: str) -> tuple[bool, dict]: |
| try: |
| r = requests.delete( |
| f"{RAG_API_URL}{endpoint}", |
| headers={"Authorization": f"Bearer {RAG_API_KEY}"}, |
| timeout=15, |
| ) |
| return r.status_code == 200, r.json() |
| except Exception as e: |
| return False, {"error": str(e)} |
|
|
|
|
| def get_health() -> dict: |
| try: |
| r = requests.get(f"{RAG_API_URL}/health", timeout=10) |
| return r.json() if r.status_code == 200 else {} |
| except Exception: |
| return {} |
|
|
|
|
| def render_login(): |
| col1, col2, col3 = st.columns([1, 1.2, 1]) |
| with col2: |
| st.markdown(f""" |
| <div style="text-align:center;padding:3rem 0 1.5rem 0;">{LUCIDE_ICONS['lock']}</div> |
| <div class="login-title">NEXUS RAG</div> |
| <div class="login-subtitle">Restricted Access — Authenticate to Continue</div> |
| """, unsafe_allow_html=True) |
|
|
| if st.session_state.get("login_error"): |
| st.markdown(f'<div class="login-error">{st.session_state.login_error}</div>', unsafe_allow_html=True) |
|
|
| with st.form("login_form", clear_on_submit=False): |
| username = st.text_input("Username", placeholder="Enter username") |
| password = st.text_input("Password", type="password", placeholder="Enter password") |
| submitted = st.form_submit_button("Sign In", use_container_width=True) |
|
|
| if submitted: |
| if not username or not password: |
| st.session_state.login_error = "Username and password are required." |
| st.rerun() |
| else: |
| success = do_login(username, password) |
| if success: |
| st.session_state.login_error = "" |
| st.session_state.authenticated = True |
| st.rerun() |
| else: |
| st.session_state.login_error = "Invalid credentials. Access denied." |
| st.rerun() |
|
|
| st.markdown(""" |
| <div style="text-align:center;margin-top:2rem;font-size:0.72rem;color:#2e2e4e;letter-spacing:1px;"> |
| NEXUS RAG ENGINE — SECURED |
| </div> |
| """, unsafe_allow_html=True) |
|
|
|
|
| def render_sidebar(health: dict) -> str: |
| with st.sidebar: |
| status = health.get("status", "offline") |
| status_color = "#10b981" if status == "healthy" else "#f59e0b" if status == "degraded" else "#e94560" |
| status_label = status.upper() if status else "OFFLINE" |
|
|
| st.markdown(f""" |
| <div class="nexus-brand"> |
| <div> |
| <div class="nexus-brand-title">NEXUS RAG</div> |
| <div class="nexus-brand-sub">Engine Dashboard</div> |
| </div> |
| <div style="margin-left:auto;"> |
| <span style="color:{status_color};font-size:0.7rem;font-weight:700; |
| background:rgba(0,0,0,0.3);padding:0.2rem 0.5rem;border-radius:20px; |
| border:1px solid {status_color}33;"> |
| {status_label} |
| </span> |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| if health: |
| uptime = health.get("uptime_seconds", 0) |
| hours = int(uptime // 3600) |
| minutes = int((uptime % 3600) // 60) |
| qdrant_ok = health.get("qdrant_connection", "error") == "ok" |
| cache_n = health.get("cache_entries", 0) |
| st.markdown(f""" |
| <div class="sidebar-status-box"> |
| <div class="sidebar-status-row"> |
| <span>Uptime</span> |
| <span class="sidebar-status-val">{hours}h {minutes}m</span> |
| </div> |
| <div class="sidebar-status-row"> |
| <span>Qdrant</span> |
| <span class="sidebar-status-val" style="color:{'#10b981' if qdrant_ok else '#e94560'}"> |
| {'Connected' if qdrant_ok else 'Error'} |
| </span> |
| </div> |
| <div class="sidebar-status-row"> |
| <span>Cache</span> |
| <span class="sidebar-status-val">{cache_n} entries</span> |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| pages = ["Overview", "Upload", "Test Search", "Documents", "Analytics"] |
|
|
| for label in pages: |
| is_active = st.session_state.get("current_page", "Overview") == label |
| if st.button( |
| label, |
| key=f"nav_{label}", |
| use_container_width=True, |
| type="primary" if is_active else "secondary", |
| ): |
| st.session_state.current_page = label |
| st.rerun() |
|
|
| st.markdown("<br>", unsafe_allow_html=True) |
| st.markdown("<div style='border-top:1px solid #1e1e2e;padding-top:1rem;'></div>", unsafe_allow_html=True) |
| st.markdown( |
| f"<div style='font-size:0.72rem;color:#2e2e4e;margin-bottom:0.5rem;'>" |
| f"Signed in as <span style='color:#4a4a6a;'>{APP_USERNAME}</span></div>", |
| unsafe_allow_html=True, |
| ) |
|
|
| st.markdown('<div class="logout-btn">', unsafe_allow_html=True) |
| if st.button("Sign Out", key="logout_btn", use_container_width=True): |
| do_logout() |
| st.markdown("</div>", unsafe_allow_html=True) |
|
|
| return st.session_state.get("current_page", "Overview") |
|
|
|
|
| def page_overview(stats: dict, health: dict): |
| st.markdown('<div class="page-title">Overview</div>', unsafe_allow_html=True) |
| st.markdown('<div class="page-subtitle">System status and key metrics at a glance.</div>', unsafe_allow_html=True) |
|
|
| storage = stats.get("storage", {}) |
| documents = stats.get("documents", {}) |
| queries = stats.get("queries", {}) |
|
|
| c1, c2, c3, c4 = st.columns(4) |
| for col, val, label, sub in [ |
| (c1, documents.get("total_parents", 0), "Parent Chunks", "Indexed parent segments"), |
| (c2, documents.get("total_children", 0), "Child Chunks", "Searchable vectors"), |
| (c3, queries.get("today", 0), "Queries Today", "RAG calls this session"), |
| (c4, queries.get("cache_size", 0), "Cache Entries", "In-memory cached results"), |
| ]: |
| with col: |
| st.markdown(f""" |
| <div class="card" style="text-align:center;"> |
| <div class="metric-val">{val:,}</div> |
| <div class="metric-lbl">{label}</div> |
| <div class="metric-sub">{sub}</div> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| st.markdown("<br>", unsafe_allow_html=True) |
| col_l, col_r = st.columns(2) |
|
|
| with col_l: |
| used = storage.get("used_mb", 0) |
| total = storage.get("total_mb", 1024) |
| pct = storage.get("percentage", 0) |
| bar_color = COLOR_SUCCESS if pct < 60 else COLOR_WARNING if pct < 85 else COLOR_PRIMARY |
| fig_gauge = go.Figure(go.Indicator( |
| mode="gauge+number", |
| value=pct, |
| number={"suffix": "%", "font": {"color": COLOR_PRIMARY, "size": 32}}, |
| title={"text": f"Storage {used:.1f} MB / {total} MB", "font": {"color": "#6b7280", "size": 13}}, |
| gauge={ |
| "axis": {"range": [0, 100], "tickcolor": "#1e1e2e", "tickfont": {"color": "#4a4a6a"}}, |
| "bar": {"color": bar_color}, |
| "bgcolor": "#0a0a0f", |
| "borderwidth": 0, |
| "steps": [ |
| {"range": [0, 60], "color": "#0d0d18"}, |
| {"range": [60, 85], "color": "#111122"}, |
| {"range": [85, 100], "color": "#150a0a"}, |
| ], |
| "threshold": {"line": {"color": COLOR_PRIMARY, "width": 2}, "thickness": 0.75, "value": 90}, |
| }, |
| )) |
| fig_gauge.update_layout(**PLOTLY_LAYOUT, height=260) |
| st.plotly_chart(fig_gauge, use_container_width=True) |
|
|
| with col_r: |
| top_queries = stats.get("top_queries", []) |
| st.markdown('<div style="font-size:0.8rem;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:1px;margin-bottom:0.8rem;">Top Queries</div>', unsafe_allow_html=True) |
| if top_queries: |
| max_count = top_queries[0].get("count", 1) |
| for q in top_queries[:6]: |
| qtext = q.get("query", "")[:45] |
| count = q.get("count", 0) |
| pct_bar = int((count / max_count) * 100) |
| st.markdown(f""" |
| <div style="margin-bottom:0.6rem;"> |
| <div style="display:flex;justify-content:space-between;margin-bottom:3px;"> |
| <span style="font-size:0.8rem;color:#a0aec0;">{qtext}</span> |
| <span style="font-size:0.78rem;color:{COLOR_PRIMARY};font-weight:600;">{count}x</span> |
| </div> |
| <div style="background:#1e1e2e;border-radius:2px;height:3px;"> |
| <div style="background:{COLOR_PRIMARY};width:{pct_bar}%;height:3px;border-radius:2px;"></div> |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
| else: |
| st.markdown('<div class="alert-info">No query logs yet.</div>', unsafe_allow_html=True) |
|
|
| rate_limits = stats.get("rate_limits", {}) |
| if rate_limits: |
| st.markdown('<div style="font-size:0.8rem;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:1px;margin:1rem 0 0.8rem 0;">Rate Limit — Current Hour</div>', unsafe_allow_html=True) |
| rl_cols = st.columns(min(len(rate_limits), 4)) |
| for i, (key, count) in enumerate(rate_limits.items()): |
| pct_rl = (count / 3000) * 100 |
| rl_color = COLOR_SUCCESS if pct_rl < 70 else COLOR_WARNING if pct_rl < 90 else COLOR_PRIMARY |
| with rl_cols[i % 4]: |
| st.markdown(f""" |
| <div class="card"> |
| <div style="font-size:0.72rem;color:#4a4a6a;margin-bottom:0.3rem;font-family:monospace;">{key}</div> |
| <div style="font-size:1.4rem;font-weight:700;color:{rl_color};">{count}<span style="font-size:0.75rem;color:#4a4a6a;">/3000</span></div> |
| <div style="background:#1e1e2e;border-radius:2px;height:3px;margin-top:0.5rem;"> |
| <div style="background:{rl_color};width:{min(pct_rl,100):.1f}%;height:3px;border-radius:2px;"></div> |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
|
|
| def page_upload(): |
| st.markdown('<div class="page-title">Upload Documents</div>', unsafe_allow_html=True) |
| st.markdown('<div class="page-subtitle">Add .md or .txt files to the Nexus RAG knowledge base.</div>', unsafe_allow_html=True) |
|
|
| ok, collections_data = api_get("/collections") |
| existing_collections = collections_data.get("collections", []) if ok else [] |
|
|
| with st.form("upload_form", clear_on_submit=True): |
| uploaded_files = st.file_uploader("Select files", type=["md", "txt"], accept_multiple_files=True, help="Max 50MB per file") |
| c1, c2 = st.columns(2) |
| with c1: |
| col_mode = st.selectbox("Collection mode", ["Use existing", "Create new"]) |
| with c2: |
| if col_mode == "Use existing" and existing_collections: |
| collection_name = st.selectbox("Collection", existing_collections) |
| else: |
| collection_name = st.text_input("Collection name", placeholder="e.g. devops, tutorials") |
| metadata_raw = st.text_area("Metadata JSON (optional)", placeholder='{"category": "devops"}', height=70) |
| submitted = st.form_submit_button("Upload", use_container_width=True) |
|
|
| if submitted: |
| if not uploaded_files: |
| st.markdown('<div class="alert-error">Select at least one file.</div>', unsafe_allow_html=True) |
| return |
| if not collection_name or not collection_name.strip(): |
| st.markdown('<div class="alert-error">Collection name is required.</div>', unsafe_allow_html=True) |
| return |
| meta_dict = {} |
| if metadata_raw.strip(): |
| try: |
| meta_dict = json.loads(metadata_raw.strip()) |
| except json.JSONDecodeError: |
| st.markdown('<div class="alert-error">Invalid JSON in metadata.</div>', unsafe_allow_html=True) |
| return |
|
|
| progress = st.progress(0, text="Preparing...") |
| results_box = st.container() |
| success_n, fail_n = 0, 0 |
|
|
| for i, f in enumerate(uploaded_files): |
| progress.progress(i / len(uploaded_files), text=f"Uploading {f.name}...") |
| raw = f.read() |
| size_mb = len(raw) / (1024 * 1024) |
| if size_mb > 50: |
| with results_box: |
| st.markdown(f'<div class="alert-error">{f.name} exceeds 50MB ({size_mb:.1f}MB)</div>', unsafe_allow_html=True) |
| fail_n += 1 |
| continue |
| ok, resp = api_post("/upload", files={"file": (f.name, raw, "text/plain")}, data={"collection": collection_name.strip(), "metadata": json.dumps(meta_dict)}) |
| with results_box: |
| if ok and resp.get("success"): |
| st.markdown(f'<div class="alert-success">{f.name} — {resp.get("parents_created",0)} parents, {resp.get("children_created",0)} children — {size_mb:.2f}MB — {resp.get("processing_time_ms",0):.0f}ms</div>', unsafe_allow_html=True) |
| success_n += 1 |
| else: |
| st.markdown(f'<div class="alert-error">{f.name} — {resp.get("error") or resp.get("detail","Unknown error")}</div>', unsafe_allow_html=True) |
| fail_n += 1 |
|
|
| progress.progress(1.0, text="Done") |
| st.markdown("<hr>", unsafe_allow_html=True) |
| c1, c2, c3 = st.columns(3) |
| c1.metric("Uploaded", success_n) |
| c2.metric("Failed", fail_n) |
| c3.metric("Collection", collection_name) |
|
|
|
|
| def page_test_search(): |
| st.markdown('<div class="page-title">Test Search</div>', unsafe_allow_html=True) |
| st.markdown('<div class="page-subtitle">Run live queries against the Nexus RAG engine.</div>', unsafe_allow_html=True) |
|
|
| ok, collections_data = api_get("/collections") |
| collections = ["all"] + (collections_data.get("collections", []) if ok else []) |
|
|
| with st.form("search_form"): |
| query_text = st.text_area("Query", placeholder="Type your query here...", height=90) |
| c1, c2, c3 = st.columns(3) |
| with c1: |
| selected_col = st.selectbox("Collection", collections) |
| with c2: |
| top_k = st.slider("Top K", 1, 20, 5) |
| with c3: |
| use_rerank = st.checkbox("Reranking", value=True) |
| submitted = st.form_submit_button("Search", use_container_width=True) |
|
|
| if submitted: |
| if not query_text.strip(): |
| st.markdown('<div class="alert-error">Enter a query.</div>', unsafe_allow_html=True) |
| return |
| with st.spinner("Processing..."): |
| t0 = time.time() |
| ok, resp = api_post("/query", json_data={"query": query_text.strip(), "collection": selected_col, "top_k": top_k, "use_reranking": use_rerank}) |
| elapsed = (time.time() - t0) * 1000 |
|
|
| if not ok: |
| st.markdown(f'<div class="alert-error">Query failed: {resp.get("error","Unknown")}</div>', unsafe_allow_html=True) |
| return |
|
|
| c1, c2, c3, c4 = st.columns(4) |
| c1.metric("Latency", f"{resp.get('processing_time_ms', elapsed):.0f}ms") |
| c2.metric("Sources", len(resp.get("sources", []))) |
| c3.metric("Cached", "Yes" if resp.get("cached") else "No") |
| c4.metric("Reranked", "Yes" if use_rerank else "No") |
|
|
| st.markdown("<hr>", unsafe_allow_html=True) |
| tab_ctx, tab_src = st.tabs(["Context Output", "Sources Detail"]) |
|
|
| with tab_ctx: |
| context = resp.get("context", "") |
| if context: |
| st.markdown(f'<div style="font-size:0.78rem;color:#4a4a6a;margin-bottom:0.5rem;">Context length: {len(context):,} chars</div>', unsafe_allow_html=True) |
| st.text_area("", value=context, height=400, label_visibility="collapsed") |
| else: |
| st.markdown('<div class="alert-info">No context returned.</div>', unsafe_allow_html=True) |
|
|
| with tab_src: |
| sources = resp.get("sources", []) |
| if sources: |
| for i, src in enumerate(sources, 1): |
| score = src.get("score", 0) |
| score_cls = "score-high" if score > 0.7 else "score-med" if score > 0.5 else "score-low" |
| preview = src.get("text", "")[:300] |
| dots = "..." if len(src.get("text", "")) > 300 else "" |
| st.markdown(f""" |
| <div class="result-block"> |
| <div style="display:flex;justify-content:space-between;margin-bottom:0.5rem;"> |
| <span style="font-size:0.78rem;color:#4a4a6a;">Result #{i}</span> |
| <span class="{score_cls}">{score:.4f}</span> |
| </div> |
| <div style="font-size:0.875rem;color:#a0aec0;margin-bottom:0.5rem;">{preview}{dots}</div> |
| <div style="font-size:0.72rem;color:#4a4a6a;"> |
| {src.get('filename','unknown')} · {src.get('collection','unknown')} · {src.get('doc_id','')[:12]}... |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
| else: |
| st.markdown('<div class="alert-info">No sources found.</div>', unsafe_allow_html=True) |
|
|
|
|
| def page_documents(): |
| st.markdown('<div class="page-title">Documents</div>', unsafe_allow_html=True) |
| st.markdown('<div class="page-subtitle">Manage all indexed documents in the knowledge base.</div>', unsafe_allow_html=True) |
|
|
| ok, collections_data = api_get("/collections") |
| collections = ["all"] + (collections_data.get("collections", []) if ok else []) |
|
|
| c1, c2 = st.columns([3, 1]) |
| with c1: |
| filter_col = st.selectbox("Filter by collection", collections, key="doc_filter_col") |
| with c2: |
| st.markdown("<br>", unsafe_allow_html=True) |
| if st.button("Refresh", use_container_width=True): |
| st.rerun() |
|
|
| col_param = "" if filter_col == "all" else f"&collection={filter_col}" |
| ok, docs_data = api_get(f"/documents?limit=200{col_param}") |
|
|
| if not ok: |
| st.markdown('<div class="alert-error">Failed to fetch documents.</div>', unsafe_allow_html=True) |
| return |
|
|
| documents = docs_data.get("documents", []) |
| total = docs_data.get("total", 0) |
|
|
| st.markdown(f'<div style="font-size:0.78rem;color:#4a4a6a;margin-bottom:1rem;">{total} document(s) found</div>', unsafe_allow_html=True) |
|
|
| if not documents: |
| st.markdown('<div class="alert-info">No documents indexed yet.</div>', unsafe_allow_html=True) |
| return |
|
|
| if "del_confirm" not in st.session_state: |
| st.session_state.del_confirm = {} |
|
|
| st.markdown('<div class="card" style="padding:0;">', unsafe_allow_html=True) |
| st.markdown(""" |
| <div class="table-header"> |
| <span style="flex:3;">Filename</span> |
| <span style="flex:2;">Collection</span> |
| <span style="flex:2;">Doc ID</span> |
| <span style="flex:1;text-align:center;">Parents</span> |
| <span style="flex:1;text-align:center;">Action</span> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| for doc in documents: |
| doc_id = doc.get("doc_id", "") |
| filename = doc.get("filename", "unknown") |
| collection = doc.get("collection", "general") |
| parent_count = doc.get("parent_count", 0) |
| ext = "MD" if filename.endswith(".md") else "TXT" |
| badge_color = "badge-blue" if ext == "MD" else "badge-gray" |
|
|
| c1, c2, c3, c4, c5 = st.columns([3, 2, 2, 1, 1]) |
| with c1: |
| st.markdown(f'<span class="badge {badge_color}">{ext}</span> <span style="font-size:0.85rem;color:#e2e8f0;">{filename}</span>', unsafe_allow_html=True) |
| with c2: |
| st.markdown(f'<span style="font-size:0.85rem;color:#6b7280;">{collection}</span>', unsafe_allow_html=True) |
| with c3: |
| st.markdown(f'<code style="font-size:0.72rem;color:#4a4a6a;">{doc_id[:16]}...</code>', unsafe_allow_html=True) |
| with c4: |
| st.markdown(f'<div style="text-align:center;font-size:0.875rem;color:#e2e8f0;font-weight:600;">{parent_count}</div>', unsafe_allow_html=True) |
| with c5: |
| if st.session_state.del_confirm.get(doc_id): |
| if st.button("Confirm", key=f"confirm_{doc_id}", use_container_width=True): |
| ok, _ = api_delete(f"/delete/{doc_id}") |
| if ok: |
| st.session_state.del_confirm[doc_id] = False |
| st.rerun() |
| else: |
| st.markdown('<div class="alert-error">Delete failed.</div>', unsafe_allow_html=True) |
| st.session_state.del_confirm[doc_id] = False |
| else: |
| if st.button("Delete", key=f"del_{doc_id}", use_container_width=True): |
| st.session_state.del_confirm[doc_id] = True |
| st.rerun() |
|
|
| st.markdown("<hr style='margin:0.1rem 0;'>", unsafe_allow_html=True) |
|
|
| st.markdown("</div>", unsafe_allow_html=True) |
|
|
|
|
| def page_analytics(stats: dict): |
| st.markdown('<div class="page-title">Analytics</div>', unsafe_allow_html=True) |
| st.markdown('<div class="page-subtitle">Performance insights, usage trends, and system health reports.</div>', unsafe_allow_html=True) |
|
|
| queries_data = stats.get("queries", {}) |
| top_queries = stats.get("top_queries", []) |
| storage = stats.get("storage", {}) |
| documents = stats.get("documents", {}) |
|
|
| c1, c2, c3, c4, c5 = st.columns(5) |
| for col, val, label in [ |
| (c1, queries_data.get("total_logged", 0), "Total Logged"), |
| (c2, queries_data.get("today", 0), "Today"), |
| (c3, queries_data.get("cache_size", 0), "Cache Size"), |
| (c4, f"{storage.get('used_mb', 0):.1f}", "Storage MB"), |
| (c5, f"{storage.get('percentage', 0):.1f}%", "Usage"), |
| ]: |
| with col: |
| st.markdown(f""" |
| <div class="card" style="text-align:center;padding:0.8rem;"> |
| <div style="font-size:1.5rem;font-weight:800;color:{COLOR_PRIMARY};">{val}</div> |
| <div style="font-size:0.7rem;color:#4a4a6a;text-transform:uppercase;letter-spacing:1px;">{label}</div> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| st.markdown("<br>", unsafe_allow_html=True) |
| row1_l, row1_r = st.columns(2) |
|
|
| with row1_l: |
| st.markdown('<div style="font-size:0.8rem;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:1px;margin-bottom:0.5rem;">Top Queries — Frequency</div>', unsafe_allow_html=True) |
| if top_queries: |
| df_q = pd.DataFrame(top_queries) |
| df_q.columns = ["Query", "Count"] |
| df_q["Query"] = df_q["Query"].str[:35] |
| fig = px.bar(df_q.head(10), x="Count", y="Query", orientation="h", color="Count", color_continuous_scale=["#1e1e2e", COLOR_PRIMARY]) |
| fig.update_layout(**PLOTLY_LAYOUT, height=320, coloraxis_showscale=False) |
| fig.update_traces(marker_line_width=0) |
| st.plotly_chart(fig, use_container_width=True) |
| else: |
| st.markdown('<div class="alert-info">No query data yet.</div>', unsafe_allow_html=True) |
|
|
| with row1_r: |
| st.markdown('<div style="font-size:0.8rem;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:1px;margin-bottom:0.5rem;">Storage Distribution</div>', unsafe_allow_html=True) |
| child_count = documents.get("total_children", 0) |
| parent_count = documents.get("total_parents", 0) |
| used_mb = storage.get("used_mb", 0) |
| free_mb = max(storage.get("total_mb", 1024) - used_mb, 0) |
| if child_count + parent_count > 0: |
| fig_pie = go.Figure(go.Pie( |
| labels=["Child Vectors", "Parent Texts", "Free"], |
| values=[child_count * 3, parent_count * 1, max(free_mb, 1)], |
| hole=0.55, |
| marker=dict(colors=[COLOR_PRIMARY, COLOR_SECONDARY, "#1e1e2e"]), |
| textfont=dict(color="#6b7280", size=11), |
| )) |
| fig_pie.update_layout(**PLOTLY_LAYOUT, height=320) |
| fig_pie.update_traces(textposition="outside") |
| st.plotly_chart(fig_pie, use_container_width=True) |
| else: |
| st.markdown('<div class="alert-info">No storage data yet.</div>', unsafe_allow_html=True) |
|
|
| row2_l, row2_r = st.columns(2) |
|
|
| with row2_l: |
| st.markdown('<div style="font-size:0.8rem;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:1px;margin-bottom:0.5rem;">Chunk Ratio Analysis</div>', unsafe_allow_html=True) |
| if parent_count > 0: |
| ratio = child_count / parent_count |
| fig_bar = go.Figure() |
| fig_bar.add_trace(go.Bar( |
| x=["Parent Chunks", "Child Chunks"], |
| y=[parent_count, child_count], |
| marker_color=[COLOR_SECONDARY, COLOR_PRIMARY], |
| marker_line_width=0, |
| text=[f"{parent_count:,}", f"{child_count:,}"], |
| textposition="outside", |
| textfont=dict(color="#6b7280", size=11), |
| )) |
| fig_bar.update_layout(**PLOTLY_LAYOUT, height=280, showlegend=False) |
| st.plotly_chart(fig_bar, use_container_width=True) |
| st.markdown(f""" |
| <div class="card" style="text-align:center;padding:0.7rem;"> |
| <span style="font-size:0.78rem;color:#4a4a6a;">Avg children per parent: </span> |
| <span style="font-size:1rem;font-weight:700;color:{COLOR_PRIMARY};">{ratio:.1f}x</span> |
| </div> |
| """, unsafe_allow_html=True) |
| else: |
| st.markdown('<div class="alert-info">No chunk data yet.</div>', unsafe_allow_html=True) |
|
|
| with row2_r: |
| st.markdown('<div style="font-size:0.8rem;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:1px;margin-bottom:0.5rem;">Cache vs Live Queries</div>', unsafe_allow_html=True) |
| total_logged = queries_data.get("total_logged", 0) |
| cache_size = queries_data.get("cache_size", 0) |
| live_queries = max(total_logged - cache_size, 0) |
| if total_logged > 0: |
| fig_donut = go.Figure(go.Pie( |
| labels=["Live (Computed)", "Cached (Fast)"], |
| values=[live_queries, cache_size], |
| hole=0.6, |
| marker=dict(colors=[COLOR_PRIMARY, COLOR_SUCCESS]), |
| textfont=dict(color="#6b7280", size=11), |
| )) |
| fig_donut.update_layout(**PLOTLY_LAYOUT, height=280) |
| fig_donut.update_traces(textposition="outside") |
| st.plotly_chart(fig_donut, use_container_width=True) |
| hit_rate = (cache_size / total_logged * 100) if total_logged > 0 else 0 |
| st.markdown(f""" |
| <div class="card" style="text-align:center;padding:0.7rem;"> |
| <span style="font-size:0.78rem;color:#4a4a6a;">Cache hit rate: </span> |
| <span style="font-size:1rem;font-weight:700;color:{COLOR_SUCCESS};">{hit_rate:.1f}%</span> |
| </div> |
| """, unsafe_allow_html=True) |
| else: |
| st.markdown('<div class="alert-info">No query data yet.</div>', unsafe_allow_html=True) |
|
|
| st.markdown("<br>", unsafe_allow_html=True) |
| st.markdown('<div style="font-size:0.8rem;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:1px;margin-bottom:0.5rem;">Storage Utilization</div>', unsafe_allow_html=True) |
| pct = storage.get("percentage", 0) |
| fig_bullet = go.Figure(go.Indicator( |
| mode="number+gauge+delta", |
| value=pct, |
| delta={"reference": 70, "increasing": {"color": COLOR_PRIMARY}, "decreasing": {"color": COLOR_SUCCESS}}, |
| number={"suffix": "%", "font": {"color": COLOR_PRIMARY, "size": 28}}, |
| gauge={ |
| "shape": "bullet", |
| "axis": {"range": [0, 100], "tickfont": {"color": "#4a4a6a"}}, |
| "threshold": {"line": {"color": COLOR_PRIMARY, "width": 2}, "thickness": 0.75, "value": 90}, |
| "bgcolor": "#0a0a0f", |
| "steps": [ |
| {"range": [0, 60], "color": "#0d1117"}, |
| {"range": [60, 80], "color": "#111827"}, |
| {"range": [80, 100], "color": "#1a0a0a"}, |
| ], |
| "bar": {"color": COLOR_PRIMARY}, |
| }, |
| title={"text": f"Used {storage.get('used_mb',0):.1f} MB of {storage.get('total_mb',1024)} MB", "font": {"color": "#4a4a6a", "size": 12}}, |
| )) |
| fig_bullet.update_layout(**PLOTLY_LAYOUT, height=160) |
| st.plotly_chart(fig_bullet, use_container_width=True) |
|
|
| st.markdown("<br>", unsafe_allow_html=True) |
| st.markdown('<div style="font-size:0.8rem;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:1px;margin-bottom:0.8rem;">Engine Configuration</div>', unsafe_allow_html=True) |
| config_items = [ |
| ("Embedding Model", "LazarusNLP/all-indobert-base-v2"), |
| ("Reranking Model", "cross-encoder/ms-marco-MiniLM-L-6-v2"), |
| ("Vector Dimension", "768"), |
| ("Parent Chunk", "1500 chars"), |
| ("Child Chunk", "500 chars"), |
| ("Child Overlap", "50 chars"), |
| ("Rerank Candidates", "20"), |
| ("Cache TTL", "3600s"), |
| ("Rate Limit", "3000 req/hr"), |
| ("Max File Size", "50 MB"), |
| ] |
| cfg_c1, cfg_c2 = st.columns(2) |
| for i, (key, val) in enumerate(config_items): |
| col = cfg_c1 if i % 2 == 0 else cfg_c2 |
| with col: |
| st.markdown(f""" |
| <div style="display:flex;justify-content:space-between;align-items:center; |
| padding:0.45rem 0.8rem;border-radius:6px;margin-bottom:0.3rem; |
| background:{'#0d0d18' if i//2%2==0 else '#0a0a0f'};"> |
| <span style="font-size:0.82rem;color:#6b7280;">{key}</span> |
| <code style="font-size:0.82rem;color:{COLOR_PRIMARY};background:rgba(233,69,96,0.08); |
| padding:0.1rem 0.4rem;border-radius:4px;">{val}</code> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
|
|
| def main(): |
| if not RAG_API_URL or not RAG_API_KEY: |
| st.markdown('<div class="alert-error">RAG_API_URL and RAG_API_KEY secrets not configured.</div>', unsafe_allow_html=True) |
| return |
|
|
| if not APP_PASSWORD: |
| st.markdown('<div class="alert-error">PASSWORD secret not configured.</div>', unsafe_allow_html=True) |
| return |
|
|
| get_cookie_manager() |
|
|
| if "authenticated" not in st.session_state: |
| st.session_state.authenticated = False |
| if "login_error" not in st.session_state: |
| st.session_state.login_error = "" |
| if "current_page" not in st.session_state: |
| st.session_state.current_page = "Overview" |
| if "del_confirm" not in st.session_state: |
| st.session_state.del_confirm = {} |
|
|
| if not st.session_state.authenticated: |
| if check_auth(): |
| st.session_state.authenticated = True |
| else: |
| render_login() |
| return |
|
|
| health = get_health() |
| ok_stats, stats = api_get("/stats") |
| stats = stats if ok_stats else {} |
|
|
| page = render_sidebar(health) |
|
|
| if page == "Overview": |
| page_overview(stats, health) |
| elif page == "Upload": |
| page_upload() |
| elif page == "Test Search": |
| page_test_search() |
| elif page == "Documents": |
| page_documents() |
| elif page == "Analytics": |
| page_analytics(stats) |
|
|
|
|
| if __name__ == "__main__": |
| main() |