Shubham170793 commited on
Commit
cce4565
Β·
verified Β·
1 Parent(s): 4236749

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +60 -165
src/streamlit_app.py CHANGED
@@ -1,4 +1,3 @@
1
- # streamlit_app.py
2
  import os
3
  import re
4
  import shutil
@@ -42,7 +41,6 @@ def clean_cache(max_size_gb: float = 2.0):
42
  for f in files
43
  )
44
  size_gb = size_bytes / (1024 ** 3)
45
- # remove if too big OR contains 'torch' to keep space free
46
  if size_gb > max_size_gb or "torch" in folder:
47
  shutil.rmtree(folder, ignore_errors=True)
48
  total_deleted += size_gb
@@ -66,25 +64,19 @@ os.environ.update({
66
  })
67
 
68
  # -------------------------
69
- # App-specific imports (user modules)
70
  # -------------------------
71
- # Make sure these modules exist (same as your earlier working project).
72
  from ingestion import extract_text_from_pdf, chunk_text
73
  from vectorstore import build_faiss_index
74
  from qa import retrieve_chunks, generate_answer, cache_embeddings, embed_chunks, genai_generate
75
 
76
  # -------------------------
77
- # Helper: TOC-based suggestion generator (robust)
78
  # -------------------------
79
- def generate_dynamic_suggestions_from_toc(toc: List[Tuple[str,str]], chunks: List[str], doc_name: str = "Document") -> List[str]:
80
- """
81
- Prefer TOC-derived natural suggestions. If genai_generate is available we can optionally use it,
82
- otherwise produce deterministic, high-quality suggestions from TOC titles.
83
- """
84
  if not chunks:
85
  return []
86
 
87
- # Clean TOC titles
88
  titles = []
89
  for sec, raw_title in toc or []:
90
  title = re.sub(r"^\s*[\dA-Za-z.\-]+\s*", "", raw_title)
