ktejeshnaidu commited on
Commit
950ba0f
Β·
verified Β·
1 Parent(s): c0c8469

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +298 -573
app.py CHANGED
@@ -1,399 +1,188 @@
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,233 +191,169 @@ def safe_api_call(method, endpoint, **kwargs):
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()
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ DocuMind - Streamlit Frontend for HuggingFace Spaces
3
+ Updated visually to match the specific dark-sidebar & green-chat UI mockup.
4
  """
5
 
6
  import streamlit as st
 
7
  import json
8
  import time
9
+ import requests
10
+
11
+ # ============================================================================
12
+ # STREAMLIT CONFIG (Must be first)
13
+ # ============================================================================
14
 
 
 
 
15
  st.set_page_config(
16
+ page_title="DocuMind AI - Digital Curator",
17
  page_icon="🧠",
18
  layout="wide",
19
+ initial_sidebar_state="expanded"
20
  )
21
 
22
+ # ============================================================================
23
+ # CUSTOM CSS INJECTION (Theming to match the new mockup)
24
+ # ============================================================================
25
+ custom_css = """
26
+ <style>
27
+ /* Global Font and Backgrounds */
28
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
29
+
30
+ html, body, [class*="css"] {
31
+ font-family: 'Inter', sans-serif;
32
+ }
33
+
34
+ /* Main App Background */
35
+ .stApp {
36
+ background-color: #F8F9FA;
37
+ }
38
+
39
+ /* Sidebar Background & Styling (Dark Theme) */
40
+ [data-testid="stSidebar"] {
41
+ background-color: #172430 !important;
42
+ border-right: none;
43
+ }
44
+ [data-testid="stSidebar"] * {
45
+ color: #FFFFFF !important;
46
+ }
47
+
48
+ /* Hide default header and footer */
49
+ header {visibility: hidden;}
50
+ footer {visibility: hidden;}
51
+
52
+ /* Sidebar Headers & Text */
53
+ .sidebar-title {
54
+ font-size: 1.25rem;
55
+ font-weight: 700;
56
+ color: #FFFFFF;
57
+ margin-bottom: 2px;
58
+ }
59
+ .sidebar-subtitle {
60
+ font-size: 0.8rem;
61
+ color: #8A9CA8;
62
+ margin-bottom: 25px;
63
+ }
64
+ .recent-header {
65
+ font-size: 0.75rem;
66
+ font-weight: 600;
67
+ color: #8A9CA8;
68
+ margin-top: 20px;
69
+ margin-bottom: 15px;
70
+ letter-spacing: 1px;
71
+ }
72
+ .recent-item {
73
+ color: #D1D8DD;
74
+ font-size: 0.95rem;
75
+ padding: 8px 0;
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 10px;
79
+ cursor: pointer;
80
+ }
81
+
82
+ /* Upload Box override for Dark Sidebar */
83
+ [data-testid="stFileUploader"] {
84
+ border: 1px solid #2B3B4A;
85
+ border-radius: 12px;
86
+ padding: 15px 10px;
87
+ background-color: transparent;
88
+ text-align: center;
89
+ }
90
+ [data-testid="stFileUploader"] > div > div {
91
+ background-color: transparent;
92
+ }
93
+
94
+ /* Chat bubbles */
95
+ [data-testid="stChatMessage"] {
96
+ border-radius: 12px;
97
+ padding: 15px 20px;
98
+ margin-bottom: 10px;
99
+ max-width: 85%;
100
+ }
101
+ /* Assistant Bubble */
102
+ [data-testid="stChatMessage"]:has([data-testid="assistantAvatar"]) {
103
+ background-color: #EEF0F2;
104
+ color: #111827;
105
+ margin-right: auto;
106
+ }
107
+ /* User Bubble */
108
+ [data-testid="stChatMessage"]:has([data-testid="userAvatar"]) {
109
+ background-color: #0C4A42;
110
+ color: white !important;
111
+ margin-left: auto;
112
+ }
113
+ [data-testid="stChatMessage"]:has([data-testid="userAvatar"]) p {
114
+ color: white !important;
115
+ }
116
+
117
+ /* Welcome Header */
118
+ .welcome-header {
119
+ text-align: center;
120
+ color: #111827;
121
+ font-size: 2.5rem;
122
+ font-weight: 700;
123
+ margin-top: 60px;
124
+ margin-bottom: 15px;
125
+ }
126
+ .welcome-sub {
127
+ text-align: center;
128
+ color: #4B5563;
129
+ font-size: 1.1rem;
130
+ margin-bottom: 50px;
131
+ line-height: 1.5;
132
+ }
133
+
134
+ /* Top Nav Badge */
135
+ .top-badge-container {
136
+ display: flex;
137
+ justify-content: flex-end;
138
+ padding: 10px 20px 0 0;
139
+ }
140
+ .status-badge {
141
+ background-color: #E2E8F0;
142
+ color: #475569;
143
+ padding: 6px 12px;
144
+ border-radius: 6px;
145
+ font-size: 0.75rem;
146
+ font-weight: 700;
147
+ letter-spacing: 0.5px;
148
+ }
149
+ </style>
150
+ """
151
+ st.markdown(custom_css, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
 
 
 
 
153
 
154
+ # ============================================================================
155
+ # API CONFIGURATION & UTILITIES
156
+ # ============================================================================
 
157
 
158
+ API_URL = "http://127.0.0.1:8000"
159
+ API_TIMEOUT = 30 # seconds
 
 
 
 
 
160
 
161
+ def check_backend_health(retries=5):
162
+ """Check if backend API is running"""
163
+ for attempt in range(retries):
164
+ try:
165
+ response = requests.get(f"{API_URL}/docs", timeout=5)
166
+ if response.status_code == 200:
167
+ return True
168
+ except:
169
+ if attempt < retries - 1:
170
+ time.sleep(1)
171
+ return False
172
 
 
 
 
173
  def safe_api_call(method, endpoint, **kwargs):
174
+ """Safely call API with error handling"""
175
  try:
176
  url = f"{API_URL}{endpoint}"
177
+ kwargs.setdefault('timeout', API_TIMEOUT)
178
+
179
  if method == "GET":
180
  response = requests.get(url, **kwargs)
181
  elif method == "POST":
182
  response = requests.post(url, **kwargs)
183
  else:
184
  return None, "Invalid method"
185
+
186
  return response, None
187
  except requests.exceptions.Timeout:
188
  return None, "⏱️ Request timed out"
 
191
  except Exception as e:
192
  return None, f"❌ Error: {str(e)}"
193
 
194
+ # Initialize Session States
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  if "messages" not in st.session_state:
196
  st.session_state.messages = []
 
197
  if "backend_checked" not in st.session_state:
198
  st.session_state.backend_checked = False
199
  st.session_state.backend_healthy = False
200
 
201
+ # Health check on load
 
 
 
 
 
202
  if not st.session_state.backend_checked:
203
+ st.session_state.backend_healthy = check_backend_health(retries=1)
 
 
204
  st.session_state.backend_checked = True
205
 
206
+ # ============================================================================
207
+ # UI: SIDEBAR
208
+ # ============================================================================
209
  with st.sidebar:
210
+ # Logo & Title
211
+ st.markdown("""
212
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 10px;">
213
+ <div style="font-size: 2rem;">β—§</div>
214
  <div>
