ktejeshnaidu commited on
Commit
5078486
·
verified ·
1 Parent(s): 08220ff

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +277 -707
app.py CHANGED
@@ -1,795 +1,365 @@
1
  """
2
- DocuMind AI - Minimal Enterprise RAG Chat UI
3
- Single-file Streamlit app matching a clean document-intelligence interface.
4
-
5
- Features:
6
- - Minimal light UI inspired by the provided reference image
7
- - Left sidebar with New Chat + Upload
8
- - Native Streamlit sidebar open/close toggle (top-left)
9
- - Chat history with user/assistant bubbles
10
- - Optional source citations from backend
11
- - Works with an existing FastAPI backend
12
  """
13
 
14
- from __future__ import annotations
15
-
16
- import html
17
  import json
18
  import time
19
- from datetime import datetime
20
-
21
  import requests
22
- import streamlit as st
23
-
24
 
25
  # ============================================================================
26
- # PAGE CONFIG
27
  # ============================================================================
 
28
  st.set_page_config(
29
  page_title="DocuMind AI",
30
  page_icon="🧠",
31
  layout="wide",
32
- initial_sidebar_state="expanded",
33
  )
34
 
35
-
36
  # ============================================================================
37
- # THEME + MINIMAL CSS
38
  # ============================================================================
