sriiram18 commited on
Commit
100026d
Β·
verified Β·
1 Parent(s): 7fe41b2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +537 -374
app.py CHANGED
@@ -1,23 +1,29 @@
1
  import streamlit as st
2
- from transformers import AutoTokenizer, AutoModelForCausalLM
 
 
 
 
 
3
  import torch
4
  import time
5
- from datetime import datetime, timezone, timedelta
6
- import html as html_lib
7
-
8
- # ─── TIMEZONE (IST = UTC+5:30) ────────────────────────────────────────────────
9
- IST = timezone(timedelta(hours=5, minutes=30))
10
-
11
- def get_ist_time():
12
- return datetime.now(IST).strftime("%H:%M")
13
 
14
  st.set_page_config(
15
- page_title="TurboChat AI",
16
- page_icon="⚑",
17
- layout="centered",
18
  initial_sidebar_state="expanded"
19
  )
20
 
 
 
 
 
 
 
 
 
21
  # ─── GLOBAL CSS ───────────────────────────────────────────────────────────────
22
  st.markdown("""
23
  <style>
@@ -37,241 +43,292 @@ html, body, [data-testid="stAppViewContainer"] {
37
  footer, #MainMenu { visibility: hidden; }
38
 
39
  /* ── Header ── */
40
- .chat-header {
41
- text-align: center;
42
- padding: 20px 0 10px;
43
  border-bottom: 1px solid #21262d;
44
  margin-bottom: 20px;
 
45
  }
46
- .chat-title {
47
  font-family: 'Orbitron', monospace;
48
- font-size: 1.8rem;
49
- font-weight: 900;
50
- color: #fff;
51
- letter-spacing: 2px;
52
  }
53
- .chat-title span { color: #58a6ff; }
54
- .chat-sub {
55
  font-family: 'Share Tech Mono', monospace;
56
- font-size: 0.7rem;
57
- color: #8b949e;
58
- letter-spacing: 3px;
59
- margin-top: 4px;
60
  }
61
 
62
- /* ── Chat bubbles ── */
63
- .chat-wrap {
64
- display: flex;
65
- flex-direction: column;
66
- gap: 14px;
67
- padding: 10px 0;
68
- }
69
- .msg-user {
70
- display: flex;
71
- justify-content: flex-end;
72
- animation: slideInRight 0.3s ease both;
73
- }
74
- .msg-ai {
75
- display: flex;
76
- justify-content: flex-start;
77
- animation: slideInLeft 0.3s ease both;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  .bubble-user {
80
  background: linear-gradient(135deg, #1f6feb, #388bfd);
81
- color: #fff;
82
- padding: 12px 18px;
83
  border-radius: 18px 18px 4px 18px;
84
- max-width: 78%;
85
- font-size: 0.97rem;
86
- line-height: 1.6;
87
  box-shadow: 0 4px 15px rgba(31,111,235,0.25);
88
  }
89
  .bubble-ai {
90
- background: #161b22;
91
- color: #c9d1d9;
92
- padding: 12px 18px;
93
  border-radius: 18px 18px 18px 4px;
94
- max-width: 78%;
95
- font-size: 0.97rem;
96
- line-height: 1.7;
97
  border: 1px solid #30363d;
98
  box-shadow: 0 4px 15px rgba(0,0,0,0.3);
99
- position: relative;
100
  }
101
- .bubble-ai::before {
102
- content: '⚑';
103
- position: absolute;
104
- top: -10px; left: 12px;
105
- font-size: 0.7rem;
106
- background: #0d1117;
107
- padding: 0 4px;
108
- border-radius: 50%;
109
  }
110
  .msg-meta {
111
  font-family: 'Share Tech Mono', monospace;
112
- font-size: 0.62rem;
113
- color: #8b949e;
114
- margin-top: 4px;
115
- padding: 0 6px;
116
  }
117
 
118
- /* ── Typing animation ── */
119
- .typing-indicator {
120
- display: flex;
121
- align-items: center;
122
- gap: 5px;
123
- padding: 14px 18px;
124
- background: #161b22;
125
- border: 1px solid #30363d;
126
- border-radius: 18px 18px 18px 4px;
127
- width: fit-content;
128
- animation: slideInLeft 0.3s ease both;
129
  }
130
- .typing-dot {
131
- width: 7px; height: 7px;
132
- background: #58a6ff;
133
- border-radius: 50%;
134
- animation: bounce 1.2s ease-in-out infinite;
 
135
  }
136
- .typing-dot:nth-child(2) { animation-delay: 0.2s; }
137
- .typing-dot:nth-child(3) { animation-delay: 0.4s; }
138
- @keyframes bounce {
139
- 0%,60%,100% { transform: translateY(0); opacity:0.4; }
140
- 30% { transform: translateY(-8px); opacity:1; }
141
  }
142
-
143
- /* ── Stats bar ── */
144
- .stats-bar {
145
- display: flex;
146
- gap: 16px;
147
- flex-wrap: wrap;
148
- padding: 8px 12px;
149
- background: #161b22;
150
- border: 1px solid #21262d;
151
- border-radius: 8px;
152
- margin-bottom: 12px;
153
  font-family: 'Share Tech Mono', monospace;
154
- font-size: 0.68rem;
155
- color: #8b949e;
156
  }
157
- .stat-item { display: flex; align-items: center; gap: 5px; }
158
- .stat-val { color: #58a6ff; font-weight: 600; }
159
 
160
- /* ── Input area ── */
161
- div[data-testid="column"]:first-child .stTextInput > div > div > input {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  background: #161b22 !important;
163
  border: 1px solid #30363d !important;
164
- border-right: none !important;
165
- border-radius: 12px 0 0 12px !important;
166
  color: #c9d1d9 !important;
167
  font-family: 'Rajdhani', sans-serif !important;
168
  font-size: 0.97rem !important;
169
- padding: 12px 18px !important;
170
- height: 48px !important;
171
  }
172
- div[data-testid="column"]:first-child .stTextInput > div > div > input:focus {
173
  border-color: #58a6ff !important;
174
- box-shadow: none !important;
175
- outline: none !important;
176
- }
177
- /* Send button column */
178
- div[data-testid="column"]:last-child .stButton > button {
179
- background: linear-gradient(135deg, #1f6feb, #388bfd) !important;
180
- color: #fff !important;
181
- border: 1px solid #1f6feb !important;
182
- border-left: none !important;
183
- border-radius: 0 12px 12px 0 !important;
184
- height: 48px !important;
185
- width: 100% !important;
186
- font-size: 1.1rem !important;
187
- letter-spacing: 0 !important;
188
- box-shadow: 0 0 14px rgba(31,111,235,0.3) !important;
189
- transition: all 0.2s ease !important;
190
- margin-top: 0 !important;
191
- padding: 0 !important;
192
- }
193
- div[data-testid="column"]:last-child .stButton > button:hover {
194
- box-shadow: 0 0 24px rgba(31,111,235,0.55) !important;
195
- color: #fff !important;
196
- }
197
- /* Remove gap between columns so they touch */
198
- div[data-testid="column"]:first-child { padding-right: 0 !important; }
199
- div[data-testid="column"]:last-child { padding-left: 0 !important; }
200
 
201
  /* ── Buttons ── */
202
  .stButton > button {
203
- background: #21262d !important;
204
- color: #c9d1d9 !important;
205
- border: 1px solid #30363d !important;
206
- border-radius: 8px !important;
207
  font-family: 'Share Tech Mono', monospace !important;
208
- font-size: 0.75rem !important;
209
- letter-spacing: 1px !important;
210
- transition: all 0.25s ease !important;
211
  }
212
  .stButton > button:hover {
213
  background: #30363d !important;
214
- border-color: #58a6ff !important;
215
- color: #58a6ff !important;
 
 
 
 
 
 
 
 
216
  }
217
 
218
-
219
-
220
- /* ── Sidebar inputs ── */
221
- [data-testid="stSidebar"] textarea,
222
- [data-testid="stSidebar"] input {
223
- background: #0d1117 !important;
224
- border: 1px solid #30363d !important;
225
- color: #c9d1d9 !important;
226
- border-radius: 6px !important;
227
  }
228
- [data-testid="stSidebar"] .stSelectbox > div > div {
229
- background: #0d1117 !important;
230
- border: 1px solid #30363d !important;
231
- color: #c9d1d9 !important;
 
 
 
 
 
 
 
 
 
 
232
  }
233
 
234
- /* ── Welcome card ── */
235
  .welcome-card {
236
- background: #161b22;
237
- border: 1px solid #21262d;
238
- border-radius: 12px;
239
- padding: 28px;
240
- text-align: center;
241
- margin: 20px 0;
242
  animation: fadeInUp 0.6s ease both;
243
  }
244
- .welcome-icon { font-size: 2.5rem; margin-bottom: 10px; }
245
- .welcome-title {
246
  font-family: 'Orbitron', monospace;
247
  font-size: 1rem; color: #fff; margin-bottom: 8px;
248
  }
249
- .welcome-sub { font-size: 0.9rem; color: #8b949e; line-height: 1.6; }
250
  .tip-chip {
251
  display: inline-block;
252
- background: rgba(88,166,255,0.1);
253
- border: 1px solid rgba(88,166,255,0.3);
254
- color: #58a6ff;
255
- padding: 4px 12px; border-radius: 20px;
256
  font-family: 'Share Tech Mono', monospace;
257
- font-size: 0.68rem; margin: 4px 3px;
258
  }
259
 
260
- /* ── Animations ── */
261
- @keyframes slideInRight {
262
- from { opacity:0; transform: translateX(20px); }
263
- to { opacity:1; transform: translateX(0); }
 
264
  }
265
- @keyframes slideInLeft {
266
- from { opacity:0; transform: translateX(-20px); }
267
- to { opacity:1; transform: translateX(0); }
268
  }
269
- @keyframes fadeInUp {
270
- from { opacity:0; transform: translateY(16px); }
271
- to { opacity:1; transform: translateY(0); }
 
 
 
272
  }
273
 
274
- /* scrollbar */
 
 
 
 
 
275
  ::-webkit-scrollbar { width: 5px; }
276
  ::-webkit-scrollbar-track { background: #0d1117; }
277
  ::-webkit-scrollbar-thumb { background: #30363d; border-radius: 3px; }
@@ -279,225 +336,331 @@ div[data-testid="column"]:last-child { padding-left: 0 !important; }
279
  </style>
280
  """, unsafe_allow_html=True)
