absiitr commited on
Commit
8601c8e
Β·
verified Β·
1 Parent(s): 637d303

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +179 -224
app.py CHANGED
@@ -13,187 +13,124 @@ import torch
13
  # ---------------- CONFIGURATION ----------------
14
  logging.basicConfig(level=logging.INFO)
15
 
16
- # Load API key from Hugging Face secrets (or env)
17
  GROQ_API_KEY = st.secrets.get("GROQ_API_KEY", os.environ.get("GROQ_API_KEY"))
18
  GROQ_MODEL = "llama-3.1-8b-instant"
19
 
20
- # Initialize Groq client silently (no top green message)
21
  client = None
22
  if GROQ_API_KEY:
23
  try:
24
  client = Groq(api_key=GROQ_API_KEY)
25
- logging.info("Groq client initialized (silent).")
 
26
  except Exception as e:
27
- logging.exception("Groq init failed.")
28
  client = None
29
  else:
30
- # Keep a visible but non-green hint if key is missing
31
  st.warning("⚠️ GROQ_API_KEY not found. Please add it to Hugging Face secrets.")
32
 
33
  # ---------------- STREAMLIT UI SETUP ----------------
34
  st.set_page_config(page_title="PDF Assistant", page_icon="πŸ“˜", layout="wide")
35
 
36
- # ---------------- CSS (layout + styling) ----------------
37
- st.markdown(
38
- """
39
  <style>
40
- :root{
41
- --primary:#1e3a8a;
42
- --bg:#0e1117;
43
- --bg2:#1a1d29;
44
- --text:#f0f2f6;
45
- }
46
-
47
- /* Fixed sidebar */
48
- section[data-testid="stSidebar"] {
49
  position: fixed;
 
50
  height: 100vh;
51
- overflow-y: auto;
52
- padding-top: 0.5rem;
53
- width: 300px;
54
  }
55
 
56
- /* Main content offset to the right of sidebar */
57
- .main {
58
- margin-left: 320px;
59
- padding-top: 16px;
 
60
  }
61
 
62
- /* Header (title + creator) */
63
- .header-left {
64
- display: flex;
65
- flex-direction: column;
66
- align-items: flex-start;
67
- gap: 4px;
68
- margin-left: 8px;
69
  }
70
- .header-title {
71
- font-size: 1.6rem;
72
- font-weight: 600;
73
- margin: 0;
74
  }
75
- .header-creator {
76
- font-size: 0.9rem;
77
- color: var(--text);
78
  }
79
 
80
- /* Chat scroll area */
81
- .chat-area {
82
- height: calc(100vh - 200px);
83
- overflow-y: auto;
84
- padding: 1rem 2rem;
 
 
 
85
  }
86
 
87
- /* Chat bubble styles */
88
  .chat-user {
89
  background: #2d3748;
90
- padding: 12px 14px;
91
- border-radius: 18px 18px 4px 18px;
92
- margin: 12px 0 12px auto;
93
- max-width: 75%;
94
- color: var(--text);
95
- line-height: 1.4;
96
- word-break: break-word;
97
  }
98
  .chat-bot {
99
- background: var(--primary);
100
- padding: 12px 14px;
101
- border-radius: 18px 18px 18px 4px;
102
- margin: 12px auto 12px 0;
103
- max-width: 75%;
104
- color: white;
105
- line-height: 1.4;
106
- word-break: break-word;
107
- }
108
- .sources {
109
- font-size: 0.8rem;
110
- opacity: 0.75;
111
- margin-top: 8px;
112
- border-top: 1px solid rgba(255,255,255,0.08);
113
- padding-top: 6px;
114
  }
115
 
116
- /* Sticky input bar at bottom of main area */
117
- .input-bar {
118
- position: sticky;
119
- bottom: 0;
120
- left: 320px;
121
- right: 0;
122
- background: var(--bg);
123
- padding: 12px 20px;
124
- border-top: 1px solid rgba(255,255,255,0.06);
125
- z-index: 999;
126
  }
127
 
128
- /* Form layout */
129
- .input-row {
130
- max-width: 980px;
131
- margin: 0 auto;
132
- display: flex;
133
- gap: 8px;
134
- align-items: center;
135
- }
136
- .text-input {
137
- flex: 1;
138
- background: transparent;
139
- border: 1px solid rgba(255,255,255,0.06);
140
- padding: 10px 14px;
141
- border-radius: 999px;
142
- color: var(--text);
143
- outline: none;
144
- font-size: 1rem;
145
- }
146
- .text-input::placeholder {
147
- color: rgba(255,255,255,0.45);
148
- }
149
- .submit-arrow {
150
- background: var(--primary);
151
- color: white;
152
  border: none;
153
- height: 42px;
154
- width: 42px;
155
- border-radius: 50%;
156
- display: inline-flex;
157
- align-items: center;
158
- justify-content: center;
159
- font-weight: 700;
160
- cursor: pointer;
161
- }
162
- .submit-arrow:disabled {
163
- opacity: 0.45;
164
- cursor: not-allowed;
165
- }
166
-
167
- /* Small responsive tweak */
168
- @media (max-width: 768px) {
169
- section[data-testid="stSidebar"] {
170
- position: relative;
171
- width: 100%;
172
- height: auto;
173
- }
174
- .main {
175
- margin-left: 0;
176
- }
177
- .input-bar { left: 0; }
178
  }
179
  </style>
180
- """,
181
- unsafe_allow_html=True,
182
- )
183
 