@@ -93,55 +85,43 @@ def generate_dynamic_suggestions_from_toc(toc: List[Tuple[str,str]], chunks: Lis
93
  titles.append(title)
94
 
95
  if not titles:
96
- # fallback to text-based suggestion: find verbs/imperatives in first chunks
97
  sample = " ".join(chunks[:4])
98
  sents = re.split(r'(?<=[.?!])\s+', sample)
99
  suggestions = []
100
  for s in sents:
101
- if re.search(r'\b(set up|configure|how to|install|enable|prepare|step|procedure|process)\b', s, re.I):
102
- clean = s.strip()
103
- clean = re.sub(r'[.?!]+$', '', clean)
104
- q = clean[0].upper() + clean[1:]
105
  if len(q) < 140:
106
  suggestions.append(q if q.endswith('?') else q + '?')
107
- return list(dict.fromkeys(suggestions))[:7]
108
 
109
- # Try AI-based suggestion generation first (non-critical)
110
  try:
111
- prompt = f"""You are an assistant that converts section titles into user-friendly, short questions.
112
  Document: {doc_name}
113
- Table of contents:
114
- {chr(10).join(['- ' + t for t in titles[:20]])}
115
- Provide 5-7 concise, natural questions (each under 20 words) based on the TOC that a user might ask."""
116
  ai_resp = genai_generate(prompt)
117
- # extract lines that end with ?
118
  qs = re.findall(r'([^\n?.!]+\?)', ai_resp)
119
- clean_qs = []
120
- for q in qs:
121
- q = q.strip()
122
- if 8 < len(q) < 140:
123
- clean_qs.append(q if q.endswith('?') else q + '?')
124
  if clean_qs:
125
- # dedupe preserving order
126
  return list(dict.fromkeys(clean_qs))[:7]
127
  except Exception:
128
- # silently fallback to deterministic below
129
  pass
130
 
131
- # Deterministic fallback: turn titles into "What is ..." or "How do I ..." where appropriate
132
  suggestions = []
133
  for t in titles[:15]:
134
  low = t.lower()
135
  if re.search(r'\b(set up|install|configure|enable|define|create|prepare)\b', low):
136
- suggestions.append(f"How do I {re.sub(r'[^a-zA-Z0-9 \-]', '', low)}?")
 
137
  elif re.search(r'\b(purpose|overview|objective|scope|what is)\b', low):
138
  suggestions.append(f"What is {t.strip().rstrip('.')}?")
139
  elif re.search(r'\b(step|procedure|process|task)\b', low):
140
  suggestions.append(f"What are the steps for {t.strip().rstrip('.')}?")
141
  else:
142
- # generic
143
  suggestions.append(f"What is described in '{t}'?")
144
- # dedupe & cleanup
145
  seen, final = set(), []
146
  for s in suggestions:
147
  s = re.sub(r'\s+', ' ', s).strip()
@@ -151,50 +131,29 @@ Provide 5-7 concise, natural questions (each under 20 words) based on the TOC th
151
  return final[:7]
152
 
153
  # -------------------------
154
- # CSS / UI polish
155
  # -------------------------
156
- st.markdown(
157
- """
158
- <style>
159
- /* layout tweaks */
160
- .css-18e3th9 { padding-top: 2rem; } /* top padding */
161
- .section-header { font-weight:700; font-size:1.2rem; margin-top:22px; margin-bottom:8px; color: #f3f4f6; }
162
- .info-card { background: linear-gradient(180deg, rgba(16,24,39,0.9), rgba(6,10,14,0.9)); padding:12px 16px; border-radius:10px; color:#e6eef6; box-shadow: 0 6px 20px rgba(2,6,23,0.5); }
163
- /* chips */
164
- .suggest-chip {
165
- display:inline-block; margin:6px 8px; padding:10px 16px; border-radius:999px;
166
- background: rgba(31,41,55,0.65); border: 1px solid rgba(148,163,184,0.08);
167
- color:#e6eef6; font-size:14px; cursor:pointer; transition: transform .12s ease, box-shadow .12s ease;
168
- }
169
- .suggest-chip:hover { transform: translateY(-4px); box-shadow: 0 8px 20px rgba(37,99,235,0.12); background: rgba(37,99,235,0.12); }
170
- .suggest-chip.active { background: linear-gradient(90deg, rgba(59,130,246,0.12), rgba(99,102,241,0.06)); border:1px solid rgba(99,102,241,0.22); color:#eaf2ff; box-shadow: 0 8px 30px rgba(37,99,235,0.12); }
171
- /* input */
172
- .stTextInput>div>div>input { background: rgba(17,24,39,0.75); border-radius:8px; padding:12px 14px; color:#e6eef6; border:1px solid rgba(255,255,255,0.03); }
173
- .stTextInput>div>div>input:focus { box-shadow: 0 6px 20px rgba(37,99,235,0.06); border:1px solid rgba(37,99,235,0.3); }
174
- /* assistant card */
175
- .assistant-card {
176
- background: linear-gradient(180deg, rgba(6,10,14,0.7), rgba(10,15,20,0.6));
177
- border-left: 4px solid rgba(59,130,246,0.9);
178
- padding:18px 20px; border-radius:10px; color:#f8fbff; margin-bottom:10px;
179
- box-shadow: 0 10px 40px rgba(2,6,23,0.5);
180
- }
181
- .assistant-note { color: #f1f5f9; margin-bottom:8px; }
182
- .expander-half { background: rgba(255,255,255,0.02); border-radius:8px; padding:8px; margin-top:8px; }
183
- /* compact section spacing */
184
- .section-compact { margin-bottom: 10px; }
185
- </style>
186
- """,
187
- unsafe_allow_html=True,
188
- )
189
 
190
  # -------------------------
191
- # Page header
192
  # -------------------------
193
  st.title("πŸ“„ Enterprise Knowledge Assistant")
194
  st.caption("Query SAP documentation and enterprise PDFs β€” powered by reasoning and retrieval.")
195
 
196
  # -------------------------
197
- # Sidebar (compact)
198
  # -------------------------
199
  with st.sidebar:
200
  if "reasoning_mode" not in st.session_state:
@@ -212,36 +171,30 @@ with st.sidebar:
212
 
213
  st.markdown("---")
214
  st.header("βš™οΈ Settings")
215
- chunk_size = st.slider("Chunk Size (chars)", min_value=200, max_value=1500, value=1000, step=50)
216
- overlap = st.slider("Chunk Overlap (chars)", min_value=50, max_value=200, value=120, step=10)
217
- top_k = st.slider("Top K Results", min_value=1, max_value=10, value=5)
218
  st.markdown("---")
219
  st.caption("✨ Built by Shubham Sharma")
220
 
221
  # -------------------------
222
- # Initialize central session keys (safe)
223
  # -------------------------
224
  for k, v in {
225
  "show_more": False,
226
  "user_query_input": "",
227
- "auto_run_after_suggest": False,
228
  "selected_suggestion_idx": None,
229
- "last_doc_path": None,
230
- "cached_embeddings_loaded": False,
231
  }.items():
232
  if k not in st.session_state:
233
  st.session_state[k] = v
234
 
235
  # -------------------------
236
- # File paths & base
237
  # -------------------------
238
  BASE_DIR = os.path.dirname(__file__)
239
  SAMPLE_PATH = os.path.join(BASE_DIR, "sample.pdf")
240
  text, chunks, index, embeddings, toc = None, None, None, None, None
241
 
242
- # -------------------------
243
- # Document selection + upload handling
244
- # -------------------------
245
  if doc_choice == "-- Select --":
246
  st.info("⬅️ Please choose a document from the sidebar to begin.")
247
  else:
@@ -249,7 +202,7 @@ else:
249
  temp_path = SAMPLE_PATH
250
  st.success("πŸ“˜ Using built-in Sample PDF.")
251
  else:
252
- uploaded_file = st.file_uploader("πŸ“‚ Upload your PDF (optional)", type="pdf")
253
  if uploaded_file:
254
  temp_path = os.path.join("/tmp", uploaded_file.name)
255
  with open(temp_path, "wb") as f:
@@ -258,112 +211,54 @@ else:
258
  else:
259
  temp_path = None
260
 
261
- # if user swapped to a different doc, clear previous session suggestions
262
- if temp_path and st.session_state.get("last_doc_path") != temp_path:
263
- st.session_state["last_doc_path"] = temp_path
264
- st.session_state["user_query_input"] = ""
265
- st.session_state["selected_suggestion_idx"] = None
266
- st.session_state["auto_run_after_suggest"] = False
267
- st.session_state["cached_embeddings_loaded"] = False
268
-
269
- # process doc
270
  if temp_path:
271
- with st.spinner("πŸ” Extracting & processing document..."):
272
  text, toc = extract_text_from_pdf(temp_path)
273
  chunks = chunk_text(text, chunk_size=chunk_size)
274
  st.markdown("<div class='info-card'>βœ… Document loaded successfully.</div>", unsafe_allow_html=True)
275
 
276
- # suggestions computed once per document (cached in session)
277
- if "suggestions_for_doc" not in st.session_state or st.session_state.get("last_doc_path") != temp_path:
278
- st.session_state["suggestions_for_doc"] = generate_dynamic_suggestions_from_toc(toc, chunks, os.path.basename(temp_path))
279
-
280
- # embeddings + index (cache embeddings via provided helper)
281
- with st.spinner("βš™οΈ Preparing embeddings & index..."):
282
- try:
283
- embeddings = cache_embeddings(os.path.basename(temp_path), chunks, embed_chunks)
284
- st.session_state["cached_embeddings_loaded"] = True
285
- st.success(f"πŸ“¦ Loaded cached embeddings for '{os.path.basename(temp_path)}'.")
286
- except Exception as e:
287
- # still try to embed via embed_chunks
288
- embeddings = embed_chunks(chunks)
289
- st.session_state["cached_embeddings_loaded"] = False
290
 
 
 
291
  index = build_faiss_index(embeddings)
292
- st.markdown("<div class='info-card'>πŸš€ Document ready β€” you can now ask questions below.</div>", unsafe_allow_html=True)
293
 
294
  # -------------------------
295
- # Ask a Question UI
296
  # -------------------------
297
- st.markdown("<div class='section-header'>πŸ€– Ask a Question</div>", unsafe_allow_html=True)
298
 
299
- suggestions = st.session_state.get("suggestions_for_doc") or []
300
-
301
- # --- show top chips, show more toggle ---
302
- if suggestions:
303
  st.markdown("#### πŸ’‘ Suggested Questions")
304
- visible = suggestions if st.session_state["show_more"] else suggestions[:3]
305
-
306
- # lay out chips into up to 3 columns
307
- cols = st.columns(3)
308
  for i, q in enumerate(visible):
309
- # index offset in full suggestions
310
- full_idx = i
311
  col = cols[i % 3]
312
- # chip button that sets the query
313
- if col.button(f"πŸ” {q}", key=f"sugg_btn_{full_idx}"):
314
- st.session_state["user_query_input"] = q
315
- st.session_state["selected_suggestion_idx"] = full_idx
316
- st.session_state["auto_run_after_suggest"] = True
317
-
318
- # visual chip (for aesthetic)
319
- chip_html = f"<div class='suggest-chip {'active' if st.session_state['selected_suggestion_idx']==full_idx else ''}'>{q}</div>"
320
- col.markdown(chip_html, unsafe_allow_html=True)
321
-
322
- toggle_text = "Show less β–²" if st.session_state["show_more"] else "Show more β–Ό"
323
- if st.button(toggle_text, key="toggle_more"):
324
- st.session_state["show_more"] = not st.session_state["show_more"]
325
  st.experimental_rerun()
326
 
327
- # query input (canonical)
328
- user_query = st.text_input("Type your question or pick one above:", value=st.session_state["user_query_input"], key="user_query_input")
329
 
330
- # If a suggestion requested auto-run, run now (only once)
331
- if st.session_state.get("auto_run_after_suggest"):
332
- st.session_state["auto_run_after_suggest"] = False
333
- # proceed to answer if query not empty (we fall through into the answer-rendering below)
334
-
335
- # -------------------------
336
- # Answer generation (single place)
337
- # -------------------------
338
- if user_query and user_query.strip():
339
  st.caption("Mode: 🧠 Reasoning" if st.session_state.reasoning_mode else "Mode: πŸ“„ Strict Document")
340
- with st.spinner("πŸ’­ Retrieving context & generating answer..."):
341
  retrieved = retrieve_chunks(user_query, index, chunks, top_k=top_k, embeddings=embeddings)
342
  answer = generate_answer(user_query, retrieved, reasoning_mode=st.session_state.reasoning_mode)
343
 
344
- # assistant card (glassy)
345
- st.markdown("<div class='section-header'>βœ… Assistant’s Answer</div>", unsafe_allow_html=True)
346
- st.markdown(f"<div class='assistant-card'><div class='assistant-note'>🧠 Assistant</div>{answer}</div>", unsafe_allow_html=True)
347
 
348
- # supporting context + TOC collapsed (kept tidy & optional)
349
- with st.expander("πŸ“„ Supporting Context", expanded=False):
350
  for i, r in enumerate(retrieved, start=1):
351
- st.markdown(f"<div class='expander-half'><b>Chunk {i}:</b> {r}</div>", unsafe_allow_html=True)
352
-
353
- if toc:
354
- with st.expander("πŸ“š Table of Contents (detected)", expanded=False):
355
- toc_text = "\n".join([f"{sec}. {title}" for sec, title in toc])
356
- st.text_area("TOC Preview", toc_text, height=160)
357
 
358
- # -------------------------
359
- # Document preview / stats
360
- # -------------------------
361
  st.markdown("---")
362
  st.subheader("πŸ“‘ Document Preview")
363
- if text:
364
- st.text_area("Extracted text (first 1000 chars)", text[:1000], height=200)
365
- st.caption(f"πŸ“¦ {len(chunks)} chunks processed.")
366
- else:
367
- st.info("No document text available yet.")
368
-
369
- # End of file
 
 
1
  import os
2
  import re
3
  import shutil
 
41
  for f in files
42
  )