281
 
282
- # ─── MODEL LOAD ───────────────────────────────────────────────────────────────
283
- @st.cache_resource
284
- def load_model():
285
- model_name = "microsoft/Phi-3-mini-4k-instruct"
286
- tokenizer = AutoTokenizer.from_pretrained(model_name)
287
- model = AutoModelForCausalLM.from_pretrained(
288
- model_name,
289
- torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32
 
 
 
 
 
 
290
  )
291
- model.eval()
292
- if torch.cuda.is_available():
293
- model = model.to("cuda")
294
- return tokenizer, model
295
 
296
- # ─── SESSION STATE ────────────────────────────────────────────────────────────
297
- if "messages" not in st.session_state: st.session_state.messages = []
298
- if "total_tokens" not in st.session_state: st.session_state.total_tokens = 0
299
- if "total_time" not in st.session_state: st.session_state.total_time = 0.0
300
- if "msg_count" not in st.session_state: st.session_state.msg_count = 0
301
- if "last_q" not in st.session_state: st.session_state.last_q = ""
302
- if "input_key" not in st.session_state: st.session_state.input_key = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
 
304
  # ─── SIDEBAR ──────────────────────────────────────────────────────────────────
305
  with st.sidebar:
306
- st.markdown("""
307
- <div style="text-align:center;padding:16px 0 20px;">
308
- <div style="font-family:'Orbitron',monospace;font-size:1rem;color:#fff;letter-spacing:2px;">⚑ TURBO<span style="color:#58a6ff;">CHAT</span></div>
309
- <div style="font-family:'Share Tech Mono',monospace;font-size:0.65rem;color:#8b949e;letter-spacing:3px;margin-top:4px;">PHI-3 MINI</div>
 
 
 
 
 
 
 
 
 
 
 
310
  </div>
311
  """, unsafe_allow_html=True)
312
 
313
- st.markdown("**🎭 SYSTEM PROMPT**")
314
- system_prompt = st.text_area(
315
- "",
316
- value="""You, in the role of TurboChat, are an assistant designed to deliver fast, reliable, and high-quality responses while maintaining clarity and efficiency.
317
- You specialize in breaking down complex information into easily understandable insights without losing accuracy or depth.
318
- Your main goal and objective are to provide responses that are precise, logically structured, and immediately useful to the user.
319
- Your task is to interpret user queries correctly, detect ambiguity, apply reasoning, and present the most relevant answer in the most efficient format possible.
320
- To make this work as it should, you must prioritize clarity, accuracy, and usefulness over verbosity, eliminate redundant explanations, and ensure every sentence contributes meaningful value.
321
- When appropriate, you organize information into structured sections, lists, or steps to improve readability and comprehension. If a question is unclear, you ask a concise clarification question before answering.
322
- If multiple solutions exist, you present the most practical and high-impact options first.""",
323
- height=100,
324
- label_visibility="collapsed",
325
- key="sys_prompt"
326
- )
327
 
