ktejeshnaidu commited on
Commit
32f61fa
Β·
verified Β·
1 Parent(s): 5af6d2c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +631 -172
app.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
- DocuMind - Streamlit Frontend for HuggingFace Spaces
3
- Simplified version that works with existing FastAPI backend
4
  """
5
 
6
  import streamlit as st
@@ -8,226 +8,685 @@ import requests
8
  import json
9
  import os
10
  import time
 
11
 
12
  # ============================================================================
13
- # STREAMLIT CONFIG (Must be first)
14
  # ============================================================================
15
-
16
  st.set_page_config(
17
- page_title="DocuMind - Enterprise RAG",
18
  page_icon="🧠",
19
  layout="wide",
20
  initial_sidebar_state="expanded"
21
  )
22
 
23
  # ============================================================================
24
- # API CONFIGURATION
25
  # ============================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
- # Use localhost for internal communication
 
 
28
  API_URL = "http://127.0.0.1:8000"
29
- API_TIMEOUT = 30 # seconds
30
 
31
  # ============================================================================
32
- # UTILITY FUNCTIONS
33
  # ============================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
- def check_backend_health(retries=5):
36
- """Check if backend API is running"""
37
- for attempt in range(retries):
38
- try:
39
- response = requests.get(f"{API_URL}/docs", timeout=5)
40
- if response.status_code == 200:
41
- return True
42
- except:
43
- if attempt < retries - 1:
44
- time.sleep(1)
45
- return False
46
-
47
- def safe_api_call(method, endpoint, **kwargs):
48
- """Safely call API with error handling"""
49
  try:
 
 
 
 
 
 
 
 
50
  url = f"{API_URL}{endpoint}"
51
- kwargs.setdefault('timeout', API_TIMEOUT)
52
-
53
- if method == "GET":
54
- response = requests.get(url, **kwargs)
55
- elif method == "POST":
56
- response = requests.post(url, **kwargs)
57
- else:
58
- return None, "Invalid method"
59
-
60
- return response, None
61
  except requests.exceptions.Timeout:
62
  return None, "⏱️ Request timed out"
63
  except requests.exceptions.ConnectionError:
64
  return None, "⚠️ Backend not responding"
65
  except Exception as e:
66
- return None, f"❌ Error: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
  # ============================================================================
69
- # STREAMLIT FRONTEND UI
70
  # ============================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
- st.title("🧠 DocuMind")
73
- st.markdown("**Enterprise Document Intelligence Chatbot**")
74
- st.markdown("---")
 
 
 
75
 
76
- # Health check on load
77
- if "backend_checked" not in st.session_state:
78
- st.session_state.backend_checked = False
79
- st.session_state.backend_healthy = False
 
 
 
 
 
 
 
 
80
 
81
- if not st.session_state.backend_checked:
82
- with st.spinner("πŸ”„ Starting backend..."):
83
- time.sleep(2) # Give backend time to start
84
- st.session_state.backend_healthy = check_backend_health()
85
- st.session_state.backend_checked = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
- # Show status
88
- if not st.session_state.backend_healthy:
89
- st.warning(
90
- "⚠️ Backend is starting up. This may take 30-60 seconds on first load. "
91
- "Please refresh the page if you see this message for more than 1 minute."
92
- )
 
93
 
94
- # --- Sidebar for Document Upload ---
95
- with st.sidebar:
96
- st.header("🏒 Document Knowledge Base")
97
- st.markdown("Upload PDFs, DOCX, or TXT documents to add them to the system.")
98
-
99
- uploaded_file = st.file_uploader("Upload a new document", type=["txt", "pdf", "docx"])
100
-
101
- if uploaded_file and st.button("Ingest Document", key="ingest_btn"):
102
- with st.spinner("Ingesting document (creating chunks & embeddings)..."):
103
- files = {"file": (uploaded_file.name, uploaded_file.getvalue())}
104
- response, error = safe_api_call("POST", "/ingest", files=files)
105
-
106
- if error:
107
- st.error(error)
108
- elif response and response.status_code == 200:
109
- st.success(f"βœ… {uploaded_file.name} ingested successfully!")
110
- else:
111
- st.error(f"❌ Failed to ingest: {response.text if response else 'Unknown error'}")
112
-
113
- st.divider()
114
-
115
- st.subheader("πŸ“„ Indexed Documents")
116
- response, error = safe_api_call("GET", "/sources")
117
-
118
- if error:
119
- st.warning(f"Could not fetch documents: {error}")
120
- elif response and response.status_code == 200:
121
- documents = response.json().get("documents", [])
122
- if documents:
123
- for doc in documents:
124
- st.markdown(f"- `{doc}`")
125
- else:
126
- st.info("No documents indexed yet.")
127
- else:
128
- st.info("Backend not ready yet...")
129
 
130
- # --- Main Chat Interface ---
131
- st.subheader("πŸ’¬ Chat with Your Documents")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
- # Initialize session state
134
- if "messages" not in st.session_state:
135
- st.session_state.messages = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
- # Display chat history
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  for msg in st.session_state.messages:
139
- with st.chat_message(msg["role"]):
140
- st.markdown(msg["content"])
141
- if "sources" in msg and msg.get("sources"):
142
- with st.expander("πŸ“š Show Sources"):
143
- for idx, src in enumerate(msg["sources"]):
144
- score = src.get('score', 0)
145
- st.caption(f"**Source {idx+1}** [Relevance: {score:.2%}]")
146
- st.markdown(f"**From:** `{src.get('source', 'Unknown')}`")
147
- content = src.get('content', '')
148
- if len(content) > 500:
149
- st.markdown(f"> {content[:500]}...")
150
- else:
151
- st.markdown(f"> {content}")
152
-
153
- # Chat input
154
- user_input = st.chat_input("Ask a question about your documents...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
  if user_input:
157
- # Add user message to history
158
- st.session_state.messages.append({"role": "user", "content": user_input})
159
-
160
- # Display user message
161
- with st.chat_message("user"):
162
- st.markdown(user_input)
163
-
164
- # Get assistant response
165
- with st.chat_message("assistant"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  placeholder = st.empty()
167
- full_response = ""
168
- sources = []
169
-
170
- response, error = safe_api_call(
171
- "POST",
172
- "/query",
173
- json={"question": user_input},
174
- stream=True
175
- )
176
-
177
- if error:
178
- error_msg = error
179
- st.error(error_msg)
180
- full_response = error_msg
181
- elif response:
182
  try:
183
- for line in response.iter_lines():
 
184
  if line:
185
  try:
186
- decoded_line = line.decode('utf-8')
187
- data = json.loads(decoded_line)
188
-
189
  if data.get("type") == "sources":
190
  sources = data.get("data", [])
191
  elif data.get("type") == "token":
192
- full_response += data.get("content", "")
193
- placeholder.markdown(full_response + "β–Œ")
 
 
 
 
 
194
  except json.JSONDecodeError:
195
  continue
196
-
197
- placeholder.markdown(full_response)
198
-
199
- # Display sources if available
200
- if sources:
201
- with st.expander("πŸ“š Show Sources"):
202
- for idx, src in enumerate(sources):
203
- score = src.get('score', 0)
204
- st.caption(f"**Source {idx+1}** [Relevance: {score:.2%}]")
205
- st.markdown(f"**From:** `{src.get('source', 'Unknown')}`")
206
- content = src.get('content', '')
207
- if len(content) > 500:
208
- st.markdown(f"> {content[:500]}...")
209
- else:
210
- st.markdown(f"> {content}")
211
-
212
  except Exception as e:
213
- error_msg = f"❌ Error processing response: {str(e)}"
214
- placeholder.markdown(error_msg)
215
- st.error(error_msg)
216
- full_response = error_msg
217
-
218
- # Save assistant message
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  st.session_state.messages.append({
220
  "role": "assistant",
221
  "content": full_response,
222
- "sources": sources
 
223
  })
224
 
225
- # Footer
226
- st.divider()
227
- st.markdown(
228
- "<div style='text-align: center; color: var(--color-text-secondary); font-size: 0.85em;'>"
229
- "DocuMind - Enterprise RAG Chatbot | "
230
- "<a href='https://github.com/TejeshNaiduKona/DocuMind' target='_blank'>GitHub</a>"
231
- "</div>",
232
- unsafe_allow_html=True
233
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ DocuMind - Enterprise RAG Chatbot
3
+ Redesigned UI matching modern document intelligence interface
4
  """