215
+ <div class="sidebar-title">DocuMind AI</div>
216
+ <div class="sidebar-subtitle">Digital Curator</div>
217
  </div>
218
  </div>
219
+ """, unsafe_allow_html=True)
220
+
221
+ # New Chat Button
222
+ if st.button("οΌ‹ New Chat", use_container_width=True, type="primary"):
 
223
  st.session_state.messages = []
 
224
  st.rerun()
225
+
226
+ st.write("") # Spacer
227
+
228
+ # Upload Area
229
+ uploaded_file = st.file_uploader("Upload Document", type=["txt", "pdf", "docx"], label_visibility="collapsed")
230
+ # Custom badge for the limit underneath the uploader
231
+ st.markdown("""
232
+ <div style="text-align: center; margin-top: -15px; margin-bottom: 20px;">
233
+ <span style="background-color: #2B3B4A; color: #8A9CA8; font-size: 0.7rem; padding: 3px 8px; border-radius: 12px; font-weight: 600;">200MB LIMIT</span>
234
+ </div>
235
+ """, unsafe_allow_html=True)
236
+
237
+ if uploaded_file and st.button("Process Document", use_container_width=True):
238
+ with st.spinner("Analyzing document..."):
239
+ files = {"file": (uploaded_file.name, uploaded_file.getvalue())}
240
+ response, error = safe_api_call("POST", "/ingest", files=files)
241
+ if error:
242
+ st.error(error)
243
+ elif response and response.status_code == 200:
244
+ st.success("βœ… Added to knowledge base!")
245
+ else:
246
+ st.error("❌ Upload failed.")
247
+
248
+ st.divider()
249
+
250
+ # Recents List
251
+ st.markdown("<div class='recent-header'>RECENTS</div>", unsafe_allow_html=True)
252
+
253
+ # Try fetching real recents, otherwise show static UI
254
  response, error = safe_api_call("GET", "/sources")
255
+ if not error and response and response.status_code == 200:
256
+ documents = response.json().get("documents", [])
257
+ for doc in documents[:5]: # Show top 5
258
+ st.markdown(f"<div class='recent-item'>πŸ•’ {doc[:20]}{'...' if len(doc)>20 else ''}</div>", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  else:
260
+ # Static Mockup UI items based on the image
261
+ st.markdown("<div class='recent-item'>πŸ•’ Q3 Financial Report</div>", unsafe_allow_html=True)
262
+ st.markdown("<div class='recent-item'>πŸ•’ Product Roadmap 2024</div>", unsafe_allow_html=True)
263
+ st.markdown("<div class='recent-item'>πŸ•’ Research Summary</div>", unsafe_allow_html=True)
264
+
265
+ # ============================================================================
266
+ # UI: MAIN INTERFACE
267
+ # ============================================================================
268
+
269
+ # Top Navigation Badge
270
+ st.markdown("""
271
+ <div class="top-badge-container">
272
+ <span class="status-badge">CURRENT SESSION ACTIVE</span>
 
 
 
 
 
 
 
 
 
