Spaces:
Sleeping
Sleeping
Vineetiitg commited on
Commit ·
fcd33ac
1
Parent(s): d816f3a
feat(ui): revamp frontend with glassmorphism styling, role-based observability tabs, and live chat termination
Browse files- ui/app.py +322 -41
- ui/styles.css +206 -0
ui/app.py
CHANGED
|
@@ -3,9 +3,26 @@ import time
|
|
| 3 |
from pathlib import Path
|
| 4 |
import requests
|
| 5 |
import streamlit as st
|
|
|
|
| 6 |
|
| 7 |
-
st.set_page_config(page_title="Support Docs Copilot", page_icon="
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
BACKEND_BASE_URL = os.getenv("BACKEND_BASE_URL", "http://127.0.0.1:8000")
|
| 11 |
BACKEND_STREAM_URL = os.getenv("BACKEND_URL", f"{BACKEND_BASE_URL}/chat/stream")
|
|
@@ -19,6 +36,8 @@ if "role" not in st.session_state:
|
|
| 19 |
st.session_state.role = ""
|
| 20 |
if "username" not in st.session_state:
|
| 21 |
st.session_state.username = ""
|
|
|
|
|
|
|
| 22 |
|
| 23 |
|
| 24 |
def headers() -> dict:
|
|
@@ -41,18 +60,21 @@ def post_json(path: str, payload: dict | None = None) -> dict:
|
|
| 41 |
|
| 42 |
def poll_job_status(job_id: str, status_text: str = "Processing in background..."):
|
| 43 |
with st.status(status_text, expanded=True) as status:
|
| 44 |
-
st.write("Job enqueued in Redis...")
|
|
|
|
| 45 |
for _ in range(120):
|
| 46 |
try:
|
| 47 |
res = get_json(f"/tasks/status/{job_id}")
|
| 48 |
job_status = res.get("status", "unknown")
|
| 49 |
-
|
| 50 |
if err_msg := res.get("error"):
|
| 51 |
st.error(f"Error details: {err_msg}")
|
| 52 |
if job_status in ("complete", "success"):
|
|
|
|
| 53 |
status.update(label="Job Completed Successfully!", state="complete", expanded=False)
|
| 54 |
return res.get("result")
|
| 55 |
elif job_status in ("not_found", "error", "failed", "error_try_again"):
|
|
|
|
| 56 |
status.update(label=f"Job Finished ({job_status})", state="complete" if job_status == "complete" else "error", expanded=True)
|
| 57 |
return res.get("result")
|
| 58 |
except Exception:
|
|
@@ -64,12 +86,24 @@ def poll_job_status(job_id: str, status_text: str = "Processing in background...
|
|
| 64 |
|
| 65 |
# Authentication Sidebar
|
| 66 |
with st.sidebar:
|
| 67 |
-
st.
|
| 68 |
if st.session_state.token and st.session_state.role:
|
| 69 |
if st.session_state.role == "admin":
|
| 70 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
else:
|
| 72 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
if st.button("🚪 Logout", use_container_width=True):
|
| 74 |
st.session_state.token = ""
|
| 75 |
st.session_state.role = ""
|
|
@@ -77,8 +111,8 @@ with st.sidebar:
|
|
| 77 |
st.rerun()
|
| 78 |
else:
|
| 79 |
st.write("Login to access role-specific UI features.")
|
| 80 |
-
username_input = st.text_input("Username", key="sb_user")
|
| 81 |
-
password_input = st.text_input("Password", type="password", key="sb_pass")
|
| 82 |
if st.button("��� Login", use_container_width=True, type="primary"):
|
| 83 |
try:
|
| 84 |
response = requests.post(
|
|
@@ -98,22 +132,87 @@ with st.sidebar:
|
|
| 98 |
st.error(f"Login request failed: {exc}")
|
| 99 |
|
| 100 |
st.divider()
|
| 101 |
-
st.caption(f"Backend
|
| 102 |
try:
|
| 103 |
ready_data = get_json("/ready")
|
| 104 |
if ready_data.get("ready"):
|
| 105 |
-
st.
|
| 106 |
else:
|
| 107 |
-
st.
|
| 108 |
except Exception:
|
| 109 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
|
| 112 |
# Tab Renderers
|
| 113 |
def render_chat_tab():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
for message in st.session_state.messages:
|
| 115 |
with st.chat_message(message["role"]):
|
| 116 |
st.markdown(message["content"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
if user_query := st.chat_input("Ask a support question..."):
|
| 119 |
with st.chat_message("user"):
|
|
@@ -124,7 +223,7 @@ def render_chat_tab():
|
|
| 124 |
try:
|
| 125 |
response = requests.post(
|
| 126 |
f"{BACKEND_BASE_URL}/chat/stream",
|
| 127 |
-
json={"query": user_query, "chat_history": st.session_state.messages[:-1]},
|
| 128 |
headers=headers(),
|
| 129 |
stream=True,
|
| 130 |
timeout=120,
|
|
@@ -142,35 +241,82 @@ def render_chat_tab():
|
|
| 142 |
break
|
| 143 |
placeholder.markdown(full_answer + "▌")
|
| 144 |
placeholder.markdown(full_answer)
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
except requests.RequestException as exc:
|
| 147 |
st.error(f"Chat request failed: {exc}")
|
| 148 |
|
| 149 |
|
| 150 |
def render_documents_tab():
|
| 151 |
-
st.
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
try:
|
| 155 |
data = get_json("/documents")
|
| 156 |
documents = data.get("documents", [])
|
| 157 |
if not documents:
|
| 158 |
-
st.info("
|
| 159 |
else:
|
| 160 |
for doc in documents:
|
| 161 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
except requests.RequestException as exc:
|
| 163 |
st.error(f"Failed to load documents: {exc}")
|
| 164 |
|
| 165 |
|
| 166 |
def render_admin_portal_tab():
|
| 167 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
uploaded_files = st.file_uploader(
|
| 169 |
-
"
|
| 170 |
type=["txt", "md", "pdf", "docx", "html", "htm"],
|
| 171 |
accept_multiple_files=True,
|
| 172 |
)
|
| 173 |
-
if uploaded_files and st.button("Save
|
| 174 |
try:
|
| 175 |
files = [("files", (file.name, file.getvalue(), file.type or "application/octet-stream")) for file in uploaded_files]
|
| 176 |
response = requests.post(
|
|
@@ -180,35 +326,79 @@ def render_admin_portal_tab():
|
|
| 180 |
timeout=120,
|
| 181 |
)
|
| 182 |
response.raise_for_status()
|
| 183 |
-
st.success(f"
|
| 184 |
st.json(response.json())
|
| 185 |
except requests.RequestException as exc:
|
| 186 |
st.error(f"Upload failed: {exc}")
|
|
|
|
| 187 |
|
| 188 |
-
st.
|
|
|
|
| 189 |
col1, col2 = st.columns(2)
|
| 190 |
with col1:
|
| 191 |
-
force = st.checkbox("Force recreate index")
|
| 192 |
-
if st.button("Run
|
| 193 |
try:
|
| 194 |
res = post_json("/admin/ingest", {"data_dir": DATA_DIR, "force": force})
|
| 195 |
st.json(res)
|
| 196 |
if job_id := res.get("job_id"):
|
| 197 |
-
poll_job_status(job_id, "Ingesting documents via Arq Worker...")
|
|
|
|
|
|
|
| 198 |
except requests.RequestException as exc:
|
| 199 |
st.error(f"Ingestion failed: {exc}")
|
| 200 |
with col2:
|
| 201 |
-
|
|
|
|
| 202 |
try:
|
| 203 |
st.json(post_json("/admin/reset"))
|
|
|
|
| 204 |
except requests.RequestException as exc:
|
| 205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
|
| 207 |
|
| 208 |
def render_evaluation_tab():
|
| 209 |
-
st.
|
| 210 |
st.write("Evaluate how accurately and faithfully the copilot answers support questions using the RAGAS framework.")
|
| 211 |
|
|
|
|
| 212 |
if st.button("🚀 Run RAG Evaluation Now", type="primary"):
|
| 213 |
try:
|
| 214 |
res = post_json("/admin/eval")
|
|
@@ -218,34 +408,115 @@ def render_evaluation_tab():
|
|
| 218 |
st.success("Evaluation task dispatched!")
|
| 219 |
except requests.RequestException as exc:
|
| 220 |
st.error(f"Evaluation failed: {exc}. Ensure you have remaining OpenRouter credits/limits.")
|
|
|
|
| 221 |
|
| 222 |
st.divider()
|
| 223 |
-
st.
|
| 224 |
try:
|
| 225 |
res = get_json("/admin/eval")
|
| 226 |
-
|
|
|
|
| 227 |
except requests.RequestException:
|
| 228 |
-
st.info("No evaluation report available yet. Click the button above to run your first evaluation!")
|
| 229 |
|
| 230 |
|
| 231 |
def render_langsmith_tab():
|
| 232 |
-
st.
|
| 233 |
st.write("Monitor RAG agent steps, prompt tokens, and latency in real-time by adding these variables to your `.env`:")
|
| 234 |
st.code("LANGCHAIN_TRACING_V2=true\nLANGCHAIN_API_KEY=your_langsmith_api_key\nLANGCHAIN_PROJECT=\"Support Docs Copilot\"", language="env")
|
| 235 |
|
| 236 |
st.divider()
|
| 237 |
-
st.
|
| 238 |
-
st.caption(f"Backend
|
| 239 |
try:
|
| 240 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
except requests.RequestException as exc:
|
| 242 |
st.error(f"Readiness check failed: {exc}")
|
| 243 |
|
| 244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
# Render Role-Specific UI Layouts
|
| 246 |
if st.session_state.role == "admin":
|
| 247 |
-
chat_tab, docs_tab, admin_tab, eval_tab, langsmith_tab = st.tabs([
|
| 248 |
"💬 Chat Copilot",
|
|
|
|
| 249 |
"📚 Documents",
|
| 250 |
"🛠️ Admin Portal",
|
| 251 |
"📊 RAGAS Evaluation",
|
|
@@ -253,6 +524,8 @@ if st.session_state.role == "admin":
|
|
| 253 |
])
|
| 254 |
with chat_tab:
|
| 255 |
render_chat_tab()
|
|
|
|
|
|
|
| 256 |
with docs_tab:
|
| 257 |
render_documents_tab()
|
| 258 |
with admin_tab:
|
|
@@ -272,9 +545,17 @@ else:
|
|
| 272 |
with docs_tab:
|
| 273 |
render_documents_tab()
|
| 274 |
with status_tab:
|
| 275 |
-
st.
|
| 276 |
try:
|
| 277 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
except requests.RequestException as exc:
|
| 279 |
st.error(f"Readiness check failed: {exc}")
|
| 280 |
st.divider()
|
|
|
|
| 3 |
from pathlib import Path
|
| 4 |
import requests
|
| 5 |
import streamlit as st
|
| 6 |
+
import uuid
|
| 7 |
|
| 8 |
+
st.set_page_config(page_title="Support Docs Copilot", page_icon="🚀", layout="wide")
|
| 9 |
+
|
| 10 |
+
def load_css():
|
| 11 |
+
css_path = Path(__file__).parent / "styles.css"
|
| 12 |
+
if css_path.exists():
|
| 13 |
+
st.markdown(f"<style>{css_path.read_text(encoding='utf-8')}</style>", unsafe_allow_html=True)
|
| 14 |
+
|
| 15 |
+
load_css()
|
| 16 |
+
|
| 17 |
+
# Header & Banner
|
| 18 |
+
st.markdown("""
|
| 19 |
+
<div style="padding: 0.5rem 0; margin-bottom: 1rem;">
|
| 20 |
+
<h1 style="font-size: 2.6rem; margin: 0;">🚀 Support Docs Copilot</h1>
|
| 21 |
+
<p style="color: #94a3b8; font-size: 1.05rem; margin-top: 0.2rem;">
|
| 22 |
+
Next-Gen Agentic RAG Assistant powered by LangGraph, Speculative Retrieval & Cohere Reranking
|
| 23 |
+
</p>
|
| 24 |
+
</div>
|
| 25 |
+
""", unsafe_allow_html=True)
|
| 26 |
|
| 27 |
BACKEND_BASE_URL = os.getenv("BACKEND_BASE_URL", "http://127.0.0.1:8000")
|
| 28 |
BACKEND_STREAM_URL = os.getenv("BACKEND_URL", f"{BACKEND_BASE_URL}/chat/stream")
|
|
|
|
| 36 |
st.session_state.role = ""
|
| 37 |
if "username" not in st.session_state:
|
| 38 |
st.session_state.username = ""
|
| 39 |
+
if "session_id" not in st.session_state:
|
| 40 |
+
st.session_state.session_id = str(uuid.uuid4())
|
| 41 |
|
| 42 |
|
| 43 |
def headers() -> dict:
|
|
|
|
| 60 |
|
| 61 |
def poll_job_status(job_id: str, status_text: str = "Processing in background..."):
|
| 62 |
with st.status(status_text, expanded=True) as status:
|
| 63 |
+
st.write("Job enqueued in Redis worker queue...")
|
| 64 |
+
status_placeholder = st.empty()
|
| 65 |
for _ in range(120):
|
| 66 |
try:
|
| 67 |
res = get_json(f"/tasks/status/{job_id}")
|
| 68 |
job_status = res.get("status", "unknown")
|
| 69 |
+
status_placeholder.markdown(f"Status: **{job_status}** ⏳")
|
| 70 |
if err_msg := res.get("error"):
|
| 71 |
st.error(f"Error details: {err_msg}")
|
| 72 |
if job_status in ("complete", "success"):
|
| 73 |
+
status_placeholder.markdown("Status: **Complete** ✅")
|
| 74 |
status.update(label="Job Completed Successfully!", state="complete", expanded=False)
|
| 75 |
return res.get("result")
|
| 76 |
elif job_status in ("not_found", "error", "failed", "error_try_again"):
|
| 77 |
+
status_placeholder.markdown(f"Status: **{job_status}** ❌")
|
| 78 |
status.update(label=f"Job Finished ({job_status})", state="complete" if job_status == "complete" else "error", expanded=True)
|
| 79 |
return res.get("result")
|
| 80 |
except Exception:
|
|
|
|
| 86 |
|
| 87 |
# Authentication Sidebar
|
| 88 |
with st.sidebar:
|
| 89 |
+
st.markdown("### 🔐 Authentication")
|
| 90 |
if st.session_state.token and st.session_state.role:
|
| 91 |
if st.session_state.role == "admin":
|
| 92 |
+
st.markdown(f"""
|
| 93 |
+
<div class="glass-card" style="padding: 1rem; border-left: 4px solid #a855f7;">
|
| 94 |
+
<p style="margin: 0; font-size: 0.9rem; color: #cbd5e1;">Logged in as:</p>
|
| 95 |
+
<p style="margin: 0; font-size: 1.1rem; font-weight: 600; color: #f8fafc;">👑 {st.session_state.username or 'Admin'}</p>
|
| 96 |
+
<span class="status-badge badge-purple" style="margin-top: 0.5rem;">Administrator</span>
|
| 97 |
+
</div>
|
| 98 |
+
""", unsafe_allow_html=True)
|
| 99 |
else:
|
| 100 |
+
st.markdown(f"""
|
| 101 |
+
<div class="glass-card" style="padding: 1rem; border-left: 4px solid #60a5fa;">
|
| 102 |
+
<p style="margin: 0; font-size: 0.9rem; color: #cbd5e1;">Logged in as:</p>
|
| 103 |
+
<p style="margin: 0; font-size: 1.1rem; font-weight: 600; color: #f8fafc;">👤 {st.session_state.username or 'User'}</p>
|
| 104 |
+
<span class="status-badge badge-blue" style="margin-top: 0.5rem;">User</span>
|
| 105 |
+
</div>
|
| 106 |
+
""", unsafe_allow_html=True)
|
| 107 |
if st.button("🚪 Logout", use_container_width=True):
|
| 108 |
st.session_state.token = ""
|
| 109 |
st.session_state.role = ""
|
|
|
|
| 111 |
st.rerun()
|
| 112 |
else:
|
| 113 |
st.write("Login to access role-specific UI features.")
|
| 114 |
+
username_input = st.text_input("Username", key="sb_user", placeholder="admin or user")
|
| 115 |
+
password_input = st.text_input("Password", type="password", key="sb_pass", placeholder="••••••••")
|
| 116 |
if st.button("��� Login", use_container_width=True, type="primary"):
|
| 117 |
try:
|
| 118 |
response = requests.post(
|
|
|
|
| 132 |
st.error(f"Login request failed: {exc}")
|
| 133 |
|
| 134 |
st.divider()
|
| 135 |
+
st.caption(f"🔗 Backend API: `{BACKEND_BASE_URL}`")
|
| 136 |
try:
|
| 137 |
ready_data = get_json("/ready")
|
| 138 |
if ready_data.get("ready"):
|
| 139 |
+
st.markdown('<span class="status-badge badge-green">🟢 System Online</span>', unsafe_allow_html=True)
|
| 140 |
else:
|
| 141 |
+
st.markdown('<span class="status-badge badge-yellow">🟡 System Degraded</span>', unsafe_allow_html=True)
|
| 142 |
except Exception:
|
| 143 |
+
st.markdown('<span class="status-badge" style="background: rgba(239,68,68,0.2); color: #f87171; border: 1px solid #ef4444;">🔴 Backend Offline</span>', unsafe_allow_html=True)
|
| 144 |
+
|
| 145 |
+
st.divider()
|
| 146 |
+
st.markdown("### 💬 Recent Chats")
|
| 147 |
+
if st.button("➕ New Support Topic", use_container_width=True, type="primary", key="new_chat_btn"):
|
| 148 |
+
st.session_state.session_id = str(uuid.uuid4())
|
| 149 |
+
st.session_state.messages = []
|
| 150 |
+
st.rerun()
|
| 151 |
+
|
| 152 |
+
if not st.session_state.token:
|
| 153 |
+
st.info("🔒 **Not logged in.** Please login above to access and resume your saved recent chat history.")
|
| 154 |
+
else:
|
| 155 |
+
if st.session_state.role == "admin":
|
| 156 |
+
st.caption(f"Showing saved sessions for **👑 Admin ({st.session_state.username})**")
|
| 157 |
+
else:
|
| 158 |
+
st.caption(f"Showing saved sessions for **👤 {st.session_state.username}**")
|
| 159 |
+
|
| 160 |
+
try:
|
| 161 |
+
sessions_res = get_json("/api/v1/sessions")
|
| 162 |
+
sessions_list = sessions_res.get("sessions", [])
|
| 163 |
+
if not sessions_list:
|
| 164 |
+
st.caption("No recent sessions found for your account.")
|
| 165 |
+
else:
|
| 166 |
+
for s in sessions_list[:10]:
|
| 167 |
+
sid = s.get("session_id", "default")
|
| 168 |
+
preview = s.get("last_preview", sid[:8] + "...")
|
| 169 |
+
btn_label = f"💬 {preview}" if sid != st.session_state.get("session_id") else f"🟢 {preview}"
|
| 170 |
+
if st.button(btn_label, key=f"sess_{sid}", use_container_width=True):
|
| 171 |
+
st.session_state.session_id = sid
|
| 172 |
+
msg_res = get_json(f"/api/v1/sessions/{sid}/messages")
|
| 173 |
+
st.session_state.messages = msg_res.get("messages", [])
|
| 174 |
+
st.rerun()
|
| 175 |
+
except Exception:
|
| 176 |
+
st.caption("Could not load sessions.")
|
| 177 |
|
| 178 |
|
| 179 |
# Tab Renderers
|
| 180 |
def render_chat_tab():
|
| 181 |
+
col_stop, col_info = st.columns([1, 4])
|
| 182 |
+
with col_stop:
|
| 183 |
+
if st.button("🛑 Stop Generation", key="term_btn_top", use_container_width=True):
|
| 184 |
+
if sid := st.session_state.get("session_id"):
|
| 185 |
+
try:
|
| 186 |
+
post_json(f"/api/v1/sessions/{sid}/terminate")
|
| 187 |
+
st.toast("🛑 Sent termination signal to active generation!")
|
| 188 |
+
except Exception:
|
| 189 |
+
pass
|
| 190 |
+
with col_info:
|
| 191 |
+
st.caption("💡 Tip: Click **🛑 Stop Generation** anytime during text output to immediately terminate an ongoing chat response.")
|
| 192 |
+
|
| 193 |
+
st.divider()
|
| 194 |
+
|
| 195 |
for message in st.session_state.messages:
|
| 196 |
with st.chat_message(message["role"]):
|
| 197 |
st.markdown(message["content"])
|
| 198 |
+
if message["role"] == "assistant":
|
| 199 |
+
conf = message.get("confidence")
|
| 200 |
+
sources = message.get("sources")
|
| 201 |
+
if conf is not None or sources:
|
| 202 |
+
cols = st.columns([1, 4])
|
| 203 |
+
with cols[0]:
|
| 204 |
+
if conf is not None:
|
| 205 |
+
badge_class = "badge-green" if conf >= 0.8 else ("badge-yellow" if conf >= 0.5 else "badge-purple")
|
| 206 |
+
st.markdown(f'<span class="status-badge {badge_class}">🎯 Conf: {conf*100:.0f}%</span>', unsafe_allow_html=True)
|
| 207 |
+
with cols[1]:
|
| 208 |
+
if sources:
|
| 209 |
+
with st.expander(f"📚 Cited Sources ({len(sources)} documents referenced)"):
|
| 210 |
+
for idx, src in enumerate(sources, 1):
|
| 211 |
+
src_name = src.get("source", src.get("doc_id", "Unknown Document"))
|
| 212 |
+
score = src.get("relevance_score", src.get("similarity_score", 0.0))
|
| 213 |
+
st.markdown(f"**{idx}. {src_name}** `(Relevance Score: {score:.2f})`")
|
| 214 |
+
if snippet := src.get("content_snippet"):
|
| 215 |
+
st.caption(f'"{snippet[:200]}..."')
|
| 216 |
|
| 217 |
if user_query := st.chat_input("Ask a support question..."):
|
| 218 |
with st.chat_message("user"):
|
|
|
|
| 223 |
try:
|
| 224 |
response = requests.post(
|
| 225 |
f"{BACKEND_BASE_URL}/chat/stream",
|
| 226 |
+
json={"query": user_query, "chat_history": st.session_state.messages[:-1], "session_id": st.session_state.get("session_id")},
|
| 227 |
headers=headers(),
|
| 228 |
stream=True,
|
| 229 |
timeout=120,
|
|
|
|
| 241 |
break
|
| 242 |
placeholder.markdown(full_answer + "▌")
|
| 243 |
placeholder.markdown(full_answer)
|
| 244 |
+
|
| 245 |
+
# Append locally immediately so output ALWAYS displays on screen
|
| 246 |
+
new_msg = {"role": "assistant", "content": full_answer}
|
| 247 |
+
st.session_state.messages.append(new_msg)
|
| 248 |
+
|
| 249 |
+
# Fetch updated session messages from backend ONLY if backend has more or equal messages (preventing erasure)
|
| 250 |
+
time.sleep(0.35)
|
| 251 |
+
sid = st.session_state.get("session_id")
|
| 252 |
+
if sid:
|
| 253 |
+
try:
|
| 254 |
+
res_msgs = get_json(f"/api/v1/sessions/{sid}/messages")
|
| 255 |
+
if res_msgs and (msgs := res_msgs.get("messages")) and len(msgs) >= len(st.session_state.messages):
|
| 256 |
+
st.session_state.messages = msgs
|
| 257 |
+
except Exception:
|
| 258 |
+
pass
|
| 259 |
+
st.rerun()
|
| 260 |
except requests.RequestException as exc:
|
| 261 |
st.error(f"Chat request failed: {exc}")
|
| 262 |
|
| 263 |
|
| 264 |
def render_documents_tab():
|
| 265 |
+
st.markdown("### 📚 Indexed Knowledge Base")
|
| 266 |
+
st.write("Manage your RAG vector store documents. You can inspect chunk counts, content hashes, or remove individual files.")
|
| 267 |
+
|
| 268 |
+
col_ref, col_spacer = st.columns([1, 5])
|
| 269 |
+
with col_ref:
|
| 270 |
+
if st.button("🔄 Refresh List", key="ref_docs", use_container_width=True):
|
| 271 |
+
st.rerun()
|
| 272 |
+
|
| 273 |
try:
|
| 274 |
data = get_json("/documents")
|
| 275 |
documents = data.get("documents", [])
|
| 276 |
if not documents:
|
| 277 |
+
st.info("💡 Knowledge base is currently empty. Login as an Admin and go to **🛠️ Admin Portal** to ingest documents.")
|
| 278 |
else:
|
| 279 |
for doc in documents:
|
| 280 |
+
with st.container():
|
| 281 |
+
st.markdown(f"""
|
| 282 |
+
<div class="glass-card" style="padding: 1.2rem; margin-bottom: 0.6rem;">
|
| 283 |
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
| 284 |
+
<div>
|
| 285 |
+
<h4 style="margin: 0; color: #60a5fa;">📄 {doc.get('source')}</h4>
|
| 286 |
+
<p style="margin: 0.3rem 0 0 0; color: #94a3b8; font-size: 0.85rem;">
|
| 287 |
+
<b>ID:</b> <code>{doc.get('doc_id')}</code> | <b>Chunks:</b> <span class="status-badge badge-blue">{doc.get('chunk_count')} chunks</span> | <b>Hash:</b> <code>{str(doc.get('content_hash'))[:10]}...</code>
|
| 288 |
+
</p>
|
| 289 |
+
</div>
|
| 290 |
+
</div>
|
| 291 |
+
</div>
|
| 292 |
+
""", unsafe_allow_html=True)
|
| 293 |
+
if st.session_state.role == "admin":
|
| 294 |
+
col_del, col_blank = st.columns([1, 5])
|
| 295 |
+
with col_del:
|
| 296 |
+
if st.button("🗑️ Delete File & Embeddings", key=f"del_{doc.get('doc_id')}", use_container_width=True):
|
| 297 |
+
try:
|
| 298 |
+
requests.delete(f"{BACKEND_BASE_URL}/admin/documents/{doc.get('doc_id')}", headers=headers(), timeout=10)
|
| 299 |
+
st.success(f"🗑️ Deleted file '{doc.get('source')}' from disk and removed its embeddings from Qdrant!")
|
| 300 |
+
st.rerun()
|
| 301 |
+
except Exception as exc:
|
| 302 |
+
st.error(f"Delete failed: {exc}")
|
| 303 |
+
st.divider()
|
| 304 |
except requests.RequestException as exc:
|
| 305 |
st.error(f"Failed to load documents: {exc}")
|
| 306 |
|
| 307 |
|
| 308 |
def render_admin_portal_tab():
|
| 309 |
+
st.markdown("### 🛠️ Document Ingestion & Index Control")
|
| 310 |
+
st.write("Upload new knowledge documents, trigger hybrid vector index rebuilds via Arq worker, or reset the collection.")
|
| 311 |
+
|
| 312 |
+
st.markdown('<div class="glass-card">', unsafe_allow_html=True)
|
| 313 |
+
st.markdown("#### 📤 Upload Documents to Storage")
|
| 314 |
uploaded_files = st.file_uploader(
|
| 315 |
+
"Select files (.pdf, .docx, .txt, .md, .html)",
|
| 316 |
type=["txt", "md", "pdf", "docx", "html", "htm"],
|
| 317 |
accept_multiple_files=True,
|
| 318 |
)
|
| 319 |
+
if uploaded_files and st.button("💾 Save Uploaded Files to Backend", type="primary"):
|
| 320 |
try:
|
| 321 |
files = [("files", (file.name, file.getvalue(), file.type or "application/octet-stream")) for file in uploaded_files]
|
| 322 |
response = requests.post(
|
|
|
|
| 326 |
timeout=120,
|
| 327 |
)
|
| 328 |
response.raise_for_status()
|
| 329 |
+
st.success(f"✅ Successfully saved {len(uploaded_files)} file(s) to backend storage!")
|
| 330 |
st.json(response.json())
|
| 331 |
except requests.RequestException as exc:
|
| 332 |
st.error(f"Upload failed: {exc}")
|
| 333 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 334 |
|
| 335 |
+
st.markdown('<div class="glass-card">', unsafe_allow_html=True)
|
| 336 |
+
st.markdown("#### ⚙️ Hybrid Vector Indexing")
|
| 337 |
col1, col2 = st.columns(2)
|
| 338 |
with col1:
|
| 339 |
+
force = st.checkbox("⚠️ Force recreate index (wipes existing embeddings)")
|
| 340 |
+
if st.button("🚀 Run Document Ingestion", use_container_width=True, type="primary"):
|
| 341 |
try:
|
| 342 |
res = post_json("/admin/ingest", {"data_dir": DATA_DIR, "force": force})
|
| 343 |
st.json(res)
|
| 344 |
if job_id := res.get("job_id"):
|
| 345 |
+
result = poll_job_status(job_id, "Ingesting & embedding documents via Arq Worker...")
|
| 346 |
+
if result and result.get("status") == "SUCCESS":
|
| 347 |
+
st.success("✅ Ingestion complete! Switch to the 📚 Documents tab or click Refresh list to see your new documents.")
|
| 348 |
except requests.RequestException as exc:
|
| 349 |
st.error(f"Ingestion failed: {exc}")
|
| 350 |
with col2:
|
| 351 |
+
st.write("")
|
| 352 |
+
if st.button("🗑️ Reset Entire Index", use_container_width=True):
|
| 353 |
try:
|
| 354 |
st.json(post_json("/admin/reset"))
|
| 355 |
+
st.success("Index reset successfully.")
|
| 356 |
except requests.RequestException as exc:
|
| 357 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 358 |
+
|
| 359 |
+
st.markdown('<div class="glass-card">', unsafe_allow_html=True)
|
| 360 |
+
st.markdown("#### 🗑️ One-Click Post-Ingestion File & Embedding Management")
|
| 361 |
+
st.write("Easily remove uploaded files from storage and wipe their vector embeddings in one click after running ingestion.")
|
| 362 |
+
try:
|
| 363 |
+
docs_res = get_json("/documents")
|
| 364 |
+
ingested_docs = docs_res.get("documents", [])
|
| 365 |
+
if not ingested_docs:
|
| 366 |
+
st.caption("No ingested documents currently found.")
|
| 367 |
+
else:
|
| 368 |
+
selected_to_delete = []
|
| 369 |
+
for doc in ingested_docs:
|
| 370 |
+
col_name, col_btn = st.columns([3, 1])
|
| 371 |
+
with col_name:
|
| 372 |
+
if st.checkbox(f"📄 **{doc.get('source')}** (`{doc.get('chunk_count')} chunks`)", key=f"adm_chk_{doc.get('doc_id')}"):
|
| 373 |
+
selected_to_delete.append(doc)
|
| 374 |
+
with col_btn:
|
| 375 |
+
if st.button("🗑️ Delete (1-Click)", key=f"adm_del_{doc.get('doc_id')}", use_container_width=True):
|
| 376 |
+
try:
|
| 377 |
+
requests.delete(f"{BACKEND_BASE_URL}/admin/documents/{doc.get('doc_id')}", headers=headers(), timeout=10)
|
| 378 |
+
st.success(f"🗑️ Deleted file '{doc.get('source')}' and removed its embeddings!")
|
| 379 |
+
st.rerun()
|
| 380 |
+
except Exception as exc:
|
| 381 |
+
st.error(f"Delete failed: {exc}")
|
| 382 |
+
if selected_to_delete:
|
| 383 |
+
st.write("")
|
| 384 |
+
if st.button(f"🗑️ Delete {len(selected_to_delete)} Selected File(s) & Embeddings in One Click", type="primary", use_container_width=True):
|
| 385 |
+
for d in selected_to_delete:
|
| 386 |
+
try:
|
| 387 |
+
requests.delete(f"{BACKEND_BASE_URL}/admin/documents/{d.get('doc_id')}", headers=headers(), timeout=10)
|
| 388 |
+
except Exception:
|
| 389 |
+
pass
|
| 390 |
+
st.success(f"🗑️ Successfully deleted {len(selected_to_delete)} file(s) and removed their embeddings!")
|
| 391 |
+
st.rerun()
|
| 392 |
+
except Exception as exc:
|
| 393 |
+
st.caption(f"Could not load ingested documents: {exc}")
|
| 394 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 395 |
|
| 396 |
|
| 397 |
def render_evaluation_tab():
|
| 398 |
+
st.markdown("### 📊 Automated Quality Assessment (RAGAS)")
|
| 399 |
st.write("Evaluate how accurately and faithfully the copilot answers support questions using the RAGAS framework.")
|
| 400 |
|
| 401 |
+
st.markdown('<div class="glass-card">', unsafe_allow_html=True)
|
| 402 |
if st.button("🚀 Run RAG Evaluation Now", type="primary"):
|
| 403 |
try:
|
| 404 |
res = post_json("/admin/eval")
|
|
|
|
| 408 |
st.success("Evaluation task dispatched!")
|
| 409 |
except requests.RequestException as exc:
|
| 410 |
st.error(f"Evaluation failed: {exc}. Ensure you have remaining OpenRouter credits/limits.")
|
| 411 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 412 |
|
| 413 |
st.divider()
|
| 414 |
+
st.markdown("#### 📑 Latest Evaluation Report")
|
| 415 |
try:
|
| 416 |
res = get_json("/admin/eval")
|
| 417 |
+
report_text = res.get("report", "No evaluation report available.")
|
| 418 |
+
st.markdown(f'<div class="glass-card">{report_text}</div>', unsafe_allow_html=True)
|
| 419 |
except requests.RequestException:
|
| 420 |
+
st.info("💡 No evaluation report available yet. Click the button above to run your first evaluation!")
|
| 421 |
|
| 422 |
|
| 423 |
def render_langsmith_tab():
|
| 424 |
+
st.markdown("### 📈 Observability & Tracing (LangSmith)")
|
| 425 |
st.write("Monitor RAG agent steps, prompt tokens, and latency in real-time by adding these variables to your `.env`:")
|
| 426 |
st.code("LANGCHAIN_TRACING_V2=true\nLANGCHAIN_API_KEY=your_langsmith_api_key\nLANGCHAIN_PROJECT=\"Support Docs Copilot\"", language="env")
|
| 427 |
|
| 428 |
st.divider()
|
| 429 |
+
st.markdown("#### 🔍 System Readiness Diagnostics")
|
| 430 |
+
st.caption(f"Backend API Base URL: `{BACKEND_BASE_URL}`")
|
| 431 |
try:
|
| 432 |
+
ready_data = get_json("/ready")
|
| 433 |
+
cols = st.columns(3)
|
| 434 |
+
with cols[0]:
|
| 435 |
+
st.metric("Overall Status", "🟢 Ready" if ready_data.get("ready") else "🟡 Degraded")
|
| 436 |
+
with cols[1]:
|
| 437 |
+
st.metric("Vector Store", "🟢 Online" if ready_data.get("vector_store") else "🔴 Offline")
|
| 438 |
+
with cols[2]:
|
| 439 |
+
st.metric("LLM Provider", "🟢 Connected" if ready_data.get("llm") else "🔴 Offline")
|
| 440 |
+
st.json(ready_data)
|
| 441 |
except requests.RequestException as exc:
|
| 442 |
st.error(f"Readiness check failed: {exc}")
|
| 443 |
|
| 444 |
|
| 445 |
+
def render_observability_dashboard_tab():
|
| 446 |
+
st.markdown("### 👀 Live Session Observability Dashboard")
|
| 447 |
+
st.write("Monitor ongoing user chat threads across the enterprise, inspect RAG source citations, and inject supervisor guidance.")
|
| 448 |
+
|
| 449 |
+
col1, col2 = st.columns([1, 2])
|
| 450 |
+
with col1:
|
| 451 |
+
st.markdown("#### 🧵 Active Enterprise Threads")
|
| 452 |
+
if st.button("🔄 Refresh Live Sessions", key="ref_obs", use_container_width=True):
|
| 453 |
+
st.rerun()
|
| 454 |
+
try:
|
| 455 |
+
res = get_json("/api/v1/admin/sessions")
|
| 456 |
+
all_sess = res.get("sessions", [])
|
| 457 |
+
if not all_sess:
|
| 458 |
+
st.info("No active user sessions found.")
|
| 459 |
+
selected_sess = None
|
| 460 |
+
else:
|
| 461 |
+
# Sort all user sessions by latest timestamp in descending order
|
| 462 |
+
all_sess.sort(key=lambda x: x.get("updated_at", 0), reverse=True)
|
| 463 |
+
options = {}
|
| 464 |
+
for s in all_sess:
|
| 465 |
+
uid = s.get("user_id", "unknown")
|
| 466 |
+
sid = s["session_id"]
|
| 467 |
+
preview = s.get("last_preview", sid[:8] + "...")
|
| 468 |
+
t_val = s.get("updated_at", time.time())
|
| 469 |
+
t_str = time.strftime('%H:%M:%S', time.localtime(t_val)) if t_val else "recently"
|
| 470 |
+
label = f"[{t_str}] 👤 {uid} | {preview} ({sid[:6]})"
|
| 471 |
+
options[label] = (uid, sid)
|
| 472 |
+
selected_label = st.radio("Select Thread (Sorted by Recent Activity):", list(options.keys()), key="obs_radio")
|
| 473 |
+
selected_sess = options[selected_label] if selected_label else None
|
| 474 |
+
except Exception as exc:
|
| 475 |
+
st.error(f"Failed to fetch live sessions: {exc}")
|
| 476 |
+
selected_sess = None
|
| 477 |
+
|
| 478 |
+
with col2:
|
| 479 |
+
st.markdown("#### 🔬 Live Thread Inspection & Intervention")
|
| 480 |
+
if selected_sess:
|
| 481 |
+
target_uid, target_sid = selected_sess
|
| 482 |
+
try:
|
| 483 |
+
thread_res = get_json(f"/api/v1/admin/sessions/{target_uid}/{target_sid}/messages")
|
| 484 |
+
msgs = thread_res.get("messages", [])
|
| 485 |
+
summary = thread_res.get("summary")
|
| 486 |
+
|
| 487 |
+
if summary:
|
| 488 |
+
st.markdown(f'<div class="glass-card" style="border-left: 4px solid #38bdf8;"><b>🧠 Dense Background Memory Summary:</b><br>{summary}</div>', unsafe_allow_html=True)
|
| 489 |
+
|
| 490 |
+
st.markdown(f"**Viewing Session:** `{target_sid}` | **User Account:** `{target_uid}`")
|
| 491 |
+
|
| 492 |
+
with st.container(height=400, border=True):
|
| 493 |
+
for m in msgs:
|
| 494 |
+
role_icon = "👤 User" if m["role"] == "user" else ("👑 Supervisor" if m["role"] == "supervisor" else "🤖 Copilot")
|
| 495 |
+
st.markdown(f"**{role_icon}** ({time.strftime('%H:%M:%S', time.localtime(m.get('timestamp', time.time())))}):")
|
| 496 |
+
st.markdown(m.get("content", ""))
|
| 497 |
+
if sources := m.get("sources"):
|
| 498 |
+
with st.expander(f"📚 Inspect {len(sources)} Cited RAG Sources (Confidence: {m.get('confidence', 0.0)*100:.0f}%)"):
|
| 499 |
+
st.json(sources)
|
| 500 |
+
st.divider()
|
| 501 |
+
|
| 502 |
+
st.markdown("#### 🚨 Inject Supervisor Guidance")
|
| 503 |
+
intervene_msg = st.text_input("Type clarification or correction message for this thread...", key="inv_input")
|
| 504 |
+
if st.button("Inject Message into Thread", type="primary", key="inv_btn"):
|
| 505 |
+
if intervene_msg:
|
| 506 |
+
post_json(f"/api/v1/admin/sessions/{target_uid}/{target_sid}/message", {"message": intervene_msg, "role": "supervisor"})
|
| 507 |
+
st.success("Supervisor intervention injected successfully!")
|
| 508 |
+
st.rerun()
|
| 509 |
+
except Exception as exc:
|
| 510 |
+
st.error(f"Could not load thread details: {exc}")
|
| 511 |
+
else:
|
| 512 |
+
st.caption("Select an active thread from the left column to inspect chat history, verify citations, and intervene.")
|
| 513 |
+
|
| 514 |
+
|
| 515 |
# Render Role-Specific UI Layouts
|
| 516 |
if st.session_state.role == "admin":
|
| 517 |
+
chat_tab, obs_tab, docs_tab, admin_tab, eval_tab, langsmith_tab = st.tabs([
|
| 518 |
"💬 Chat Copilot",
|
| 519 |
+
"👀 Live Observability",
|
| 520 |
"📚 Documents",
|
| 521 |
"🛠️ Admin Portal",
|
| 522 |
"📊 RAGAS Evaluation",
|
|
|
|
| 524 |
])
|
| 525 |
with chat_tab:
|
| 526 |
render_chat_tab()
|
| 527 |
+
with obs_tab:
|
| 528 |
+
render_observability_dashboard_tab()
|
| 529 |
with docs_tab:
|
| 530 |
render_documents_tab()
|
| 531 |
with admin_tab:
|
|
|
|
| 545 |
with docs_tab:
|
| 546 |
render_documents_tab()
|
| 547 |
with status_tab:
|
| 548 |
+
st.markdown("### ⚙️ System Status")
|
| 549 |
try:
|
| 550 |
+
ready_data = get_json("/ready")
|
| 551 |
+
cols = st.columns(3)
|
| 552 |
+
with cols[0]:
|
| 553 |
+
st.metric("Overall Status", "🟢 Ready" if ready_data.get("ready") else "🟡 Degraded")
|
| 554 |
+
with cols[1]:
|
| 555 |
+
st.metric("Vector Store", "🟢 Online" if ready_data.get("vector_store") else "🔴 Offline")
|
| 556 |
+
with cols[2]:
|
| 557 |
+
st.metric("LLM Provider", "🟢 Connected" if ready_data.get("llm") else "🔴 Offline")
|
| 558 |
+
st.json(ready_data)
|
| 559 |
except requests.RequestException as exc:
|
| 560 |
st.error(f"Readiness check failed: {exc}")
|
| 561 |
st.divider()
|
ui/styles.css
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
| 2 |
+
|
| 3 |
+
/* Global Typography & Background */
|
| 4 |
+
html, body, [class*="css"] {
|
| 5 |
+
font-family: 'Outfit', -apple-system, BlinkMacSystemFont, sans-serif !important;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
/* App Main Background with Subtle Ambient Glow */
|
| 9 |
+
.stApp {
|
| 10 |
+
background: radial-gradient(circle at 15% 20%, rgba(99, 102, 241, 0.08) 0%, transparent 40%),
|
| 11 |
+
radial-gradient(circle at 85% 80%, rgba(168, 85, 247, 0.08) 0%, transparent 40%),
|
| 12 |
+
linear-gradient(180deg, #0b0f19 0%, #0f172a 100%) !important;
|
| 13 |
+
color: #f8fafc;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
/* Glassmorphic Containers and Cards */
|
| 17 |
+
.glass-card {
|
| 18 |
+
background: rgba(30, 41, 59, 0.45);
|
| 19 |
+
backdrop-filter: blur(16px);
|
| 20 |
+
-webkit-backdrop-filter: blur(16px);
|
| 21 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 22 |
+
border-radius: 16px;
|
| 23 |
+
padding: 1.5rem;
|
| 24 |
+
margin-bottom: 1.2rem;
|
| 25 |
+
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.2);
|
| 26 |
+
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.glass-card:hover {
|
| 30 |
+
transform: translateY(-2px);
|
| 31 |
+
border-color: rgba(168, 85, 247, 0.3);
|
| 32 |
+
box-shadow: 0 12px 40px 0 rgba(168, 85, 247, 0.15);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
/* Header & Title Gradient */
|
| 36 |
+
h1 {
|
| 37 |
+
font-weight: 700 !important;
|
| 38 |
+
background: linear-gradient(135deg, #60a5fa 0%, #c084fc 50%, #f472b6 100%);
|
| 39 |
+
-webkit-background-clip: text !important;
|
| 40 |
+
-webkit-text-fill-color: transparent !important;
|
| 41 |
+
letter-spacing: -0.5px;
|
| 42 |
+
margin-bottom: 0.5rem !important;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
h2, h3 {
|
| 46 |
+
font-weight: 600 !important;
|
| 47 |
+
color: #e2e8f0 !important;
|
| 48 |
+
letter-spacing: -0.3px;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/* Sidebar Styling */
|
| 52 |
+
section[data-testid="stSidebar"] {
|
| 53 |
+
background: rgba(15, 23, 42, 0.8) !important;
|
| 54 |
+
backdrop-filter: blur(20px);
|
| 55 |
+
border-right: 1px solid rgba(255, 255, 255, 0.06);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
section[data-testid="stSidebar"] hr {
|
| 59 |
+
border-color: rgba(255, 255, 255, 0.08);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/* Tab Navigation Styling */
|
| 63 |
+
div[data-baseweb="tab-list"] {
|
| 64 |
+
background: rgba(30, 41, 59, 0.5) !important;
|
| 65 |
+
padding: 0.35rem !important;
|
| 66 |
+
border-radius: 12px !important;
|
| 67 |
+
border: 1px solid rgba(255, 255, 255, 0.06) !important;
|
| 68 |
+
gap: 0.5rem;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
button[data-baseweb="tab"] {
|
| 72 |
+
font-family: 'Outfit', sans-serif !important;
|
| 73 |
+
font-weight: 500 !important;
|
| 74 |
+
font-size: 0.95rem !important;
|
| 75 |
+
border-radius: 8px !important;
|
| 76 |
+
padding: 0.6rem 1.2rem !important;
|
| 77 |
+
color: #94a3b8 !important;
|
| 78 |
+
transition: all 0.2s ease !important;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
button[data-baseweb="tab"][aria-selected="true"] {
|
| 82 |
+
background: linear-gradient(135deg, rgba(99, 102, 241, 0.25) 0%, rgba(168, 85, 247, 0.25) 100%) !important;
|
| 83 |
+
color: #f8fafc !important;
|
| 84 |
+
border: 1px solid rgba(168, 85, 247, 0.4) !important;
|
| 85 |
+
box-shadow: 0 4px 15px rgba(168, 85, 247, 0.15) !important;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
/* Button Animations & Gradients */
|
| 89 |
+
button[kind="primary"], .stButton > button[type="primary"], div[data-testid="stFormSubmitButton"] > button {
|
| 90 |
+
background: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%) !important;
|
| 91 |
+
color: white !important;
|
| 92 |
+
font-weight: 600 !important;
|
| 93 |
+
border: none !important;
|
| 94 |
+
border-radius: 10px !important;
|
| 95 |
+
padding: 0.6rem 1.4rem !important;
|
| 96 |
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
| 97 |
+
box-shadow: 0 4px 15px rgba(168, 85, 247, 0.3) !important;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
button[kind="primary"]:hover, .stButton > button[type="primary"]:hover {
|
| 101 |
+
transform: translateY(-2px) scale(1.01) !important;
|
| 102 |
+
box-shadow: 0 8px 25px rgba(168, 85, 247, 0.5) !important;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
button[kind="secondary"], .stButton > button {
|
| 106 |
+
background: rgba(51, 65, 85, 0.4) !important;
|
| 107 |
+
color: #e2e8f0 !important;
|
| 108 |
+
border: 1px solid rgba(255, 255, 255, 0.12) !important;
|
| 109 |
+
border-radius: 10px !important;
|
| 110 |
+
transition: all 0.2s ease !important;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
button[kind="secondary"]:hover, .stButton > button:hover {
|
| 114 |
+
background: rgba(51, 65, 85, 0.8) !important;
|
| 115 |
+
border-color: rgba(255, 255, 255, 0.25) !important;
|
| 116 |
+
transform: translateY(-1px) !important;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
/* Chat Message Styling */
|
| 120 |
+
div[data-testid="stChatMessage"] {
|
| 121 |
+
background: rgba(30, 41, 59, 0.35) !important;
|
| 122 |
+
border: 1px solid rgba(255, 255, 255, 0.05) !important;
|
| 123 |
+
border-radius: 16px !important;
|
| 124 |
+
padding: 1.25rem !important;
|
| 125 |
+
margin-bottom: 1rem !important;
|
| 126 |
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15) !important;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
div[data-testid="stChatMessage"][data-testid*="user"] {
|
| 130 |
+
background: linear-gradient(135deg, rgba(30, 41, 59, 0.5) 0%, rgba(49, 46, 129, 0.3) 100%) !important;
|
| 131 |
+
border: 1px solid rgba(99, 102, 241, 0.25) !important;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
/* Chat Input Styling */
|
| 135 |
+
div[data-testid="stChatInput"] > div {
|
| 136 |
+
background: rgba(15, 23, 42, 0.8) !important;
|
| 137 |
+
border: 1px solid rgba(255, 255, 255, 0.15) !important;
|
| 138 |
+
border-radius: 14px !important;
|
| 139 |
+
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3) !important;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
div[data-testid="stChatInput"] > div:focus-within {
|
| 143 |
+
border-color: #a855f7 !important;
|
| 144 |
+
box-shadow: 0 0 0 2px rgba(168, 85, 247, 0.3), 0 8px 30px rgba(0, 0, 0, 0.4) !important;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
/* Badges & Metrics */
|
| 148 |
+
.status-badge {
|
| 149 |
+
display: inline-block;
|
| 150 |
+
padding: 0.25rem 0.75rem;
|
| 151 |
+
border-radius: 9999px;
|
| 152 |
+
font-size: 0.8rem;
|
| 153 |
+
font-weight: 600;
|
| 154 |
+
text-transform: uppercase;
|
| 155 |
+
letter-spacing: 0.5px;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.badge-green {
|
| 159 |
+
background: rgba(16, 185, 129, 0.15);
|
| 160 |
+
color: #34d399;
|
| 161 |
+
border: 1px solid rgba(16, 185, 129, 0.3);
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.badge-yellow {
|
| 165 |
+
background: rgba(245, 158, 11, 0.15);
|
| 166 |
+
color: #fbbf24;
|
| 167 |
+
border: 1px solid rgba(245, 158, 11, 0.3);
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.badge-purple {
|
| 171 |
+
background: rgba(168, 85, 247, 0.15);
|
| 172 |
+
color: #c084fc;
|
| 173 |
+
border: 1px solid rgba(168, 85, 247, 0.3);
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.badge-blue {
|
| 177 |
+
background: rgba(59, 130, 246, 0.15);
|
| 178 |
+
color: #60a5fa;
|
| 179 |
+
border: 1px solid rgba(59, 130, 246, 0.3);
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
/* Source Citation Accordion */
|
| 183 |
+
div[data-testid="stExpander"] {
|
| 184 |
+
background: rgba(15, 23, 42, 0.4) !important;
|
| 185 |
+
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
| 186 |
+
border-radius: 12px !important;
|
| 187 |
+
overflow: hidden;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
div[data-testid="stExpander"] summary {
|
| 191 |
+
font-weight: 500 !important;
|
| 192 |
+
color: #cbd5e1 !important;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
/* Metrics & Stats Box */
|
| 196 |
+
div[data-testid="stMetric"] {
|
| 197 |
+
background: rgba(30, 41, 59, 0.4);
|
| 198 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 199 |
+
border-radius: 12px;
|
| 200 |
+
padding: 1rem;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
/* Code blocks & Monospace */
|
| 204 |
+
code, pre {
|
| 205 |
+
font-family: 'JetBrains Mono', monospace !important;
|
| 206 |
+
}
|