Suunil-Dabral commited on
Commit
2038939
Β·
verified Β·
1 Parent(s): dc1be60

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +274 -267
src/streamlit_app.py CHANGED
@@ -18,42 +18,91 @@ st.set_page_config(page_title="FoodHub Support", page_icon="πŸ•", layout="cente
18
  st.markdown(
19
  """
20
  <style>
21
- .main { max-width: 480px; margin: 0 auto; }
 
 
 
22
  .foodhub-card {
23
  background: linear-gradient(135deg, #fff3e0, #ffe0cc);
24
- padding: 16px 20px;
25
- border-radius: 18px;
26
  border: 1px solid #f5c28c;
27
- margin-bottom: 16px;
28
  }
29
  .foodhub-header-title {
30
- margin-bottom: 4px;
31
  color: #e65c2b;
32
- font-size: 26px;
33
  font-weight: 800;
 
34
  }
35
- .foodhub-header-subtitle { margin: 0; color: #444; font-size: 14px; }
36
  .foodhub-assistant-pill {
37
  display: inline-flex; align-items: center; gap: 6px;
38
  background: #ffffff; border-radius: 999px;
39
- padding: 6px 10px; font-size: 11px; color: #555;
40
- border: 1px solid #ffd3a3; margin-top: 8px;
41
  }
42
  .status-dot { width: 8px; height: 8px; border-radius: 50%; background: #2ecc71; }
43
- .quick-actions-title { font-size: 13px; font-weight: 600; color: #555; margin-top: 4px; margin-bottom: 4px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  .bot-bubble {
45
  background: #eef7f2; border-radius: 12px;
46
  padding: 10px 12px; font-size: 14px;
47
- margin-top: 12px; line-height: 1.35;
48
  }
49
- .user-label { font-size: 12px; color: #777; margin-top: 8px; margin-bottom: 2px; }
50
- .bot-label { font-size: 12px; color: #777; margin-top: 12px; margin-bottom: 2px; }
 
 
 
 
 
 
51
  code {
52
  background: rgba(0,0,0,0.06);
53
  padding: 2px 6px;
54
  border-radius: 8px;
55
  font-size: 13px;
56
  }
 
 
 
 
 
 
57
  </style>
58
  """,
59
  unsafe_allow_html=True,
@@ -67,7 +116,7 @@ ASK_ORDER_ID_LINE = "Please share your **Order ID** (for example: `O12488`) so I
67
  ORDER_ID_REGEX = r"\b[oO]\d{3,}\b"
68
 
69
  def format_for_bubble(text: str) -> str:
70
- """Safely render markdown-like **bold** and `code` inside HTML chat bubbles."""
71
  t = html.escape(str(text or ""))
72
  t = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", t) # **bold**
73
  t = re.sub(r"`([^`]+)`", r"<code>\1</code>", t) # `code`
@@ -79,48 +128,39 @@ def extract_order_id(text: str) -> Optional[str]:
79
  return m.group(0).upper() if m else None
80
 
81
  def needs_order_id(message: str) -> bool:
82
- """
83
- True if message is asking anything order-specific that must NOT be answered without an Order ID.
84
- """
85
  t = (message or "").lower().strip()
86
-
87
- # exclude simple greetings / thanks only
88
  greetings = {"hi", "hello", "hey", "good morning", "good afternoon", "good evening"}
89
  thanks = {"thanks", "thank you", "thx", "thankyou"}
90
  if t in greetings or t in thanks:
91
  return False
92
-
93
- # If they already provided Order ID, no need to ask.
94
  if extract_order_id(message):
95
  return False
96
 
97
- # High-confidence order intents
98
  phrases = [
99
  "where is my order", "track my order", "track order", "order status", "delivery status",
100
- "status of my order", "when will my order arrive", "when will it arrive", "eta",
101
  "order delayed", "delay in delivery", "order not delivered", "late delivery",
102
- "order details", "show me the details", "details of my order", "details for my order",
103
- "what is in my order", "what's in my order", "items in my order", "my order items",
104
- "what did i order", "what have i ordered",
105
  "cancel my order", "cancel order",
106
  "refund", "payment issue", "payment problem", "payment failed", "charged", "transaction",
107
- "money back", "return my money",
108
  "modify my order", "change my order", "postpone my order", "deliver at different address",
109
- "change address", "different address", "deliver to", "reschedule delivery",
 
 
110
  ]
111
  if any(p in t for p in phrases):
112
  return True
113
 
114
- # Generic "order" mention with an action keyword
115
- if "order" in t and any(k in t for k in ["status", "track", "details", "item", "cancel", "refund", "payment", "where", "when", "modify", "change", "postpone", "address", "deliver", "reschedule"]):
 
 
 
116
  return True
117
 
118
  return False
119
 
120
  def is_third_party_order_request(message: str) -> bool:
121
- """
122
- Detect requests for someone else's order. We refuse for privacy.
123
- """
124
  t = (message or "").lower()
125
  third_party = [
126
  "my friend", "friend's", "friends'", "my brother", "my sister",
@@ -153,6 +193,10 @@ def _is_escalation_intent(message: str) -> bool:
153
  "multiple follow-ups",
154
  "i have followed multiple times",
155
  "i have followed up multiple times",
 
 
 
 
156
  ]
157
  return any(k in t for k in escalation_keywords)
158
 
@@ -161,27 +205,71 @@ def _is_payment_refund_intent(message: str) -> bool:
161
  keywords = [
162
  "payment", "refund", "money back", "charged", "chargeback",
163
  "transaction", "payment failed", "payment issue", "payment problem",
164
- "upi", "card", "debit", "credit", "wallet"
 
165
  ]
166
  return any(k in t for k in keywords)
167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  # ================== DB CONFIG (ALIGNED WITH YOUR NOTEBOOK) ==================
169
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
170
- CLEAN_DB_PATH = os.path.join(BASE_DIR, "orders_clean.db") # must match your notebook output
171
 
172
- # Startup sanity check
173
  try:
174
  with sqlite3.connect(CLEAN_DB_PATH) as _conn:
175
  _cur = _conn.cursor()
176
  _cur.execute("SELECT name FROM sqlite_master WHERE type='table';")
177
  _tables = [r[0] for r in _cur.fetchall()]
178
  if "orders_clean" not in _tables:
179
- st.error(
180
- f"Connected to {CLEAN_DB_PATH!r}, but table 'orders_clean' was not found.\n\n"
181
- "Ensure your notebook saved df_eng like:\n"
182
- "df_eng.to_sql('orders_clean', conn, if_exists='replace', index=False)\n\n"
183
- "and that orders_clean.db is present next to app.py."
184
- )
185
  st.stop()
186
  except Exception as e:
187
  st.error(f"Could not open database at {CLEAN_DB_PATH!r}: {e}")
@@ -198,7 +286,6 @@ client = Groq(api_key=groq_api_key)
198
  # ================== SCHEMA (for LLM prompt only) ==================
199
  ORDERS_SCHEMA = """
200
  Table: orders_clean
201
-
202
  Columns:
203
  - order_id (TEXT)
204
  - cust_id (TEXT)
@@ -219,63 +306,38 @@ Columns:
219
  - is_canceled (INTEGER 0/1)
220
  """
221
 
222
- # =====================================================================
223
- # SQL SAFETY FIREWALL
224
- # =====================================================================
225
  def is_safe_sql(sql: str) -> bool:
226
  if not isinstance(sql, str):
227
  return False
228
  s = sql.lower().strip()
229
-
230
  if not s.startswith("select"):
231
  return False
232
-
233
- # block stacked queries like: SELECT ...; DROP ...
234
  if re.search(r";\s*\S", s):
235
  return False
236
-
237
  forbidden = ["drop", "delete", "update", "insert", "alter", "truncate", "create"]
238
  if any(k in s for k in forbidden):
239
  return False
240
-
241
  if any(tok in s for tok in ["--", "/*", "*/"]):
242
  return False
243
-
244
  return True
245
 
246
- # =====================================================================
247
- # run_sql_query()
248
- # =====================================================================
249
  def run_sql_query(sql: str) -> pd.DataFrame:
250
  if not isinstance(sql, str):
251
  return pd.DataFrame([{"message": "🚫 Invalid SQL type (expected string)."}])
252
  sql = sql.strip()
253
-
254
  if not is_safe_sql(sql):
255
  return pd.DataFrame([{"message": "🚫 Blocked unsafe or unsupported SQL."}])
256
-
257
  try:
258
  with sqlite3.connect(CLEAN_DB_PATH) as conn:
259
  df = pd.read_sql_query(sql, conn)
260
-
261
- # convert timestamp columns (if present)
262
  for col in ["order_time", "preparing_eta", "prepared_time", "delivery_eta", "delivery_time"]:
263
  if col in df.columns:
264
  df[col] = pd.to_datetime(df[col], errors="coerce")
265
-
266
  return df
267
  except Exception as e:
268
  return pd.DataFrame([{"message": f"⚠️ SQL execution error: {str(e)}"}])
269
 
270
- # =====================================================================
271
- # llm_to_sql()
272
- # =====================================================================
273
  def llm_to_sql(user_message: str) -> str:
274
- """
275
- Convert natural language to a safe SELECT query for orders_clean.
276
- - Fast-path when Order ID exists.
277
- - If order_id required but missing: return NEED_ORDER_ID.
278
- """
279
  msg = user_message or ""
280
  text = msg.lower().strip()
281
 
@@ -284,25 +346,12 @@ def llm_to_sql(user_message: str) -> str:
284
  sql = f"SELECT * FROM orders_clean WHERE LOWER(order_id) = LOWER('{oid}')"
285
  return sql if is_safe_sql(sql) else "SELECT 'Unable to answer safely.' AS message;"
286
 
287
- # If the message is order-specific without Order ID, force NEED_ORDER_ID
288
- must_have_order_id_intents = [
289
- "where is my order", "track my order", "track order", "order status", "delivery status",
290
- "when will my order arrive", "when will it arrive", "eta",
291
- "order delayed", "delay in delivery", "order not delivered", "late delivery",
292
- "order details", "show me the details", "details of my order", "details for my order",
293
- "cancel my order", "cancel order",
294
- "refund", "payment",
295
- "modify my order", "change my order", "postpone my order",
296
- "deliver at different address", "change address", "reschedule delivery",
297
- ]
298
- if any(p in text for p in must_have_order_id_intents) or needs_order_id(msg):
299
  return "SELECT 'NEED_ORDER_ID' AS message;"
300
 
301
  system_prompt = f"""
302
  You are an expert SQLite assistant for a food delivery company.
303
-
304
  You ONLY generate valid SQLite SELECT queries using this schema:
305
-
306
  {ORDERS_SCHEMA}
307
 
308
  RULES:
@@ -315,104 +364,48 @@ RULES:
315
  - If unsure:
316
  SELECT 'Unable to answer with available data.' AS message;
317
  """
318
-
319
  response = client.chat.completions.create(
320
  model="llama-3.3-70b-versatile",
321
  temperature=0.1,
322
- messages=[
323
- {"role": "system", "content": system_prompt},
324
- {"role": "user", "content": msg},
325
- ],
326
  )
327
-
328
  sql = (response.choices[0].message.content or "").strip()
329
-
330
- # strip ``` fences if any
331
  if sql.startswith("```"):
332
  sql = re.sub(r"^```sql", "", sql, flags=re.IGNORECASE).strip()
333
  sql = re.sub(r"^```", "", sql).strip()
334
  sql = sql.replace("```", "").strip()
335
-
336
- # Force FROM orders_clean
337
  sql = re.sub(r"\bfrom\s+\w+\b", "FROM orders_clean", sql, flags=re.IGNORECASE)
338
-
339
- # Normalize order_id filter
340
  sql = re.sub(
341
  r"where\s+order_id\s*=\s*'([^']+)'",
342
  r"WHERE LOWER(order_id) = LOWER('\1')",
343
  sql,
344
  flags=re.IGNORECASE,
345
  )
346
-
347
  return sql if is_safe_sql(sql) else "SELECT 'Unable to answer safely.' AS message;"
348
 
349
- # =====================================================================
350
- # analyze_sentiment_and_escalation()
351
- # =====================================================================
352
  def analyze_sentiment_and_escalation(user_message: str) -> dict:
353
  user_message = "" if user_message is None else str(user_message).strip()
354
-
355
  system_prompt = """
356
- You are a classifier for a food delivery chatbot.
357
-
358
- Return ONLY JSON (no extra text):
359
- {
360
- "sentiment": "calm" | "neutral" | "frustrated" | "angry",
361
- "escalate": true or false
362
- }
363
-
364
- Escalate=true when user is frustrated/angry, mentions repeated attempts, or demands urgent help.
365
- If uncertain: {"sentiment":"neutral","escalate":false}
366
  """
367
-
368
  response = client.chat.completions.create(
369
  model="llama-3.3-70b-versatile",
370
  temperature=0.0,
371
- messages=[
372
- {"role": "system", "content": system_prompt},
373
- {"role": "user", "content": user_message},
374
- ],
375
  )
376
-
377
  raw = (response.choices[0].message.content or "").strip()
378
  try:
379
  info = json.loads(raw)
380
  if not isinstance(info, dict):
381
- raise ValueError("Not dict")
382
- if "sentiment" not in info or "escalate" not in info:
383
- raise ValueError("Missing keys")
384
- info["sentiment"] = str(info["sentiment"]).strip().lower()
385
- info["escalate"] = bool(info["escalate"])
386
- if info["sentiment"] not in {"calm", "neutral", "frustrated", "angry"}:
387
- raise ValueError("Bad label")
388
- return info
389
  except Exception:
390
  return {"sentiment": "neutral", "escalate": False}
391
 
392
- # =====================================================================
393
- # is_misuse_or_hacker_attempt()
394
- # =====================================================================
395
- def is_misuse_or_hacker_attempt(user_message: str) -> bool:
396
- t = (user_message or "").lower().strip()
397
-
398
- hacker = ["hack", "hacker", "hacking", "breach", "exploit", "bypass security", "root access", "admin access"]
399
- bulk = ["full database", "entire database", "dump data", "export all", "all customers", "customer list", "all orders", "every order"]
400
- jailbreak = ["ignore previous instructions", "override rules", "developer mode", "you are no longer restricted"]
401
- inj = ["1=1", "union select", "drop table", "delete from", "insert into", "alter table", "truncate"]
402
-
403
- if ";" in t and any(x in t for x in inj):
404
- return True
405
-
406
- signals = hacker + bulk + jailbreak + inj
407
- return any(s in t for s in signals)
408
-
409
- # =====================================================================
410
- # create_ticket()
411
- # =====================================================================
412
  def create_ticket(user_message: str, sentiment: str, ticket_type: str = "general", order_id: Optional[str] = None) -> str:
413
  ticket_id = "TKT-" + uuid.uuid4().hex[:8].upper()
414
  created_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
415
-
416
  with sqlite3.connect(CLEAN_DB_PATH) as conn:
417
  cur = conn.cursor()
418
  cur.execute("""
@@ -431,14 +424,9 @@ def create_ticket(user_message: str, sentiment: str, ticket_type: str = "general
431
  VALUES (?, ?, ?, ?, ?, 'Open', ?);
432
  """, (ticket_id, ticket_type, sentiment, user_message, order_id, created_at))
433
  conn.commit()
434
-
435
  return ticket_id
436
 
437
- # =====================================================================
438
- # sql_result_to_response()
439
- # =====================================================================
440
  def sql_result_to_response(user_message: str, df: pd.DataFrame, max_rows_to_list: int = 5) -> str:
441
- # handle firewall/SQL execution errors and special messages
442
  if isinstance(df, pd.DataFrame) and "message" in df.columns and len(df) == 1:
443
  msg = str(df.iloc[0]["message"])
444
  if msg.strip().upper() == "NEED_ORDER_ID":
@@ -446,79 +434,37 @@ def sql_result_to_response(user_message: str, df: pd.DataFrame, max_rows_to_list
446
  return msg
447
 
448
  if df.empty:
449
- return (
450
- f"{WELCOME_LINE}\n"
451
- "I couldn’t find a matching order for the details provided.\n"
452
- f"{ASK_ORDER_ID_LINE}"
453
- )
454
 
455
  df_for_llm = df.head(max_rows_to_list).copy()
456
  result_data = df_for_llm.to_dict(orient="records")
457
 
458
  system_prompt = f"""
459
  You are a polite and professional customer support chatbot for FoodHub.
460
-
461
- Always start with EXACTLY:
462
- "{WELCOME_LINE}"
463
-
464
- VERY IMPORTANT POLICY:
465
- - If ANY matching record exists in the data I give you β†’ The order is CONFIRMED.
466
- - NEVER tell the user to re-check their order ID if matching rows exist.
467
- - NEVER say "order not found", "unable to locate order", or similar messages
468
- when there is at least one matching record.
469
- - Instead, confidently acknowledge the order and use available data.
470
-
471
- CRITICAL RULES:
472
- - NEVER invent or guess items, ETA, status, payment, or any order details.
473
- - Use ONLY the data provided in "Matching order data".
474
- - If information is missing, say it's unavailable in tracking.
475
-
476
- DELIVERED TEMPLATE (use when order_status_std indicates delivered OR is_delivered == 1):
477
- - Use this meaning and style (you may include the order id if available):
478
- "I've checked on your order, and I'm happy to inform you that it has been delivered βœ…. You should have received your order.
479
- If you have any further concerns or issues, please let me know. If you need anything else, I’m here to help! πŸ’›"
480
-
481
- If NOT delivered:
482
- - Provide the latest known status.
483
- - If delivery_time is missing but delivery_eta exists: share ETA.
484
- - If both delivery_time and delivery_eta missing: apologize for limited tracking.
485
-
486
- Keep it concise and helpful.
487
  """
488
-
489
- content = f"User question: {user_message}\n\nMatching order data (up to {max_rows_to_list} rows):\n{result_data}"
490
-
491
  resp = client.chat.completions.create(
492
  model="llama-3.3-70b-versatile",
493
  temperature=0.2,
494
- messages=[
495
- {"role": "system", "content": system_prompt},
496
- {"role": "user", "content": content},
497
- ],
498
  )
499
  return (resp.choices[0].message.content or "").strip()
500
 
501
- # =====================================================================
502
- # answer_user_question()
503
- # =====================================================================
504
  def answer_user_question(user_message: str) -> str:
505
- # Safety net: never answer order intents without Order ID
506
  if needs_order_id(user_message) and not extract_order_id(user_message):
507
  return f"{WELCOME_LINE}\n{ASK_ORDER_ID_LINE}"
508
-
509
  sql = llm_to_sql(user_message)
510
  df = run_sql_query(sql)
511
- return sql_result_to_response(user_message, df, max_rows_to_list=5)
512
 
513
- # =====================================================================
514
- # chatbot_response() – main orchestration (FIXED)
515
- # =====================================================================
516
  def chatbot_response(user_message: str) -> str:
517
  text = (user_message or "").strip()
518
  t = text.lower().strip()
519
  order_id = extract_order_id(text)
520
 
521
- # Greetings / thanks
522
  greetings = ["hi", "hello", "hey", "good morning", "good afternoon", "good evening"]
523
  thanks_words = ["thank you", "thanks", "thx", "thankyou"]
524
 
@@ -526,36 +472,17 @@ def chatbot_response(user_message: str) -> str:
526
  return f"{WELCOME_LINE}\n{ASK_ORDER_ID_LINE}"
527
 
528
  if any(w in t for w in thanks_words):
529
- return (
530
- "You’re most welcome! πŸ’›\n"
531
- "If you need help with an order, please share your Order ID (like `O12488`)."
532
- )
533
-
534
- # Hacking / misuse (polite privacy rejection)
535
- if is_misuse_or_hacker_attempt(text):
536
- return (
537
- f"{WELCOME_LINE}\n"
538
- "πŸ˜” Sorry, I can’t assist with that.\n"
539
- "For privacy and security reasons, I can only help with your own order using a valid Order ID. πŸ’›"
540
- )
541
 
542
- # Third-party privacy
543
  if is_third_party_order_request(text):
544
- return (
545
- f"{WELCOME_LINE}\n"
546
- "πŸ˜” I can’t help with someone else’s order details for privacy reasons.\n\n"
547
- "Please ask them to contact FoodHub Support directly, or have them share their **Order ID** themselves. πŸ’›"
548
- )
549
 
550
- # FIX 1: Escalation intent must be handled BEFORE any generic Order ID gating
551
  if _is_escalation_intent(text):
552
  senti = analyze_sentiment_and_escalation(text)
553
- ticket_id = create_ticket(
554
- user_message=text,
555
- sentiment=senti.get("sentiment", "frustrated"),
556
- ticket_type="escalation",
557
- order_id=order_id
558
- )
559
  return (
560
  f"{WELCOME_LINE}\n"
561
  "πŸ’› I’m really sorry you’ve had to follow up multiple times.\n\n"
@@ -564,7 +491,6 @@ def chatbot_response(user_message: str) -> str:
564
  f"{ASK_ORDER_ID_LINE if not order_id else 'If you need anything else, I’m here to help! πŸ’›'}"
565
  )
566
 
567
- # FIX: Payment / Refund intent should use empathetic ask (Response 2 style)
568
  if _is_payment_refund_intent(text) and not order_id:
569
  return (
570
  "πŸ’› I’m sorry you’re facing an issue with the payment or refund. I’ll help you with this right away.\n\n"
@@ -573,7 +499,6 @@ def chatbot_response(user_message: str) -> str:
573
  "If the payment was received, the refund will be processed within 48 hours."
574
  )
575
 
576
- # FIX 2: Cancel intent without Order ID should return the preferred message (not generic welcome+ask)
577
  if _is_cancel_intent(text) and not order_id:
578
  return (
579
  "πŸ’› I’m sorry to hear you’d like to cancel your order β€” I completely understand and I’m here to help. "
@@ -581,27 +506,28 @@ def chatbot_response(user_message: str) -> str:
581
  "process the cancellation for you?"
582
  )
583
 
584
- # Global rule: any order-related request without Order ID β†’ welcome + ask for Order ID
585
  if needs_order_id(text) and not order_id:
586
  return f"{WELCOME_LINE}\n{ASK_ORDER_ID_LINE}"
587
 
588
- # Normal path
589
  return answer_user_question(text)
590
 
591
- def chatbot(msg: str) -> str:
592
- return chatbot_response(msg)
593
-
594
  # ================== SESSION STATE ==================
595
  if "welcome_shown" not in st.session_state:
596
  st.session_state["welcome_shown"] = False
597
-
598
- # ================== UI ==================
 
 
 
 
 
 
599
  st.markdown(
600
  """
601
  <div class="foodhub-card">
602
- <div style="display:flex; align-items:flex-start; gap:12px;">
603
- <div style="font-size:32px;">πŸ•</div>
604
- <div style="flex:1%;">
605
  <div class="foodhub-header-title">FoodHub AI Support</div>
606
  <p class="foodhub-header-subtitle">
607
  Ask about order status, delivery updates, cancellations, or payment/refund issues.
@@ -622,39 +548,120 @@ if not st.session_state["welcome_shown"]:
622
  st.caption("Because great food deserves great service πŸ’›")
623
  st.session_state["welcome_shown"] = True
624
 
625
- st.markdown('<div class="quick-actions-title">Quick help options</div>', unsafe_allow_html=True)
626
- col1, col2 = st.columns(2)
627
- if col1.button("πŸ“ Track my order", use_container_width=True):
628
- st.session_state["chat_input"] = "Where is my order?"
629
- if col2.button("πŸ’³ Payment issue", use_container_width=True):
630
- st.session_state["chat_input"] = "I have an issue with my payment / refund."
631
-
632
- col3, col4 = st.columns(2)
633
- if col3.button("❌ Cancel order", use_container_width=True):
634
- st.session_state["chat_input"] = "I want to cancel my order."
635
- if col4.button("🧾 Order details", use_container_width=True):
636
- st.session_state["chat_input"] = "Show me the details for my order."
637
-
638
- query = st.text_input(
639
- "πŸ’¬ Type your question here:",
640
- key="chat_input",
641
- placeholder="e.g. Where is my order O12488?",
642
- )
643
-
644
- send_clicked = st.button("Send", use_container_width=True)
645
-
646
- if send_clicked:
647
- if query.strip():
648
- reply = chatbot(query.strip())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
649
 
650
- # UI adds πŸ€– prefix visually; reply itself should start with Welcome line (as requested)
651
- user_html = format_for_bubble(f"πŸ’¬ {query.strip()}")
652
- bot_html = format_for_bubble(f"πŸ€– {reply}")
 
 
 
 
 
 
 
653
 
654
- st.markdown('<div class="user-label">You</div>', unsafe_allow_html=True)
655
- st.markdown(f"<div class='bot-bubble' style='background:#fff;'>{user_html}</div>", unsafe_allow_html=True)
 
 
656
 
657
- st.markdown('<div class="bot-label">FoodHub Assistant</div>', unsafe_allow_html=True)
658
- st.markdown(f"<div class='bot-bubble'>{bot_html}</div>", unsafe_allow_html=True)
659
- else:
660
  st.warning("Please enter a message to continue.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  st.markdown(
19
  """
20
  <style>
21
+ /* Keep app compact and centered */
22
+ .main { max-width: 980px; margin: 0 auto; }
23
+
24
+ /* Banner */
25
  .foodhub-card {
26
  background: linear-gradient(135deg, #fff3e0, #ffe0cc);
27
+ padding: 12px 14px;
28
+ border-radius: 16px;
29
  border: 1px solid #f5c28c;
30
+ margin-bottom: 10px;
31
  }
32
  .foodhub-header-title {
33
+ margin-bottom: 2px;
34
  color: #e65c2b;
35
+ font-size: 20px;
36
  font-weight: 800;
37
+ line-height: 1.1;
38
  }
39
+ .foodhub-header-subtitle { margin: 0; color: #444; font-size: 12px; line-height: 1.25; }
40
  .foodhub-assistant-pill {
41
  display: inline-flex; align-items: center; gap: 6px;
42
  background: #ffffff; border-radius: 999px;
43
+ padding: 4px 8px; font-size: 10.5px; color: #555;
44
+ border: 1px solid #ffd3a3; margin-top: 6px;
45
  }
46
  .status-dot { width: 8px; height: 8px; border-radius: 50%; background: #2ecc71; }
47
+
48
+ .quick-actions-title {
49
+ font-size: 12px;
50
+ font-weight: 700;
51
+ color: #555;
52
+ margin-top: 6px;
53
+ margin-bottom: 6px;
54
+ }
55
+
56
+ /* Make Streamlit buttons compact so 4 columns fit nicely */
57
+ div.stButton > button {
58
+ padding: 0.25rem 0.35rem !important;
59
+ font-size: 0.80rem !important;
60
+ height: 34px !important;
61
+ border-radius: 12px !important;
62
+ white-space: nowrap !important;
63
+ }
64
+
65
+ /* Chat scroll container */
66
+ .chat-shell {
67
+ border: 1px solid rgba(0,0,0,0.10);
68
+ border-radius: 14px;
69
+ background: #ffffff;
70
+ padding: 8px;
71
+ margin-top: 10px;
72
+ margin-bottom: 10px;
73
+ }
74
+ .chat-window {
75
+ max-height: 280px; /* adjust if you want taller/shorter */
76
+ overflow-y: auto;
77
+ padding: 2px 6px;
78
+ }
79
+
80
+ /* Bubbles */
81
  .bot-bubble {
82
  background: #eef7f2; border-radius: 12px;
83
  padding: 10px 12px; font-size: 14px;
84
+ margin-top: 6px; line-height: 1.35;
85
  }
86
+ .user-bubble {
87
+ background: #ffffff; border-radius: 12px;
88
+ padding: 10px 12px; font-size: 14px;
89
+ margin-top: 6px; line-height: 1.35;
90
+ border: 1px solid rgba(0,0,0,0.06);
91
+ }
92
+ .user-label, .bot-label { font-size: 12px; color: #777; margin-top: 10px; margin-bottom: 2px; }
93
+
94
  code {
95
  background: rgba(0,0,0,0.06);
96
  padding: 2px 6px;
97
  border-radius: 8px;
98
  font-size: 13px;
99
  }
100
+
101
+ /* Mobile-friendly: reduce chat height a bit */
102
+ @media (max-width: 720px) {
103
+ .chat-window { max-height: 240px; }
104
+ div.stButton > button { font-size: 0.76rem !important; height: 32px !important; }
105
+ }
106
  </style>
107
  """,
108
  unsafe_allow_html=True,
 
116
  ORDER_ID_REGEX = r"\b[oO]\d{3,}\b"
117
 
118
  def format_for_bubble(text: str) -> str:
119
+ """Safely render markdown-like **bold** and `code` inside HTML bubbles."""
120
  t = html.escape(str(text or ""))
121
  t = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", t) # **bold**
122
  t = re.sub(r"`([^`]+)`", r"<code>\1</code>", t) # `code`
 
128
  return m.group(0).upper() if m else None
129
 
130
  def needs_order_id(message: str) -> bool:
 
 
 
131
  t = (message or "").lower().strip()
 
 
132
  greetings = {"hi", "hello", "hey", "good morning", "good afternoon", "good evening"}
133
  thanks = {"thanks", "thank you", "thx", "thankyou"}
134
  if t in greetings or t in thanks:
135
  return False
 
 
136
  if extract_order_id(message):
137
  return False
138
 
 
139
  phrases = [
140
  "where is my order", "track my order", "track order", "order status", "delivery status",
141
+ "status of my order", "when will my order arrive", "eta",
142
  "order delayed", "delay in delivery", "order not delivered", "late delivery",
143
+ "order details", "show me the details",
 
 
144
  "cancel my order", "cancel order",
145
  "refund", "payment issue", "payment problem", "payment failed", "charged", "transaction",
 
146
  "modify my order", "change my order", "postpone my order", "deliver at different address",
147
+ "order never arrived", "wrong or missing items", "food damage", "quality issue",
148
+ "food safety", "delivery safety", "charged more than once",
149
+ "another order issue"
150
  ]
151
  if any(p in t for p in phrases):
152
  return True
153
 
154
+ if "order" in t and any(k in t for k in [
155
+ "status", "track", "details", "cancel", "refund", "payment", "where", "when",
156
+ "modify", "change", "postpone", "address", "deliver",
157
+ "missing", "wrong", "quality", "damage", "safety", "arrived"
158
+ ]):
159
  return True
160
 
161
  return False
162
 
163
  def is_third_party_order_request(message: str) -> bool:
 
 
 
164
  t = (message or "").lower()
165
  third_party = [
166
  "my friend", "friend's", "friends'", "my brother", "my sister",
 
193
  "multiple follow-ups",
194
  "i have followed multiple times",
195
  "i have followed up multiple times",
196
+ "i want an update on my refund",
197
+ "refund update",
198
+ "update on my refund",
199
+ "refund status"
200
  ]
201
  return any(k in t for k in escalation_keywords)
202
 
 
205
  keywords = [
206
  "payment", "refund", "money back", "charged", "chargeback",
207
  "transaction", "payment failed", "payment issue", "payment problem",
208
+ "upi", "card", "debit", "credit", "wallet",
209
+ "charged more than once", "double charged", "charged twice"
210
  ]
211
  return any(k in t for k in keywords)
212
 
213
+ # ================== QUICK BUTTON RESPONSE MAP (EXACT TEXT YOU APPROVED) ==================
214
+ QUICK_BUTTON_EXACT_REPLIES = {
215
+ "Order never arrived.": (
216
+ "Welcome to FoodHub Support! πŸ‘‹\n"
217
+ "I’m really sorry to hear that your order hasn’t arrived β€” I understand how frustrating that can be. πŸ’›\n\n"
218
+ "To help you right away, could you please share your Order ID (for example: O12488)?\n"
219
+ "Once I have the Order ID, I’ll quickly check the delivery status, identify what went wrong, and help you with the next steps, "
220
+ "including a replacement or refund if applicable."
221
+ ),
222
+ "Food damage or quality issue.": (
223
+ "Welcome to FoodHub Support! πŸ‘‹\n"
224
+ "I’m really sorry to hear that you’re facing a food damage or quality issue β€” that’s not the experience we want you to have. πŸ’›\n\n"
225
+ "Please share your Order ID (for example: O12488) so I can review the order details and assist you with a replacement or refund "
226
+ "as per our quality policy."
227
+ ),
228
+ "My card was charged more than once.": (
229
+ "Welcome to FoodHub Support! πŸ‘‹\n"
230
+ "I understand how concerning it can be to see your card charged more than once β€” I’m here to help. πŸ’›\n\n"
231
+ "Could you please share your Order ID (for example: O12488)?\n"
232
+ "Once I have it, I’ll verify the payment records and, if a duplicate charge is confirmed, initiate a refund "
233
+ "(usually processed within 48 hours)."
234
+ ),
235
+ "Wrong or missing items.": (
236
+ "Welcome to FoodHub Support! πŸ‘‹\n"
237
+ "I’m sorry to hear that your order had wrong or missing items. Thank you for letting us know. πŸ’›\n\n"
238
+ "Please share your Order ID (for example: O12488) so I can review the order and help arrange a replacement or refund as per our policy."
239
+ ),
240
+ "Another order issue.": (
241
+ "Welcome to FoodHub Support! πŸ‘‹\n"
242
+ "I’m here to help with your order-related concern. πŸ’›\n\n"
243
+ "It looks like you may have more than one order associated with your account. To assist you correctly, please share the Order ID "
244
+ "(for example: O12488) for the order you’re referring to.\n\n"
245
+ "Once I have the correct Order ID, I’ll review the details and help resolve the issue as quickly as possible."
246
+ ),
247
+ "Delivery or food safety issue.": (
248
+ "Welcome to FoodHub Support! πŸ‘‹\n"
249
+ "I’m really sorry to hear that you’re facing a delivery or food safety issue. Your safety and satisfaction are extremely important to us. πŸ’›\n\n"
250
+ "Please share your Order ID (for example: O12488).\n"
251
+ "Once I have it, I’ll review the delivery handling and food safety details and assist you with a replacement or refund as per our safety policy."
252
+ ),
253
+ "I want an update on my refund.": (
254
+ "Welcome to FoodHub Support! πŸ‘‹\n"
255
+ "I understand you’re looking for an update on your refund, and I’ll be happy to check that for you. πŸ’›\n\n"
256
+ "Please share your Order ID (for example: O12488).\n"
257
+ "Once I have it, I’ll confirm whether the refund has been initiated, the refund amount, and the expected timeline for it to reflect in your account "
258
+ "(usually within 48 hours, depending on your bank)."
259
+ ),
260
+ }
261
+
262
  # ================== DB CONFIG (ALIGNED WITH YOUR NOTEBOOK) ==================
263
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
264
+ CLEAN_DB_PATH = os.path.join(BASE_DIR, "orders_clean.db")
265
 
 
266
  try:
267
  with sqlite3.connect(CLEAN_DB_PATH) as _conn:
268
  _cur = _conn.cursor()
269
  _cur.execute("SELECT name FROM sqlite_master WHERE type='table';")
270
  _tables = [r[0] for r in _cur.fetchall()]
271
  if "orders_clean" not in _tables:
272
+ st.error("Table 'orders_clean' not found in orders_clean.db (must be next to app.py).")
 
 
 
 
 
273
  st.stop()
274
  except Exception as e:
275
  st.error(f"Could not open database at {CLEAN_DB_PATH!r}: {e}")
 
286
  # ================== SCHEMA (for LLM prompt only) ==================
287
  ORDERS_SCHEMA = """
288
  Table: orders_clean
 
289
  Columns:
290
  - order_id (TEXT)
291
  - cust_id (TEXT)
 
306
  - is_canceled (INTEGER 0/1)
307
  """
308
 
 
 
 
309
  def is_safe_sql(sql: str) -> bool:
310
  if not isinstance(sql, str):
311
  return False
312
  s = sql.lower().strip()
 
313
  if not s.startswith("select"):
314
  return False
 
 
315
  if re.search(r";\s*\S", s):
316
  return False
 
317
  forbidden = ["drop", "delete", "update", "insert", "alter", "truncate", "create"]
318
  if any(k in s for k in forbidden):
319
  return False
 
320
  if any(tok in s for tok in ["--", "/*", "*/"]):
321
  return False
 
322
  return True
323
 
 
 
 
324
  def run_sql_query(sql: str) -> pd.DataFrame:
325
  if not isinstance(sql, str):
326
  return pd.DataFrame([{"message": "🚫 Invalid SQL type (expected string)."}])
327
  sql = sql.strip()
 
328
  if not is_safe_sql(sql):
329
  return pd.DataFrame([{"message": "🚫 Blocked unsafe or unsupported SQL."}])
 
330
  try:
331
  with sqlite3.connect(CLEAN_DB_PATH) as conn:
332
  df = pd.read_sql_query(sql, conn)
 
 
333
  for col in ["order_time", "preparing_eta", "prepared_time", "delivery_eta", "delivery_time"]:
334
  if col in df.columns:
335
  df[col] = pd.to_datetime(df[col], errors="coerce")
 
336
  return df
337
  except Exception as e:
338
  return pd.DataFrame([{"message": f"⚠️ SQL execution error: {str(e)}"}])
339
 
 
 
 
340
  def llm_to_sql(user_message: str) -> str:
 
 
 
 
 
341
  msg = user_message or ""
342
  text = msg.lower().strip()
343
 
 
346
  sql = f"SELECT * FROM orders_clean WHERE LOWER(order_id) = LOWER('{oid}')"
347
  return sql if is_safe_sql(sql) else "SELECT 'Unable to answer safely.' AS message;"
348
 
349
+ if needs_order_id(msg):
 
 
 
 
 
 
 
 
 
 
 
350
  return "SELECT 'NEED_ORDER_ID' AS message;"
351
 
352
  system_prompt = f"""
353
  You are an expert SQLite assistant for a food delivery company.
 
354
  You ONLY generate valid SQLite SELECT queries using this schema:
 
355
  {ORDERS_SCHEMA}
356
 
357
  RULES:
 
364
  - If unsure:
365
  SELECT 'Unable to answer with available data.' AS message;
366
  """
 
367
  response = client.chat.completions.create(
368
  model="llama-3.3-70b-versatile",
369
  temperature=0.1,
370
+ messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": msg}],
 
 
 
371
  )
 
372
  sql = (response.choices[0].message.content or "").strip()
 
 
373
  if sql.startswith("```"):
374
  sql = re.sub(r"^```sql", "", sql, flags=re.IGNORECASE).strip()
375
  sql = re.sub(r"^```", "", sql).strip()
376
  sql = sql.replace("```", "").strip()
 
 
377
  sql = re.sub(r"\bfrom\s+\w+\b", "FROM orders_clean", sql, flags=re.IGNORECASE)
 
 
378
  sql = re.sub(
379
  r"where\s+order_id\s*=\s*'([^']+)'",
380
  r"WHERE LOWER(order_id) = LOWER('\1')",
381
  sql,
382
  flags=re.IGNORECASE,
383
  )
 
384
  return sql if is_safe_sql(sql) else "SELECT 'Unable to answer safely.' AS message;"
385
 
 
 
 
386
  def analyze_sentiment_and_escalation(user_message: str) -> dict:
387
  user_message = "" if user_message is None else str(user_message).strip()
 
388
  system_prompt = """
389
+ Return ONLY JSON:
390
+ {"sentiment":"calm"|"neutral"|"frustrated"|"angry","escalate":true|false}
 
 
 
 
 
 
 
 
391
  """
 
392
  response = client.chat.completions.create(
393
  model="llama-3.3-70b-versatile",
394
  temperature=0.0,
395
+ messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}],
 
 
 
396
  )
 
397
  raw = (response.choices[0].message.content or "").strip()
398
  try:
399
  info = json.loads(raw)
400
  if not isinstance(info, dict):
401
+ raise ValueError
402
+ return {"sentiment": str(info.get("sentiment", "neutral")).strip().lower(), "escalate": bool(info.get("escalate", False))}
 
 
 
 
 
 
403
  except Exception:
404
  return {"sentiment": "neutral", "escalate": False}
405
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  def create_ticket(user_message: str, sentiment: str, ticket_type: str = "general", order_id: Optional[str] = None) -> str:
407
  ticket_id = "TKT-" + uuid.uuid4().hex[:8].upper()
408
  created_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
 
409
  with sqlite3.connect(CLEAN_DB_PATH) as conn:
410
  cur = conn.cursor()
411
  cur.execute("""
 
424
  VALUES (?, ?, ?, ?, ?, 'Open', ?);
425
  """, (ticket_id, ticket_type, sentiment, user_message, order_id, created_at))
426
  conn.commit()
 
427
  return ticket_id
428
 
 
 
 
429
  def sql_result_to_response(user_message: str, df: pd.DataFrame, max_rows_to_list: int = 5) -> str:
 
430
  if isinstance(df, pd.DataFrame) and "message" in df.columns and len(df) == 1:
431
  msg = str(df.iloc[0]["message"])
432
  if msg.strip().upper() == "NEED_ORDER_ID":
 
434
  return msg
435
 
436
  if df.empty:
437
+ return f"{WELCOME_LINE}\nI couldn’t find a matching order.\n{ASK_ORDER_ID_LINE}"
 
 
 
 
438
 
439
  df_for_llm = df.head(max_rows_to_list).copy()
440
  result_data = df_for_llm.to_dict(orient="records")
441
 
442
  system_prompt = f"""
443
  You are a polite and professional customer support chatbot for FoodHub.
444
+ Always start with EXACTLY: "{WELCOME_LINE}"
445
+ Use ONLY the data given.
446
+ End warmly.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
  """
448
+ content = f"User question: {user_message}\n\nMatching order data:\n{result_data}"
 
 
449
  resp = client.chat.completions.create(
450
  model="llama-3.3-70b-versatile",
451
  temperature=0.2,
452
+ messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": content}],
 
 
 
453
  )
454
  return (resp.choices[0].message.content or "").strip()
455
 
 
 
 
456
  def answer_user_question(user_message: str) -> str:
 
457
  if needs_order_id(user_message) and not extract_order_id(user_message):
458
  return f"{WELCOME_LINE}\n{ASK_ORDER_ID_LINE}"
 
459
  sql = llm_to_sql(user_message)
460
  df = run_sql_query(sql)
461
+ return sql_result_to_response(user_message, df)
462
 
 
 
 
463
  def chatbot_response(user_message: str) -> str:
464
  text = (user_message or "").strip()
465
  t = text.lower().strip()
466
  order_id = extract_order_id(text)
467
 
 
468
  greetings = ["hi", "hello", "hey", "good morning", "good afternoon", "good evening"]
469
  thanks_words = ["thank you", "thanks", "thx", "thankyou"]
470
 
 
472
  return f"{WELCOME_LINE}\n{ASK_ORDER_ID_LINE}"
473
 
474
  if any(w in t for w in thanks_words):
475
+ return "You’re most welcome! πŸ’›\nIf you need help with an order, please share your Order ID (like `O12488`)."
 
 
 
 
 
 
 
 
 
 
 
476
 
 
477
  if is_third_party_order_request(text):
478
+ return f"{WELCOME_LINE}\nπŸ˜” I can’t help with someone else’s order details for privacy reasons.\n\nPlease ask them to contact FoodHub Support directly. πŸ’›"
479
+
480
+ if "hack" in t or "hacker" in t or "all orders" in t or "dump" in t or "database" in t:
481
+ return f"{WELCOME_LINE}\nπŸ˜” Sorry, I can’t assist with that.\nFor privacy and security reasons, I can only help with your own order using a valid Order ID. πŸ’›"
 
482
 
 
483
  if _is_escalation_intent(text):
484
  senti = analyze_sentiment_and_escalation(text)
485
+ ticket_id = create_ticket(text, senti.get("sentiment", "frustrated"), "escalation", order_id)
 
 
 
 
 
486
  return (
487
  f"{WELCOME_LINE}\n"
488
  "πŸ’› I’m really sorry you’ve had to follow up multiple times.\n\n"
 
491
  f"{ASK_ORDER_ID_LINE if not order_id else 'If you need anything else, I’m here to help! πŸ’›'}"
492
  )
493
 
 
494
  if _is_payment_refund_intent(text) and not order_id:
495
  return (
496
  "πŸ’› I’m sorry you’re facing an issue with the payment or refund. I’ll help you with this right away.\n\n"
 
499
  "If the payment was received, the refund will be processed within 48 hours."
500
  )
501
 
 
502
  if _is_cancel_intent(text) and not order_id:
503
  return (
504
  "πŸ’› I’m sorry to hear you’d like to cancel your order β€” I completely understand and I’m here to help. "
 
506
  "process the cancellation for you?"
507
  )
508
 
 
509
  if needs_order_id(text) and not order_id:
510
  return f"{WELCOME_LINE}\n{ASK_ORDER_ID_LINE}"
511
 
 
512
  return answer_user_question(text)
513
 
 
 
 
514
  # ================== SESSION STATE ==================
515
  if "welcome_shown" not in st.session_state:
516
  st.session_state["welcome_shown"] = False
517
+ if "messages" not in st.session_state:
518
+ st.session_state["messages"] = []
519
+ if "pending_message" not in st.session_state:
520
+ st.session_state["pending_message"] = None
521
+ if "pending_quick_exact" not in st.session_state:
522
+ st.session_state["pending_quick_exact"] = None
523
+
524
+ # ================== UI HEADER ==================
525
  st.markdown(
526
  """
527
  <div class="foodhub-card">
528
+ <div style="display:flex; align-items:flex-start; gap:10px;">
529
+ <div style="font-size:28px; line-height:1;">πŸ•</div>
530
+ <div style="flex:1;">
531
  <div class="foodhub-header-title">FoodHub AI Support</div>
532
  <p class="foodhub-header-subtitle">
533
  Ask about order status, delivery updates, cancellations, or payment/refund issues.
 
548
  st.caption("Because great food deserves great service πŸ’›")
549
  st.session_state["welcome_shown"] = True
550
 
551
+ # ================== QUICK BUTTONS (3 x 4 GRID) ==================
552
+ st.markdown('<div class="quick-actions-title">Quick help</div>', unsafe_allow_html=True)
553
+
554
+ r1 = st.columns(4)
555
+ if r1[0].button("πŸ“ Track", use_container_width=True):
556
+ st.session_state["pending_message"] = "Where is my order?"
557
+ if r1[1].button("πŸ’³ Payment", use_container_width=True):
558
+ st.session_state["pending_message"] = "I have an issue with my payment / refund."
559
+ if r1[2].button("❌ Cancel", use_container_width=True):
560
+ st.session_state["pending_message"] = "I want to cancel my order."
561
+ if r1[3].button("🧾 Details", use_container_width=True):
562
+ st.session_state["pending_message"] = "Show me the details for my order."
563
+
564
+ r2 = st.columns(4)
565
+ if r2[0].button("πŸ“¦ Not arrived", use_container_width=True):
566
+ st.session_state["pending_quick_exact"] = "Order never arrived."
567
+ if r2[1].button("🧾 Missing", use_container_width=True):
568
+ st.session_state["pending_quick_exact"] = "Wrong or missing items."
569
+ if r2[2].button("πŸ₯‘ Damage", use_container_width=True):
570
+ st.session_state["pending_quick_exact"] = "Food damage or quality issue."
571
+ if r2[3].button("πŸ›‘οΈ Safety", use_container_width=True):
572
+ st.session_state["pending_quick_exact"] = "Delivery or food safety issue."
573
+
574
+ r3 = st.columns(4)
575
+ if r3[0].button("πŸ’³ Charged 2x", use_container_width=True):
576
+ st.session_state["pending_quick_exact"] = "My card was charged more than once."
577
+ if r3[1].button("πŸ” Refund", use_container_width=True):
578
+ st.session_state["pending_quick_exact"] = "I want an update on my refund."
579
+ if r3[2].button("πŸ§‘β€πŸ’» Follow-ups", use_container_width=True):
580
+ st.session_state["pending_message"] = "I have followed multiple times about my order."
581
+ if r3[3].button("❓ Other", use_container_width=True):
582
+ st.session_state["pending_quick_exact"] = "Another order issue."
583
+
584
+ st.markdown("---")
585
+
586
+ # ================== CHAT HISTORY (SCROLLABLE CONTAINER ABOVE INPUT) ==================
587
+ def render_chat_history_inside_scroll():
588
+ parts = []
589
+ for m in st.session_state["messages"]:
590
+ if m["role"] == "user":
591
+ parts.append(
592
+ f"<div class='user-label'>You</div>"
593
+ f"<div class='user-bubble'>{format_for_bubble('πŸ’¬ ' + m['content'])}</div>"
594
+ )
595
+ else:
596
+ parts.append(
597
+ f"<div class='bot-label'>FoodHub Assistant</div>"
598
+ f"<div class='bot-bubble'>{format_for_bubble('πŸ€– ' + m['content'])}</div>"
599
+ )
600
+
601
+ # auto-scroll to bottom (best-effort)
602
+ parts.append(
603
+ """
604
+ <script>
605
+ const shells = window.parent.document.querySelectorAll('.chat-window');
606
+ if (shells && shells.length) {
607
+ const w = shells[shells.length - 1];
608
+ w.scrollTop = w.scrollHeight;
609
+ }
610
+ </script>
611
+ """
612
+ )
613
 
614
+ st.markdown(
615
+ f"""
616
+ <div class="chat-shell">
617
+ <div class="chat-window">
618
+ {''.join(parts) if parts else "<div style='color:#777; font-size:12px;'>Start by typing a message or using the quick buttons above.</div>"}
619
+ </div>
620
+ </div>
621
+ """,
622
+ unsafe_allow_html=True,
623
+ )
624
 
625
+ # ================== SEND LOGIC ==================
626
+ def append_user_and_bot(user_text: str, bot_text: str):
627
+ st.session_state["messages"].append({"role": "user", "content": user_text})
628
+ st.session_state["messages"].append({"role": "assistant", "content": bot_text})
629
 
630
+ def send_message(user_text: str):
631
+ user_text = (user_text or "").strip()
632
+ if not user_text:
633
  st.warning("Please enter a message to continue.")
634
+ return
635
+ st.session_state["messages"].append({"role": "user", "content": user_text})
636
+ reply = chatbot_response(user_text)
637
+ st.session_state["messages"].append({"role": "assistant", "content": reply})
638
+
639
+ # Process quick clicks BEFORE rendering the chat (so user sees result instantly)
640
+ if st.session_state.get("pending_quick_exact"):
641
+ q = st.session_state["pending_quick_exact"]
642
+ st.session_state["pending_quick_exact"] = None
643
+ exact = QUICK_BUTTON_EXACT_REPLIES.get(q, f"{WELCOME_LINE}\n{ASK_ORDER_ID_LINE}")
644
+ append_user_and_bot(q, exact)
645
+ st.rerun()
646
+
647
+ if st.session_state.get("pending_message"):
648
+ pm = st.session_state["pending_message"]
649
+ st.session_state["pending_message"] = None
650
+ send_message(pm)
651
+ st.rerun()
652
+
653
+ # Render scrollable chat history
654
+ render_chat_history_inside_scroll()
655
+
656
+ # ================== INPUT (FIXED BELOW CHAT) ==================
657
+ with st.form("chat_form", clear_on_submit=True):
658
+ query = st.text_input(
659
+ "πŸ’¬ Type your question here:",
660
+ key="chat_input",
661
+ placeholder="e.g. Where is my order O12488?",
662
+ )
663
+ send_clicked = st.form_submit_button("Send", use_container_width=True)
664
+
665
+ if send_clicked:
666
+ send_message(query)
667
+ st.rerun()