43
  size_gb = size_bytes / (1024 ** 3)
 
44
  if size_gb > max_size_gb or "torch" in folder:
45
  shutil.rmtree(folder, ignore_errors=True)
46
  total_deleted += size_gb
 
64
  })
65
 
66
  # -------------------------
67
+ # App-specific imports
68
  # -------------------------
 
69
  from ingestion import extract_text_from_pdf, chunk_text
70
  from vectorstore import build_faiss_index
71
  from qa import retrieve_chunks, generate_answer, cache_embeddings, embed_chunks, genai_generate
72
 
73
  # -------------------------
74
+ # Smart suggestion generator (fixed f-string)
75
  # -------------------------
76
+ def generate_dynamic_suggestions_from_toc(toc: List[Tuple[str, str]], chunks: List[str], doc_name="Document") -> List[str]:
 
 
 
 
77
  if not chunks:
78
  return []
79
 
 
80
  titles = []
81
  for sec, raw_title in toc or []:
82
  title = re.sub(r"^\s*[\dA-Za-z.\-]+\s*", "", raw_title)
 
85
  titles.append(title)
86
 
87
  if not titles:
 
88
  sample = " ".join(chunks[:4])
89
  sents = re.split(r'(?<=[.?!])\s+', sample)
90
  suggestions = []