39
- st.markdown(
40
- """
41
- <style>
42
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
43
-
44
- :root {
45
- --bg: #f6f8fb;
46
- --panel: #ffffff;
47
- --panel-soft: #f2f5f8;
48
- --panel-dark: #0f4c5c;
49
- --panel-dark-2: #0b3d49;
50
- --text: #0f172a;
51
- --muted: #667085;
52
- --muted-2: #94a3b8;
53
- --border: rgba(15, 23, 42, 0.08);
54
- --shadow: 0 8px 30px rgba(15, 23, 42, 0.06);
55
- --radius-xl: 28px;
56
- --radius-lg: 22px;
57
- --radius-md: 16px;
58
- --radius-sm: 12px;
59
- --accent: #0f4c5c;
60
- --accent-soft: rgba(15, 76, 92, 0.10);
61
- }
62
-
63
- html, body, [data-testid="stAppViewContainer"] {
64
- background: var(--bg) !important;
65
- color: var(--text) !important;
66
- font-family: 'Inter', sans-serif !important;
67
  }
68
-
69
- *, *::before, *::after { box-sizing: border-box; }
70
-
71
- /* Hide Streamlit chrome */
72
- #MainMenu, footer, header, [data-testid="stToolbar"], [data-testid="stDecoration"] {
73
- display: none !important;
74
- }
75
-
76
- /* Sidebar */
77
  [data-testid="stSidebar"] {
78
- background: linear-gradient(180deg, #f8fafc 0%, #f5f7fa 100%) !important;
79
- border-right: 1px solid var(--border) !important;
80
- }
81
-
82
- [data-testid="stSidebarContent"] {
83
- padding-top: 0 !important;
84
- }
85
-
86
- /* Main container */
87
- .main .block-container {
88
- padding: 0 !important;
89
- max-width: 100% !important;
90
- }
91
-
92
- /* Sidebar collapse button */
93
- [data-testid="collapsedControl"] {
94
- color: #0f172a !important;
95
- background: rgba(255,255,255,0.75) !important;
96
- border: 1px solid var(--border) !important;
97
- border-radius: 999px !important;
98
- box-shadow: var(--shadow) !important;
99
- }
100
-
101
- /* Buttons */
102
- .stButton > button {
103
- border-radius: 18px !important;
104
- border: 1px solid var(--border) !important;
105
- background: var(--panel) !important;
106
- color: var(--text) !important;
107
- font-family: 'Inter', sans-serif !important;
108
- font-weight: 600 !important;
109
- transition: 0.18s ease !important;
110
- box-shadow: none !important;
111
- width: 100% !important;
112
- padding: 0.8rem 1rem !important;
113
- }
114
- .stButton > button:hover {
115
- transform: translateY(-1px) !important;
116
- border-color: rgba(15, 76, 92, 0.18) !important;
117
- background: rgba(15, 76, 92, 0.04) !important;
118
- }
119
-
120
- /* Sidebar file uploader */
121
- [data-testid="stFileUploader"] {
122
- background: var(--panel) !important;
123
- border: 1px dashed rgba(15, 23, 42, 0.10) !important;
124
- border-radius: 18px !important;
125
- padding: 10px !important;
126
- box-shadow: none !important;
127
- }
128
- [data-testid="stFileUploader"] label,
129
- [data-testid="stFileUploadDropzone"] p,
130
- [data-testid="stFileUploadDropzone"] span {
131
- color: var(--muted) !important;
132
- font-family: 'Inter', sans-serif !important;
133
- }
134
-
135
- /* Chat input */
136
- [data-testid="stChatInput"] {
137
- background: transparent !important;
138
- }
139
- [data-testid="stChatInput"] textarea {
140
- background: var(--panel) !important;
141
- border-radius: 24px !important;
142
- border: 1px solid rgba(15, 23, 42, 0.08) !important;
143
- box-shadow: var(--shadow) !important;
144
- font-family: 'Inter', sans-serif !important;
145
- padding: 1rem 1rem 1rem 1rem !important;
146
- color: var(--text) !important;
147
- }
148
- [data-testid="stChatInput"] textarea::placeholder {
149
- color: var(--muted-2) !important;
150
- }
151
- [data-testid="stChatInput"] button {
152
- background: var(--accent) !important;
153
- color: #fff !important;
154
- border-radius: 999px !important;
155
- }
156
-
157
- /* Spacers */
158
- hr {
159
- border: none !important;
160
- border-top: 1px solid rgba(15, 23, 42, 0.06) !important;
161
- margin: 0 !important;
162
- }
163
-
164
- /* Scrollbar */
165
- ::-webkit-scrollbar { width: 8px; height: 8px; }
166
- ::-webkit-scrollbar-track { background: transparent; }
167
- ::-webkit-scrollbar-thumb { background: rgba(100, 116, 139, 0.25); border-radius: 999px; }
168
- ::-webkit-scrollbar-thumb:hover { background: rgba(100, 116, 139, 0.45); }
169
-
170
- /* Hero */
171
- .hero-wrap {
172
- display: flex;
173
- flex-direction: column;
174
- align-items: center;
175
- justify-content: center;
176
- text-align: center;
177
- padding: 5rem 1.25rem 2rem;
178
- min-height: 48vh;
179
- }
180
- .hero-title {
181
- font-size: clamp(2rem, 3.5vw, 3.5rem);
182
- line-height: 1.08;
183
- font-weight: 800;
184
- letter-spacing: -0.04em;
185
- color: #0f172a;
186
- margin: 0 0 1rem 0;
187
  }
188
- .hero-subtitle {
189
- max-width: 760px;
190
- font-size: 1.02rem;
191
- line-height: 1.65;
192
- color: var(--muted);
193
- margin: 0;
 
194
  }
195
 
196
- /* Session header */
197
- .session-row {
198
- display: flex;
199
- align-items: center;
200
- gap: 0.75rem;
201
- padding: 1rem 1.25rem 0.85rem 1.25rem;
202
- }
203
- .session-label {
204
- font-size: 0.95rem;
205
- font-weight: 800;
206
- letter-spacing: 0.02em;
207
- color: #0f4c5c;
208
- text-transform: uppercase;
209
- }
210
- .session-badge {
211
- display: inline-flex;
212
- align-items: center;
213
- padding: 0.28rem 0.65rem;
214
- border-radius: 999px;
215
- background: rgba(15, 76, 92, 0.10);
216
- color: #0f4c5c;
217
- font-size: 0.7rem;
218
- font-weight: 800;
219
- letter-spacing: 0.08em;
220
- }
221
-
222
- /* Sidebar content blocks */
223
- .sidebar-brand {
224
- padding: 1.1rem 1rem 1rem;
225
- border-bottom: 1px solid rgba(15, 23, 42, 0.06);
226
- }
227
- .brand-row {
228
  display: flex;
229
  align-items: center;
230
- gap: 0.75rem;
 
 
231
  }
232
- .brand-icon {
233
- width: 44px;
234
- height: 44px;
235
- border-radius: 14px;
236
- background: linear-gradient(135deg, #0f4c5c 0%, #116579 100%);
237
  color: white;
 
 
 
238
  display: flex;
239
- align-items: center;
240
  justify-content: center;
241
- font-size: 1.1rem;
242
- box-shadow: var(--shadow);
 
243
  }
244
  .brand-title {
245
- font-size: 1.05rem;
246
- font-weight: 800;
247
- color: #0f172a;
248
- line-height: 1.1;
249
  }
250
  .brand-subtitle {
251
- font-size: 0.78rem;
252
- color: var(--muted-2);
253
- margin-top: 0.15rem;
254
  }
255
 
256
- .sidebar-section {
257
- padding: 1rem;
258
- }
259
- .sidebar-heading {
260
- font-size: 0.72rem;
261
- font-weight: 800;
262
- letter-spacing: 0.12em;
263
- text-transform: uppercase;
264
- color: #64748b;
265
- margin: 0 0 0.6rem 0;
266
- }
267
- .recents-item {
268
- display: flex;
269
- align-items: center;
270
- gap: 0.6rem;
271
- padding: 0.65rem 0.2rem;
272
- color: #334155;
273
- font-size: 0.92rem;
274
- border-radius: 12px;
275
  }
276
- .recents-item span.icon {
277
- color: #64748b;
278
- font-size: 0.85rem;
279
  }
280
 
281
- /* Message bubbles */
282
- .msg-user-wrap {
283
- display: flex;
284
- justify-content: flex-end;
285
- margin: 0.8rem 0 0.25rem;
286
- }
287
- .msg-user {
288
- max-width: min(720px, 66%);
289
- background: linear-gradient(180deg, #0f4c5c 0%, #0b3d49 100%);
290
- color: #fff;
291
- border-radius: 22px 22px 6px 22px;
292
- padding: 1rem 1.1rem;
293
- box-shadow: var(--shadow);
294
- font-size: 0.98rem;
295
- line-height: 1.65;
296
- border: 1px solid rgba(255,255,255,0.08);
297
- white-space: pre-wrap;
298
- word-break: break-word;
299
- }
300
- .msg-bot-row {
301
- display: flex;
302
- gap: 0.75rem;
303
- align-items: flex-start;
304
- margin: 1rem 0 0.3rem;
305
- }
306
- .bot-avatar {
307
- width: 36px;
308
- height: 36px;
309
- border-radius: 12px;
310
- background: linear-gradient(135deg, #0f4c5c 0%, #116579 100%);
311
- color: #fff;
312
- display: flex;
313
- align-items: center;
314
- justify-content: center;
315
- font-size: 1rem;
316
- flex-shrink: 0;
317
- box-shadow: var(--shadow);
318
  }
319
- .msg-bot {
320
- max-width: min(820px, 72%);
321
- background: #eef2f6;
322
- color: #0f172a;
323
- border: 1px solid rgba(15, 23, 42, 0.06);
324
- border-radius: 8px 22px 22px 22px;
325
- padding: 1rem 1.1rem;
326
- font-size: 0.98rem;
327
- line-height: 1.7;
328
- white-space: normal;
329
- word-break: break-word;
330
- box-shadow: 0 2px 10px rgba(15, 23, 42, 0.03);
331
  }
332
- .msg-time {
333
- font-size: 0.72rem;
334
- color: #94a3b8;
335
- margin-top: 0.35rem;
336
  }
337
- .typing-cursor {
338
- opacity: 0.85;
339
  }
340
 
341
- /* Source cards */
342
- .source-card {
343
- background: #fff;
344
- border: 1px solid rgba(15, 23, 42, 0.07);
345
- border-radius: 16px;
346
- padding: 0.9rem 1rem;
347
- margin: 0.45rem 0;
348
- box-shadow: 0 2px 10px rgba(15, 23, 42, 0.03);
349
- }
350
- .source-head {
 
 
 
351
  display: flex;
352
- justify-content: space-between;
353
  align-items: center;
354
- margin-bottom: 0.35rem;
355
- font-size: 0.8rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
  font-weight: 700;
 
 
357
  }
358
- .source-title { color: #0f4c5c; }
359
- .source-score {
360
- color: #64748b;
361
- background: #f1f5f9;
362
- padding: 0.1rem 0.45rem;
363
- border-radius: 999px;
364
  font-weight: 700;
 
 
365
  }
366
- .source-file {
367
- font-size: 0.78rem;
368
- color: #64748b;
369
- margin-bottom: 0.4rem;
370
- }
371
- .source-snippet {
372
- font-size: 0.86rem;
373
- line-height: 1.55;
374
- color: #334155;
375
- border-left: 2px solid rgba(15, 76, 92, 0.18);
376
- padding-left: 0.7rem;
377
  }
378
 
379
- /* Bottom spacing so chat input doesn't overlap content */
380
- .bottom-space {
381
- height: 1.75rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  }
383
- </style>
384
- """,
385
- unsafe_allow_html=True,
386
- )
387
-
388
-
389
- # ============================================================================
390
- # CONFIG
391
- # ============================================================================
392
- API_URL = "http://127.0.0.1:8000"
393
- API_TIMEOUT = 60
394
-
395
 