5
 
6
  import streamlit as st
 
8
  import json
9
  import os
10
  import time
11
+ from datetime import datetime
12
 
13
  # ============================================================================
14
+ # PAGE CONFIG (Must be first)
15
  # ============================================================================
 
16
  st.set_page_config(
17
+ page_title="DocuMind AI",
18
  page_icon="🧠",
19
  layout="wide",
20
  initial_sidebar_state="expanded"
21
  )
22
 
23
  # ============================================================================
24
+ # CUSTOM CSS β€” Full UI Override
25
  # ============================================================================
26
+ st.markdown("""
27
+ <style>
28
+ @import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,300&display=swap');
29
+
30
+ /* ── ROOT VARIABLES ── */
31
+ :root {
32
+ --bg-base: #0d0f14;
33
+ --bg-card: #141720;
34
+ --bg-sidebar: #111318;
35
+ --bg-input: #1a1d27;
36
+ --bg-user-msg: #1e2235;
37
+ --bg-bot-msg: #181b26;
38
+ --accent: #4ade80;
39
+ --accent-dim: #22c55e22;
40
+ --accent-muted: #4ade8044;
41
+ --border: #ffffff0f;
42
+ --border-hover: #ffffff1a;
43
+ --text-primary: #f0f2f8;
44
+ --text-secondary:#8892aa;
45
+ --text-muted: #50586e;
46
+ --badge-active: #4ade80;
47
+ --badge-bg: #4ade8022;
48
+ --radius-sm: 8px;
49
+ --radius-md: 14px;
50
+ --radius-lg: 20px;
51
+ --shadow: 0 4px 24px #00000055;
52
+ }
53
+
54
+ /* ── GLOBAL RESET ── */
55
+ *, *::before, *::after { box-sizing: border-box; }
56
+
57
+ html, body, [data-testid="stAppViewContainer"] {
58
+ font-family: 'DM Sans', sans-serif;
59
+ background: var(--bg-base) !important;
60
+ color: var(--text-primary) !important;
61
+ }
62
+
63
+ /* ── HIDE STREAMLIT CHROME ── */
64
+ #MainMenu, footer, header,
65
+ [data-testid="stToolbar"],
66
+ [data-testid="stDecoration"],
67
+ .stDeployButton,
68
+ [data-testid="collapsedControl"] { display: none !important; }
69
+
70
+ /* ── SIDEBAR ── */
71
+ [data-testid="stSidebar"] {
72
+ background: var(--bg-sidebar) !important;
73
+ border-right: 1px solid var(--border) !important;
74
+ min-width: 240px !important;
75
+ max-width: 240px !important;
76
+ }
77
+ [data-testid="stSidebar"] > div:first-child {
78
+ padding: 0 !important;
79
+ }
80
+ [data-testid="stSidebarContent"] {
81
+ padding: 0 !important;
82
+ background: transparent !important;
83
+ }
84
+
85
+ /* ── MAIN AREA ── */
86
+ [data-testid="stMain"] {
87
+ background: var(--bg-base) !important;
88
+ }
89
+ .main .block-container {
90
+ padding: 0 !important;
91
+ max-width: 100% !important;
92
+ }
93
+
94
+ /* ── HIDE DEFAULT SIDEBAR WIDGETS LABELS ── */
95
+ [data-testid="stSidebar"] .stMarkdown p {
96
+ color: var(--text-secondary);
97
+ font-size: 0.78rem;
98
+ font-family: 'DM Sans', sans-serif;
99
+ }
100
+
101
+ /* ── FILE UPLOADER ── */
102
+ [data-testid="stFileUploader"] {
103
+ background: var(--bg-input) !important;
104
+ border: 1px dashed var(--border-hover) !important;
105
+ border-radius: var(--radius-md) !important;
106
+ padding: 8px !important;
107
+ }
108
+ [data-testid="stFileUploader"] label {
109
+ color: var(--text-secondary) !important;
110
+ font-size: 0.82rem !important;
111
+ }
112
+ [data-testid="stFileUploadDropzone"] {
113
+ background: transparent !important;
114
+ border: none !important;
115
+ }
116
+ [data-testid="stFileUploadDropzone"] p,
117
+ [data-testid="stFileUploadDropzone"] span {
118
+ color: var(--text-muted) !important;
119
+ font-size: 0.78rem !important;
120
+ }
121
+
122
+ /* ── BUTTONS ── */
123
+ .stButton > button {
124
+ background: var(--bg-input) !important;
125
+ color: var(--text-primary) !important;
126
+ border: 1px solid var(--border-hover) !important;
127
+ border-radius: var(--radius-sm) !important;
128
+ font-family: 'DM Sans', sans-serif !important;
129
+ font-size: 0.82rem !important;
130
+ font-weight: 500 !important;
131
+ padding: 6px 14px !important;
132
+ transition: all 0.2s ease !important;
133
+ width: 100% !important;
134
+ }
135
+ .stButton > button:hover {
136
+ background: var(--accent-dim) !important;
137
+ border-color: var(--accent-muted) !important;
138
+ color: var(--accent) !important;
139
+ }
140
+
141
+ /* ── CHAT INPUT ── */
142
+ [data-testid="stChatInput"] {
143
+ background: var(--bg-input) !important;
144
+ border: 1px solid var(--border-hover) !important;
145
+ border-radius: var(--radius-lg) !important;
146
+ padding: 4px 8px !important;
147
+ }
148
+ [data-testid="stChatInput"] textarea {
149
+ background: transparent !important;
150
+ color: var(--text-primary) !important;
151
+ font-family: 'DM Sans', sans-serif !important;
152
+ font-size: 0.9rem !important;
153
+ border: none !important;
154
+ outline: none !important;
155
+ }
156
+ [data-testid="stChatInput"] textarea::placeholder {
157
+ color: var(--text-muted) !important;
158
+ }
159
+ [data-testid="stChatInput"] button {
160
+ background: var(--accent) !important;
161
+ border-radius: 50% !important;
162
+ color: #000 !important;
163
+ }
164
+
165
+ /* ── CHAT MESSAGES ── */
166
+ [data-testid="stChatMessage"] {
167
+ background: transparent !important;
168
+ border: none !important;
169
+ padding: 4px 0 !important;
170
+ }
171
+
172
+ /* User messages */
173
+ [data-testid="stChatMessage"][data-testid*="user"],
174
+ .stChatMessage:has([data-testid="chatAvatarIcon-user"]) {
175
+ flex-direction: row-reverse !important;
176
+ }
177
+
178
+ /* ── SPINNER ── */
179
+ .stSpinner > div {
180
+ border-top-color: var(--accent) !important;
181
+ }
182
+
183
+ /* ── ALERTS / WARNINGS ── */
184
+ .stAlert {
185
+ background: var(--bg-card) !important;
186
+ border: 1px solid var(--border) !important;
187
+ border-radius: var(--radius-md) !important;
188
+ color: var(--text-secondary) !important;
189
+ font-size: 0.82rem !important;
190
+ }
191
+ .stSuccess {
192
+ border-color: var(--accent-muted) !important;
193
+ background: var(--accent-dim) !important;
194
+ }
195
+
196
+ /* ── DIVIDER ── */
197
+ hr {
198
+ border-color: var(--border) !important;
199
+ margin: 8px 0 !important;
200
+ }
201
+
202
+ /* ── EXPANDER ── */
203
+ .streamlit-expander {
204
+ background: var(--bg-card) !important;
205
+ border: 1px solid var(--border) !important;
206
+ border-radius: var(--radius-md) !important;
207
+ }
208
+ .streamlit-expander summary {
209
+ color: var(--text-secondary) !important;
210
+ font-size: 0.82rem !important;
211
+ }
212
+
213
+ /* ── SCROLLBAR ── */
214
+ ::-webkit-scrollbar { width: 4px; height: 4px; }
215
+ ::-webkit-scrollbar-track { background: transparent; }
216
+ ::-webkit-scrollbar-thumb { background: var(--border-hover); border-radius: 4px; }
217
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
218
+ </style>
219
+ """, unsafe_allow_html=True)
220
 
