ktejeshnaidu commited on
Commit
c0c8469
·
verified ·
1 Parent(s): 67efdc2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +574 -336
app.py CHANGED
@@ -1,186 +1,399 @@
1
  """
2
- DocuMind - Streamlit Frontend for HuggingFace Spaces
3
- Updated visually to match the modern "Digital Curator" UI mockup.
4
  """
5
 
6
  import streamlit as st
 
7
  import json
8
- import os
9
  import time
10
- import requests
11
-
12
- # ============================================================================
13
- # STREAMLIT CONFIG (Must be first)
14
- # ============================================================================
15
 
 
 
 
16
  st.set_page_config(
17
- page_title="DocuMind AI - Digital Curator",
18
  page_icon="🧠",
19
  layout="wide",
20
- initial_sidebar_state="expanded"
21
  )
22
 
23
- # ============================================================================
24
- # CUSTOM CSS INJECTION (Theming to match the mockup)
25
- # ============================================================================
26
- custom_css = """
27
- <style>
28
- /* Global Font and Backgrounds */
29
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
30
-
31
- html, body, [class*="css"] {
32
- font-family: 'Inter', sans-serif;
33
- }
34
-
35
- /* Main App Background */
36
- .stApp {
37
- background-color: #FDFDFD;
38
- }
39
-
40
- /* Sidebar Background & Styling */
41
- [data-testid="stSidebar"] {
42
- background-color: #F4F6F8 !important;
43
- border-right: 1px solid #E5E9ED;
44
- }
45
-
46
- /* Hide default header and footer */
47
- header {visibility: hidden;}
48
- footer {visibility: hidden;}
49
-
50
- /* Sidebar custom elements */
51
- .sidebar-title {
52
- font-size: 1.25rem;
53
- font-weight: 700;
54
- color: #111827;
55
- margin-bottom: 2px;
56
- }
57
- .sidebar-subtitle {
58
- font-size: 0.8rem;
59
- color: #9CA3AF;
60
- margin-bottom: 20px;
61
- }
62
- .recent-item {
63
- color: #4B5563;
64
- font-size: 0.9rem;
65
- padding: 8px 0;
66
- display: flex;
67
- align-items: center;
68
- gap: 10px;
69
- cursor: pointer;
70
- }
71
-
72
- /* Upload Box override */
73
- [data-testid="stFileUploader"] > div {
74
- background-color: #0A3D4D;
75
- color: white;
76
- border-radius: 12px;
77
- padding: 10px;
78
- border: none;
79
- }
80
- [data-testid="stFileUploader"] small {
81
- color: #8BA8B0 !important;
82
- }
83
-
84
- /* Chat bubbles */
85
- [data-testid="stChatMessage"] {
86
- border-radius: 12px;
87
- padding: 15px 20px;
88
- margin-bottom: 10px;
89
- }
90
- /* Assistant Bubble */
91
- [data-testid="stChatMessage"]:has([data-testid="assistantAvatar"]) {
92
- background-color: #EFEFEF;
93
- color: #111827;
94
- }
95
- /* User Bubble */
96
- [data-testid="stChatMessage"]:has([data-testid="userAvatar"]) {
97
- background-color: #0A3D4D;
98
- color: white;
99
- }
100
-
101
- /* User text color fix for dark background */
102
- [data-testid="stChatMessage"]:has([data-testid="userAvatar"]) p {
103
- color: white !important;
104
- }
105
-
106
- /* Welcome Header */
107
- .welcome-header {
108
- text-align: center;
109
- color: #0A3D4D;
110
- font-size: 2.5rem;
111
- font-weight: 700;
112
- margin-top: 50px;
113
- margin-bottom: 10px;
114
- }
115
- .welcome-sub {
116
- text-align: center;
117
- color: #4B5563;
118
- font-size: 1.1rem;
119
- margin-bottom: 50px;
120
- }
121
-
122
- /* Top Nav */
123
- .top-nav {
124
- display: flex;
125
- justify-content: space-between;
126
- align-items: center;
127
- padding: 10px 0 30px 0;
128
- border-bottom: 1px solid #E5E9ED;
129
- margin-bottom: 30px;
130
- }
131
- .nav-title {
132
- color: #0A3D4D;
133
- font-weight: 600;
134
- font-size: 1.1rem;
135
- display: flex;
136
- align-items: center;
137
- gap: 10px;
138
- }
139
- .status-badge {
140
- background-color: #D1E5E8;
141
- color: #0A3D4D;
142
- padding: 2px 8px;
143
- border-radius: 12px;
144
- font-size: 0.7rem;
145
- font-weight: 700;
146
- }
147
- </style>
148
- """
149
- st.markdown(custom_css, unsafe_allow_html=True)
150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
- # ============================================================================
153
- # API CONFIGURATION & UTILITIES
154
- # ============================================================================
155
 