396
- # ============================================================================
397
- # SESSION STATE
398
- # ============================================================================
399
- if "messages" not in st.session_state:
400
- st.session_state.messages = []
401
- if "backend_ok" not in st.session_state:
402
- st.session_state.backend_ok = False
403
- if "backend_checked" not in st.session_state:
404
- st.session_state.backend_checked = False
405
- if "chat_sessions" not in st.session_state:
406
- st.session_state.chat_sessions = [
407
- "Q3 Financial Report",
408
- "Product Roadmap 2024",
409
- "Research Summary",
410
- ]
411
- if "ingested_docs" not in st.session_state:
412
- st.session_state.ingested_docs = []
413
- if "show_sources" not in st.session_state:
414
- st.session_state.show_sources = True
415
 
416
 
417
  # ============================================================================
418
- # HELPERS
419
  # ============================================================================
420
- def check_backend() -> bool:
421
- try:
422
- r = requests.get(f"{API_URL}/docs", timeout=5)
423
- return r.status_code == 200
424
- except Exception:
425
- return False
426
 
 
 
427
 
428
- def api_call(method: str, endpoint: str, **kwargs):
429
  try:
430
- kwargs.setdefault("timeout", API_TIMEOUT)
431
  url = f"{API_URL}{endpoint}"
432
- fn = requests.get if method.upper() == "GET" else requests.post
433
- return fn(url, **kwargs), None
434
- except requests.exceptions.Timeout:
435
- return None, "⏱️ Request timed out"
436
- except requests.exceptions.ConnectionError:
437
- return None, "⚠️ Backend not responding"
438
- except Exception as e:
439
- return None, f"Error: {e}"
440
-
441
-
442
- def now_time() -> str:
443
- return datetime.now().strftime("%-I:%M %p")
444
-
445
-
446
- def esc(text: str) -> str:
447
- return html.escape(text or "")
448
-
449
-
450
- def render_text_block(text: str) -> str:
451
- """Convert plain text to safe HTML for display in a bubble."""
452
- safe = esc(text).replace("\n", "<br>")
453
- return safe
454
-
455
-
456
- def render_assistant_text(text: str) -> str:
457
- """Render assistant content with lightweight bullet styling."""
458
- lines = (text or "").splitlines()
459
- out = []
460
- for raw in lines:
461
- line = raw.strip()
462
- if not line:
463
- continue
464
-
465
- if line.startswith(("- ", "• ", "* ")):
466
- item = esc(line[2:])
467
- if ":" in item:
468
- key, val = item.split(":", 1)
469
- out.append(
470
- f"<div style='display:flex;gap:0.6rem;align-items:flex-start;margin:0.35rem 0;'>"
471
- f"<span style='color:#0f4c5c;font-weight:900;line-height:1.2;'>✓</span>"
472
- f"<span><strong>{key.strip()}:</strong> {val.strip()}</span>"
473
- f"</div>"
474
- )
475
- else:
476
- out.append(
477
- f"<div style='display:flex;gap:0.6rem;align-items:flex-start;margin:0.35rem 0;'>"
478
- f"<span style='color:#0f4c5c;font-weight:900;line-height:1.2;'>✓</span>"
479
- f"<span>{item}</span>"
480
- f"</div>"
481
- )
482
  else:
483
- out.append(f"<div style='margin:0.22rem 0;'>{esc(line)}</div>")
484
- return "".join(out)
485
-
486
-
487
- # ============================================================================
488
- # BOOTSTRAP
489
- # ============================================================================
490
- if not st.session_state.backend_checked:
491
- with st.spinner("Starting up..."):
492
- time.sleep(1.2)
493
- st.session_state.backend_ok = check_backend()
494
- st.session_state.backend_checked = True
495
-
496
- try:
497
- resp, _ = api_call("GET", "/sources")
498
- if resp and resp.status_code == 200:
499
- st.session_state.ingested_docs = resp.json().get("documents", [])
500
- except Exception:
501
- pass
502
 
 
 
 
503
 
504
  # ============================================================================
505
- # SIDEBAR
506
  # ============================================================================
507
  with st.sidebar:
508
- st.markdown(
509
- """
510
- <div class="sidebar-brand">
511
- <div class="brand-row">
512
- <div class="brand-icon">🧠</div>
513
- <div>
514
- <div class="brand-title">DocuMind AI</div>
515
- <div class="brand-subtitle">Digital Curator</div>
516
- </div>
517
  </div>
518
  </div>
519
- """,
520
- unsafe_allow_html=True,
521
- )
522
-
523
- st.markdown('<div class="sidebar-section">', unsafe_allow_html=True)
524
- if st.button("+ New Chat", key="new_chat"):
525
  st.session_state.messages = []
526
  st.rerun()
527
- st.markdown("</div>", unsafe_allow_html=True)
 
 
 
 
 
 
 
528
 
529
- st.markdown('<div class="sidebar-section" style="padding-top:0;">', unsafe_allow_html=True)
530
- st.markdown('<div class="sidebar-heading">Upload</div>', unsafe_allow_html=True)
531
- uploaded_file = st.file_uploader(
532
- "Upload document",
533
- type=["txt", "pdf", "docx"],
534
- label_visibility="collapsed",
535
- )
536
- if uploaded_file and st.button("Ingest Document", key="ingest_btn"):
537
  with st.spinner("Processing..."):
538
  files = {"file": (uploaded_file.name, uploaded_file.getvalue())}
539
- resp, err = api_call("POST", "/ingest", files=files)
540
- if err:
541
- st.error(err)
542
- elif resp and resp.status_code == 200:
543
- st.success("Ingested successfully")
544
- st.session_state.ingested_docs.append(uploaded_file.name)
545
  else:
546
- detail = "Unknown error"
547
- try:
548
- detail = resp.json().get("detail", resp.text)
549
- except Exception:
550
- if resp is not None:
551
- detail = resp.text
552
- st.error(detail)
553
- st.markdown("</div>", unsafe_allow_html=True)
554
-
555
- st.markdown('<div class="sidebar-section">', unsafe_allow_html=True)
556
- st.markdown('<div class="sidebar-heading">Recents</div>', unsafe_allow_html=True)
557
- recent_items = st.session_state.ingested_docs or st.session_state.chat_sessions
558
- for item in recent_items[:6]:
559
- label = item if len(item) <= 26 else item[:23] + "..."
560
- st.markdown(
561
- f"<div class='recents-item'><span class='icon'>🕘</span><span>{esc(label)}</span></div>",
562
- unsafe_allow_html=True,
563
- )
564
- st.markdown("</div>", unsafe_allow_html=True)
565
 
566
 
567
  # ============================================================================
568
- # MAIN UI
569
  # ============================================================================
570
- st.markdown(
571
- """
572
- <div class="session-row">
573
- <div class="session-label">CURRENT SESSION</div>
574
- <div class="session-badge">ACTIVE</div>
575
- </div>
576
- <hr>
577
- """,
578
- unsafe_allow_html=True,
579
- )
580
 