221
+ # ============================================================================
222
+ # API CONFIG
223
+ # ============================================================================
224
  API_URL = "http://127.0.0.1:8000"
225
+ API_TIMEOUT = 60
226
 
227
  # ============================================================================
228
+ # SESSION STATE INIT
229
  # ============================================================================
230
+ if "messages" not in st.session_state:
231
+ st.session_state.messages = []
232
+ if "backend_ok" not in st.session_state:
233
+ st.session_state.backend_ok = False
234
+ if "backend_checked" not in st.session_state:
235
+ st.session_state.backend_checked = False
236
+ if "chat_sessions" not in st.session_state:
237
+ st.session_state.chat_sessions = ["Q3 Financial Report", "Product Roadmap 2...", "Research Summary"]
238
+ if "mode_summary" not in st.session_state:
239
+ st.session_state.mode_summary = False
240
+ if "mode_citations" not in st.session_state:
241
+ st.session_state.mode_citations = True
242
+ if "ingested_docs" not in st.session_state:
243
+ st.session_state.ingested_docs = []
244
 
245
+ # ============================================================================
246
+ # HELPERS
247
+ # ============================================================================
248
+ def check_backend():
 
 
 
 
 
 
 
 
 
 
249
  try:
250
+ r = requests.get(f"{API_URL}/docs", timeout=5)
251
+ return r.status_code == 200
252
+ except:
253
+ return False
254
+
255
+ def api_call(method, endpoint, **kwargs):
256
+ try:
257
+ kwargs.setdefault("timeout", API_TIMEOUT)
258
  url = f"{API_URL}{endpoint}"