156
- API_URL = "http://127.0.0.1:8000"
157
- API_TIMEOUT = 30 # seconds
 
158
 
159
- def check_backend_health(retries=5):
160
- """Check if backend API is running"""
161
- for attempt in range(retries):
162
- try:
163
- response = requests.get(f"{API_URL}/docs", timeout=5)
164
- if response.status_code == 200:
165
- return True
166
- except:
167
- if attempt < retries - 1:
168
- time.sleep(1)
169
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  def safe_api_call(method, endpoint, **kwargs):
172
- """Safely call API with error handling"""
173
  try:
174
  url = f"{API_URL}{endpoint}"
175
- kwargs.setdefault('timeout', API_TIMEOUT)
176
-
177
  if method == "GET":
178
  response = requests.get(url, **kwargs)
179
  elif method == "POST":
180
  response = requests.post(url, **kwargs)
181
  else:
182
  return None, "Invalid method"
183
-
184
  return response, None
185
  except requests.exceptions.Timeout:
186
  return None, "⏱️ Request timed out"
@@ -189,208 +402,233 @@ def safe_api_call(method, endpoint, **kwargs):
189
  except Exception as e:
190
  return None, f"❌ Error: {str(e)}"
191
 
192
- # Initialize Session States
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  if "messages" not in st.session_state:
194
  st.session_state.messages = []
 
195
  if "backend_checked" not in st.session_state:
196
  st.session_state.backend_checked = False
197
  st.session_state.backend_healthy = False
198
 
199
- # Health check on load
 
 
 
 
 
200
  if not st.session_state.backend_checked:
201
- st.session_state.backend_healthy = check_backend_health(retries=1) # Fast check
 
 
202
  st.session_state.backend_checked = True
203
 
204
- # ============================================================================
205
- # UI: SIDEBAR
206
- # ============================================================================
207
  with st.sidebar:
208
- # Logo & Title
209
- st.markdown("""
210
- <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 5px;">
211
- <div style="background-color: #0A3D4D; color: white; border-radius: 8px; width: 32px; height: 32px; display: flex; justify-content: center; align-items: center;">🧠</div>
212
  <div>
213
- <div class="sidebar-title">DocuMind AI</div>
214
- <div class="sidebar-subtitle">Digital Curator</div>
215
  </div>
216
  </div>
217
- """, unsafe_allow_html=True)
218
-
219
- # New Chat Button
220
- if st.button("+ New Chat", use_container_width=True, type="secondary"):
 
221
  st.session_state.messages = []
 
222
  st.rerun()
223
-
224
- st.write("") # Spacer
225
-
226
- # Upload Area
227
- uploaded_file = st.file_uploader("Upload Document (200MB LIMIT)", type=["txt", "pdf", "docx"], label_visibility="collapsed")
228
- if uploaded_file and st.button("Process Document", use_container_width=True, type="primary"):
229
- with st.spinner("Analyzing document..."):
230
- files = {"file": (uploaded_file.name, uploaded_file.getvalue())}
231
- response, error = safe_api_call("POST", "/ingest", files=files)
232
- if error:
233
- st.error(error)
234
- elif response and response.status_code == 200:
235
- st.success("✅ Added to knowledge base!")
236
- else:
237
- st.error("❌ Upload failed.")
238
-
239
- st.markdown("<br>", unsafe_allow_html=True)
240
-
241
- # Recents List
242
- st.markdown("<div style='font-size: 0.75rem; font-weight: 600; color: #9CA3AF; margin-bottom: 10px; letter-spacing: 1px;'>RECENTS</div>", unsafe_allow_html=True)
243
-
244
- # Try fetching real recents, otherwise show static UI for design purposes
 
 
 
 
 
 
245
  response, error = safe_api_call("GET", "/sources")