184
  # ---------------- SESSION STATE ----------------
185
  if "chat" not in st.session_state:
186
  st.session_state.chat = []
 
187
  if "vectorstore" not in st.session_state:
188
  st.session_state.vectorstore = None
 
189
  if "retriever" not in st.session_state:
190
  st.session_state.retriever = None
 
191
  if "uploaded_file_name" not in st.session_state:
192
  st.session_state.uploaded_file_name = None
 
193
  if "uploader_key" not in st.session_state:
194
  st.session_state.uploader_key = 0
195
 
196
- # ---------------- HELPERS / LOGIC ----------------
197
  def clear_chat_history():
198
  st.session_state.chat = []
199
 
@@ -208,47 +145,54 @@ def clear_memory():
208
  st.success("Memory cleared. Please upload a new PDF.")
209
 
210
  def process_pdf(uploaded_file):
 
211
  try:
212
  with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
213
  tmp.write(uploaded_file.getvalue())
214
  path = tmp.name
215
-
216
  loader = PyPDFLoader(path)
217
  docs = loader.load()
218
-
219
- splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=50)
 
 
 
220
  chunks = splitter.split_documents(docs)
221
-
222
  embeddings = HuggingFaceEmbeddings(
223
  model_name="sentence-transformers/all-MiniLM-L6-v2",
224
  model_kwargs={"device": "cpu"},
225
- encode_kwargs={"normalize_embeddings": True},
226
  )
227
-
228
  vectorstore = Chroma.from_documents(chunks, embeddings)
229
  retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
230
-
231
  st.session_state.vectorstore = vectorstore
232
  st.session_state.retriever = retriever
233
-
234
  if os.path.exists(path):
235
  os.unlink(path)
236
-
237
  return len(chunks)
 
238
  except Exception as e:
239
- logging.exception("PDF processing error")
240
- st.error(f"Error processing PDF: {e}")
241
  return None
242
 
243
  def ask_question(question):
 
244
  if not client:
245
  return None, 0, "Groq client is not initialized. Check API key setup."
 
246
  if not st.session_state.retriever:
247
  return None, 0, "Upload PDF first to initialize the knowledge base."
 
248
  try:
249
  docs = st.session_state.retriever.invoke(question)
250
  context = "\n\n".join(d.page_content for d in docs)