581
- if not st.session_state.backend_ok:
582
- st.markdown(
583
- """
584
- <div style="margin:1rem 1.25rem 0; padding:0.85rem 1rem; border-radius:16px;
585
- border:1px solid rgba(245, 158, 11, 0.18); background: rgba(245, 158, 11, 0.08);
586
- color:#b45309; font-size:0.9rem;">
587
- ⚠️ Backend is starting up. If it still shows after a minute, refresh the page.
588
- </div>
589
- """,
590
- unsafe_allow_html=True,
591
- )
592
 
593
- # Empty state
594
- if not st.session_state.messages:
595
- st.markdown(
596
- """
597
- <div class="hero-wrap">
598
- <h1 class="hero-title">How can I assist your research today?</h1>
599
- <p class="hero-subtitle">
600
- I can analyze documents, summarize findings, or answer specific questions about your archived data.
601
- </p>
602
- </div>
603
- """,
604
- unsafe_allow_html=True,
605
- )
606
 
607
- # Chat history
608
  for msg in st.session_state.messages:
609
- role = msg.get("role", "assistant")
610
- content = msg.get("content", "")
611
- ts = msg.get("time", "")
612
- sources = msg.get("sources", [])
 
613
 
614
- if role == "user":
615
- st.markdown(
616
- f"""
617
- <div class="msg-user-wrap">
618
- <div>
619
- <div class="msg-user">{render_text_block(content)}</div>
620
- <div class="msg-time" style="text-align:right; padding-right:0.4rem;">{esc(ts)}</div>
621
- </div>
622
- </div>
623
- """,
624
- unsafe_allow_html=True,
625
- )
626
- else:
627
- st.markdown(
628
- f"""
629
- <div class="msg-bot-row">
630
- <div class="bot-avatar">🧠</div>
631
- <div>
632
- <div class="msg-bot">{render_assistant_text(content)}</div>
633
- <div class="msg-time">{esc(ts)}</div>
634
- </div>
635
- </div>
636
- """,
637
- unsafe_allow_html=True,
638
- )
639
 
640
- if sources and st.session_state.show_sources:
641
- with st.expander(f"📚 {len(sources)} source(s) cited"):
642
- for i, src in enumerate(sources, start=1):
643
- score = float(src.get("score", 0) or 0)
644
- source_name = esc(str(src.get("source", "Unknown")))
645
- snippet = esc(str(src.get("content", ""))[:400])
646
- if len(str(src.get("content", ""))) > 400:
647
- snippet += "..."
648
-
649
- st.markdown(
650
- f"""
651
- <div class="source-card">
652
- <div class="source-head">
653
- <span class="source-title">Source {i}</span>
654
- <span class="source-score">{score:.0%} match</span>
655
- </div>
656
- <div class="source-file">📄 {source_name}</div>
657
- <div class="source-snippet">{snippet}</div>
658
- </div>
659
- """,
660
- unsafe_allow_html=True,
661
- )
662
-
663
-
664
- # Chat input
665
- st.markdown('<div class="bottom-space"></div>', unsafe_allow_html=True)
666
  user_input = st.chat_input("Ask a question...")
667
 
668
  if user_input:
669
- user_ts = now_time()
670
- st.session_state.messages.append({"role": "user", "content": user_input, "time": user_ts})
671
-
672
- # Render user bubble immediately
673
- st.markdown(
674
- f"""
675
- <div class="msg-user-wrap">
676
- <div>
677
- <div class="msg-user">{render_text_block(user_input)}</div>
678
- <div class="msg-time" style="text-align:right; padding-right:0.4rem;">{esc(user_ts)}</div>
679
- </div>
680
- </div>
681
- """,
682
- unsafe_allow_html=True,
683
- )
684
-
685
- full_response = ""
686
- sources: list[dict] = []
687
- assistant_placeholder = st.empty()
688
-
689
- # Assistant shell
690
- st.markdown(
691
- """
692
- <div class="msg-bot-row">
693
- <div class="bot-avatar">🧠</div>
694
- <div style="width:100%;">
695
- """,
696
- unsafe_allow_html=True,
697
- )
698
-
699
- resp, err = api_call("POST", "/query", json={"question": user_input}, stream=True)
700
-
701
- if err:
702
- full_response = err
703
- assistant_placeholder.markdown(
704
- f"""
705
- <div class="msg-bot" style="border-color: rgba(239, 68, 68, 0.25); color:#b91c1c; background:#fff5f5;">
706
- {esc(err)}
707
- </div>
708
- """,
709
- unsafe_allow_html=True,
710
  )