328
- st.markdown("---")
329
- st.markdown("**βš™οΈ GENERATION SETTINGS**")
 
 
330
 
331
- max_tokens = st.slider("Max Tokens", 50, 300, 120, 10)
332
- temperature = st.slider("Temperature", 0.1, 1.5, 0.6, 0.1,
333
- help="Higher = more creative, Lower = more focused")
334
- top_p = st.slider("Top-P", 0.1, 1.0, 0.9, 0.05,
335
- help="Nucleus sampling threshold")
 
 
 
 
 
 
 
 
 
336
 
337
  st.markdown("---")
338
- st.markdown("**πŸ“Š SESSION STATS**")
339
- st.markdown(f"""
340
- <div style="font-family:'Share Tech Mono',monospace;font-size:0.72rem;color:#8b949e;line-height:2;">
341
- πŸ’¬ Messages &nbsp;&nbsp;<span style="color:#58a6ff;">{st.session_state.msg_count}</span><br>
342
- πŸ”€ Tokens &nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#58a6ff;">{st.session_state.total_tokens}</span><br>
343
- ⏱️ Total time &nbsp;<span style="color:#58a6ff;">{st.session_state.total_time:.1f}s</span>
344
- </div>
345
- """, unsafe_allow_html=True)
346
 
347
- st.markdown("---")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  if st.button("πŸ—‘οΈ CLEAR CHAT", use_container_width=True):
349
- st.session_state.messages = []
350
- st.session_state.total_tokens = 0
351
- st.session_state.total_time = 0.0
352
- st.session_state.msg_count = 0
353
  st.rerun()
354
 
355
- st.markdown("""
356
- <div style="text-align:center;margin-top:16px;font-family:'Share Tech Mono',monospace;font-size:0.62rem;color:#484f58;">
357
- Built by <a href="https://github.com/sriramsai18" style="color:#58a6ff;text-decoration:none;">Sriram Sai</a>
358
- </div>
359
- """, unsafe_allow_html=True)
 
 
 
 
360
 
361
- # ─── HEADER ───────────────────────────────────────────────────────────────────
 
362
  st.markdown("""
363
- <div class="chat-header">
364
- <div class="chat-title">TURBO<span>CHAT</span> ⚑</div>
365
- <div class="chat-sub">POWERED BY LLM's</div>
 
 
366
  </div>
367
  """, unsafe_allow_html=True)
368
 
369
- # ─── CHAT HISTORY ─────────────────────────────────────────────────────────────
370
- if not st.session_state.messages:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  st.markdown("""
372
  <div class="welcome-card">
373
- <div class="welcome-icon">⚑</div>
374
- <div class="welcome-title">TURBOCHAT IS READY</div>
375
- <div class="welcome-sub">
376
- Ask me anything β€” code, concepts, advice, or just chat.<br>
377
- Powered by LLM.
378
  </div>
379
  <br>
380
- <span class="tip-chip">πŸ’‘ Try: Explain neural networks</span>
381
- <span class="tip-chip">πŸ’‘ Try: Write a Python function</span>
382
- <span class="tip-chip">πŸ’‘ Try: What is RAG?</span>
 
383
  </div>
384
  """, unsafe_allow_html=True)
 
385
  else:
386
- chat_html = '<div class="chat-wrap">'
387
- for msg in st.session_state.messages:
388
- safe_content = html_lib.escape(msg["content"])
389
- if msg["role"] == "user":
390
- chat_html += f"""
391
- <div class="msg-user">
392
- <div>
393
- <div class="bubble-user">{safe_content}</div>
394
- <div class="msg-meta" style="text-align:right;">YOU Β· {msg.get("time","")}</div>
395
- </div>
396
- </div>"""
397
- else:
398
- chat_html += f"""
399
- <div class="msg-ai">
400
- <div>
401
- <div class="bubble-ai">{safe_content}</div>
402
- <div class="msg-meta">⚑ TURBOCHAT · {msg.get("time","")} · {msg.get("tokens","")} tokens · {msg.get("elapsed","")}s</div>
403
- </div>
404
- </div>"""
405
- chat_html += '</div>'
406
- st.markdown(chat_html, unsafe_allow_html=True)
407
-
408
- # Typing indicator slot sits HERE β€” above the input box
409
- typing_slot = st.empty()
410
-
411
- # ─── INPUT ROW ────────────────────────────────────────────────────────────────
412
- st.write("")
413
- col_input, col_btn = st.columns([6, 0.7])
414
- with col_input:
415
- user_input = st.text_input(
416
- "",
417
- placeholder="Type your message and press Enter...",
418
- label_visibility="collapsed",
419
- key=f"user_input_{st.session_state.input_key}"
420
- )
421
- with col_btn:
422
- send = st.button("⚑", use_container_width=True)
423
-
424
- # ─── GENERATE ─────────────────────────────────────────────────────────────────
425
- if (send or user_input) and user_input.strip() and user_input.strip() != st.session_state.last_q:
426
-
427
- ts = get_ist_time()
428
- st.session_state.messages.append({
429
- "role": "user",
430
- "content": user_input.strip(),
431
- "time": ts
432
- })
433
- st.session_state.msg_count += 1
434
-
435
- # Show typing indicator above the input
436
- typing_slot.markdown("""
437
- <div class="msg-ai">
438
- <div class="typing-indicator">
439
- <div class="typing-dot"></div>
440
- <div class="typing-dot"></div>
441
- <div class="typing-dot"></div>
442
  </div>
443
- </div>
444
- """, unsafe_allow_html=True)
445
-
446
- # generate response
447
- try:
448
- tokenizer, model = load_model()
449
-
450
- prompt = f"<|system|>\n{system_prompt.strip()}\n<|end|>\n"
451
- for m in st.session_state.messages:
452
- if m["role"] == "user":
453
- prompt += f"<|user|>\n{m['content']}\n<|end|>\n"
454
- elif m["role"] == "assistant":
455
- prompt += f"<|assistant|>\n{m['content']}\n<|end|>\n"
456
- prompt += "<|assistant|>\n"
457
-
458
- input_ids = tokenizer(prompt, return_tensors="pt").input_ids
459
- if torch.cuda.is_available():
460
- input_ids = input_ids.to("cuda")
461
-
462
- start_time = time.time()
463
-
464
- with torch.no_grad():
465
- output = model.generate(
466
- input_ids,
467
- max_new_tokens=max_tokens,
468
- temperature=temperature,
469
- top_p=top_p,
470
- do_sample=True,
471
- pad_token_id=tokenizer.eos_token_id,
472
- repetition_penalty=1.1,
473
- )
474
-
475
- elapsed = round(time.time() - start_time, 1)
476
- new_tokens = output[0][input_ids.shape[-1]:]
477
- response = tokenizer.decode(new_tokens, skip_special_tokens=True).strip()
478
- token_count = len(new_tokens)
479
-
480
- st.session_state.total_tokens += token_count
481
- st.session_state.total_time += elapsed
482
-
483
- st.session_state.messages.append({
484
- "role": "assistant",
485
- "content": response,
486
- "time": get_ist_time(),
487
- "tokens": token_count,
488
- "elapsed": elapsed
489
- })
490
-
491
- except Exception as e:
492
  st.session_state.messages.append({
493
- "role": "assistant",
494
- "content": f"⚠️ Error: {str(e)}",
495
- "time": get_ist_time(),
496
- "tokens": 0,
497
- "elapsed": 0
498
  })