251
-
252
  prompt = f"""
253
  You are a strict RAG Q&A assistant.
254
  Use ONLY the context provided. If the answer is not found, reply:
@@ -265,108 +209,119 @@ FINAL ANSWER:
265
  response = client.chat.completions.create(
266
  model=GROQ_MODEL,
267
  messages=[
268
- {"role": "system", "content": "Use only the PDF content. If answer not found, say: 'I cannot find this in the PDF.'"},
269
- {"role": "user", "content": prompt},
 
270
  ],
271
- temperature=0.0,
272
  )
 
273
  answer = response.choices[0].message.content.strip()
274
  return answer, len(docs), None
 
275
  except APIError as e:
276
  return None, 0, f"Groq API Error: {str(e)}"
277
  except Exception as e:
278
- logging.exception("Ask error")
279
  return None, 0, f"General error: {str(e)}"
280
 
281
- # ---------------- SIDEBAR (Upload + Controls) ----------------
282
- with st.sidebar:
283
- st.markdown("### Upload PDF", unsafe_allow_html=True)
284
- uploaded = st.file_uploader("Choose a PDF file", type=["pdf"], key=st.session_state.uploader_key, label_visibility="collapsed")
285
- if uploaded and uploaded.name != st.session_state.uploaded_file_name:
286
- # reset chat and process
287
- st.session_state.chat = []
288
- st.session_state.uploaded_file_name = None
289
- with st.spinner(f"Processing '{uploaded.name}'..."):
290
- chunks_count = process_pdf(uploaded)
291
- if chunks_count is not None:
292
- st.success(f"βœ… PDF processed successfully! {chunks_count} chunks created.")
293
- st.session_state.uploaded_file_name = uploaded.name
294
- else:
295
- st.error("❌ Failed to process PDF")
296
- st.session_state.uploaded_file_name = None
297
- st.experimental_rerun()
298
 
299
- st.markdown("---")
 
 
 
 
 
 
 
300
  st.header("Controls")
301
  st.button("πŸ—‘οΈ Clear Chat History", on_click=clear_chat_history, use_container_width=True)
302
  st.button("πŸ”₯ Clear PDF Memory", on_click=clear_memory, use_container_width=True)
303
-
304
  st.markdown("---")
305
- st.subheader("Status")
306
  if st.session_state.uploaded_file_name:
307
- st.success(f"βœ… Active PDF:\n`{st.session_state.uploaded_file_name}`")
308
  else:
309
- st.info("⬆️ Upload a PDF to start chatting!")
310
-
311
- # ---------------- MAIN HEADER (Top-left Title + Creator) ----------------
312
- # uses "main" margin via CSS
313
- st.markdown('<div class="main">', unsafe_allow_html=True)
314
- st.markdown(
315
- """
316
- <div class="header-left">
317
- <div class="header-title">πŸ“˜ PDF Assistant</div>
318
- <div class="header-creator">Created by <a href="https://www.linkedin.com/in/abhishek-iitr/" target="_blank" style="color: #9fc2ff;">Abhishek Saxena</a></div>
319
- </div>
320
- """,
321
- unsafe_allow_html=True,
322
- )
323
 
324
- # ---------------- CHAT AREA ----------------
325
- st.markdown('<div class="chat-area">', unsafe_allow_html=True)
 
 
 
 
326
 
327
- if not st.session_state.chat:
328
- st.markdown('<div style="color:rgba(255,255,255,0.55); padding:20px 0;">Ask a question about your document to start the conversation.</div>', unsafe_allow_html=True)
329
- else:
330
- for role, msg in st.session_state.chat:
331
- if role == "user":
332
- st.markdown(f"<div class='chat-user'>{msg}</div>", unsafe_allow_html=True)
 
 
 
 
333
  else:
334
- st.markdown(f"<div class='chat-bot'>{msg}</div>", unsafe_allow_html=True)
335
-
336
- st.markdown("</div>", unsafe_allow_html=True) # close chat-area
337
-
338
- # ---------------- INPUT BAR (Enter to submit + arrow button) ----------------
339
- st.markdown('<div class="input-bar">', unsafe_allow_html=True)
340
- st.markdown('<div class="input-row">', unsafe_allow_html=True)
341
-
342
- # Build a form: pressing Enter in the input will submit the form.
343
- with st.form(key="ask_form", clear_on_submit=True):
344
- cols = st.columns([1, 0.12])
345
- with cols[0]:
346
- q = st.text_input(
347
- "Type your question",
348
- key="question_input",
349
- placeholder="Ask anything about your PDF document...",
350
- disabled=(st.session_state.uploaded_file_name is None or client is None),
 
 
 
351
  label_visibility="collapsed",
 
352
  )
353
- with cols[1]:
354
- # Arrow submit button (visible). Pressing Enter also triggers the form submit.
355
- submit = st.form_submit_button("➀", help="Send (Enter or click)", disabled=(st.session_state.uploaded_file_name is None or client is None))
356
-
357
- st.markdown("</div></div>", unsafe_allow_html=True) # close input-row and input-bar
358
-
359
- if submit and q:
360
- # Append user message
361
- st.session_state.chat.append(("user", q))
362
-
363
- with st.spinner("Thinking..."):
364
- answer, sources, error = ask_question(q)
365
- if answer:
366
- bot_msg = f"{answer}<div class='sources'>Context Chunks Used: {sources}</div>"
367
- st.session_state.chat.append(("bot", bot_msg))
368
- else:
369
- st.session_state.chat.append(("bot", f"πŸ”΄ **Error:** {error}"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
 
371
- # Rerun so new messages show and form clears
372
- st.experimental_rerun()
 
13
  # ---------------- CONFIGURATION ----------------
14
  logging.basicConfig(level=logging.INFO)
15
 
16
+ # Load API key from Hugging Face secrets
17
  GROQ_API_KEY = st.secrets.get("GROQ_API_KEY", os.environ.get("GROQ_API_KEY"))
18
  GROQ_MODEL = "llama-3.1-8b-instant"
19
 
20
+ # Initialize Groq client
21
  client = None
22
  if GROQ_API_KEY:
23
  try:
24
  client = Groq(api_key=GROQ_API_KEY)
25
+ # βœ… Removed the st.success message (Problem #1)
26
+ logging.info("βœ… Groq client initialized successfully.")
27
  except Exception as e:
28
+ st.error(f"❌ Failed to initialize Groq client: {e}")
29
  client = None
30
  else:
 
31
  st.warning("⚠️ GROQ_API_KEY not found. Please add it to Hugging Face secrets.")
32
 
33
  # ---------------- STREAMLIT UI SETUP ----------------
34
  st.set_page_config(page_title="PDF Assistant", page_icon="πŸ“˜", layout="wide")
35
 
36
+ # ---------------- CSS ----------------
37
+ st.markdown("""
 