711
- elif resp:
712
- try:
713
- buf = ""
714
- for line in resp.iter_lines():
715
- if not line:
716
- continue
717
- try:
718
- data = json.loads(line.decode("utf-8"))
719
- except json.JSONDecodeError:
720
- continue
721
-
722
- if data.get("type") == "sources":
723
- sources = data.get("data", []) or []
724
- elif data.get("type") == "token":
725
- buf += data.get("content", "")
726
- full_response = buf
727
- assistant_placeholder.markdown(
728
- f"""
729
- <div class="msg-bot">{render_assistant_text(buf)}<span class="typing-cursor">▌</span></div>
730
- """,
731
- unsafe_allow_html=True,
732
- )
733
-
734
- assistant_placeholder.markdown(
735
- f"""
736
- <div class="msg-bot">{render_assistant_text(full_response)}</div>
737
- """,
738
- unsafe_allow_html=True,
739
- )
740
- except Exception as e:
741
- full_response = f"Error: {e}"
742
- assistant_placeholder.markdown(
743
- f"""
744
- <div class="msg-bot" style="border-color: rgba(239, 68, 68, 0.25); color:#b91c1c; background:#fff5f5;">
745
- {esc(full_response)}
746
- </div>
747
- """,
748
- unsafe_allow_html=True,
749
- )
750
-
751
- st.markdown("</div></div>", unsafe_allow_html=True)
752
-
753
- if sources and st.session_state.show_sources:
754
- with st.expander(f"📚 {len(sources)} source(s) cited"):
755
- for i, src in enumerate(sources, start=1):
756
- score = float(src.get("score", 0) or 0)
757
- source_name = esc(str(src.get("source", "Unknown")))
758
- snippet = esc(str(src.get("content", ""))[:400])
759
- if len(str(src.get("content", ""))) > 400:
760
- snippet += "..."
761
-
762
- st.markdown(
763
- f"""
764
- <div class="source-card">
765
- <div class="source-head">
766
- <span class="source-title">Source {i}</span>
767
- <span class="source-score">{score:.0%} match</span>
768
- </div>
769
- <div class="source-file">📄 {source_name}</div>
770
- <div class="source-snippet">{snippet}</div>
771
- </div>
772
- """,
773
- unsafe_allow_html=True,
774
- )
775
-
776
- bot_ts = now_time()
777
- st.session_state.messages.append(
778
- {
779
- "role": "assistant",
780
- "content": full_response,
781
- "sources": sources,
782
- "time": bot_ts,
783
- }
784
- )
785
-
786
-
787
- # ============================================================================
788
- # FOOTER NOTE
789
- # ============================================================================
790
- st.markdown(
791
- """
792
- <div style="height:1.5rem;"></div>
793
- """,
794
- unsafe_allow_html=True,
795
- )
 