499
-
500
- typing_slot.empty()
501
- st.session_state.last_q = user_input.strip()
502
- st.session_state.input_key += 1
503
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
+ from PyPDF2 import PdfReader
3
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
4
+ from langchain_huggingface import HuggingFaceEmbeddings, HuggingFacePipeline
5
+ from langchain_community.vectorstores import FAISS
6
+ from langchain_core.prompts import PromptTemplate
7
+ from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
8
  import torch
9
  import time
10
+ import base64
 
 
 
 
 
 
 
11
 
12
  st.set_page_config(
13
+ page_title="QueryDocs AI",
14
+ page_icon="πŸ“š",
15
+ layout="wide",
16
  initial_sidebar_state="expanded"
17
  )
18
 
19
+ # ─── HELPERS ──────────────────────────────────────────────────────────────────
20
+ def img_to_base64(path):
21
+ try:
22
+ with open(path, "rb") as f:
23
+ return base64.b64encode(f.read()).decode()
24
+ except:
25
+ return None
26
+
27
  # ─── GLOBAL CSS ───────────────────────────────────────────────────────────────
28
  st.markdown("""
29
  <style>
 
43
  footer, #MainMenu { visibility: hidden; }
44
 
45
  /* ── Header ── */
46
+ .app-header {
47
+ display: flex; align-items: center; gap: 14px;
48
+ padding: 16px 0 14px;
49
  border-bottom: 1px solid #21262d;
50
  margin-bottom: 20px;
51
+ animation: fadeInDown 0.5s ease both;
52
  }
53
+ .app-title {
54
  font-family: 'Orbitron', monospace;
55
+ font-size: 1.6rem; font-weight: 900;
56
+ color: #fff; letter-spacing: 2px;
 
 
57
  }
58
+ .app-title span { color: #58a6ff; }
59
+ .app-sub {
60
  font-family: 'Share Tech Mono', monospace;
61
+ font-size: 0.68rem; color: #8b949e;
62
+ letter-spacing: 3px; margin-top: 3px;
 
 
63
  }
64
 
65
+ /* ── Profile card in sidebar ── */
66
+ .profile-card {
67
+ text-align: center;
68
+ padding: 20px 0 16px;
69
+ border-bottom: 1px solid #21262d;
70
+ margin-bottom: 16px;
71
+ animation: fadeInDown 0.5s ease both;
72
+ }
73
+ .profile-avatar {
74
+ width: 72px; height: 72px;
75
+ border-radius: 50%;
76
+ overflow: hidden;
77
+ border: 2px solid #58a6ff;
78
+ box-shadow: 0 0 0 3px rgba(88,166,255,0.15),
79
+ 0 0 20px rgba(88,166,255,0.2);
80
+ margin: 0 auto 10px;
81
+ animation: pulse-av 2.5s ease-in-out infinite;
82
+ }
83
+ .profile-avatar img {
84
+ width: 100%; height: 100%;
85
+ object-fit: cover; border-radius: 50%;
86
+ }
87
+ @keyframes pulse-av {
88
+ 0%,100%{box-shadow:0 0 0 3px rgba(88,166,255,0.15),0 0 20px rgba(88,166,255,0.2);}
89
+ 50% {box-shadow:0 0 0 4px rgba(88,166,255,0.3),0 0 30px rgba(88,166,255,0.35);}
90
+ }
91
+ .profile-name {
92
+ font-family: 'Orbitron', monospace;
93
+ font-size: 0.82rem; font-weight: 700;
94
+ color: #fff; letter-spacing: 1px;
95
+ }
96
+ .profile-role {
97
+ font-family: 'Share Tech Mono', monospace;
98
+ font-size: 0.64rem; color: #58a6ff;
99
+ letter-spacing: 2px; margin-top: 3px;
100
+ }
101
+ .profile-links {
102
+ display: flex; justify-content: center;
103
+ gap: 8px; margin-top: 10px; flex-wrap: wrap;
104
+ }
105
+ .p-link {
106
+ font-family: 'Share Tech Mono', monospace;
107
+ font-size: 0.62rem; color: #8b949e !important;
108
+ text-decoration: none !important;
109
+ background: #0d1117;
110
+ border: 1px solid #30363d;
111
+ padding: 3px 10px; border-radius: 20px;
112
+ transition: all 0.25s;
113
+ }
114
+ .p-link:hover { color: #58a6ff !important; border-color: #58a6ff; }
115
+
116
+ /* ── Upload zone ── */
117
+ .upload-card {
118
+ background: #161b22;
119
+ border: 2px dashed #30363d;
120
+ border-radius: 12px;
121
+ padding: 32px;
122
+ text-align: center;
123
+ transition: all 0.3s ease;
124
+ animation: fadeInUp 0.5s ease both;
125
+ }
126
+ .upload-card:hover { border-color: #58a6ff; }
127
+ .upload-icon { font-size: 2.5rem; margin-bottom: 10px; }
128
+ .upload-title {
129
+ font-family: 'Orbitron', monospace;
130
+ font-size: 0.95rem; color: #fff; margin-bottom: 6px;
131
  }
132
+ .upload-sub { font-size: 0.85rem; color: #8b949e; }
133
+
134
+ /* ── PDF info banner ── */
135
+ .pdf-banner {
136
+ background: #161b22;
137
+ border: 1px solid #21262d;
138
+ border-left: 3px solid #58a6ff;
139
+ border-radius: 8px;
140
+ padding: 12px 18px;
141
+ display: flex; align-items: center; gap: 12px;
142
+ margin-bottom: 16px;
143
+ animation: fadeInUp 0.4s ease both;
144
+ }
145
+ .pdf-name {
146
+ font-weight: 700; font-size: 0.95rem; color: #fff;
147
+ }
148
+ .pdf-meta {
149
+ font-family: 'Share Tech Mono', monospace;
150
+ font-size: 0.68rem; color: #8b949e; margin-top: 2px;
151
+ }
152
+
153
+ /* ── Chat bubbles ── */
154
+ .chat-wrap { display: flex; flex-direction: column; gap: 16px; padding: 8px 0; }
155
+ .msg-user { display: flex; justify-content: flex-end; animation: slideInR 0.3s ease both; }
156
+ .msg-ai { display: flex; justify-content: flex-start; animation: slideInL 0.3s ease both; }
157
  .bubble-user {
158
  background: linear-gradient(135deg, #1f6feb, #388bfd);
159
+ color: #fff; padding: 12px 18px;
 
160
  border-radius: 18px 18px 4px 18px;
161
+ max-width: 75%; font-size: 0.97rem; line-height: 1.6;
 
 
162
  box-shadow: 0 4px 15px rgba(31,111,235,0.25);
163
  }
164
  .bubble-ai {
165
+ background: #161b22; color: #c9d1d9;
166
+ padding: 14px 18px;
 
167
  border-radius: 18px 18px 18px 4px;
168
+ max-width: 80%; font-size: 0.95rem; line-height: 1.75;
 
 
169
  border: 1px solid #30363d;
170
  box-shadow: 0 4px 15px rgba(0,0,0,0.3);
 
171
  }
172
+ .bubble-ai .answer-label {
173
+ font-family: 'Share Tech Mono', monospace;
174
+ font-size: 0.65rem; color: #58a6ff;
175
+ letter-spacing: 2px; margin-bottom: 8px;
176
+ text-transform: uppercase;
 
 
 
177
  }
178
  .msg-meta {
179
  font-family: 'Share Tech Mono', monospace;
180
+ font-size: 0.62rem; color: #484f58;
181
+ margin-top: 4px; padding: 0 6px;
 
 
182
  }
183
 
184
+ .answer-text {
185
+ white-space: pre-wrap;
186
+ word-break: break-word;
187
+ line-height: 1.75;
188
+ font-size: 0.95rem;
 
 
 
 
 
 
189
  }
190
+
191
+ /* ── Source chunks ── */
192
+ .sources-wrap {
193
+ margin-top: 10px;
194
+ border-top: 1px solid #21262d;
195
+ padding-top: 10px;
196
  }
197
+ .source-label {
198
+ font-family: 'Share Tech Mono', monospace;
199
+ font-size: 0.62rem; color: #8b949e;
200
+ letter-spacing: 2px; margin-bottom: 6px;
 
201
  }
202
+ .source-chip {
203
+ display: inline-block;
204
+ background: rgba(88,166,255,0.08);
205
+ border: 1px solid rgba(88,166,255,0.2);
206
+ color: #58a6ff; padding: 3px 10px;
207
+ border-radius: 4px; font-size: 0.72rem;
 
 
 
 
 
208
  font-family: 'Share Tech Mono', monospace;
209
+ margin: 3px 3px; cursor: pointer;
 
210
  }
 
 
211
 
212
+ /* ── Typing indicator ── */
213
+ .typing-wrap { display: flex; justify-content: flex-start; }
214
+ .typing-box {
215
+ display: flex; align-items: center; gap: 5px;
216
+ padding: 14px 18px; background: #161b22;
217
+ border: 1px solid #30363d;
218
+ border-radius: 18px 18px 18px 4px;
219
+ }
220
+ .t-dot {
221
+ width: 7px; height: 7px; background: #58a6ff;
222
+ border-radius: 50%;
223
+ animation: tdot 1.2s ease-in-out infinite;
224
+ }
225
+ .t-dot:nth-child(2){animation-delay:0.2s;}
226
+ .t-dot:nth-child(3){animation-delay:0.4s;}
227
+ @keyframes tdot{0%,60%,100%{transform:translateY(0);opacity:0.4;}30%{transform:translateY(-8px);opacity:1;}}
228
+
229
+ /* ── Input ── */
230
+ .stTextInput > div > div > input {
231
  background: #161b22 !important;
232
  border: 1px solid #30363d !important;
233
+ border-radius: 8px !important;
 
234
  color: #c9d1d9 !important;
235
  font-family: 'Rajdhani', sans-serif !important;
236
  font-size: 0.97rem !important;
 
 
237
  }
238
+ .stTextInput > div > div > input:focus {
239
  border-color: #58a6ff !important;
240
+ box-shadow: 0 0 12px rgba(88,166,255,0.15) !important;
241
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
  /* ── Buttons ── */
244
  .stButton > button {
245
+ background: #21262d !important; color: #c9d1d9 !important;
246
+ border: 1px solid #30363d !important; border-radius: 8px !important;
 
 
247
  font-family: 'Share Tech Mono', monospace !important;
248
+ font-size: 0.73rem !important; letter-spacing: 1px !important;
249
+ transition: all 0.25s !important;
 
250
  }
251
  .stButton > button:hover {
252
  background: #30363d !important;
253
+ border-color: #58a6ff !important; color: #58a6ff !important;
254
+ }
255
+ .send-btn > button {
256
+ background: linear-gradient(135deg,#1f6feb,#388bfd) !important;
257
+ color: #fff !important; border: none !important;
258
+ box-shadow: 0 0 16px rgba(31,111,235,0.3) !important;
259
+ }
260
+ .send-btn > button:hover {
261
+ box-shadow: 0 0 28px rgba(31,111,235,0.55) !important;
262
+ transform: translateY(-2px) !important; color: #fff !important;
263
  }
264
 
265
+ /* ── Stats ── */
266
+ .stat-row {
267
+ display: flex; gap: 10px; flex-wrap: wrap;
268
+ margin-bottom: 14px;
 
 
 
 
 
269
  }
270
+ .stat-box {
271
+ flex: 1; min-width: 80px;
272
+ background: #161b22; border: 1px solid #21262d;
273
+ border-top: 2px solid #58a6ff; border-radius: 6px;
274
+ padding: 10px 12px; text-align: center;
275
+ }
276
+ .stat-num {
277
+ font-family: 'Orbitron', monospace;
278
+ font-size: 1.2rem; font-weight: 900; color: #58a6ff;
279
+ }
280
+ .stat-lbl {
281
+ font-family: 'Share Tech Mono', monospace;
282
+ font-size: 0.6rem; color: #8b949e;
283
+ letter-spacing: 1px; margin-top: 3px;
284
  }
285
 
286
+ /* ── Welcome ── */
287
  .welcome-card {
288
+ background: #161b22; border: 1px solid #21262d;
289
+ border-radius: 12px; padding: 30px;
290
+ text-align: center; margin: 10px 0;
 
 
 
291
  animation: fadeInUp 0.6s ease both;
292
  }
293
+ .wc-icon { font-size: 2.8rem; margin-bottom: 12px; }
294
+ .wc-title {
295
  font-family: 'Orbitron', monospace;
296
  font-size: 1rem; color: #fff; margin-bottom: 8px;
297
  }
298
+ .wc-sub { font-size: 0.9rem; color: #8b949e; line-height: 1.65; }
299
  .tip-chip {
300
  display: inline-block;
301
+ background: rgba(88,166,255,0.08);
302
+ border: 1px solid rgba(88,166,255,0.25);
303
+ color: #58a6ff; padding: 4px 12px;
304
+ border-radius: 20px; margin: 4px 3px;
305
  font-family: 'Share Tech Mono', monospace;
306
+ font-size: 0.67rem;
307
  }
308
 
309
+ /* ── File uploader ── */
310
+ [data-testid="stFileUploader"] {
311
+ background: #161b22 !important;
312
+ border: 2px dashed #30363d !important;
313
+ border-radius: 10px !important;
314
  }
315
+ [data-testid="stFileUploader"]:hover {
316
+ border-color: #58a6ff !important;
 
317
  }
318
+
319
+ /* ── Glow divider ── */
320
+ .glow-div {
321
+ border: none; height: 1px;
322
+ background: linear-gradient(90deg,transparent,#58a6ff,transparent);
323
+ margin: 16px 0; box-shadow: 0 0 8px rgba(88,166,255,0.3);
324
  }
325
 
326
+ /* ── Animations ── */
327
+ @keyframes fadeInDown { from{opacity:0;transform:translateY(-16px)} to{opacity:1;transform:translateY(0)} }
328
+ @keyframes fadeInUp { from{opacity:0;transform:translateY(16px)} to{opacity:1;transform:translateY(0)} }
329
+ @keyframes slideInR { from{opacity:0;transform:translateX(20px)} to{opacity:1;transform:translateX(0)} }
330
+ @keyframes slideInL { from{opacity:0;transform:translateX(-20px)} to{opacity:1;transform:translateX(0)} }
331
+
332
  ::-webkit-scrollbar { width: 5px; }
333
  ::-webkit-scrollbar-track { background: #0d1117; }
334
  ::-webkit-scrollbar-thumb { background: #30363d; border-radius: 3px; }
 
336
  </style>
337
  """, unsafe_allow_html=True)