259
+ fn = requests.get if method == "GET" else requests.post
260
+ return fn(url, **kwargs), None
 
 
 
 
 
 
 
 
261
  except requests.exceptions.Timeout:
262
  return None, "⏱️ Request timed out"
263
  except requests.exceptions.ConnectionError:
264
  return None, "⚠️ Backend not responding"
265
  except Exception as e:
266
+ return None, f"Error: {e}"
267
+
268
+ def now_time():
269
+ return datetime.now().strftime("%-I:%M %p")
270
+
271
+ # Backend health check (once per session)
272
+ if not st.session_state.backend_checked:
273
+ with st.spinner("Starting up..."):
274
+ time.sleep(2)
275
+ st.session_state.backend_ok = check_backend()
276
+ st.session_state.backend_checked = True
277
+
278
+ # Refresh doc list
279
+ try:
280
+ resp, _ = api_call("GET", "/sources")
281
+ if resp and resp.status_code == 200:
282
+ st.session_state.ingested_docs = resp.json().get("documents", [])
283
+ except:
284
+ pass
285
 
286
  # ============================================================================
287
+ # SIDEBAR
288
  # ============================================================================
289
+ with st.sidebar:
290
+ # ── Logo ──
291
+ st.markdown("""
292
+ <div style="padding: 22px 20px 16px; border-bottom: 1px solid #ffffff0f;">
293
+ <div style="display:flex; align-items:center; gap:10px; margin-bottom:2px;">
294
+ <div style="width:32px;height:32px;background:linear-gradient(135deg,#4ade80,#22c55e);
295
+ border-radius:8px;display:flex;align-items:center;justify-content:center;
296
+ font-size:16px;flex-shrink:0;">🧠</div>
297
+ <div>
298
+ <div style="font-family:'Syne',sans-serif;font-weight:700;font-size:1rem;
299
+ color:#f0f2f8;line-height:1.1;">DocuMind AI</div>
300
+ <div style="font-size:0.68rem;color:#50586e;letter-spacing:0.04em;">Digital Curator</div>
301
+ </div>
302
+ </div>
303
+ </div>
304
+ """, unsafe_allow_html=True)
305
 