38
  <style>
39
+ /* -------------------------------------- */
40
+ /* 1. SCROLL FIX FOR SIDEBAR & MAIN AREA */
41
+ /* -------------------------------------- */
42
+ /* Fix the sidebar and prevent it from scrolling */
43
+ [data-testid="stSidebar"] {
 
 
 
 
44
  position: fixed;
45
+ overflow-y: hidden !important;
46
  height: 100vh;
47
+ z-index: 99;
 
 
48
  }
49
 
50
+ /* Allow the main content area to scroll independently */
51
+ .main .block-container {
52
+ overflow-y: auto;
53
+ height: 100vh;
54
+ padding-bottom: 5rem; /* Space for scrolling */
55
  }
56
 
57
+ /* Hide main page scrollbar for cleaner look */
58
+ ::-webkit-scrollbar {
59
+ width: 8px;
 
 
 
 
60
  }
61
+ ::-webkit-scrollbar-track {
62
+ background: #0e1117;
 
 
63
  }
64
+ ::-webkit-scrollbar-thumb {
65
+ background: #2d3748;
66
+ border-radius: 4px;
67
  }
68
 
69
+ /* -------------------------------------- */
70
+ /* 2. CHAT & UI STYLING */
71
+ /* -------------------------------------- */
72
+ :root {
73
+ --primary-color: #1e3a8a;
74
+ --background-color: #0e1117;
75
+ --secondary-background-color: #1a1d29;
76
+ --text-color: #f0f2f6;
77
  }
78
 
 
79
  .chat-user {
80
  background: #2d3748;
81
+ padding: 12px;
82
+ border-radius: 10px 10px 2px 10px;
83
+ margin: 6px 0 6px auto;
84
+ max-width: 85%;
85
+ text-align: right;
86
+ color: var(--text-color);
 
87
  }
88
  .chat-bot {
89
+ background: var(--primary-color);
90
+ padding: 12px;
91
+ border-radius: 10px 10px 10px 2px;
92
+ margin: 6px auto 6px 0;
93
+ max-width: 85%;
94
+ text-align: left;
95
+ color: #ffffff;
 
 
 
 
 
 
 
 
96
  }