338
 
339
+ # ─── SESSION STATE ────────────────────────────────────────────────────────────
340
+ if "messages" not in st.session_state: st.session_state.messages = []
341
+ if "vectorstore" not in st.session_state: st.session_state.vectorstore = None
342
+ if "pdf_name" not in st.session_state: st.session_state.pdf_name = None
343
+ if "pdf_pages" not in st.session_state: st.session_state.pdf_pages = 0
344
+ if "pdf_chunks" not in st.session_state: st.session_state.pdf_chunks = 0
345
+ if "q_count" not in st.session_state: st.session_state.q_count = 0
346
+ if "last_q" not in st.session_state: st.session_state.last_q = ""
347
+
348
+ # ─── MODEL LOADERS ────────────────────────────────────────────────────────────
349
+ @st.cache_resource(show_spinner=False)
350
+ def load_embeddings():
351
+ return HuggingFaceEmbeddings(
352
+ model_name="sentence-transformers/all-MiniLM-L6-v2"
353
  )
 
 
 
 
354
 
355
+ @st.cache_resource(show_spinner=False)
356
+ def load_llm():
357
+ model_id = "TinyLlama/TinyLlama-1.1B-chat-v1.0"
358
+ tokenizer = AutoTokenizer.from_pretrained(model_id)
359
+ model = AutoModelForCausalLM.from_pretrained(
360
+ model_id,
361
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
362
+ low_cpu_mem_usage=True,
363
+ device_map="cuda" if torch.cuda.is_available() else None
364
+ )
365
+ if not torch.cuda.is_available():
366
+ model = model.to("cpu")
367
+ pipe = pipeline(
368
+ "text-generation", model=model, tokenizer=tokenizer,
369
+ max_new_tokens=512, temperature=0.3, do_sample=True,
370
+ pad_token_id=tokenizer.eos_token_id, repetition_penalty=1.1
371
+ )
372
+ return HuggingFacePipeline(pipeline=pipe)
373
+
374
+ # ─── PDF PROCESSOR ────────────────────────────────────────────────────────────
375
+ def process_pdf(uploaded_file):
376
+ reader = PdfReader(uploaded_file)
377
+ raw_text = ""
378
+ for page in reader.pages:
379
+ text = page.extract_text()
380
+ if text:
381
+ raw_text += text
382
+
383
+ if not raw_text.strip():
384
+ raise ValueError("No readable text found. PDF may be scanned/image-based.")
385
+
386
+ splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
387
+ chunks = splitter.split_text(raw_text)
388
+ embeddings = load_embeddings()
389
+ vectorstore = FAISS.from_texts(chunks, embeddings)
390
+ return vectorstore, len(reader.pages), len(chunks)
391
+
392
+ # ─── ANSWER FUNCTION ──────────────────────────────────────────────────────────
393
+ def get_answer(question, vectorstore):
394
+ retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
395
+ relevant_docs = retriever.invoke(question)
396
+ context = "\n\n".join([f"---\n{doc.page_content}" for doc in relevant_docs])
397
+ sources = [doc.page_content[:120] + "..." for doc in relevant_docs]
398
+
399
+ prompt_template = PromptTemplate(
400
+ input_variables=["context", "question"],
401
+ template="""<|system|>
402
+ You are QueryDocs AI, an intelligent document assistant. Use ONLY the context provided to answer the question clearly and accurately. If the answer is not in the context, say so honestly.
403
+ <|user|>
404
+ CONTEXT:
405
+ {context}
406
+
407
+ QUESTION:
408
+ {question}
409
+ <|assistant|>
410
+ """
411
+ )
412
+ llm = load_llm()
413
+ chain = prompt_template | llm
414
+ result = chain.invoke({"context": context, "question": question})
415
+
416
+ # extract only the assistant reply
417
+ if "<|assistant|>" in result:
418
+ answer = result.split("<|assistant|>")[-1].strip()
419
+ else:
420
+ answer = result.strip()
421
+
422
+ return answer, sources
423
 
