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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +359 -205
app.py CHANGED
@@ -1,242 +1,396 @@
1
  """
2
- DocuMind - Modern UI + FastAPI Backend (Single File)
 
3
  """
4
 
5
  import streamlit as st
6
- import requests
7
  import json
 
8
  import time
 
 
 
 
 
9
 
10
- # =========================================================
11
- # CONFIG
12
- # =========================================================
13
  st.set_page_config(
14
- page_title="DocuMind AI",
15
  page_icon="🧠",
16
  layout="wide",
17
  initial_sidebar_state="expanded"
18
  )
19
 
20
- API_URL = "http://127.0.0.1:8000"
21
- API_TIMEOUT = 30
22
-
23
- # =========================================================
24
- # CUSTOM CSS (UI MAGIC)
25
- # =========================================================
26
- st.markdown("""
27
  <style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- /* Background */
30
- body {
31
- background-color: #f7f9fb;
32
- }
33
-
34
- /* Sidebar */
35
- section[data-testid="stSidebar"] {
36
- background-color: #ffffff;
37
- border-right: 1px solid #e6e8eb;
38
- }
39
-
40
- /* Buttons */
41
- .stButton>button {
42
- border-radius: 12px;
43
- background-color: #0f4c5c;
44
- color: white;
45
- border: none;
46
- padding: 10px;
47
- width: 100%;
48
- }
49
-
50
- /* Header */
51
- .main-title {
52
- text-align: center;
53
- font-size: 34px;
54
- font-weight: 600;
55
- margin-top: 20px;
56
- }
57
-
58
- .sub-text {
59
- text-align: center;
60
- color: gray;
61
- margin-bottom: 30px;
62
- }
63
-
64
- /* Chat bubbles */
65
- .chat-user {
66
- background-color: #0f4c5c;
67
- color: white;
68
- padding: 12px 16px;
69
- border-radius: 18px;
70
- margin: 10px 0;
71
- max-width: 70%;
72
- margin-left: auto;
73
- }
74
-
75
- .chat-assistant {
76
- background-color: #f1f3f5;
77
- color: black;
78
- padding: 12px 16px;
79
- border-radius: 18px;
80
- margin: 10px 0;
81
- max-width: 70%;
82
- }
83
-
84
- /* Input box */
85
- [data-testid="stChatInput"] {
86
- position: fixed;
87
- bottom: 20px;
88
- left: 300px;
89
- right: 20px;
90
- }
91
 
92
- </style>
93
- """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
- # =========================================================
96
- # API FUNCTIONS
97
- # =========================================================
98
  def safe_api_call(method, endpoint, **kwargs):
 
99
  try:
100
  url = f"{API_URL}{endpoint}"
101
- kwargs.setdefault("timeout", API_TIMEOUT)
102
-
103
  if method == "GET":
104
- res = requests.get(url, **kwargs)
 
 
105
  else:
106
- res = requests.post(url, **kwargs)
107
-
108
- return res, None
 
 
 
 
109
  except Exception as e:
110
- return None, str(e)
111
 
112
- # =========================================================
113
- # SESSION STATE
114
- # =========================================================
115
  if "messages" not in st.session_state:
116
  st.session_state.messages = []
117
-
118
- # =========================================================
119
- # SIDEBAR
120
- # =========================================================
 
 
 
 
 
 
 
 
121
  with st.sidebar:
122
- st.markdown("## 🧠 DocuMind AI")
123
- st.caption("Digital Curator")
124
-
125
- if st.button("βž• New Chat"):
 
 
 
 
 
 
 
 
 
126
  st.session_state.messages = []
127
-
128
- st.markdown("### πŸ“€ Upload")
129
-
130
- uploaded_file = st.file_uploader("", type=["pdf", "txt", "docx"])
131
-
132
- if uploaded_file and st.button("Upload Document"):
133
- files = {"file": (uploaded_file.name, uploaded_file.getvalue())}
134
- res, err = safe_api_call("POST", "/ingest", files=files)
135
-
136
- if err:
137
- st.error(err)
138
- elif res and res.status_code == 200:
139
- st.success("Uploaded successfully!")
140
- else:
141
- st.error("Upload failed")
142
-
143
- st.markdown("---")
144
- st.markdown("### πŸ“„ Recents")
145
-
146
- res, err = safe_api_call("GET", "/sources")
147
- if res and res.status_code == 200:
148
- docs = res.json().get("documents", [])
149
- for d in docs:
150
- st.markdown(f"β€’ {d}")
151
-
152
- # =========================================================
153
- # MAIN HEADER
154
- # =========================================================
155
- st.markdown(
156
- '<div class="main-title">How can I assist your research today?</div>',
157
- unsafe_allow_html=True
158
- )
159
-
160
- st.markdown(
161
- '<div class="sub-text">I can analyze documents, summarize findings, or answer questions.</div>',
162
- unsafe_allow_html=True
163
- )
164
-
165
- # =========================================================
166
- # CHAT DISPLAY
167
- # =========================================================
168
- for msg in st.session_state.messages:
169
- if msg["role"] == "user":
170
- st.markdown(
171
- f'<div class="chat-user">{msg["content"]}</div>',
172
- unsafe_allow_html=True
173
- )
174
  else:
175
- st.markdown(
176
- f'<div class="chat-assistant">{msg["content"]}</div>',
177
- unsafe_allow_html=True
178
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
- if msg.get("sources"):
181
- with st.expander("πŸ“š Sources"):
182
- for src in msg["sources"]:
183
- st.caption(src.get("source", "Unknown"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
- # =========================================================
186
- # CHAT INPUT
187
- # =========================================================
188
- user_input = st.chat_input("Ask a question about your documents...")
189
 
190
  if user_input:
191
- # Show user message instantly
192
- st.session_state.messages.append({
193
- "role": "user",
194
- "content": user_input
195
- })
196
-
197
- with st.spinner("Thinking..."):
198
- res, err = safe_api_call(
199
- "POST",
200
- "/query",
201
- json={"question": user_input},
202
- stream=True
203
- )
204
-
205
- full_response = ""
206
- sources = []
207
-
208
- if err:
209
- full_response = err
210
-
211
- elif res:
212
- try:
213
- for line in res.iter_lines():
214
- if line:
215
- data = json.loads(line.decode("utf-8"))
216
-
217
- if data.get("type") == "token":
218
- full_response += data.get("content", "")
219
-
220
- elif data.get("type") == "sources":
221
- sources = data.get("data", [])
222
-
223
- except Exception as e:
224
- full_response = str(e)
225
-
226
- # Save assistant response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  st.session_state.messages.append({
228
  "role": "assistant",
229
  "content": full_response,
230
  "sources": sources
231
- })
232
-
233
- st.rerun()
234
-
235
- # =========================================================
236
- # FOOTER
237
- # =========================================================
238
- st.markdown("---")
239
- st.markdown(
240
- "<div style='text-align:center; font-size:12px; color:gray;'>DocuMind AI β€’ Enterprise RAG</div>",
241
- unsafe_allow_html=True
242
- )
 
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"
187
+ except requests.exceptions.ConnectionError:
188
+ return None, "⚠️ Backend not responding"
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
+ })