91
  for s in sents:
92
+ if re.search(r'\b(set up|configure|install|enable|procedure|process|how to|step)\b', s, re.I):
93
+ s = re.sub(r'[.?!]+$', '', s.strip())
94
+ q = s[0].upper() + s[1:]
 
95
  if len(q) < 140:
96
  suggestions.append(q if q.endswith('?') else q + '?')
97
+ return suggestions[:7]
98
 
 
99
  try:
100
+ prompt = f"""Generate 5-7 user-friendly, short questions based on the document's table of contents:
101
  Document: {doc_name}
102
+ TOC:
103
+ {chr(10).join(['- ' + t for t in titles[:15]])}"""
 
104
  ai_resp = genai_generate(prompt)
 
105
  qs = re.findall(r'([^\n?.!]+\?)', ai_resp)
106
+ clean_qs = [q.strip() for q in qs if 8 < len(q) < 140]
 
 
 
 
107
  if clean_qs:
 
108
  return list(dict.fromkeys(clean_qs))[:7]
109
  except Exception:
 
110
  pass
111
 
 
112
  suggestions = []
113
  for t in titles[:15]:
114
  low = t.lower()
115
  if re.search(r'\b(set up|install|configure|enable|define|create|prepare)\b', low):
116
+ cleaned = re.sub(r'[^a-zA-Z0-9 \-]', '', low)
117
+ suggestions.append(f"How do I {cleaned}?")
118
  elif re.search(r'\b(purpose|overview|objective|scope|what is)\b', low):