424
  # ─── SIDEBAR ──────────────────────────────────────────────────────────────────
425
  with st.sidebar:
426
+ # Profile card
427
+ img_b64 = img_to_base64("assets/NANII.png")
428
+ avatar = (f'<img src="data:image/png;base64,{img_b64}" alt="Sriram">'
429
+ if img_b64 else
430
+ '<div style="width:72px;height:72px;background:#1f6feb;border-radius:50%;margin:0 auto;"></div>')
431
+
432
+ st.markdown(f"""
433
+ <div class="profile-card">
434
+ <div class="profile-avatar">{avatar}</div>
435
+ <div class="profile-name">SRIRAM SAI</div>
436
+ <div class="profile-role">AI &amp; ML ENGINEER</div>
437
+ <div class="profile-links">
438
+ <a class="p-link" href="https://github.com/sriramsai18" target="_blank">πŸ’» GitHub</a>
439
+ <a class="p-link" href="https://www.linkedin.com/in/sriram-sai-laggisetti/" target="_blank">πŸ’Ό LinkedIn</a>
440
+ </div>
441
  </div>
442
  """, unsafe_allow_html=True)
443
 
444
+ # PDF upload
445
+ st.markdown('<div style="font-family:\'Share Tech Mono\',monospace;font-size:0.7rem;color:#8b949e;letter-spacing:2px;margin-bottom:8px;">πŸ“„ UPLOAD DOCUMENT</div>', unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
446
 
447
+ uploaded_file = st.file_uploader(
448
+ "Upload PDF", type=["pdf"],
449
+ label_visibility="collapsed"
450
+ )
451
 
452
+ if uploaded_file:
453
+ if st.session_state.pdf_name != uploaded_file.name:
454
+ with st.spinner("πŸ” Processing PDF..."):
455
+ try:
456
+ vs, pages, chunks = process_pdf(uploaded_file)
457
+ st.session_state.vectorstore = vs
458
+ st.session_state.pdf_name = uploaded_file.name
459
+ st.session_state.pdf_pages = pages
460
+ st.session_state.pdf_chunks = chunks
461
+ st.session_state.messages = []
462
+ st.session_state.q_count = 0
463
+ st.success("βœ… PDF ready!")
464
+ except Exception as e:
465
+ st.error(f"❌ {str(e)}")
466
 
467
  st.markdown("---")
 
 
 
 
 
 
 
 
468
 
469
+ # Stats
470
+ if st.session_state.pdf_name:
471
+ st.markdown(f"""
472
+ <div style="font-family:'Share Tech Mono',monospace;font-size:0.7rem;color:#8b949e;margin-bottom:10px;letter-spacing:2px;">πŸ“Š DOCUMENT STATS</div>
473
+ <div style="display:flex;flex-direction:column;gap:6px;">
474
+ <div style="background:#0d1117;border:1px solid #21262d;border-radius:6px;padding:8px 12px;font-family:'Share Tech Mono',monospace;font-size:0.7rem;">
475
+ πŸ“„ <span style="color:#c9d1d9;">{st.session_state.pdf_name[:22]}{'...' if len(st.session_state.pdf_name)>22 else ''}</span>
476
+ </div>
477
+ <div style="display:flex;gap:6px;">
478
+ <div style="flex:1;background:#0d1117;border:1px solid #21262d;border-top:2px solid #58a6ff;border-radius:6px;padding:8px;text-align:center;">
479
+ <div style="font-family:'Orbitron',monospace;font-size:1rem;color:#58a6ff;">{st.session_state.pdf_pages}</div>
480
+ <div style="font-family:'Share Tech Mono',monospace;font-size:0.58rem;color:#8b949e;">PAGES</div>
481
+ </div>
482
+ <div style="flex:1;background:#0d1117;border:1px solid #21262d;border-top:2px solid #58a6ff;border-radius:6px;padding:8px;text-align:center;">
483
+ <div style="font-family:'Orbitron',monospace;font-size:1rem;color:#58a6ff;">{st.session_state.pdf_chunks}</div>
484
+ <div style="font-family:'Share Tech Mono',monospace;font-size:0.58rem;color:#8b949e;">CHUNKS</div>
485
+ </div>
486
+ <div style="flex:1;background:#0d1117;border:1px solid #21262d;border-top:2px solid #58a6ff;border-radius:6px;padding:8px;text-align:center;">
487
+ <div style="font-family:'Orbitron',monospace;font-size:1rem;color:#58a6ff;">{st.session_state.q_count}</div>
488
+ <div style="font-family:'Share Tech Mono',monospace;font-size:0.58rem;color:#8b949e;">ASKED</div>
489
+ </div>
490
+ </div>
491
+ </div>
492
+ """, unsafe_allow_html=True)
493
+ st.markdown("---")
494
+
495
+ # Clear button
496
  if st.button("πŸ—‘οΈ CLEAR CHAT", use_container_width=True):
497
+ st.session_state.messages = []
498
+ st.session_state.q_count = 0
 
 
499
  st.rerun()
500
 
501
+ # New PDF button
502
+ if st.button("πŸ“„ LOAD NEW PDF", use_container_width=True):
503
+ st.session_state.vectorstore = None
504
+ st.session_state.pdf_name = None
505
+ st.session_state.pdf_pages = 0
506
+ st.session_state.pdf_chunks = 0
507
+ st.session_state.messages = []
508
+ st.session_state.q_count = 0
509
+ st.rerun()
510
 
511
+ # ─── MAIN AREA ────────────────────────────────────────────────────────────────
512
+ # Header
513
  st.markdown("""
514
+ <div class="app-header">
515
+ <div>
516
+ <div class="app-title">QUERY<span>DOCS</span> AI πŸ“š</div>
517
+ <div class="app-sub">INTELLIGENT DOCUMENT Q&amp;A Β· RAG PIPELINE </div>
518
+ </div>
519
  </div>
520
  """, unsafe_allow_html=True)
521
 
522
+ # PDF banner (when loaded)
523
+ if st.session_state.pdf_name:
524
+ st.markdown(f"""
525
+ <div class="pdf-banner">
526
+ <span style="font-size:1.4rem;">πŸ“„</span>
527
+ <div>
528
+ <div class="pdf-name">{st.session_state.pdf_name}</div>
529
+ <div class="pdf-meta">{st.session_state.pdf_pages} pages Β· {st.session_state.pdf_chunks} chunks Β· ready to query</div>
530
+ </div>
531
+ <span style="margin-left:auto;background:rgba(88,166,255,0.1);border:1px solid rgba(88,166,255,0.3);
532
+ color:#58a6ff;padding:4px 12px;border-radius:20px;
533
+ font-family:'Share Tech Mono',monospace;font-size:0.65rem;">● ACTIVE</span>
534
+ </div>
535
+ """, unsafe_allow_html=True)
536
+
537
+ # Chat area
538
+ if not st.session_state.vectorstore:
539
  st.markdown("""
540
  <div class="welcome-card">
541
+ <div class="wc-icon">πŸ“š</div>
542
+ <div class="wc-title">WELCOME TO QUERYDOCS AI</div>
543
+ <div class="wc-sub">
544
+ Upload any PDF document from the sidebar and start asking questions.<br>
545
+ Powered by RAG pipeline, context-aware answers.
546
  </div>
547
  <br>
548
+ <span class="tip-chip">πŸ“‹ Legal documents</span>
549
+ <span class="tip-chip">πŸ“Š Research papers</span>
550
+ <span class="tip-chip">πŸ“– Study material</span>
551
+ <span class="tip-chip">πŸ“ Reports</span>
552
  </div>
553
  """, unsafe_allow_html=True)
554
+
555
  else:
556
+ # Chat history
557
+ if st.session_state.messages:
558
+ chat_html = '<div class="chat-wrap">'
559
+ for msg in st.session_state.messages:
560
+ ts = msg.get("time", "")
561
+ if msg["role"] == "user":
562
+ import html as html_module
563
+ safe_user = html_module.escape(msg["content"])
564
+ chat_html += f"""
565
+ <div class="msg-user">
566
+ <div>
567
+ <div class="bubble-user">{safe_user}</div>
568
+ <div class="msg-meta" style="text-align:right;">YOU Β· {ts}</div>
569
+ </div>
570
+ </div>"""
571
+ else:
572
+ sources_html = ""
573
+ if msg.get("sources"):
574
+ chips = "".join(f'<span class="source-chip">πŸ“Ž Chunk {i+1}</span>'
575
+ for i, _ in enumerate(msg["sources"]))
576
+ sources_html = f"""<div class="sources-wrap">
577
+ <div class="source-label">// SOURCE CHUNKS USED</div>
578
+ {chips}
579
+ </div>"""
580
+
581
+ # Escape answer to prevent HTML injection from LLM output
582
+ import html as html_module
583
+ safe_answer = html_module.escape(msg["content"]).replace("\n", "<br>")
584
+
585
+ chat_html += f"""
586
+ <div class="msg-ai">
587
+ <div>
588
+ <div class="bubble-ai">
589
+ <div class="answer-label">// QUERYDOCS RESPONSE</div>
590
+ <div class="answer-text">{safe_answer}</div>
591
+ {sources_html}
592
+ </div>
593
+ <div class="msg-meta">πŸ“š QUERYDOCS AI Β· {ts} Β· {msg.get("elapsed","?")}s</div>
594
+ </div>
595
+ </div>"""
596
+ chat_html += '</div>'
597
+ st.markdown(chat_html, unsafe_allow_html=True)
598
+ else:
599
+ st.markdown("""
600
+ <div class="welcome-card" style="padding:20px;">
601
+ <div style="font-size:1.6rem;margin-bottom:8px;">πŸ’¬</div>
602
+ <div class="wc-title" style="font-size:0.85rem;">DOCUMENT LOADED β€” START ASKING</div>
603
+ <div class="wc-sub" style="font-size:0.82rem;">Ask anything about the uploaded document.</div>
604
+ <br>
605
+ <span class="tip-chip">πŸ’‘ Summarize this document</span>
606
+ <span class="tip-chip">πŸ’‘ What are the key findings?</span>
607
+ <span class="tip-chip">πŸ’‘ List all important dates</span>
 
 
 
 
608
  </div>
609
+ """, unsafe_allow_html=True)
610
+
611
+ # Input row
612
+ st.markdown('<hr class="glow-div">', unsafe_allow_html=True)
613
+ col_q, col_btn = st.columns((5, 1))
614
+
615
+ with col_q:
616
+ question = st.text_input(
617
+ "", placeholder="ask a question about your document...",
618
+ label_visibility="collapsed", key="question_input"
619
+ )
620
+ with col_btn:
621
+ st.markdown('<div class="send-btn">', unsafe_allow_html=True)
622
+ ask_btn = st.button("β–Ά ASK", use_container_width=True)
623
+ st.markdown('</div>', unsafe_allow_html=True)
624
+
625
+ # Generate answer β€” guard against infinite loop
626
+ if (ask_btn or question) and question.strip() and question.strip() != st.session_state.last_q:
627
+ ts = time.strftime("%H:%M")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
628
  st.session_state.messages.append({
629
+ "role": "user", "content": question.strip(), "time": ts
 
 
 
 
630
  })
631
+ st.session_state.q_count += 1
632
+
633
+ # typing indicator
634
+ typing_slot = st.empty()
635
+ typing_slot.markdown("""
636
+ <div class="typing-wrap">
637
+ <div class="typing-box">
638
+ <div class="t-dot"></div>
639
+ <div class="t-dot"></div>
640
+ <div class="t-dot"></div>
641
+ </div>
642
+ </div>
643
+ """, unsafe_allow_html=True)
644
+
645
+ try:
646
+ start = time.time()
647
+ answer, sources = get_answer(question.strip(), st.session_state.vectorstore)
648
+ elapsed = round(time.time() - start, 1)
649
+
650
+ st.session_state.messages.append({
651
+ "role": "assistant",
652
+ "content": answer,
653
+ "sources": sources,
654
+ "time": time.strftime("%H:%M"),
655
+ "elapsed": elapsed
656
+ })
657
+ except Exception as e:
658
+ st.session_state.messages.append({
659
+ "role": "assistant",
660
+ "content": f"⚠️ Error generating answer: {str(e)}",
661
+ "sources": [], "time": time.strftime("%H:%M"), "elapsed": 0
662
+ })
663
+
664
+ typing_slot.empty()
665
+ st.session_state.last_q = question.strip()
666
+ st.rerun()