97
 
98
+ .sources {
99
+ font-size: 0.8em;
100
+ opacity: 0.7;
101
+ margin-top: 10px;
102
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
103
+ padding-top: 5px;
 
 
 
 
104
  }
105
 
106
+ /* -------------------------------------- */
107
+ /* 3. INPUT FORM STYLING */
108
+ /* -------------------------------------- */
109
+ /* Align the input box and button properly */
110
+ [data-testid="stForm"] {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  border: none;
112
+ padding: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  }
114
  </style>
115
+ """, unsafe_allow_html=True)
 
 
116
 
117
  # ---------------- SESSION STATE ----------------
118
  if "chat" not in st.session_state:
119
  st.session_state.chat = []
120
+
121
  if "vectorstore" not in st.session_state:
122
  st.session_state.vectorstore = None
123
+
124
  if "retriever" not in st.session_state:
125
  st.session_state.retriever = None
126
+
127
  if "uploaded_file_name" not in st.session_state:
128
  st.session_state.uploaded_file_name = None
129
+
130
  if "uploader_key" not in st.session_state:
131
  st.session_state.uploader_key = 0
132
 
133
+ # ---------------- FUNCTIONS ----------------
134
  def clear_chat_history():
135
  st.session_state.chat = []
136
 
 
145
  st.success("Memory cleared. Please upload a new PDF.")
146
 
147
  def process_pdf(uploaded_file):
148
+ """Process uploaded PDF and create vectorstore."""
149
  try:
150
  with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
151
  tmp.write(uploaded_file.getvalue())
152
  path = tmp.name
153
+
154
  loader = PyPDFLoader(path)
155
  docs = loader.load()
156
+
157
+ splitter = RecursiveCharacterTextSplitter(
158
+ chunk_size=800,
159
+ chunk_overlap=50
160
+ )
161
  chunks = splitter.split_documents(docs)
162
+
163
  embeddings = HuggingFaceEmbeddings(
164
  model_name="sentence-transformers/all-MiniLM-L6-v2",
165
  model_kwargs={"device": "cpu"},
166
+ encode_kwargs={"normalize_embeddings": True}
167
  )
168
+
169
  vectorstore = Chroma.from_documents(chunks, embeddings)
170
  retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
171
+
172
  st.session_state.vectorstore = vectorstore
173
  st.session_state.retriever = retriever
174
+
175
  if os.path.exists(path):
176
  os.unlink(path)
177
+
178
  return len(chunks)
179
+
180
  except Exception as e:
181
+ st.error(f"Error processing PDF: {str(e)}")
 
182
  return None
183
 
184
  def ask_question(question):
185
+ """Retrieve and generate answer for the question."""
186
  if not client:
187
  return None, 0, "Groq client is not initialized. Check API key setup."
188
+
189
  if not st.session_state.retriever:
190
  return None, 0, "Upload PDF first to initialize the knowledge base."
191
+
192
  try:
193
  docs = st.session_state.retriever.invoke(question)
194
  context = "\n\n".join(d.page_content for d in docs)
195
+
196
  prompt = f"""
197
  You are a strict RAG Q&A assistant.
198
  Use ONLY the context provided. If the answer is not found, reply:
 
209
  response = client.chat.completions.create(
210
  model=GROQ_MODEL,
211
  messages=[
212
+ {"role": "system",
213
+ "content": "Use only the PDF content. If answer not found, say: 'I cannot find this in the PDF.'"},
214
+ {"role": "user", "content": prompt}
215
  ],
216
+ temperature=0.0
217
  )
218
+
219
  answer = response.choices[0].message.content.strip()
220
  return answer, len(docs), None
221
+
222
  except APIError as e:
223
  return None, 0, f"Groq API Error: {str(e)}"
224
  except Exception as e:
 
225
  return None, 0, f"General error: {str(e)}"
226
 
227
+ # ---------------- UI COMPONENTS ----------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
 
229
+ # βœ… Problem #2: Title and Creator info at Top Left
230
+ col_header, col_space = st.columns([0.6, 0.4])
231
+ with col_header:
232
+ st.title("πŸ“˜ PDF Assistant")
233
+ st.markdown("**Created by [Abhishek Saxena](https://www.linkedin.com/in/abhishek-iitr/)**")
234
+
235
+ # Sidebar Controls
236
+ with st.sidebar:
237
  st.header("Controls")
