ndash / app.py
kylsprt's picture
Update app.py
ddefba7 verified
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 &mdash; 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')} &middot; {src.get('collection','unknown')} &middot; {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()