246
- if not error and response and response.status_code == 200:
247
- documents = response.json().get("documents", [])
248
- for doc in documents[:5]: # Show top 5
249
- st.markdown(f"<div class='recent-item'>🕒 {doc[:20]}{'...' if len(doc)>20 else ''}</div>", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  else:
251
- # Fallback Mockup UI items if backend is empty/offline
252
- st.markdown("<div class='recent-item'>🕒 Q3 Financial Report</div>", unsafe_allow_html=True)
253
- st.markdown("<div class='recent-item'>🕒 Product Roadmap 2...</div>", unsafe_allow_html=True)
254
- st.markdown("<div class='recent-item'>🕒 Research Summary</div>", unsafe_allow_html=True)
255
-
256
- # Footer elements pushing to bottom
257
- st.markdown("<div style='margin-top: 150px;'></div>", unsafe_allow_html=True)
258
- st.markdown("<div class='recent-item'>⚙️ Settings</div>", unsafe_allow_html=True)
259
- st.markdown("<div class='recent-item'> Help</div>", unsafe_allow_html=True)
260
-
261
- st.divider()
262
-
263
- # User Profile
264
- st.markdown("""
265
- <div style="display: flex; align-items: center; gap: 10px;">
266
- <div style="background-color: #0A3D4D; border-radius: 50%; width: 35px; height: 35px; display: flex; justify-content: center; align-items: center; font-size: 1.2rem;">👨‍💼</div>
267
- <div>
268
- <div style="font-weight: 600; font-size: 0.9rem; color: #111827;">Alex Sterling</div>
269
- <div style="font-size: 0.75rem; color: #9CA3AF;">Pro Curator</div>
270
- </div>
271
  </div>
272
- """, unsafe_allow_html=True)
 
 
 
273
 
274
- # ============================================================================
275
- # UI: MAIN INTERFACE
276
- # ============================================================================
 
 
 
 
 
 
 
 
277
 
278
- # Top Navigation Bar
279
- st.markdown("""
280
- <div class="top-nav">
281
- <div class="nav-title">CURRENT SESSION <span class="status-badge">ACTIVE</span></div>
282
- <div style="color: #4B5563; font-size: 1.2rem; cursor: pointer;">🔔 &nbsp; 👤</div>
 
 
 
283
  </div>
284
- """, unsafe_allow_html=True)
285
-
286
- # Show backend warning cleanly if down
287
- if not st.session_state.backend_healthy:
288
- st.warning("⚠️ Backend API is offline. Starting up or unreachable.")
289
-
290
- # Welcome Screen (If no messages)
291
- if len(st.session_state.messages) == 0:
292
- st.markdown("<div class='welcome-header'>How can I assist your research today?</div>", unsafe_allow_html=True)
293
- st.markdown("<div class='welcome-sub'>I can analyze documents, summarize findings, or answer<br>specific questions about your archived data.</div>", unsafe_allow_html=True)
294
-
295
- # Container for chat messages
296
- chat_container = st.container()
297
-
298
- with chat_container:
299
- for msg in st.session_state.messages:
300
- # Determine avatar based on role
301
- is_user = msg["role"] == "user"
302
- avatar_emoji = "👤" if is_user else "🧠"
303
-
304
- # We assign an avatar placeholder. We use CSS above to target user vs assistant bubbles.
305
- with st.chat_message(msg["role"], avatar=avatar_emoji):
306
- st.markdown(msg["content"])
307
-
308
- # Display Sources if they exist
309
- if not is_user and "sources" in msg and msg.get("sources"):
310
- with st.expander("📄 View Citations"):
311
- for idx, src in enumerate(msg["sources"]):
312
- score = src.get('score', 0)
313
- st.caption(f"**{src.get('source', 'Document')}** (Match: {score:.1%})")
314
- content = src.get('content', '')
315
- st.markdown(f"<span style='color: #4B5563; font-size: 0.9rem;'>{content[:300]}...</span>", unsafe_allow_html=True)
316
-
317
- # ============================================================================
318
- # UI: CHAT INPUT & BOTTOM TOOLBAR
319
- # ============================================================================
320
-
321
- # Add some vertical spacing to push the input to the bottom
322
- st.write("")
323
 
324
- user_input = st.chat_input("Ask a question...")
 
 
 
325
 
326
- # Mockup Bottom Toolbar (Visual aesthetic matching the design)
327
- st.markdown("""
328
- <div style="display: flex; justify-content: center; gap: 30px; margin-top: -15px; color: #9CA3AF; font-size: 0.75rem; font-weight: 600; letter-spacing: 0.5px;">
329
- <span style="cursor: pointer; display: flex; align-items: center; gap: 5px;">✨ SUMMARY MODE</span>
330
- <span style="cursor: pointer; display: flex; align-items: center; gap: 5px;">📄 SOURCE CITATIONS</span>
331
- <span style="cursor: pointer; display: flex; align-items: center; gap: 5px;">🌐 TRANSLATE</span>
332
- </div>
333
- """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
 
 
 
 
 
 
 
335
 
336
  if user_input:
337
- # 1. Add and display user message immediately
338
- st.session_state.messages.append({"role": "user", "content": user_input})
339
- with chat_container:
340
- with st.chat_message("user", avatar="👤"):
341
- st.markdown(user_input)
342
-
343
- # 2. Get and display assistant response
344
- with chat_container:
345
- with st.chat_message("assistant", avatar="🧠"):
346
- placeholder = st.empty()
347
- full_response = ""
348
- sources = []
349
-
350
- response, error = safe_api_call(
351
- "POST",
352
- "/query",
353
- json={"question": user_input},
354
- stream=True
355
- )
356
-
357
- if error:
358
- st.error(error)
359
- full_response = error
360
- elif response:
361
- try:
362
- for line in response.iter_lines():
363
- if line:
364
- try:
365
- decoded_line = line.decode('utf-8')
366
- data = json.loads(decoded_line)
367
-
368
- if data.get("type") == "sources":
369
- sources = data.get("data", [])
370
- elif data.get("type") == "token":
371
- full_response += data.get("content", "")
372
- placeholder.markdown(full_response + "▌")
373
- except json.JSONDecodeError:
374
- continue
375
-
376
- placeholder.markdown(full_response)
377
-
378
- if sources:
379
- with st.expander("📄 View Citations"):
380
- for idx, src in enumerate(sources):
381
- score = src.get('score', 0)
382
- st.caption(f"**{src.get('source', 'Document')}** (Match: {score:.1%})")
383
- content = src.get('content', '')
384
- st.markdown(f"<span style='color: #4B5563; font-size: 0.9rem;'>{content[:300]}...</span>", unsafe_allow_html=True)
385
-
386
- except Exception as e:
387
- error_msg = f"❌ Error processing response: {str(e)}"
388
- placeholder.markdown(error_msg)
389
- full_response = error_msg
390
-
391
- # Save to state
392
- st.session_state.messages.append({
393
- "role": "assistant",
394
- "content": full_response,
395
- "sources": sources
396
- })
 
1
  """
2
+ DocuMind AI - Streamlit Frontend
3
+ Modern single-file UI inspired by the provided design
4
  """
5
 
6
  import streamlit as st
7
+ import requests
8
  import json
 
9
  import time
 
 
 
 
 
10
 
11
+ # =========================================================
12
+ # PAGE CONFIG
13
+ # =========================================================
14
  st.set_page_config(
15
+ page_title="DocuMind AI",
16
  page_icon="🧠",
17
  layout="wide",
18
+ initial_sidebar_state="expanded",
19
  )
20
 
21
+ API_URL = "http://127.0.0.1:8000"
22
+ API_TIMEOUT = 30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ # =========================================================
25
+ # CSS
26
+ # =========================================================
27
+ st.markdown(
28
+ """
29
+ <style>
30
+ :root {
31
+ --sidebar-bg: linear-gradient(180deg, #0f2340 0%, #0b1b33 100%);
32
+ --main-bg: #f7f9fc;
33
+ --card-bg: #f1f4f8;
34
+ --text-dark: #1a1f2b;
35
+ --muted: #6b7280;
36
+ --line: rgba(255,255,255,0.10);
37
+ --accent: #1f7a6d;
38
+ --accent-2: #2da8c9;
39
+ --user-bubble: linear-gradient(135deg, #0f6a58 0%, #0a4f47 100%);
40
+ --assistant-bubble: #eef1f5;
41
+ }
42
 
43
+ html, body, [class*="css"] {
44
+ font-family: "Inter", "Segoe UI", sans-serif;
45
+ }
46
 
47
+ .stApp {
48
+ background: var(--main-bg);
49
+ }
50
 
51
+ /* Reduce default padding */
52
+ .block-container {
53
+ padding-top: 0.9rem;
54
+ padding-bottom: 1rem;
55
+ padding-left: 1.2rem;
56
+ padding-right: 1.2rem;
57
+ }
58
+
59
+ /* Sidebar */
60
+ section[data-testid="stSidebar"] {
61
+ background: var(--sidebar-bg);
62
+ border-right: 1px solid rgba(255,255,255,0.06);
63
+ }
64
+
65
+ section[data-testid="stSidebar"] * {
66
+ color: white !important;
67
+ }
68
+
69
+ section[data-testid="stSidebar"] .stMarkdown p,
70
+ section[data-testid="stSidebar"] .stMarkdown li,
71
+ section[data-testid="stSidebar"] .stCaption {
72
+ color: rgba(255,255,255,0.84) !important;
73
+ }
74
+
75
+ /* Sidebar buttons */
76
+ section[data-testid="stSidebar"] .stButton > button {
77
+ width: 100%;
78
+ border-radius: 16px;
79
+ border: 1px solid rgba(255,255,255,0.16);
80
+ background: linear-gradient(135deg, rgba(50,139,203,0.95) 0%, rgba(60,180,197,0.85) 100%);
81
+ color: white;
82
+ font-weight: 600;
83
+ padding: 0.85rem 1rem;
84
+ box-shadow: 0 10px 30px rgba(0,0,0,0.18);
85
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
86
+ }
87
+
88
+ section[data-testid="stSidebar"] .stButton > button:hover {
89
+ transform: translateY(-1px);
90
+ box-shadow: 0 14px 34px rgba(0,0,0,0.22);
91
+ }
92
+
93
+ /* Upload button style */
94
+ section[data-testid="stSidebar"] .upload-card .stButton > button {
95
+ background: transparent;
96
+ border: 1px solid rgba(255,255,255,0.18);
97
+ box-shadow: none;
98
+ }
99
+
100
+ /* Custom sidebar cards */
101
+ .sidebar-brand {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 0.8rem;
105
+ margin: 0.25rem 0 1rem 0;
106
+ }
107
+
108
+ .brand-icon {
109
+ width: 3rem;
110
+ height: 3rem;
111
+ border-radius: 16px;
112
+ background: linear-gradient(135deg, #1e6a7a 0%, #2c8fd4 100%);
113
+ display: flex;
114
+ align-items: center;
115
+ justify-content: center;
116
+ font-size: 1.35rem;
117
+ font-weight: 800;
118
+ color: white;
119
+ box-shadow: 0 10px 24px rgba(0,0,0,0.24);
120
+ }
121
+
122
+ .brand-title {
123
+ font-size: 1.45rem;
124
+ font-weight: 800;
125
+ line-height: 1.1;
126
+ }
127
+
128
+ .brand-subtitle {
129
+ font-size: 0.95rem;
130
+ opacity: 0.78;
131
+ margin-top: 0.12rem;
132
+ }
133
+
134
+ .section-label {
135
+ margin-top: 1.2rem;
136
+ margin-bottom: 0.65rem;
137
+ font-size: 0.95rem;
138
+ letter-spacing: 0.12em;
139
+ text-transform: uppercase;
140
+ opacity: 0.78;
141
+ font-weight: 700;
142
+ }
143
+
144
+ .upload-card {
145
+ margin-top: 0.35rem;
146
+ padding: 1rem;
147
+ border-radius: 18px;
148
+ border: 1px solid rgba(255,255,255,0.16);
149
+ background: rgba(255,255,255,0.05);
150
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
151
+ }
152
+
153
+ .upload-card .stFileUploader {
154
+ margin-bottom: 0.5rem;
155
+ }
156
+
157
+ .limit-pill {
158
+ display: inline-block;
159
+ padding: 0.38rem 0.7rem;
160
+ border-radius: 999px;
161
+ background: rgba(255,255,255,0.12);
162
+ font-size: 0.78rem;
163
+ font-weight: 700;
164
+ letter-spacing: 0.04em;
165
+ margin-top: 0.6rem;
166
+ }
167
+
168
+ .divider-line {
169
+ height: 1px;
170
+ background: var(--line);
171
+ margin: 1.25rem 0;
172
+ }
173
+
174
+ .recents-item {
175
+ display: flex;
176
+ align-items: flex-start;
177
+ gap: 0.65rem;
178
+ margin: 0.9rem 0;
179
+ color: rgba(255,255,255,0.92);
180
+ font-size: 1rem;
181
+ line-height: 1.25;
182
+ }
183
+
184
+ .recent-icon {
185
+ width: 1.35rem;
186
+ flex: 0 0 1.35rem;
187
+ opacity: 0.9;
188
+ }
189
+
190
+ .bottom-user {
191
+ margin-top: 2rem;
192
+ padding-top: 1rem;
193
+ border-top: 1px solid rgba(255,255,255,0.10);
194
+ display: flex;
195
+ align-items: center;
196
+ gap: 0.75rem;
197
+ }
198
+
199
+ .avatar {
200
+ width: 2.6rem;
201
+ height: 2.6rem;
202
+ border-radius: 999px;
203
+ background: linear-gradient(135deg, #f0b56b 0%, #d97d4a 100%);
204
+ display: flex;
205
+ align-items: center;
206
+ justify-content: center;
207
+ font-size: 1.1rem;
208
+ color: white;
209
+ font-weight: 700;
210
+ }
211
+
212
+ .user-name {
213
+ font-weight: 700;
214
+ line-height: 1.1;
215
+ }
216
+
217
+ .user-role {
218
+ font-size: 0.9rem;
219
+ opacity: 0.76;
220
+ }
221
+
222
+ /* Main top badge */
223
+ .session-badge-wrap {
224
+ display: flex;
225
+ justify-content: flex-end;
226
+ margin-bottom: 0.8rem;
227
+ }
228
+
229
+ .session-badge {
230
+ padding: 0.55rem 0.9rem;
231
+ border-radius: 999px;
232
+ background: #e7ebf0;
233
+ color: #445063;
234
+ font-weight: 700;
235
+ letter-spacing: 0.04em;
236
+ font-size: 0.85rem;
237
+ }
238
+
239
+ /* Hero */
240
+ .hero {
241
+ text-align: center;
242
+ margin-top: 1.1rem;
243
+ margin-bottom: 1.8rem;
244
+ }
245
+
246
+ .hero h1 {
247
+ font-size: clamp(2.1rem, 4vw, 3.3rem);
248
+ line-height: 1.08;
249
+ margin-bottom: 0.7rem;
250
+ font-weight: 800;
251
+ color: #0f1722;
252
+ }
253
+
254
+ .hero p {
255
+ font-size: 1.1rem;
256
+ color: #556070;
257
+ line-height: 1.5;
258
+ max-width: 46rem;
259
+ margin: 0 auto;
260
+ }
261
+
262
+ /* Chat area spacing */
263
+ .chat-wrap {
264
+ max-width: 56rem;
265
+ margin: 0 auto;
266
+ padding-top: 0.5rem;
267
+ padding-bottom: 6rem;
268
+ }
269
+
270
+ /* Message rows */
271
+ .msg-row {
272
+ display: flex;
273
+ margin: 1rem 0;
274
+ align-items: flex-start;
275
+ gap: 0.7rem;
276
+ }
277
+
278
+ .msg-row.user {
279
+ justify-content: flex-end;
280
+ }
281
+
282
+ .msg-row.assistant {
283
+ justify-content: flex-start;
284
+ }
285
+
286
+ .bubble {
287
+ max-width: min(42rem, 86%);
288
+ padding: 1rem 1.15rem;
289
+ border-radius: 1.25rem;
290
+ font-size: 1.02rem;
291
+ line-height: 1.45;
292
+ box-shadow: 0 10px 25px rgba(15, 23, 34, 0.08);
293
+ white-space: pre-wrap;
294
+ word-wrap: break-word;
295
+ }
296
+
297
+ .bubble.user {
298
+ background: var(--user-bubble);
299
+ color: white;
300
+ border-bottom-right-radius: 0.45rem;
301
+ }
302
+
303
+ .bubble.assistant {
304
+ background: var(--assistant-bubble);
305
+ color: #202633;
306
+ border-bottom-left-radius: 0.45rem;
307
+ }
308
+
309
+ .assistant-avatar {
310
+ width: 2.2rem;
311
+ height: 2.2rem;
312
+ border-radius: 999px;
313
+ background: #22324f;
314
+ color: white;
315
+ display: flex;
316
+ align-items: center;
317
+ justify-content: center;
318
+ font-weight: 700;
319
+ font-size: 0.92rem;
320
+ flex: 0 0 2.2rem;
321
+ margin-top: 0.2rem;
322
+ }
323
+
324
+ .timestamp {
325
+ font-size: 0.82rem;
326
+ color: #7a8494;
327
+ margin-top: 0.35rem;
328
+ margin-left: 0.3rem;
329
+ }
330
+
331
+ /* Chat input area */
332
+ .stChatInput {
333
+ position: sticky;
334
+ bottom: 0.8rem;
335
+ z-index: 20;
336
+ }
337
 
338
+ /* Make the input look more like the screenshot */
339
+ [data-testid="stChatInput"] {
340
+ background: transparent;
341
+ }
342
+
343
+ [data-testid="stChatInput"] > div {
344
+ border-radius: 18px !important;
345
+ border: 1px solid rgba(15, 23, 34, 0.08) !important;
346
+ background: #eef2f6 !important;
347
+ box-shadow: 0 10px 30px rgba(15, 23, 34, 0.06);
348
+ }
349
+
350
+ [data-testid="stChatInput"] textarea {
351
+ background: transparent !important;
352
+ color: #1f2937 !important;
353
+ }
354
+
355
+ /* Hide default Streamlit stuff that adds clutter */
356
+ #MainMenu {visibility: hidden;}
357
+ footer {visibility: hidden;}
358
+ header {visibility: hidden;}
359
+
360
+ /* Expanders */
361
+ details {
362
+ border-radius: 14px;
363
+ }
364
+
365
+ /* Recents spacing */
366
+ .recent-doc {
367
+ font-size: 1rem;
368
+ line-height: 1.25;
369
+ margin-bottom: 0.9rem;
370
+ opacity: 0.95;
371
+ }
372
+
373
+ .muted-small {
374
+ font-size: 0.82rem;
375
+ opacity: 0.78;
376
+ }
377
+ </style>
378
+ """,
379
+ unsafe_allow_html=True,
380
+ )
381
+
382
+ # =========================================================
383
+ # API HELPERS
384
+ # =========================================================
385
  def safe_api_call(method, endpoint, **kwargs):
 
386
  try:
387
  url = f"{API_URL}{endpoint}"
388
+ kwargs.setdefault("timeout", API_TIMEOUT)
389
+
390
  if method == "GET":
391
  response = requests.get(url, **kwargs)
392
  elif method == "POST":
393
  response = requests.post(url, **kwargs)
394
  else:
395
  return None, "Invalid method"
396
+
397
  return response, None
398
  except requests.exceptions.Timeout:
399
  return None, "⏱️ Request timed out"
 
402
  except Exception as e:
403
  return None, f"❌ Error: {str(e)}"
404
 
405
+ def check_backend_health(retries=3):
406
+ for attempt in range(retries):
407
+ try:
408
+ response = requests.get(f"{API_URL}/docs", timeout=5)
409
+ if response.status_code == 200:
410
+ return True
411
+ except Exception:
412
+ if attempt < retries - 1:
413
+ time.sleep(1)
414
+ return False
415
+
416
+ # =========================================================
417
+ # SESSION STATE
418
+ # =========================================================
419
  if "messages" not in st.session_state:
420
  st.session_state.messages = []
421
+
422
  if "backend_checked" not in st.session_state:
423
  st.session_state.backend_checked = False
424
  st.session_state.backend_healthy = False
425
 
426
+ if "selected_doc" not in st.session_state:
427
+ st.session_state.selected_doc = None
428
+
429
+ # =========================================================
430
+ # BACKEND CHECK
431
+ # =========================================================
432
  if not st.session_state.backend_checked:
433
+ with st.spinner("Starting backend..."):
434
+ time.sleep(1.0)
435
+ st.session_state.backend_healthy = check_backend_health()
436
  st.session_state.backend_checked = True
437
 
438
+ # =========================================================
439
+ # SIDEBAR
440
+ # =========================================================
441
  with st.sidebar:
442
+ st.markdown(
443
+ """
444
+ <div class="sidebar-brand">
445
+ <div class="brand-icon">Ai</div>
446
  <div>
447
+ <div class="brand-title">DocuMind AI</div>
448
+ <div class="brand-subtitle">Digital Curator</div>
449
  </div>
450
  </div>
451
+ """,
452
+ unsafe_allow_html=True,
453
+ )
454
+
455
+ if st.button("+ New Chat", use_container_width=True):
456
  st.session_state.messages = []
457
+ st.session_state.selected_doc = None
458
  st.rerun()
459
+
460
+ st.markdown('<div class="section-label">Upload</div>', unsafe_allow_html=True)
461
+
462
+ st.markdown('<div class="upload-card">', unsafe_allow_html=True)
463
+ uploaded_file = st.file_uploader("Upload Document", type=["pdf", "docx", "txt"], label_visibility="collapsed")
464
+
465
+ if uploaded_file:
466
+ st.markdown('<div class="muted-small">Selected file: <b>{}</b></div>'.format(uploaded_file.name), unsafe_allow_html=True)
467
+
468
+ if st.button("Upload", use_container_width=True):
469
+ with st.spinner("Uploading and indexing..."):
470
+ files = {"file": (uploaded_file.name, uploaded_file.getvalue())}
471
+ response, error = safe_api_call("POST", "/ingest", files=files)
472
+
473
+ if error:
474
+ st.error(error)
475
+ elif response and response.status_code == 200:
476
+ st.success(f"Uploaded: {uploaded_file.name}")
477
+ else:
478
+ st.error(f"Upload failed: {response.text if response else 'Unknown error'}")
479
+
480
+ st.markdown('<div class="limit-pill">200MB LIMIT</div>', unsafe_allow_html=True)
481
+ st.markdown("</div>", unsafe_allow_html=True)
482
+
483
+ st.markdown('<div class="divider-line"></div>', unsafe_allow_html=True)
484
+
485
+ st.markdown('<div class="section-label">Recents</div>', unsafe_allow_html=True)
486
+
487
  response, error = safe_api_call("GET", "/sources")
488
+ if error:
489
+ st.markdown('<div class="muted-small">Could not fetch documents.</div>', unsafe_allow_html=True)
490
+ elif response and response.status_code == 200:
491
+ docs = response.json().get("documents", [])
492
+ if docs:
493
+ for doc in docs[:6]:
494
+ doc_label = doc if len(doc) <= 26 else doc[:24] + "..."
495
+ st.markdown(
496
+ f"""
497
+ <div class="recents-item">
498
+ <div class="recent-icon">🕘</div>
499
+ <div class="recent-doc">{doc_label}</div>
500
+ </div>
501
+ """,
502
+ unsafe_allow_html=True,
503
+ )
504
+ else:
505
+ st.markdown('<div class="muted-small">No documents indexed yet.</div>', unsafe_allow_html=True)
506
  else:
507
+ st.markdown('<div class="muted-small">Backend not ready yet.</div>', unsafe_allow_html=True)
508
+
509
+ st.markdown('<div class="bottom-user">', unsafe_allow_html=True)
510
+ st.markdown(
511
+ """
512
+ <div class="avatar">👤</div>
513
+ <div>
514
+ <div class="user-name">Alex Sterling</div>
515
+ <div class="user-role">Pro Curator</div>
 
 
 
 
 
 
 
 
 
 
 
516
  </div>
517
+ """,
518
+ unsafe_allow_html=True,
519
+ )
520
+ st.markdown("</div>", unsafe_allow_html=True)
521
 
522
+ # =========================================================
523
+ # MAIN HEADER
524
+ # =========================================================
525
+ st.markdown(
526
+ """
527
+ <div class="session-badge-wrap">
528
+ <div class="session-badge">CURRENT SESSION ACTIVE</div>
529
+ </div>
530
+ """,
531
+ unsafe_allow_html=True,
532
+ )
533
 
534
+ st.markdown(
535
+ """
536
+ <div class="hero">
537
+ <h1>How can I assist your<br>research today?</h1>
538
+ <p>
539
+ I can analyze documents, summarize findings, or answer specific questions
540
+ about your archived data.
541
+ </p>
542
  </div>
543
+ """,
544
+ unsafe_allow_html=True,
545
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
546
 
547
+ # =========================================================
548
+ # CHAT AREA
549
+ # =========================================================
550
+ st.markdown('<div class="chat-wrap">', unsafe_allow_html=True)
551
 
552
+ for idx, msg in enumerate(st.session_state.messages):
553
+ if msg["role"] == "user":
554
+ st.markdown(
555
+ f"""
556
+ <div class="msg-row user">
557
+ <div class="bubble user">{msg["content"]}</div>
558
+ </div>
559
+ """,
560
+ unsafe_allow_html=True,
561
+ )
562
+ else:
563
+ st.markdown(
564
+ f"""
565
+ <div class="msg-row assistant">
566
+ <div class="assistant-avatar">Ai</div>
567
+ <div>
568
+ <div class="bubble assistant">{msg["content"]}</div>
569
+ <div class="timestamp">{msg.get("timestamp", "")}</div>
570
+ </div>
571
+ </div>
572
+ """,
573
+ unsafe_allow_html=True,
574
+ )
575
+
576
+ if msg.get("sources"):
577
+ with st.expander("Source citations"):
578
+ for i, src in enumerate(msg["sources"], start=1):
579
+ st.markdown(f"**Source {i}**")
580
+ st.caption(f"From: {src.get('source', 'Unknown')}")
581
+ content = src.get("content", "")
582
+ st.write(content[:800] + ("..." if len(content) > 800 else ""))
583
 
584
+ st.markdown("</div>", unsafe_allow_html=True)
585
+
586
+ # =========================================================
587
+ # CHAT INPUT
588
+ # =========================================================
589
+ user_input = st.chat_input("Ask a question...")
590
 
591
  if user_input:
592
+ timestamp = time.strftime("%I:%M %p").lstrip("0")
593
+ st.session_state.messages.append(
594
+ {"role": "user", "content": user_input, "timestamp": timestamp}
595
+ )
596
+
597
+ with st.spinner("Thinking..."):
598
+ response, error = safe_api_call(
599
+ "POST",
600
+ "/query",
601
+ json={"question": user_input},
602
+ stream=True,
603
+ )
604
+
605
+ full_response = ""
606
+ sources = []
607
+
608
+ if error:
609
+ full_response = error
610
+ elif response:
611
+ try:
612
+ for line in response.iter_lines():
613
+ if not line:
614
+ continue
615
+ try:
616
+ data = json.loads(line.decode("utf-8"))
617
+ if data.get("type") == "token":
618
+ full_response += data.get("content", "")
619
+ elif data.get("type") == "sources":
620
+ sources = data.get("data", [])
621
+ except json.JSONDecodeError:
622
+ continue
623
+ except Exception as e:
624
+ full_response = f" Error processing response: {e}"
625
+
626
+ st.session_state.messages.append(
627
+ {
628
+ "role": "assistant",
629
+ "content": full_response,
630
+ "sources": sources,
631
+ "timestamp": timestamp,
632
+ }
633
+ )
634
+ st.rerun()