273
  </div>
274
+ """, unsafe_allow_html=True)
 
 
275
 
276
+ # Show backend warning cleanly if down
277
+ if not st.session_state.backend_healthy:
278
+ st.warning("⚠️ Backend API is offline. Starting up or unreachable.")
 
 
 
 
 
 
 
 
 
279
 
280
+ # Welcome Screen (If no messages)
281
+ if len(st.session_state.messages) == 0:
282
+ st.markdown("<div class='welcome-header'>How can I assist your<br>research today?</div>", unsafe_allow_html=True)
283
+ st.markdown("<div class='welcome-sub'>I can analyze documents, summarize findings, or<br>answer specific questions about your archived data.</div>", unsafe_allow_html=True)
284
 
285
+ # Container for chat messages
286
+ chat_container = st.container()
287
+
288
+ with chat_container:
289
+ for msg in st.session_state.messages:
290
+ is_user = msg["role"] == "user"
291
+ # Hide the default user avatar to match the clean dark bubble look, use logo for assistant
292
+ avatar_icon = "β—§" if not is_user else None
293
+
294
+ with st.chat_message(msg["role"], avatar=avatar_icon):
295
+ st.markdown(msg["content"])
 
 
 
 
 
 
 
 
 
 
 
 
296
 
297
+ # ============================================================================
298
+ # UI: CHAT INPUT
299
+ # ============================================================================
 
 
 
 
300
 
301
+ st.write("") # Vertical spacing
302
 
 
 
 
303
  user_input = st.chat_input("Ask a question...")
304
 
305
  if user_input:
306
+ # Add and display user message immediately
307
+ st.session_state.messages.append({"role": "user", "content": user_input})
308
+ with chat_container:
309
+ with st.chat_message("user", avatar=None):
310
+ st.markdown(user_input)
311
+
312
+ # Get and display assistant response
313
+ with chat_container:
314
+ with st.chat_message("assistant", avatar="β—§"):
315
+ placeholder = st.empty()
316
+ full_response = ""
317
+ sources = []
318
+
319
+ response, error = safe_api_call(
320
+ "POST",
321
+ "/query",
322
+ json={"question": user_input},
323
+ stream=True
324
+ )
325
+
326
+ if error:
327
+ st.error(error)
328
+ full_response = error
329
+ elif response:
330
+ try:
331
+ for line in response.iter_lines():
332
+ if line:
333
+ try:
334
+ decoded_line = line.decode('utf-8')
335
+ data = json.loads(decoded_line)
336
+
337
+ if data.get("type") == "sources":
338
+ sources = data.get("data", [])
339
+ elif data.get("type") == "token":
340
+ full_response += data.get("content", "")
341
+ placeholder.markdown(full_response + "β–Œ")
342
+ except json.JSONDecodeError:
343
+ continue
344
+
345
+ # Convert bullet points to checkmarks to match mockup design style
346
+ styled_response = full_response.replace("- ", "βœ… ")
347
+ placeholder.markdown(styled_response)
348
+
349
+ except Exception as e:
350
+ error_msg = f"❌ Error processing response: {str(e)}"
351
+ placeholder.markdown(error_msg)
352
+ full_response = error_msg
353
+
354
+ # Save to state
355
+ st.session_state.messages.append({
356
+ "role": "assistant",
357
+ "content": full_response,
358
+ "sources": sources
359
+ })