306
+ # ── New Chat Button ──
307
+ st.markdown('<div style="padding:14px 16px 8px;">', unsafe_allow_html=True)
308
+ if st.button("οΌ‹ New Chat", key="new_chat"):
309
+ st.session_state.messages = []
310
+ st.rerun()
311
+ st.markdown('</div>', unsafe_allow_html=True)
312
 
313
+ # ── Upload Section ──
314
+ st.markdown("""
315
+ <div style="padding:0 16px 8px;">
316
+ <div style="background:#1a1d27;border:1px solid #ffffff0f;border-radius:12px;padding:12px;">
317
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
318
+ <span style="font-size:0.78rem;color:#8892aa;font-weight:500;">πŸ“Ž Upload</span>
319
+ <span style="font-size:0.68rem;color:#50586e;background:#ffffff08;
320
+ padding:2px 6px;border-radius:4px;">200MB LIMIT</span>
321
+ </div>
322
+ </div>
323
+ </div>
324
+ """, unsafe_allow_html=True)
325
 
326
+ with st.container():
327
+ st.markdown('<div style="padding:0 16px 6px;">', unsafe_allow_html=True)
328
+ uploaded_file = st.file_uploader(
329
+ "Drop file here",
330
+ type=["txt", "pdf", "docx"],
331
+ label_visibility="collapsed"
332
+ )
333
+ if uploaded_file and st.button("Ingest Document", key="ingest_btn"):
334
+ with st.spinner("Processing..."):
335
+ files = {"file": (uploaded_file.name, uploaded_file.getvalue())}
336
+ resp, err = api_call("POST", "/ingest", files=files)
337
+ if err:
338
+ st.error(err)
339
+ elif resp and resp.status_code == 200:
340
+ st.success(f"βœ… Ingested!")
341
+ st.session_state.ingested_docs.append(uploaded_file.name)
342
+ else:
343
+ detail = ""
344
+ try:
345
+ detail = resp.json().get("detail", resp.text)
346
+ except:
347
+ detail = resp.text if resp else "Unknown error"
348
+ st.error(f"❌ {detail}")
349
+ st.markdown('</div>', unsafe_allow_html=True)
350
 
351
+ # ── Recents ──
352
+ st.markdown("""
353
+ <div style="padding:16px 20px 8px;">
354
+ <div style="font-size:0.68rem;color:#50586e;letter-spacing:0.08em;
355
+ font-weight:600;text-transform:uppercase;margin-bottom:10px;">Recents</div>
356
+ </div>
357
+ """, unsafe_allow_html=True)
358
 