119
  suggestions.append(f"What is {t.strip().rstrip('.')}?")
120
  elif re.search(r'\b(step|procedure|process|task)\b', low):
121
  suggestions.append(f"What are the steps for {t.strip().rstrip('.')}?")
122
  else:
 
123
  suggestions.append(f"What is described in '{t}'?")
124
+
125
  seen, final = set(), []
126
  for s in suggestions:
127
  s = re.sub(r'\s+', ' ', s).strip()
 
131
  return final[:7]
132
 
133
  # -------------------------
134
+ # CSS for modern UI
135
  # -------------------------
136
+ st.markdown("""
137
+ <style>
138
+ .section-header {font-weight:700;font-size:1.2rem;margin-top:22px;margin-bottom:8px;color:#f3f4f6;}
139
+ .info-card {background:linear-gradient(180deg,rgba(16,24,39,0.9),rgba(6,10,14,0.9));padding:12px 16px;border-radius:10px;color:#e6eef6;box-shadow:0 6px 20px rgba(2,6,23,0.5);}
140
+ .suggest-chip {display:inline-block;margin:6px 8px;padding:10px 16px;border-radius:999px;background:rgba(31,41,55,0.65);border:1px solid rgba(148,163,184,0.08);color:#e6eef6;font-size:14px;cursor:pointer;transition:transform .12s ease,box-shadow .12s ease;}
141
+ .suggest-chip:hover {transform:translateY(-4px);box-shadow:0 8px 20px rgba(37,99,235,0.12);background:rgba(37,99,235,0.12);}
142
+ .suggest-chip.active {background:linear-gradient(90deg,rgba(59,130,246,0.12),rgba(99,102,241,0.06));border:1px solid rgba(99,102,241,0.22);color:#eaf2ff;box-shadow:0 8px 30px rgba(37,99,235,0.12);}
143
+ .stTextInput>div>div>input {background:rgba(17,24,39,0.75);border-radius:8px;padding:12px 14px;color:#e6eef6;border:1px solid rgba(255,255,255,0.03);}
144
+ .stTextInput>div>div>input:focus {box-shadow:0 6px 20px rgba(37,99,235,0.06);border:1px solid rgba(37,99,235,0.3);}
145
+ .assistant-card {background:linear-gradient(180deg,rgba(6,10,14,0.7),rgba(10,15,20,0.6));border-left:4px solid rgba(59,130,246,0.9);padding:18px 20px;border-radius:10px;color:#f8fbff;margin-bottom:10px;box-shadow:0 10px 40px rgba(2,6,23,0.5);}
146
+ </style>
147
+ """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
  # -------------------------
150
+ # App Header
151
  # -------------------------
152
  st.title("πŸ“„ Enterprise Knowledge Assistant")
153
  st.caption("Query SAP documentation and enterprise PDFs β€” powered by reasoning and retrieval.")
154
 
155
  # -------------------------
156
+ # Sidebar
157
  # -------------------------
158
  with st.sidebar:
159
  if "reasoning_mode" not in st.session_state:
 
171
 
172
  st.markdown("---")
173
  st.header("βš™οΈ Settings")
174
+ chunk_size = st.slider("Chunk Size (chars)", 200, 1500, 1000, step=50)
175
+ overlap = st.slider("Chunk Overlap (chars)", 50, 200, 120, step=10)
176
+ top_k = st.slider("Top K Results", 1, 10, 5)
177
  st.markdown("---")
178
  st.caption("✨ Built by Shubham Sharma")
179
 
180
  # -------------------------
181
+ # Session state
182
  # -------------------------
183
  for k, v in {
184
  "show_more": False,
185
  "user_query_input": "",
 
186
  "selected_suggestion_idx": None,
 
 
187
  }.items():
188
  if k not in st.session_state:
189
  st.session_state[k] = v
190
 
191
  # -------------------------
192
+ # Document processing
193
  # -------------------------
194
  BASE_DIR = os.path.dirname(__file__)
195
  SAMPLE_PATH = os.path.join(BASE_DIR, "sample.pdf")
196
  text, chunks, index, embeddings, toc = None, None, None, None, None
197
 
 
 
 
198
  if doc_choice == "-- Select --":
199
  st.info("⬅️ Please choose a document from the sidebar to begin.")
200
  else:
 
202
  temp_path = SAMPLE_PATH
203
  st.success("πŸ“˜ Using built-in Sample PDF.")
204
  else:
205
+ uploaded_file = st.file_uploader("πŸ“‚ Upload your PDF", type="pdf")
206
  if uploaded_file:
207
  temp_path = os.path.join("/tmp", uploaded_file.name)
208
  with open(temp_path, "wb") as f:
 
211
  else:
212
  temp_path = None
213
 
 
 
 
 
 
 
 
 
 
214
  if temp_path:
215
+ with st.spinner("πŸ” Processing document..."):
216
  text, toc = extract_text_from_pdf(temp_path)
217
  chunks = chunk_text(text, chunk_size=chunk_size)
218
  st.markdown("<div class='info-card'>βœ… Document loaded successfully.</div>", unsafe_allow_html=True)
219
 
220
+ query_suggestions = generate_dynamic_suggestions_from_toc(toc, chunks, os.path.basename(temp_path))
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
+ with st.spinner("βš™οΈ Preparing embeddings..."):
223
+ embeddings = cache_embeddings(os.path.basename(temp_path), chunks, embed_chunks)
224
  index = build_faiss_index(embeddings)
225
+ st.markdown("<div class='info-card'>πŸš€ Document ready β€” ask questions below.</div>", unsafe_allow_html=True)
226
 
227
  # -------------------------
228
+ # Ask a Question
229
  # -------------------------
230
+ st.markdown("## πŸ€– Ask a Question")
231
 
232
+ if query_suggestions:
 
 
 
233
  st.markdown("#### πŸ’‘ Suggested Questions")
234
+ visible = query_suggestions if st.session_state.show_more else query_suggestions[:3]
235
+ cols = st.columns(min(3, len(visible)))
 
 
236
  for i, q in enumerate(visible):
 
 
237
  col = cols[i % 3]
238
+ if col.button(f"πŸ” {q}", key=f"q_{i}"):
239
+ st.session_state.user_query_input = q
240
+ st.session_state.selected_suggestion_idx = i
241
+ toggle_text = "Show less β–²" if st.session_state.show_more else "Show more β–Ό"
242
+ if st.button(toggle_text):
243
+ st.session_state.show_more = not st.session_state.show_more
 
 
 
 
 
 
 
244
  st.experimental_rerun()
245
 
246
+ user_query = st.text_input("Type your question or pick one above:", value=st.session_state.user_query_input)
 
247
 
248
+ if user_query.strip():
 
 
 
 
 
 
 
 
249
  st.caption("Mode: 🧠 Reasoning" if st.session_state.reasoning_mode else "Mode: πŸ“„ Strict Document")
250
+ with st.spinner("πŸ’­ Generating answer..."):
251
  retrieved = retrieve_chunks(user_query, index, chunks, top_k=top_k, embeddings=embeddings)
252
  answer = generate_answer(user_query, retrieved, reasoning_mode=st.session_state.reasoning_mode)
253
 
254
+ st.markdown("### βœ… Assistant’s Answer")
255
+ st.markdown(f"<div class='assistant-card'>{answer}</div>", unsafe_allow_html=True)
 
256
 
257
+ with st.expander("πŸ“„ Supporting Context"):
 
258
  for i, r in enumerate(retrieved, start=1):
259
+ st.markdown(f"**Chunk {i}:** {r}")
 
 
 
 
 
260
 
 
 
 
261
  st.markdown("---")
262
  st.subheader("πŸ“‘ Document Preview")
263
+ st.text_area("Extracted text (first 1000 chars)", text[:1000], height=200)
264
+ st.caption(f"πŸ“¦ {len(chunks)} chunks processed.")