1
  """
2
+ DocuMind - Streamlit Frontend for HuggingFace Spaces
3
+ Minimal & Clean UI implementation based on the provided 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",
17
  page_icon="🧠",
18
  layout="wide",
19
+ initial_sidebar_state="expanded"
20
  )
21
 
 
22
  # ============================================================================
23
+ # CUSTOM CSS (Theming to match the minimal clean mockup)
24
  # ============================================================================
25
+ custom_css = """
26
+ <style>
27
+ /* Base Font */
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 Backgrounds */
35
+ .stApp {
36
+ background-color: #FAFBFC;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  }
 
 
 
 
 
 
 
 
 
38
  [data-testid="stSidebar"] {
39
+ background-color: #F4F6F8 !important;
40
+ border-right: 1px solid #E5E7EB;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  }
42
+
43
+ /* Hide top bar & footer */
44
+ header {visibility: hidden;}
45
+ footer {visibility: hidden;}
46
+ .block-container {
47
+ padding-top: 2rem !important;
48
+ max-width: 900px;
49
  }
50
 
51
+ /* Sidebar Logo Header */
52
+ .sidebar-header {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  display: flex;
54
  align-items: center;
55
+ gap: 12px;
56
+ margin-bottom: 24px;
57
+ margin-top: -30px;
58
  }
59
+ .logo-icon {
60
+ background-color: #064052;
 
 
 
61
  color: white;
62
+ width: 32px;
63
+ height: 32px;
64
+ border-radius: 8px;
65
  display: flex;
 
66
  justify-content: center;
67
+ align-items: center;
68
+ font-weight: bold;
69
+ font-size: 16px;
70
  }
71
  .brand-title {
72
+ color: #111827;
73
+ font-weight: 600;
74
+ font-size: 1.1rem;
75
+ line-height: 1.2;
76
  }
77
  .brand-subtitle {
78
+ color: #9CA3AF;
79
+ font-size: 0.75rem;
 
80
  }
81
 
82
+ /* Override New Chat Button */
83
+ div[data-testid="stSidebar"] div[data-testid="stButton"] button {
84
+ width: 100%;
85
+ border-radius: 20px;
86
+ background-color: #EFEFEF;
87
+ color: #064052;
88
+ border: none;
89
+ font-weight: 500;
90
+ padding: 0.5rem 1rem;
 
 
 
 
 
 
 
 
 
 
91
  }
92
+ div[data-testid="stSidebar"] div[data-testid="stButton"] button:hover {
93
+ background-color: #E5E7EB;
 
94
  }
95
 
96
+ /* Override File Uploader to look like the dark Teal Box */
97
+ [data-testid="stFileUploader"] {
98
+ background-color: #064052;
99
+ border-radius: 16px;
100
+ padding: 1rem;
101
+ margin-top: 1rem;
102
+ border: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  }
104
+ [data-testid="stFileUploader"] > section {
105
+ text-align: center;
 
 
 
 
 
 
 
 
 
 
106
  }
107
+ [data-testid="stFileUploader"] * {
108
+ color: #FFFFFF !important;
 
 
109
  }
110
+ [data-testid="stFileUploader"] small {
111
+ display: none; /* Hide default size limit text */
112
  }
113
 
114
+ /* Recents Section */
115
+ .recents-title {
116
+ font-size: 0.7rem;
117
+ font-weight: 600;
118
+ color: #9CA3AF;
119
+ letter-spacing: 1px;
120
+ margin-top: 30px;
121
+ margin-bottom: 10px;
122
+ }
123
+ .recent-item {
124
+ color: #4B5563;
125
+ font-size: 0.85rem;
126
+ padding: 8px 0;
127
  display: flex;
 
128
  align-items: center;
129
+ gap: 10px;
130
+ cursor: pointer;
131
+ }
132
+
133
+ /* Top Bar Indicator */
134
+ .session-badge-container {
135
+ margin-bottom: 40px;
136
+ }
137
+ .session-title {
138
+ color: #064052;
139
+ font-weight: 600;
140
+ font-size: 0.9rem;
141
+ }
142
+ .badge-active {
143
+ background-color: #D1E5E8;
144
+ color: #064052;
145
+ padding: 2px 8px;
146
+ border-radius: 12px;
147
+ font-size: 0.65rem;
148
  font-weight: 700;
149
+ margin-left: 8px;
150
+ vertical-align: middle;
151
  }
152
+
153
+ /* Empty State Headers */
154
+ .welcome-title {
155
+ text-align: center;
156
+ color: #064052;
157
+ font-size: 2.2rem;
158
  font-weight: 700;
159
+ margin-top: 2rem;
160
+ margin-bottom: 1rem;
161
  }
162
+ .welcome-subtitle {
163
+ text-align: center;
164
+ color: #4B5563;
165
+ font-size: 1rem;
166
+ margin-bottom: 3rem;
167
+ line-height: 1.5;
 
 
 
 
 
168
  }
169
 
170
+ /* Chat Messages */
171
+ [data-testid="stChatMessage"] {
172
+ border-radius: 16px;
173
+ padding: 1.25rem;
174
+ margin-bottom: 1.5rem;
175
+ max-width: 85%;
176
+ }
177
+
178
+ /* Assistant Bubble */
179
+ [data-testid="stChatMessage"]:has([data-testid="assistantAvatar"]) {
180
+ background-color: #EAEAEA;
181
+ color: #111827;
182
+ margin-right: auto;
183
+ }
184
+
185
+ /* User Bubble */
186
+ [data-testid="stChatMessage"]:has([data-testid="userAvatar"]) {
187
+ background-color: #064052;
188
+ color: white;
189
+ margin-left: auto;
190
+ }
191
+ [data-testid="stChatMessage"]:has([data-testid="userAvatar"]) p {
192
+ color: white !important;
193
+ }
194
+
195
+ /* Hide User Avatar to match clean look */
196
+ [data-testid="stChatMessage"]:has([data-testid="userAvatar"]) [data-testid="chatAvatarIcon-user"] {
197
+ display: none;
198
  }
 
 
 
 
 
 
 
 
 
 
 
 
199
 
200
+ /* Chat Input */
201
+ [data-testid="stChatInput"] {
202
+ background-color: #FFFFFF;
203
+ border: 1px solid #E5E7EB;
204
+ border-radius: 30px;
205
+ padding-left: 10px;
206
+ padding-right: 10px;
207
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
208
+ }
209
+ </style>
210
+ """
211
+ st.markdown(custom_css, unsafe_allow_html=True)
 
 
 
 
 
 
 
212
 
213
 
214
  # ============================================================================
215
+ # API CONFIGURATION & UTILITIES
216
  # ============================================================================
 
 
 
 
 
 
217
 
218
+ API_URL = "http://127.0.0.1:8000"
219
+ API_TIMEOUT = 30
220
 
221
+ def safe_api_call(method, endpoint, **kwargs):
222
  try:
 
223
  url = f"{API_URL}{endpoint}"
224
+ kwargs.setdefault('timeout', API_TIMEOUT)
225
+
226
+ if method == "GET":
227
+ response = requests.get(url, **kwargs)
228
+ elif method == "POST":
229
+ response = requests.post(url, **kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  else:
231
+ return None, "Invalid method"
232
+ return response, None
233
+ except Exception as e:
234
+ return None, f"❌ Error: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
 
236
+ # Initialize Session States
237
+ if "messages" not in st.session_state:
238
+ st.session_state.messages = []
239
 
240
  # ============================================================================
241
+ # UI: SIDEBAR
242
  # ============================================================================
243
  with st.sidebar:
244
+ # Top Branding
245
+ st.markdown("""
246
+ <div class="sidebar-header">
247
+ <div class="logo-icon">🧠</div>
248
+ <div>
249
+ <div class="brand-title">DocuMind AI</div>
250
+ <div class="brand-subtitle">Digital Curator</div>
 
 
251
  </div>
252
  </div>
253
+ """, unsafe_allow_html=True)
254
+
255
+ # New Chat
256
+ if st.button("+ New Chat", use_container_width=True):
 
 
257
  st.session_state.messages = []
258
  st.rerun()
259
+
260
+ # Upload Area
261
+ uploaded_file = st.file_uploader("Upload", type=["txt", "pdf", "docx"], label_visibility="collapsed")
262
+ st.markdown("""
263
+ <div style="text-align: center; margin-top: -20px; margin-bottom: 20px;">
264
+ <span style="color: #8BA8B0; font-size: 0.65rem; font-weight: 600; letter-spacing: 0.5px;">200MB LIMIT</span>
265
+ </div>
266
+ """, unsafe_allow_html=True)
267
 
268
+ if uploaded_file and st.button("Ingest Document", use_container_width=True):
 
 
 
 
 
 
 
269
  with st.spinner("Processing..."):
270
  files = {"file": (uploaded_file.name, uploaded_file.getvalue())}
271
+ response, error = safe_api_call("POST", "/ingest", files=files)
272
+ if not error and response and response.status_code == 200:
273
+ st.success("✅ Added to knowledge base!")
 
 
 
274
  else:
275
+ st.error("❌ Upload failed.")
276
+
277
+ # Recents
278
+ st.markdown("<div class='recents-title'>RECENTS</div>", unsafe_allow_html=True)
279
+
280
+ response, error = safe_api_call("GET", "/sources")
281
+ if not error and response and response.status_code == 200:
282
+ documents = response.json().get("documents", [])
283
+ for doc in documents[:5]:
284
+ st.markdown(f"<div class='recent-item'>🕒 {doc[:25]}</div>", unsafe_allow_html=True)
285
+ else:
286
+ # Static placeholders matching the mockup
287
+ st.markdown("<div class='recent-item'>🕒 Q3 Financial Report</div>", unsafe_allow_html=True)
288
+ st.markdown("<div class='recent-item'>🕒 Product Roadmap 2...</div>", unsafe_allow_html=True)
289
+ st.markdown("<div class='recent-item'>🕒 Research Summary</div>", unsafe_allow_html=True)
 
 
 
 
290
 
291
 
292
  # ============================================================================
293
+ # UI: MAIN CONTENT
294
  # ============================================================================
 
 
 
 
 
 
 
 
 
 
295
 
296
+ # Top Session Indicator
297
+ st.markdown("""
298
+ <div class="session-badge-container">
299
+ <span class="session-title">CURRENT SESSION</span>
300
+ <span class="badge-active">ACTIVE</span>
301
+ </div>
302
+ """, unsafe_allow_html=True)
 
 
 
 
303
 
304
+ # Empty State
305
+ if len(st.session_state.messages) == 0:
306
+ st.markdown("<div class='welcome-title'>How can I assist your research today?</div>", unsafe_allow_html=True)
307
+ st.markdown("<div class='welcome-subtitle'>I can analyze documents, summarize findings, or answer<br>specific questions about your archived data.</div>", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
308
 
309
+ # Chat Container
310
  for msg in st.session_state.messages:
311
+ is_user = msg["role"] == "user"
312
+ avatar = None if is_user else "🧠"
313
+
314
+ with st.chat_message(msg["role"], avatar=avatar):
315
+ st.markdown(msg["content"])
316
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
+ # ============================================================================
319
+ # UI: CHAT INPUT
320
+ # ============================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  user_input = st.chat_input("Ask a question...")
322
 
323
  if user_input:
324
+ # 1. Show user message
325
+ st.session_state.messages.append({"role": "user", "content": user_input})
326
+ with st.chat_message("user", avatar=None):
327
+ st.markdown(user_input)
328
+
329
+ # 2. Process and show assistant response
330
+ with st.chat_message("assistant", avatar="🧠"):
331
+ placeholder = st.empty()
332
+ full_response = ""
333
+
334
+ response, error = safe_api_call(
335
+ "POST",
336
+ "/query",
337
+ json={"question": user_input},
338
+ stream=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  )
340
+
341
+ if error:
342
+ placeholder.markdown(error)
343
+ full_response = error
344
+ elif response:
345
+ try:
346
+ for line in response.iter_lines():
347
+ if line:
348
+ try:
349
+ decoded_line = line.decode('utf-8')
350
+ data = json.loads(decoded_line)
351
+ if data.get("type") == "token":
352
+ full_response += data.get("content", "")
353
+ placeholder.markdown(full_response + "")
354
+ except json.JSONDecodeError:
355
+ continue
356
+
357
+ # Render final response (Replace bullets with checkmarks to match mockup)
358
+ final_text = full_response.replace("- ", "")
359
+ placeholder.markdown(final_text)
360
+
361
+ except Exception as e:
362
+ placeholder.markdown(f"❌ Error: {str(e)}")
363
+ full_response = f"Error: {str(e)}"
364
+
365
+ st.session_state.messages.append({"role": "assistant", "content": full_response})