359
+ # Show indexed docs or placeholder sessions
360
+ display_docs = st.session_state.ingested_docs if st.session_state.ingested_docs else st.session_state.chat_sessions
361
+ for doc in display_docs[:6]:
362
+ label = doc if len(doc) <= 22 else doc[:19] + "..."
363
+ st.markdown(f"""
364
+ <div style="display:flex;align-items:center;gap:8px;padding:7px 20px;
365
+ cursor:pointer;transition:background 0.15s;border-radius:0;">
366
+ <span style="color:#50586e;font-size:0.75rem;">πŸ•“</span>
367
+ <span style="font-size:0.8rem;color:#8892aa;">{label}</span>
368
+ </div>
369
+ """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
 
371
+ # ── Bottom nav ──
372
+ st.markdown("""
373
+ <div style="position:fixed;bottom:0;left:0;width:240px;
374
+ border-top:1px solid #ffffff0f;background:#111318;padding:12px 16px;">
375
+ <div style="display:flex;flex-direction:column;gap:2px;margin-bottom:12px;">
376
+ <div style="display:flex;align-items:center;gap:8px;padding:7px 8px;
377
+ border-radius:8px;cursor:pointer;">
378
+ <span style="font-size:0.85rem;">βš™οΈ</span>
379
+ <span style="font-size:0.82rem;color:#8892aa;">Settings</span>
380
+ </div>
381
+ <div style="display:flex;align-items:center;gap:8px;padding:7px 8px;
382
+ border-radius:8px;cursor:pointer;">
383
+ <span style="font-size:0.85rem;">❓</span>
384
+ <span style="font-size:0.82rem;color:#8892aa;">Help</span>
385
+ </div>
386
+ </div>
387
+ <div style="display:flex;align-items:center;gap:10px;">
388
+ <div style="width:28px;height:28px;background:linear-gradient(135deg,#667eea,#764ba2);
389
+ border-radius:50%;display:flex;align-items:center;justify-content:center;
390
+ font-size:12px;color:white;">A</div>
391
+ <div>
392
+ <div style="font-size:0.8rem;color:#f0f2f8;font-weight:500;">Alex Sterling</div>
393
+ <div style="font-size:0.68rem;color:#50586e;">Pro Curator</div>
394
+ </div>
395
+ </div>
396
+ </div>
397
+ """, unsafe_allow_html=True)
398
 
399
+ # ============================================================================
400
+ # MAIN CHAT AREA
401
+ # ============================================================================
402
+
403
+ # ── Top bar ──
404
+ col_top1, col_top2, col_top3 = st.columns([3, 6, 1])
405
+ with col_top1:
406
+ st.markdown("""
407
+ <div style="padding:16px 0 12px;display:flex;align-items:center;gap:10px;">
408
+ <span style="font-size:0.72rem;color:#8892aa;font-weight:600;
409
+ letter-spacing:0.08em;text-transform:uppercase;">Current Session</span>
410
+ <span style="background:#4ade8022;color:#4ade80;font-size:0.62rem;
411
+ font-weight:600;padding:2px 8px;border-radius:4px;
412
+ letter-spacing:0.06em;text-transform:uppercase;">ACTIVE</span>
413
+ </div>
414
+ """, unsafe_allow_html=True)
415
+ with col_top3:
416
+ st.markdown("""
417
+ <div style="padding:16px 0 12px;display:flex;justify-content:flex-end;gap:12px;">
418
+ <span style="color:#8892aa;cursor:pointer;font-size:1.1rem;">πŸ””</span>
419
+ <span style="color:#8892aa;cursor:pointer;font-size:1.1rem;">πŸ‘€</span>
420
+ </div>
421
+ """, unsafe_allow_html=True)
422
+
423
+ st.markdown('<hr style="border-color:#ffffff0f;margin:0 0 0 0;"/>', unsafe_allow_html=True)
424
+
425
+ # Backend warning
426
+ if not st.session_state.backend_ok:
427
+ st.markdown("""
428
+ <div style="background:#f59e0b15;border:1px solid #f59e0b33;border-radius:10px;
429
+ padding:10px 16px;margin:12px 0;font-size:0.82rem;color:#f59e0b;">
430
+ ⚠️ Backend is starting up (30–60s on first load). Refresh if this persists.
431
+ </div>
432
+ """, unsafe_allow_html=True)
433
 
434
+ # ── Empty state ──
435
+ if not st.session_state.messages:
436
+ st.markdown("""
437
+ <div style="display:flex;flex-direction:column;align-items:center;
438
+ justify-content:center;padding:80px 20px 40px;text-align:center;">
439
+ <h1 style="font-family:'Syne',sans-serif;font-size:2rem;font-weight:700;
440
+ color:#f0f2f8;margin:0 0 12px;line-height:1.2;">
441
+ How can I assist your research today?
442
+ </h1>
443
+ <p style="color:#8892aa;font-size:0.95rem;max-width:420px;line-height:1.6;margin:0;">
444
+ I can analyze documents, summarize findings, or answer<br>
445
+ specific questions about your archived data.
446
+ </p>
447
+ </div>
448
+ """, unsafe_allow_html=True)
449
+
450
+ # ── Chat history ──
451
  for msg in st.session_state.messages:
452
+ role = msg["role"]
453
+ content = msg["content"]
454
+ ts = msg.get("time", "")
455
+ sources = msg.get("sources", [])
456
+
457
+ if role == "user":
458
+ st.markdown(f"""
459
+ <div style="display:flex;justify-content:flex-end;margin:8px 0 2px;">
460
+ <div style="max-width:65%;">
461
+ <div style="background:#1e2235;border:1px solid #ffffff0f;
462
+ border-radius:16px 16px 4px 16px;padding:14px 18px;
463
+ color:#f0f2f8;font-size:0.9rem;line-height:1.6;">
464
+ {content}
465
+ </div>
466
+ <div style="text-align:right;font-size:0.68rem;color:#50586e;
467
+ margin-top:4px;padding-right:4px;">{ts}</div>
468
+ </div>
469
+ </div>
470
+ """, unsafe_allow_html=True)
471
+ else:
472
+ # Format bullet-like lines
473
+ lines = content.split("\n")
474
+ formatted = ""
475
+ for line in lines:
476
+ line = line.strip()
477
+ if not line:
478
+ continue
479
+ if line.startswith(("- ", "β€’ ", "* ")):
480
+ item = line[2:]
481
+ # Bold first phrase before colon
482
+ if ":" in item:
483
+ key, val = item.split(":", 1)
484
+ formatted += f"""
485
+ <div style="display:flex;align-items:flex-start;gap:10px;margin:6px 0;">
486
+ <span style="color:#4ade80;margin-top:2px;flex-shrink:0;">βœ“</span>
487
+ <span><strong style="color:#f0f2f8;">{key.strip()}:</strong>
488
+ <span style="color:#c4cad9;">{val.strip()}</span></span>
489
+ </div>"""
490
+ else:
491
+ formatted += f"""
492
+ <div style="display:flex;align-items:flex-start;gap:10px;margin:6px 0;">
493
+ <span style="color:#4ade80;margin-top:2px;flex-shrink:0;">βœ“</span>
494
+ <span style="color:#c4cad9;">{item}</span>
495
+ </div>"""
496
+ else:
497
+ formatted += f'<p style="color:#c4cad9;margin:4px 0;line-height:1.7;">{line}</p>'
498
+
499
+ st.markdown(f"""
500
+ <div style="display:flex;justify-content:flex-start;margin:8px 0 2px;gap:10px;">
501
+ <div style="width:32px;height:32px;background:linear-gradient(135deg,#4ade80,#22c55e);
502
+ border-radius:50%;display:flex;align-items:center;justify-content:center;
503
+ font-size:14px;flex-shrink:0;margin-top:4px;">🧠</div>
504
+ <div style="max-width:68%;">
505
+ <div style="background:#181b26;border:1px solid #ffffff0f;
506
+ border-radius:4px 16px 16px 16px;padding:14px 18px;
507
+ font-size:0.9rem;line-height:1.6;">
508
+ {formatted}
509
+ </div>
510
+ <div style="font-size:0.68rem;color:#50586e;margin-top:4px;padding-left:4px;">{ts}</div>
511
+ </div>
512
+ </div>
513
+ """, unsafe_allow_html=True)
514
+
515
+ if sources and st.session_state.mode_citations:
516
+ with st.expander(f"πŸ“š {len(sources)} source(s) cited"):
517
+ for i, src in enumerate(sources):
518
+ score = src.get("score", 0)
519
+ st.markdown(f"""
520
+ <div style="background:#1a1d27;border:1px solid #ffffff0a;border-radius:8px;
521
+ padding:10px 14px;margin:6px 0;">
522
+ <div style="display:flex;justify-content:space-between;margin-bottom:6px;">
523
+ <span style="font-size:0.78rem;color:#4ade80;font-weight:600;">
524
+ Source {i+1}</span>
525
+ <span style="font-size:0.72rem;color:#8892aa;background:#ffffff08;
526
+ padding:1px 7px;border-radius:4px;">
527
+ {score:.0%} match</span>
528
+ </div>
529
+ <div style="font-size:0.76rem;color:#50586e;margin-bottom:6px;">
530
+ πŸ“„ {src.get('source','Unknown')}</div>
531
+ <div style="font-size:0.82rem;color:#8892aa;line-height:1.6;
532
+ border-left:2px solid #4ade8044;padding-left:10px;">
533
+ {src.get('content','')[:400]}{'...' if len(src.get('content',''))>400 else ''}
534
+ </div>
535
+ </div>
536
+ """, unsafe_allow_html=True)
537
+
538
+ # ── Chat Input ──
539
+ st.markdown("<div style='height:16px;'></div>", unsafe_allow_html=True)
540
+ user_input = st.chat_input("Ask a question...")
541
 
542
  if user_input:
543
+ ts = now_time()
544
+ st.session_state.messages.append({"role": "user", "content": user_input, "time": ts})
545
+
546
+ # Render user bubble immediately
547
+ st.markdown(f"""
548
+ <div style="display:flex;justify-content:flex-end;margin:8px 0 2px;">
549
+ <div style="max-width:65%;">
550
+ <div style="background:#1e2235;border:1px solid #ffffff0f;
551
+ border-radius:16px 16px 4px 16px;padding:14px 18px;
552
+ color:#f0f2f8;font-size:0.9rem;line-height:1.6;">{user_input}</div>
553
+ <div style="text-align:right;font-size:0.68rem;color:#50586e;
554
+ margin-top:4px;padding-right:4px;">{ts}</div>
555
+ </div>
556
+ </div>
557
+ """, unsafe_allow_html=True)
558
+
559
+ full_response = ""
560
+ sources = []
561
+
562
+ # Assistant bubble wrapper
563
+ with st.container():
564
+ st.markdown("""
565
+ <div style="display:flex;justify-content:flex-start;margin:8px 0;gap:10px;">
566
+ <div style="width:32px;height:32px;background:linear-gradient(135deg,#4ade80,#22c55e);
567
+ border-radius:50%;display:flex;align-items:center;justify-content:center;
568
+ font-size:14px;flex-shrink:0;margin-top:4px;">🧠</div>
569
+ <div style="max-width:68%;min-width:120px;">
570
+ """, unsafe_allow_html=True)
571
+
572
  placeholder = st.empty()
573
+
574
+ resp, err = api_call("POST", "/query", json={"question": user_input}, stream=True)
575
+
576
+ if err:
577
+ full_response = err
578
+ placeholder.markdown(f"""
579
+ <div style="background:#181b26;border:1px solid #f87171aa;
580
+ border-radius:4px 16px 16px 16px;padding:14px 18px;
581
+ color:#f87171;font-size:0.9rem;">{err}</div>
582
+ """, unsafe_allow_html=True)
583
+ elif resp:
 
 
 
 
584
  try:
585
+ buf = ""
586
+ for line in resp.iter_lines():
587
  if line:
588
  try:
589
+ data = json.loads(line.decode("utf-8"))
 
 
590
  if data.get("type") == "sources":
591
  sources = data.get("data", [])
592
  elif data.get("type") == "token":
593
+ buf += data.get("content", "")
594
+ full_response = buf
595
+ placeholder.markdown(f"""
596
+ <div style="background:#181b26;border:1px solid #ffffff0f;
597
+ border-radius:4px 16px 16px 16px;padding:14px 18px;
598
+ color:#c4cad9;font-size:0.9rem;line-height:1.7;">{buf}β–Œ</div>
599
+ """, unsafe_allow_html=True)
600
  except json.JSONDecodeError:
601
  continue
602
+
603
+ # Final render
604
+ placeholder.markdown(f"""
605
+ <div style="background:#181b26;border:1px solid #ffffff0f;
606
+ border-radius:4px 16px 16px 16px;padding:14px 18px;
607
+ color:#c4cad9;font-size:0.9rem;line-height:1.7;">{full_response}</div>
608
+ """, unsafe_allow_html=True)
609
+
 
 
 
 
 
 
 
 
610
  except Exception as e:
611
+ full_response = f"❌ Error: {e}"
612
+ placeholder.markdown(f"""
613
+ <div style="background:#181b26;border:1px solid #f87171aa;
614
+ border-radius:4px 16px 16px 16px;padding:14px 18px;
615
+ color:#f87171;font-size:0.9rem;">{full_response}</div>
616
+ """, unsafe_allow_html=True)
617
+
618
+ st.markdown("</div></div>", unsafe_allow_html=True)
619
+
620
+ if sources and st.session_state.mode_citations:
621
+ with st.expander(f"πŸ“š {len(sources)} source(s) cited"):
622
+ for i, src in enumerate(sources):
623
+ score = src.get("score", 0)
624
+ st.markdown(f"""
625
+ <div style="background:#1a1d27;border:1px solid #ffffff0a;border-radius:8px;
626
+ padding:10px 14px;margin:6px 0;">
627
+ <div style="display:flex;justify-content:space-between;margin-bottom:6px;">
628
+ <span style="font-size:0.78rem;color:#4ade80;font-weight:600;">
629
+ Source {i+1}</span>
630
+ <span style="font-size:0.72rem;color:#8892aa;">
631
+ {score:.0%} match</span>
632
+ </div>
633
+ <div style="font-size:0.76rem;color:#50586e;margin-bottom:6px;">
634
+ πŸ“„ {src.get('source','Unknown')}</div>
635
+ <div style="font-size:0.82rem;color:#8892aa;line-height:1.6;
636
+ border-left:2px solid #4ade8044;padding-left:10px;">
637
+ {src.get('content','')[:400]}{'...' if len(src.get('content',''))>400 else ''}
638
+ </div>
639
+ </div>
640
+ """, unsafe_allow_html=True)
641
+
642
+ # Save to session
643
+ bot_ts = now_time()
644
  st.session_state.messages.append({
645
  "role": "assistant",
646
  "content": full_response,
647
+ "sources": sources,
648
+ "time": bot_ts
649
  })
650
 
651
+ # ── Bottom toolbar ──
652
+ st.markdown("<div style='height:8px;'></div>", unsafe_allow_html=True)
653
+ st.markdown('<hr style="border-color:#ffffff08;margin:0;"/>', unsafe_allow_html=True)
654
+
655
+ c1, c2, c3, c4 = st.columns([1, 1, 1, 5])
656
+ with c1:
657
+ active_s = "#4ade80" if st.session_state.mode_summary else "#50586e"
658
+ if st.button("β‡… Summary Mode", key="mode_sum"):
659
+ st.session_state.mode_summary = not st.session_state.mode_summary
660
+ st.rerun()
661
+ with c2:
662
+ active_c = "#4ade80" if st.session_state.mode_citations else "#50586e"
663
+ if st.button("⊟ Source Citations", key="mode_cite"):
664
+ st.session_state.mode_citations = not st.session_state.mode_citations
665
+ st.rerun()
666
+ with c3:
667
+ if st.button("β‡Œ Translate", key="mode_trans"):
668
+ st.toast("Translation mode coming soon!", icon="🌐")
669
+
670
+ st.markdown("""
671
+ <style>
672
+ /* Bottom toolbar buttons */
673
+ div[data-testid="stHorizontalBlock"] > div:nth-child(1) button,
674
+ div[data-testid="stHorizontalBlock"] > div:nth-child(2) button,
675
+ div[data-testid="stHorizontalBlock"] > div:nth-child(3) button {
676
+ background: transparent !important;
677
+ border: none !important;
678
+ color: #50586e !important;
679
+ font-size: 0.72rem !important;
680
+ font-weight: 500 !important;
681
+ letter-spacing: 0.04em !important;
682
+ padding: 4px 8px !important;
683
+ width: auto !important;
684
+ }
685
+ div[data-testid="stHorizontalBlock"] > div:nth-child(1) button:hover,
686
+ div[data-testid="stHorizontalBlock"] > div:nth-child(2) button:hover,
687
+ div[data-testid="stHorizontalBlock"] > div:nth-child(3) button:hover {
688
+ color: #4ade80 !important;
689
+ background: transparent !important;
690
+ }
691
+ </style>
692
+ """, unsafe_allow_html=True)