238
  st.button("πŸ—‘οΈ Clear Chat History", on_click=clear_chat_history, use_container_width=True)
239
  st.button("πŸ”₯ Clear PDF Memory", on_click=clear_memory, use_container_width=True)
240
+
241
  st.markdown("---")
 
242
  if st.session_state.uploaded_file_name:
243
+ st.success(f"βœ… **Active PDF:**\n `{st.session_state.uploaded_file_name}`")
244
  else:
245
+ st.warning("⬆️ Upload a PDF to start chatting!")
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
+ # File Upload
248
+ uploaded = st.file_uploader(
249
+ "Upload your PDF",
250
+ type=["pdf"],
251
+ key=st.session_state.uploader_key
252
+ )
253
 
254
+ if uploaded and uploaded.name != st.session_state.uploaded_file_name:
255
+ st.session_state.uploaded_file_name = None
256
+ st.session_state.chat = []
257
+
258
+ with st.spinner(f"Processing '{uploaded.name}'..."):
259
+ chunks_count = process_pdf(uploaded)
260
+
261
+ if chunks_count is not None:
262
+ st.success(f"βœ… PDF processed successfully! {chunks_count} chunks created.")
263
+ st.session_state.uploaded_file_name = uploaded.name
264
  else:
265
+ st.error("❌ Failed to process PDF")
266
+ st.session_state.uploaded_file_name = None
267
+
268
+ st.rerun()
269
+
270
+ # ---------------- INPUT AREA ----------------
271
+ # βœ… Problem #3: "Enter" key submission + Arrow button using st.form
272
+ # We use a form so that pressing "Enter" on the keyboard submits the question.
273
+
274
+ disabled_input = st.session_state.uploaded_file_name is None or client is None
275
+
276
+ # Create a form container
277
+ with st.form(key='chat_form', clear_on_submit=True):
278
+ # Use columns to put the input box and the button side-by-side
279
+ col_input, col_btn = st.columns([0.85, 0.15], gap="small")
280
+
281
+ with col_input:
282
+ user_question = st.text_input(
283
+ "Ask a question",
284
+ placeholder="Ask a question about the loaded PDF...",
285
  label_visibility="collapsed",
286
+ disabled=disabled_input
287
  )
288
+
289
+ with col_btn:
290
+ # The arrow button acts as the submit button for the form
291
+ submit_btn = st.form_submit_button("➀", disabled=disabled_input, use_container_width=True)
292
+
293
+ # Processing the Input (Run only when form is submitted)
294
+ if submit_btn or (user_question and not disabled_input):
295
+ # Note: Streamlit forms usually require the submit button click check,
296
+ # but clear_on_submit=True handles the reset.
297
+
298
+ if user_question:
299
+ # Add user query to chat history
300
+ st.session_state.chat.append(("user", user_question))
301
+
302
+ # Get answer
303
+ with st.spinner("Thinking..."):
304
+ answer, sources, error = ask_question(user_question)
305
+
306
+ if answer:
307
+ bot_message = f"{answer}<div class='sources'>Context Chunks Used: {sources}</div>"
308
+ st.session_state.chat.append(("bot", bot_message))
309
+ else:
310
+ st.session_state.chat.append(("bot", f"πŸ”΄ **Error:** {error}"))
311
+
312
+ # Rerun to update the chat history display below
313
+ st.rerun()
314
+
315
+ # ---------------- CHAT HISTORY DISPLAY ----------------
316
+ st.markdown("---")
317
+ st.markdown("## Chat History")
318
+
319
+ # Display in reverse order (newest top) or standard (newest bottom)?
320
+ # Standard chat is newest bottom.
321
+ for role, msg in st.session_state.chat:
322
+ if role == "user":
323
+ st.markdown(f"<div class='chat-user'>{msg}</div>", unsafe_allow_html=True)
324
+ else:
325
+ st.markdown(f"<div class='chat-bot'>{msg}</div>", unsafe_allow_html=True)
326
 
327
+ # (Footer removed as requested, info moved to top)