Nischal Subedi commited on
Commit
badac92
·
1 Parent(s): 03254f3
Files changed (1) hide show
  1. app.py +238 -206
app.py CHANGED
@@ -12,12 +12,14 @@ try:
12
  except ImportError:
13
  print("Error: Could not import VectorDatabase from vector_db.py.")
14
  print("Please ensure vector_db.py exists in the same directory and is correctly defined.")
 
15
  exit(1)
16
 
17
  try:
18
  from langchain_openai import ChatOpenAI
19
  except ImportError:
20
  print("Error: langchain-openai not found. Please install it: pip install langchain-openai")
 
21
  exit(1)
22
 
23
  from langchain.prompts import PromptTemplate
@@ -35,7 +37,7 @@ logging.basicConfig(
35
  format='%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
36
  )
37
 
38
- # --- RAGSystem Class (Processing Logic - KEPT INTACT) ---
39
  class RAGSystem:
40
  def __init__(self, vector_db: Optional[VectorDatabase] = None):
41
  logging.info("Initializing RAGSystem")
@@ -146,7 +148,7 @@ Answer:"""
146
  context_parts.append(f"**Source: State Summary (State: {state_label})**\n{state_doc_content}")
147
 
148
  if context_parts:
149
- context = "\n\n---\n\n". தலைமையில்".join(context_parts)
150
  logging.info(f"Constructed context with {len(context_parts)} parts. Length: {len(context)} chars.")
151
  try:
152
  statutes_from_context = self.extract_statutes(context)
@@ -171,7 +173,7 @@ Answer:"""
171
 
172
  if not answer_text:
173
  logging.warning("LLM returned an empty answer.")
174
- answer_text = "<div class='error-message'>The AI model returned an empty response. This might be due to the query, context limitations, or temporary issues. Please try rephrasing your question or try again later.</div>"
175
  else:
176
  logging.info("LLM generated answer successfully.")
177
 
@@ -194,7 +196,7 @@ Answer:"""
194
  error_message = "Error: The request to the AI model timed out. The service might be busy."
195
  details = "Please try again in a few moments."
196
 
197
- formatted_error = f"<div class='error-message'>{error_message}</div>"
198
  if details:
199
  formatted_error += f"<div class='error-details'>{details}</div>"
200
 
@@ -222,6 +224,7 @@ Answer:"""
222
  raise FileNotFoundError(f"PDF file not found: {pdf_path}")
223
  try:
224
  logging.info(f"Attempting to load/verify data from PDF: {pdf_path}")
 
225
  num_states_processed = self.vector_db.process_and_load_pdf(pdf_path)
226
  doc_count = self.vector_db.document_collection.count()
227
  state_count = self.vector_db.state_collection.count()
@@ -239,35 +242,40 @@ Answer:"""
239
  logging.error(f"Failed to load or process PDF '{pdf_path}': {str(e)}", exc_info=True)
240
  raise RuntimeError(f"Failed to process PDF '{pdf_path}': {e}") from e
241
 
242
- # --- GRADIO INTERFACE (PROFESSIONAL DESIGN) ---
243
  def gradio_interface(self):
244
  def query_interface_wrapper(api_key: str, query: str, state: str) -> str:
 
245
  if not api_key or not api_key.strip() or not api_key.startswith("sk-"):
246
- return "<div class='error-message'>Please provide a valid OpenAI API key (starting with 'sk-'). <a href='https://platform.openai.com/api-keys' target='_blank'>Get one free from OpenAI</a>.</div>"
247
  if not state or state == "Select a state..." or "Error" in state:
248
- return "<div class='error-message'>Please select a valid state from the dropdown.</div>"
249
  if not query or not query.strip():
250
- return "<div class='error-message'>Please enter your question in the text box.</div>"
251
 
 
252
  result = self.process_query(query=query, state=state, openai_api_key=api_key)
253
- answer = result.get("answer", "<div class='error-message'>An unexpected error occurred.</div>")
254
 
 
255
  if "<div class='error-message'>" in answer:
 
256
  return answer
257
  else:
258
- formatted_response_content = gr.Markdown.format(
259
- f"<div class='response-header'><span class='response-icon'>📜</span>Response for {state}</div><hr class='divider'>{answer}"
260
- )
261
  return f"<div class='animated-output-content'>{formatted_response_content}</div>"
262
 
263
  try:
264
  available_states_list = self.get_states()
 
265
  dropdown_choices = ["Select a state..."] + (available_states_list if available_states_list and "Error" not in available_states_list[0] else ["Error: States unavailable"])
266
- initial_value = dropdown_choices[0]
267
- except Exception:
268
  dropdown_choices = ["Error: Critical failure loading states"]
269
  initial_value = dropdown_choices[0]
270
 
 
271
  example_queries_base = [
272
  ["What are the rules for security deposit returns?", "California"],
273
  ["Can a landlord enter my apartment without notice?", "New York"],
@@ -278,303 +286,313 @@ Answer:"""
278
  example_queries = []
279
  if available_states_list and "Error" not in available_states_list[0] and len(available_states_list) > 0:
280
  loaded_states_set = set(available_states_list)
 
281
  example_queries = [ex for ex in example_queries_base if ex[1] in loaded_states_set]
 
282
  if not example_queries:
 
283
  example_queries.append(["What basic rights do tenants have?", available_states_list[0] if available_states_list else "California"])
284
- else:
285
  example_queries.append(["What basic rights do tenants have?", "California"])
286
 
287
 
288
- # Custom CSS for a professional, clean, light, dynamic dashboard
289
  custom_css = """
290
  /* Import legible fonts from Google Fonts */
291
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Poppins:wght@600;700;800&display=swap');
292
 
293
- /* Root variables for consistent theming */
294
  :root {
295
- --primary-color: hsl(210, 70%, 45%); /* Professional deep blue */
296
- --primary-hover: hsl(210, 70%, 35%);
297
- --secondary-accent-color: hsl(40, 80%, 55%); /* Warm amber/gold for highlights */
298
- --secondary-accent-hover: hsl(40, 80%, 45%);
299
-
300
- --background-app: hsl(200, 20%, 96%); /* Very light blue-gray for overall background */
301
- --background-header: linear-gradient(135deg, hsl(200, 40%, 90%) 0%, hsl(210, 40%, 85%) 100%); /* Light blue gradient for header */
302
- --background-card: hsl(0, 0%, 100%); /* Pure white for content cards */
303
- --background-input: hsl(0, 0%, 100%); /* Pure white for input/output elements */
304
-
305
- --text-dark: hsl(210, 20%, 20%); /* Dark charcoal for primary text */
306
- --text-muted: hsl(210, 10%, 45%); /* Medium gray for secondary info/placeholders */
307
- --text-light: hsl(0, 0%, 100%); /* White for text on colored backgrounds */
308
-
309
- --border-card: hsl(210, 10%, 88%); /* Very light gray border for cards */
310
- --border-input: hsl(210, 10%, 80%); /* Slightly darker gray for input borders */
311
- --border-focus: var(--primary-color); /* Primary color for input focus */
312
-
313
  --shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
314
- --shadow-md: 0 4px 10px rgba(0,0,0,0.15);
315
- --shadow-lg: 0 8px 20px rgba(0,0,0,0.25);
316
-
317
- --error-bg: hsl(0, 80%, 95%);
318
- --error-border: hsl(0, 70%, 80%);
319
- --error-text: hsl(0, 70%, 40%);
320
-
321
- --button-secondary-bg: hsl(210, 10%, 90%); /* Light gray for secondary button */
322
- --button-secondary-text: var(--text-dark); /* Dark text for secondary button */
323
  }
324
 
325
- /* Base container and typography */
326
- body {
327
- background-color: var(--background-app) !important;
328
- }
 
 
 
 
 
 
 
 
 
 
 
329
  .gradio-container {
330
- max-width: 900px !important;
331
- margin: 0 auto !important;
332
  padding: 1.5rem !important;
333
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
334
- background-color: var(--background-app) !important;
335
- box-shadow: none !important;
336
- border-radius: 16px !important;
 
 
 
337
  }
338
 
339
- /* Header Styling (distinct background) */
340
  .app-header-wrapper {
341
- background: var(--background-header) !important;
342
- border: 2px solid var(--border-card) !important;
343
  border-radius: 16px !important;
344
- padding: 2.5rem 1.5rem !important;
345
  margin-bottom: 1.5rem !important;
346
- text-align: center !important;
347
  box-shadow: var(--shadow-md) !important;
348
- position: relative;
349
- overflow: hidden;
350
- color: var(--text-dark) !important;
351
  }
352
- .app-header-wrapper::before { /* Subtle background pattern */
353
  content: '';
354
  position: absolute;
355
  top: 0;
356
  left: 0;
357
  width: 100%;
358
  height: 100%;
359
- background: radial-gradient(circle at top left, hsla(210, 70%, 45%, 0.05) 0%, transparent 40%),
360
- radial-gradient(circle at bottom right, hsla(210, 70%, 45%, 0.05) 0%, transparent 40%);
361
  z-index: 0;
362
  opacity: 0.8;
363
  pointer-events: none;
364
  }
 
365
  .app-header-logo {
366
- font-size: 4.5rem !important;
367
  margin-bottom: 0.75rem !important;
368
  display: block !important;
369
- color: var(--primary-color) !important;
370
  position: relative;
371
- z-index: 1;
 
372
  animation: float-icon 3s ease-in-out infinite alternate;
373
  }
 
374
  @keyframes float-icon {
375
  0% { transform: translateY(0px); }
376
  50% { transform: translateY(-5px); }
377
  100% { transform: translateY(0px); }
378
  }
 
379
  .app-header-title {
380
  font-family: 'Poppins', sans-serif !important;
381
- font-size: 3rem !important;
382
- font-weight: 800 !important;
383
- color: var(--primary-color) !important;
384
  margin: 0 0 0.75rem 0 !important;
385
  line-height: 1.1 !important;
386
- letter-spacing: -0.03em !important;
387
  position: relative;
388
  z-index: 1;
389
  }
390
  .app-header-tagline {
391
- font-size: 1.25rem !important;
392
- color: var(--text-muted) !important;
393
  font-weight: 400 !important;
394
  margin: 0 !important;
395
- max-width: 700px;
396
  margin-left: auto;
397
  margin-right: auto;
398
  position: relative;
399
  z-index: 1;
400
  }
401
 
402
- /* Main Dashboard Container & Card Sections (White Backgrounds) */
403
  .main-dashboard-container {
404
  display: flex !important;
405
  flex-direction: column !important;
406
- gap: 1.25rem !important;
407
  }
 
408
  .dashboard-card-section {
409
- background: var(--background-card) !important; /* Pure white for content cards */
410
- border: 2px solid var(--border-card) !important;
411
  border-radius: 12px !important;
412
- padding: 1.75rem !important;
413
- box-shadow: var(--shadow-sm) !important;
414
- transition: all 0.3s ease-out !important;
415
- cursor: default;
416
- color: var(--text-dark) !important; /* Dark text on white cards */
417
  }
418
  .dashboard-card-section:hover {
419
  box-shadow: var(--shadow-md) !important;
420
- transform: translateY(-5px) !important;
421
  }
422
 
423
- /* Section Titles (Centered & Bold) */
424
  .sub-section-title {
425
  font-family: 'Poppins', sans-serif !important;
426
- font-size: 1.7rem !important;
427
- font-weight: 700 !important;
428
- color: var(--primary-color) !important;
429
- text-align: center !important; /* Centered */
430
- margin: 0 0 1.25rem 0 !important;
431
  padding-bottom: 0.75rem !important;
432
- border-bottom: 2px solid var(--border-card) !important; /* Subtle separator line */
433
  display: block !important;
434
  letter-spacing: -0.01em !important;
435
  }
 
 
436
  .dashboard-card-section p {
437
  line-height: 1.7 !important;
438
- color: var(--text-dark) !important;
439
  font-size: 1rem !important;
440
  }
441
  .dashboard-card-section strong {
442
- color: var(--primary-color) !important; /* Default strong text uses primary color */
443
- }
444
- .dashboard-card-section p > span { /* For specific highlights like 'Disclaimer:' */
445
- color: var(--secondary-accent-color) !important;
446
- font-weight: 600 !important;
447
  }
448
 
449
- /* Input Styling (Pure White Backgrounds) */
 
 
 
450
  .gradio-textbox textarea,
451
  .gradio-textbox input,
452
- .gradio-dropdown > div > input[type="text"],
453
- .gradio-dropdown .primary-wrap,
454
- .gradio-dropdown .scroll-hide {
455
- background: var(--background-input) !important; /* Pure white background for inputs */
456
- border: 2px solid var(--border-input) !important; /* Light gray border */
 
457
  border-radius: 8px !important;
458
- padding: 0.85rem 1rem !important;
459
  font-size: 0.98rem !important;
460
  font-family: 'Inter', sans-serif !important;
461
- color: var(--text-dark) !important;
462
- transition: border-color 0.2s ease, box-shadow 0.2s ease !important;
463
  box-shadow: var(--shadow-sm) !important;
464
  }
465
- .gradio-textbox textarea::placeholder,
466
- .gradio-textbox input::placeholder {
467
- color: var(--text-muted) !important;
468
- opacity: 0.8;
469
- }
470
  .gradio-textbox textarea:focus,
471
  .gradio-textbox input:focus,
472
  .gradio-dropdown > div > input[type="text"]:focus,
473
- .gradio-dropdown .primary-wrap.focused {
474
  outline: none !important;
475
- border-color: var(--border-focus) !important;
476
- box-shadow: 0 0 0 4px hsla(210, 70%, 45%, 0.2) !important; /* Primary color glow */
477
  }
 
 
478
  .gradio-textbox label,
479
  .gradio-dropdown label {
480
- font-weight: 600 !important;
481
- color: var(--text-dark) !important;
482
  font-size: 1rem !important;
483
  margin-bottom: 0.6rem !important;
484
  display: block !important;
485
  }
 
486
  .gradio-textbox .gr-form,
487
  .gradio-dropdown .gr-form {
488
  font-size: 0.9rem !important;
489
- color: var(--text-muted) !important;
490
- margin-top: 0.4rem !important;
491
  }
 
492
  .input-row {
493
  display: flex !important;
494
- gap: 1.25rem !important;
495
  margin-bottom: 0.5rem !important;
496
  }
497
  .input-field {
498
  flex: 1 !important;
499
  }
500
 
501
- /* Button Styling */
502
  .button-row {
503
  display: flex !important;
504
  gap: 1rem !important;
505
- justify-content: flex-end !important;
506
- margin-top: 1.5rem !important;
507
  }
508
  .gradio-button {
509
- padding: 0.85rem 1.8rem !important;
510
- border-radius: 9px !important;
511
- font-weight: 600 !important;
512
  font-size: 1rem !important;
513
- transition: all 0.2s ease-out !important;
514
  cursor: pointer !important;
515
  border: 2px solid transparent !important;
516
- text-align: center !important;
517
  }
518
  .gr-button-primary {
519
- background: var(--primary-color) !important; /* Deep blue button */
520
  color: white !important;
521
  box-shadow: var(--shadow-sm) !important;
522
  }
523
  .gr-button-primary:hover {
524
  background: var(--primary-hover) !important;
525
  box-shadow: var(--shadow-md) !important;
526
- transform: translateY(-2px) !important;
527
  }
528
- .gr-button-primary:active {
529
- transform: translateY(1px) !important; /* Press effect */
530
  box-shadow: none !important;
531
  }
532
  .gr-button-secondary {
533
- background: var(--button-secondary-bg) !important; /* Light gray button */
534
- color: var(--button-secondary-text) !important;
535
- border-color: var(--border-input) !important;
536
  }
537
  .gr-button-secondary:hover {
538
- background: hsl(210, 10%, 85%) !important;
539
  border-color: var(--primary-color) !important;
540
  transform: translateY(-2px) !important;
541
  }
542
- .gr-button-secondary:active {
543
  transform: translateY(1px) !important;
544
  box-shadow: none !important;
545
  }
546
 
547
- /* Output Styling (Pure White Background) */
548
  .output-content-wrapper {
549
- background: var(--background-input) !important; /* Pure white background for output box */
550
- border: 2px dashed var(--border-input) !important;
551
  border-radius: 8px !important;
552
  padding: 1.5rem !important;
553
- min-height: 150px !important;
554
- color: var(--text-dark) !important;
 
555
  display: flex;
556
  flex-direction: column;
557
- justify-content: center;
558
- align-items: center;
559
  }
 
560
  .animated-output-content {
561
  opacity: 0;
562
- animation: fadeInAndSlideUp 0.7s ease-out forwards;
563
- width: 100%;
 
564
  white-space: pre-wrap;
565
  overflow-wrap: break-word;
566
  word-break: break-word;
567
- text-align: left !important;
568
- color: var(--text-dark) !important;
569
  }
570
  @keyframes fadeInAndSlideUp {
571
  from { opacity: 0; transform: translateY(15px); }
572
  to { opacity: 1; transform: translateY(0); }
573
  }
 
574
  .response-header {
575
  font-size: 1.3rem !important;
576
  font-weight: 700 !important;
577
- color: var(--primary-color) !important;
578
  margin-bottom: 0.75rem !important;
579
  display: flex !important;
580
  align-items: center !important;
@@ -586,9 +604,10 @@ Answer:"""
586
  }
587
  .divider {
588
  border: none !important;
589
- border-top: 1px dashed var(--border-input) !important;
590
  margin: 1rem 0 !important;
591
  }
 
592
  .error-message {
593
  background: var(--error-bg) !important;
594
  border: 2px solid var(--error-border) !important;
@@ -601,9 +620,9 @@ Answer:"""
601
  font-size: 0.95rem !important;
602
  font-weight: 500 !important;
603
  line-height: 1.6 !important;
604
- text-align: left !important;
605
- width: 100%;
606
- box-sizing: border-box;
607
  }
608
  .error-message a {
609
  color: var(--error-text) !important;
@@ -620,22 +639,23 @@ Answer:"""
620
  margin-top: 0.5rem !important;
621
  opacity: 0.8;
622
  }
 
623
  .placeholder {
624
- background: var(--background-input) !important;
625
- border: 2px dashed var(--border-input) !important;
626
  border-radius: 8px !important;
627
  padding: 2.5rem 1.5rem !important;
628
  text-align: center !important;
629
- color: var(--text-muted) !important;
630
  font-style: italic !important;
631
  font-size: 1.1rem !important;
632
- width: 100%;
633
- box-sizing: border-box;
634
  }
635
 
636
- /* Examples Table Styling (Pure White Background Rows) */
637
  .examples-section .gr-samples-table {
638
- border: 2px solid var(--border-card) !important;
639
  border-radius: 8px !important;
640
  overflow: hidden !important;
641
  margin-top: 1rem !important;
@@ -645,59 +665,56 @@ Answer:"""
645
  padding: 0.9rem !important;
646
  border: none !important;
647
  font-size: 0.95rem !important;
648
- text-align: left !important;
649
  }
650
  .examples-section .gr-samples-table th {
651
- background: var(--background-card) !important;
652
  font-weight: 700 !important;
653
- color: var(--primary-color) !important;
654
  }
655
  .examples-section .gr-samples-table td {
656
- background: var(--background-input) !important; /* Pure white for rows */
657
- color: var(--text-dark) !important;
658
- border-top: 1px solid var(--border-input) !important;
659
  cursor: pointer !important;
660
- transition: background 0.2s ease, transform 0.1s ease !important;
661
  }
662
  .examples-section .gr-samples-table tr:hover td {
663
- background: hsl(0, 0%, 95%) !important; /* Slightly darker white on hover */
664
- transform: translateX(8px);
665
  }
 
666
  .gr-examples .gr-label,
667
  .gr-examples .label-wrap,
668
  .gr-examples .gr-accordion-header {
669
  display: none !important;
670
  }
671
 
672
- /* Footer Styling */
673
  .app-footer-wrapper {
674
- background: var(--background-card) !important;
675
- border: 2px solid var(--border-card) !important;
676
  border-radius: 12px !important;
677
  padding: 1.75rem !important;
678
  margin-top: 1.5rem !important;
679
- text-align: center !important;
680
- color: var(--text-muted) !important;
681
  }
682
  .app-footer p {
683
  margin: 0.6rem 0 !important;
684
  font-size: 0.95rem !important;
685
- color: var(--text-muted) !important;
686
  line-height: 1.6 !important;
687
  }
688
  .app-footer a {
689
- color: var(--primary-color) !important; /* Links use primary accent color */
690
  text-decoration: none !important;
691
  font-weight: 600 !important;
692
- transition: text-decoration 0.2s ease, color 0.2s ease !important; /* Smooth color transition */
693
  }
694
  .app-footer a:hover {
695
  text-decoration: underline !important;
696
- text-decoration-color: var(--primary-hover) !important;
697
- color: var(--primary-hover) !important;
698
  }
699
 
700
- /* Responsive Design */
701
  @media (max-width: 768px) {
702
  .gradio-container {
703
  padding: 1rem !important;
@@ -712,13 +729,13 @@ Answer:"""
712
  font-size: 1.4rem !important;
713
  }
714
  .input-row {
715
- flex-direction: column !important;
716
  }
717
  .button-row {
718
- flex-direction: column !important;
719
  }
720
  .gradio-button {
721
- width: 100% !important;
722
  }
723
  .dashboard-card-section {
724
  padding: 1.25rem !important;
@@ -733,8 +750,11 @@ Answer:"""
733
  }
734
  """
735
 
 
736
  with gr.Blocks(theme="shivi/calm_seafoam", css=custom_css, title="Landlord-Tenant Rights Assistant") as demo:
 
737
  with gr.Group(elem_classes="app-header-wrapper"):
 
738
  gr.Markdown(
739
  """
740
  <div class="app-header">
@@ -745,33 +765,37 @@ Answer:"""
745
  """
746
  )
747
 
 
748
  with gr.Column(elem_classes="main-dashboard-container"):
749
 
 
750
  with gr.Group(elem_classes="dashboard-card-section"):
751
- gr.Markdown("<h3 class='sub-section-title'>Welcome & Disclaimer</h3>")
752
  gr.Markdown(
753
  """
754
  Navigate landlord-tenant laws with ease. This assistant provides detailed, state-specific answers grounded in legal authority.
755
 
756
- <span style="color: var(--secondary-accent-color);">**Disclaimer:**</span> This tool is for informational purposes only and does not constitute legal advice. For specific legal guidance, always consult a licensed attorney in your jurisdiction.
757
  """
758
  )
759
 
 
760
  with gr.Group(elem_classes="dashboard-card-section"):
761
- gr.Markdown("<h3 class='sub-section-title'>OpenAI API Key</h3>")
762
  api_key_input = gr.Textbox(
763
  label="API Key",
764
- type="password",
765
  placeholder="Enter your OpenAI API key (e.g., sk-...)",
766
  info="Required to process your query. Get one from OpenAI: platform.openai.com/api-keys",
767
  lines=1,
768
- elem_classes=["input-field-group"]
769
  )
770
 
 
771
  with gr.Group(elem_classes="dashboard-card-section"):
772
- gr.Markdown("<h3 class='sub-section-title'>Ask Your Question</h3>")
773
- with gr.Row(elem_classes="input-row"):
774
- with gr.Column(elem_classes="input-field", scale=3):
775
  query_input = gr.Textbox(
776
  label="Your Question",
777
  placeholder="E.g., What are the rules for security deposit returns in my state?",
@@ -779,7 +803,7 @@ Answer:"""
779
  max_lines=8,
780
  elem_classes=["input-field-group"]
781
  )
782
- with gr.Column(elem_classes="input-field", scale=1):
783
  state_input = gr.Dropdown(
784
  label="Select State",
785
  choices=dropdown_choices,
@@ -787,29 +811,32 @@ Answer:"""
787
  allow_custom_value=False,
788
  elem_classes=["input-field-group"]
789
  )
790
- with gr.Row(elem_classes="button-row"):
791
  clear_button = gr.Button("Clear", variant="secondary", elem_classes=["gr-button-secondary"])
792
  submit_button = gr.Button("Submit Query", variant="primary", elem_classes=["gr-button-primary"])
793
 
 
794
  with gr.Group(elem_classes="dashboard-card-section"):
795
- gr.Markdown("<h3 class='sub-section-title'>Legal Assistant's Response</h3>")
796
- output = gr.HTML(
797
  value="<div class='placeholder'>The answer will appear here after submitting your query.</div>",
798
- elem_classes="output-content-wrapper"
799
  )
800
 
 
801
  with gr.Group(elem_classes="dashboard-card-section examples-section"):
802
- gr.Markdown("<h3 class='sub-section-title'>Example Questions</h3>")
803
  if example_queries:
804
  gr.Examples(
805
  examples=example_queries,
806
  inputs=[query_input, state_input],
807
  examples_per_page=5,
808
- label=""
809
  )
810
  else:
811
  gr.Markdown("<div class='placeholder'>Sample questions could not be loaded. Please ensure the vector database is populated.</div>")
812
 
 
813
  with gr.Group(elem_classes="app-footer-wrapper"):
814
  gr.Markdown(
815
  """
@@ -818,19 +845,20 @@ Answer:"""
818
  """
819
  )
820
 
 
821
  submit_button.click(
822
  fn=query_interface_wrapper,
823
  inputs=[api_key_input, query_input, state_input],
824
  outputs=output,
825
- api_name="submit_query"
826
  )
827
 
828
  clear_button.click(
829
  fn=lambda: (
830
- "",
831
- "",
832
- initial_value,
833
- "<div class='placeholder'>Inputs cleared. Ready for your next question.</div>"
834
  ),
835
  inputs=[],
836
  outputs=[api_key_input, query_input, state_input, output]
@@ -849,32 +877,36 @@ if __name__ == "__main__":
849
  PDF_PATH = os.getenv("PDF_PATH", DEFAULT_PDF_PATH)
850
  VECTOR_DB_PATH = os.getenv("VECTOR_DB_PATH", DEFAULT_DB_PATH)
851
 
 
852
  os.makedirs(os.path.dirname(VECTOR_DB_PATH), exist_ok=True)
853
 
854
  logging.info(f"Attempting to load PDF from: {PDF_PATH}")
855
  if not os.path.exists(PDF_PATH):
856
  logging.error(f"FATAL: PDF file not found at the specified path: {PDF_PATH}")
857
  print(f"\n--- CONFIGURATION ERROR ---\nPDF file ('{os.path.basename(PDF_PATH)}') not found at: {PDF_PATH}.\nPlease ensure it exists or set 'PDF_PATH' environment variable.\n---------------------------\n")
858
- exit(1)
859
 
860
  if not os.access(PDF_PATH, os.R_OK):
861
  logging.error(f"FATAL: PDF file at '{PDF_PATH}' exists but is not readable. Check file permissions.")
862
  print(f"\n--- PERMISSION ERROR ---\nPDF file ('{os.path.basename(PDF_PATH)}') found but not readable at: {PDF_PATH}\nPlease check file permissions (e.g., using 'chmod +r' in terminal).\n---------------------------\n")
863
- exit(1)
864
 
865
  logging.info(f"PDF file '{os.path.basename(PDF_PATH)}' found and is readable.")
866
 
 
867
  vector_db_instance = VectorDatabase(persist_directory=VECTOR_DB_PATH)
868
  rag = RAGSystem(vector_db=vector_db_instance)
869
 
 
870
  rag.load_pdf(PDF_PATH)
871
 
 
872
  app_interface = rag.gradio_interface()
873
- SERVER_PORT = int(os.getenv("PORT", 7860))
874
 
875
  logging.info(f"Launching Gradio app on http://0.0.0.0:{SERVER_PORT}")
876
  print(f"\n--- Gradio App Running ---\nAccess at: http://localhost:{SERVER_PORT} or your public Spaces URL\n--------------------------\n")
877
- app_interface.launch(server_name="0.0.0.0", server_port=SERVER_PORT, share=False)
878
 
879
  except ModuleNotFoundError as e:
880
  if "vector_db" in str(e):
 
12
  except ImportError:
13
  print("Error: Could not import VectorDatabase from vector_db.py.")
14
  print("Please ensure vector_db.py exists in the same directory and is correctly defined.")
15
+ # Exit if critical dependency is missing at import time
16
  exit(1)
17
 
18
  try:
19
  from langchain_openai import ChatOpenAI
20
  except ImportError:
21
  print("Error: langchain-openai not found. Please install it: pip install langchain-openai")
22
+ # Exit if critical dependency is missing at import time
23
  exit(1)
24
 
25
  from langchain.prompts import PromptTemplate
 
37
  format='%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
38
  )
39
 
40
+ # --- RAGSystem Class (Processing Logic - KEPT INTACT AS REQUESTED) ---
41
  class RAGSystem:
42
  def __init__(self, vector_db: Optional[VectorDatabase] = None):
43
  logging.info("Initializing RAGSystem")
 
148
  context_parts.append(f"**Source: State Summary (State: {state_label})**\n{state_doc_content}")
149
 
150
  if context_parts:
151
+ context = "\n\n---\n\n".join(context_parts)
152
  logging.info(f"Constructed context with {len(context_parts)} parts. Length: {len(context)} chars.")
153
  try:
154
  statutes_from_context = self.extract_statutes(context)
 
173
 
174
  if not answer_text:
175
  logging.warning("LLM returned an empty answer.")
176
+ answer_text = "<div class='error-message'><span class='error-icon'>⚠️</span>The AI model returned an empty response. This might be due to the query, context limitations, or temporary issues. Please try rephrasing your question or try again later.</div>"
177
  else:
178
  logging.info("LLM generated answer successfully.")
179
 
 
196
  error_message = "Error: The request to the AI model timed out. The service might be busy."
197
  details = "Please try again in a few moments."
198
 
199
+ formatted_error = f"<div class='error-message'><span class='error-icon'>❌</span>{error_message}</div>"
200
  if details:
201
  formatted_error += f"<div class='error-details'>{details}</div>"
202
 
 
224
  raise FileNotFoundError(f"PDF file not found: {pdf_path}")
225
  try:
226
  logging.info(f"Attempting to load/verify data from PDF: {pdf_path}")
227
+ # Assuming process_and_load_pdf is part of VectorDatabase and correctly implemented
228
  num_states_processed = self.vector_db.process_and_load_pdf(pdf_path)
229
  doc_count = self.vector_db.document_collection.count()
230
  state_count = self.vector_db.state_collection.count()
 
242
  logging.error(f"Failed to load or process PDF '{pdf_path}': {str(e)}", exc_info=True)
243
  raise RuntimeError(f"Failed to process PDF '{pdf_path}': {e}") from e
244
 
245
+ # --- GRADIO INTERFACE (NEW UI DESIGN) ---
246
  def gradio_interface(self):
247
  def query_interface_wrapper(api_key: str, query: str, state: str) -> str:
248
+ # Basic client-side validation for immediate feedback (redundant but good UX)
249
  if not api_key or not api_key.strip() or not api_key.startswith("sk-"):
250
+ return "<div class='error-message'><span class='error-icon'>⚠️</span>Please provide a valid OpenAI API key (starting with 'sk-'). <a href='https://platform.openai.com/api-keys' target='_blank'>Get one free from OpenAI</a>.</div>"
251
  if not state or state == "Select a state..." or "Error" in state:
252
+ return "<div class='error-message'><span class='error-icon'>⚠️</span>Please select a valid state from the dropdown.</div>"
253
  if not query or not query.strip():
254
+ return "<div class='error-message'><span class='error-icon'>⚠️</span>Please enter your question in the text box.</div>"
255
 
256
+ # Call the core processing logic
257
  result = self.process_query(query=query, state=state, openai_api_key=api_key)
258
+ answer = result.get("answer", "<div class='error-message'><span class='error-icon'>⚠️</span>An unexpected error occurred.</div>")
259
 
260
+ # Check if the answer already contains an error message (from deeper within process_query)
261
  if "<div class='error-message'>" in answer:
262
+ # Error messages are returned directly as they contain their own styling
263
  return answer
264
  else:
265
+ # Wrap successful response in a div with an animation class
266
+ formatted_response_content = f"<div class='response-header'><span class='response-icon'>📜</span>Response for {state}</div><hr class='divider'>{answer}"
 
267
  return f"<div class='animated-output-content'>{formatted_response_content}</div>"
268
 
269
  try:
270
  available_states_list = self.get_states()
271
+ # Ensure "Select a state..." is always the first option
272
  dropdown_choices = ["Select a state..."] + (available_states_list if available_states_list and "Error" not in available_states_list[0] else ["Error: States unavailable"])
273
+ initial_value = dropdown_choices[0] # Set initial value to the prompt
274
+ except Exception: # Catch-all for safety
275
  dropdown_choices = ["Error: Critical failure loading states"]
276
  initial_value = dropdown_choices[0]
277
 
278
+ # Define example queries, filtering based on available states
279
  example_queries_base = [
280
  ["What are the rules for security deposit returns?", "California"],
281
  ["Can a landlord enter my apartment without notice?", "New York"],
 
286
  example_queries = []
287
  if available_states_list and "Error" not in available_states_list[0] and len(available_states_list) > 0:
288
  loaded_states_set = set(available_states_list)
289
+ # Filter for examples whose state is in the loaded states
290
  example_queries = [ex for ex in example_queries_base if ex[1] in loaded_states_set]
291
+ # Add a generic example if no specific state examples match or if list is empty
292
  if not example_queries:
293
+ # Add one example using the first available state, or a common one if no states
294
  example_queries.append(["What basic rights do tenants have?", available_states_list[0] if available_states_list else "California"])
295
+ else: # Fallback if states list is problematic (e.g., empty or error)
296
  example_queries.append(["What basic rights do tenants have?", "California"])
297
 
298
 
299
+ # Custom CSS for better UI design, clear boundaries, and text alignment for HuggingFace
300
  custom_css = """
301
  /* Import legible fonts from Google Fonts */
302
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Poppins:wght@600;700;800&display=swap');
303
 
304
+ /* Root variables for consistent theming - adjusted for very light calm_seafoam feel */
305
  :root {
306
+ --primary-color: #3cb371; /* Medium Sea Green (vibrant seafoam) */
307
+ --primary-hover: #339966;
308
+ --background-primary: hsl(180, 100%, 98%); /* Very light seafoam for main cards */
309
+ --background-secondary: hsl(180, 100%, 96%); /* Slightly darker for overall app background */
310
+ --text-primary: hsl(210, 20%, 20%); /* Dark blue-gray for main text */
311
+ --text-secondary: hsl(210, 10%, 45%); /* Muted blue-gray for secondary text */
312
+ --border-color: hsl(180, 30%, 85%); /* Subtle seafoam gray for borders */
313
+ --border-focus: #3cb371; /* Focus color matches primary */
 
 
 
 
 
 
 
 
 
 
314
  --shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
315
+ --shadow-md: 0 4px 10px rgba(0,0,0,0.1);
316
+ --shadow-lg: 0 10px 20px rgba(0,0,0,0.15);
317
+ --error-bg: #FFEBEB;
318
+ --error-border: #FFCACA;
319
+ --error-text: #D32F2F;
 
 
 
 
320
  }
321
 
322
+ /* Dark mode variables - for consistency if a dark mode toggle were present */
323
+ body.dark {
324
+ --background-primary: #1F303A; /* Dark blue-green */
325
+ --background-secondary: #2C404B;
326
+ --text-primary: #E0F2F1;
327
+ --text-secondary: #A7C5C8;
328
+ --border-color: #5F7C8A;
329
+ --primary-color: #66BB6A; /* Brighter green for dark mode */
330
+ --primary-hover: #5cb85f;
331
+ --error-bg: #3F1D1D;
332
+ --error-border: #5A1A1A;
333
+ --error-text: #FF7070;
334
+ }
335
+
336
+ /* Base container improvements */
337
  .gradio-container {
338
+ max-width: 900px !important; /* Slightly smaller for focused content */
339
+ margin: 0 auto !important; /* Center the whole app */
340
  padding: 1.5rem !important;
341
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
342
+ background-color: var(--background-secondary) !important; /* Overall background */
343
+ box-shadow: none !important; /* Remove default gradio container shadow */
344
+ }
345
+ /* Ensure all main content sections have primary background */
346
+ .main-dashboard-container > * {
347
+ background-color: var(--background-primary) !important;
348
  }
349
 
350
+ /* Header styling - centered and prominent */
351
  .app-header-wrapper {
352
+ background: linear-gradient(135deg, var(--background-primary) 0%, var(--background-secondary) 100%) !important;
353
+ border: 2px solid var(--border-color) !important;
354
  border-radius: 16px !important;
355
+ padding: 2.5rem 1.5rem !important; /* More vertical padding */
356
  margin-bottom: 1.5rem !important;
357
+ text-align: center !important; /* Center text within header */
358
  box-shadow: var(--shadow-md) !important;
359
+ position: relative; /* For potential pseudo-element effects */
360
+ overflow: hidden; /* For any overflow animations */
 
361
  }
362
+ .app-header-wrapper::before { /* Subtle background pattern for dynamism */
363
  content: '';
364
  position: absolute;
365
  top: 0;
366
  left: 0;
367
  width: 100%;
368
  height: 100%;
369
+ background: radial-gradient(circle at top left, rgba(60,179,113,0.05) 0%, transparent 40%),
370
+ radial-gradient(circle at bottom right, rgba(60,179,113,0.05) 0%, transparent 40%);
371
  z-index: 0;
372
  opacity: 0.8;
373
  pointer-events: none;
374
  }
375
+
376
  .app-header-logo {
377
+ font-size: 4.5rem !important; /* Larger icon */
378
  margin-bottom: 0.75rem !important;
379
  display: block !important;
380
+ color: var(--primary-color) !important; /* Theme color */
381
  position: relative;
382
+ z-index: 1; /* Bring icon to front of pseudo-element */
383
+ /* Animation for dynamism */
384
  animation: float-icon 3s ease-in-out infinite alternate;
385
  }
386
+ /* Keyframes for floating icon */
387
  @keyframes float-icon {
388
  0% { transform: translateY(0px); }
389
  50% { transform: translateY(-5px); }
390
  100% { transform: translateY(0px); }
391
  }
392
+
393
  .app-header-title {
394
  font-family: 'Poppins', sans-serif !important;
395
+ font-size: 3rem !important; /* Even larger title */
396
+ font-weight: 800 !important; /* Bolder */
397
+ color: var(--text-primary) !important;
398
  margin: 0 0 0.75rem 0 !important;
399
  line-height: 1.1 !important;
400
+ letter-spacing: -0.03em !important; /* Tighter spacing */
401
  position: relative;
402
  z-index: 1;
403
  }
404
  .app-header-tagline {
405
+ font-size: 1.25rem !important; /* Slightly larger tagline */
406
+ color: var(--text-secondary) !important;
407
  font-weight: 400 !important;
408
  margin: 0 !important;
409
+ max-width: 700px; /* Constrain tagline width */
410
  margin-left: auto;
411
  margin-right: auto;
412
  position: relative;
413
  z-index: 1;
414
  }
415
 
416
+ /* Main container with consistent spacing */
417
  .main-dashboard-container {
418
  display: flex !important;
419
  flex-direction: column !important;
420
+ gap: 1.25rem !important; /* Consistent spacing between cards */
421
  }
422
+ /* Card sections with clear boundaries (boundeyes) and subtle dynamic effects */
423
  .dashboard-card-section {
424
+ background: var(--background-primary) !important;
425
+ border: 2px solid var(--border-color) !important; /* Distinct border */
426
  border-radius: 12px !important;
427
+ padding: 1.75rem !important; /* Consistent padding */
428
+ box-shadow: var(--shadow-sm) !important; /* Subtle shadow */
429
+ transition: all 0.3s ease-out !important; /* Smoother transition */
430
+ cursor: default; /* Indicate not directly clickable (unless examples) */
 
431
  }
432
  .dashboard-card-section:hover {
433
  box-shadow: var(--shadow-md) !important;
434
+ transform: translateY(-3px) !important; /* More pronounced lift */
435
  }
436
 
437
+ /* Centered section titles with improved typography */
438
  .sub-section-title {
439
  font-family: 'Poppins', sans-serif !important;
440
+ font-size: 1.7rem !important; /* Slightly larger */
441
+ font-weight: 700 !important; /* Bolder */
442
+ color: var(--text-primary) !important;
443
+ text-align: center !important; /* Centered text */
444
+ margin: 0 0 1.25rem 0 !important; /* More space below title */
445
  padding-bottom: 0.75rem !important;
446
+ border-bottom: 2px solid var(--border-color) !important; /* Underline effect */
447
  display: block !important;
448
  letter-spacing: -0.01em !important;
449
  }
450
+
451
+ /* Specific styling for the welcome/disclaimer markdown content */
452
  .dashboard-card-section p {
453
  line-height: 1.7 !important;
454
+ color: var(--text-primary) !important;
455
  font-size: 1rem !important;
456
  }
457
  .dashboard-card-section strong {
458
+ color: var(--primary-color) !important; /* Highlight strong text with primary color */
 
 
 
 
459
  }
460
 
461
+ /* Improved input styling with clear boundaries and focus */
462
+ .gradio-textbox, .gradio-dropdown {
463
+ margin-bottom: 0.75rem !important;
464
+ }
465
  .gradio-textbox textarea,
466
  .gradio-textbox input,
467
+ .gradio-dropdown > div > input[type="text"], /* Target dropdown input for custom values */
468
+ .gradio-dropdown .primary-wrap, /* Target dropdown wrapper */
469
+ .gradio-dropdown .scroll-hide /* Target dropdown list container for consistency */
470
+ {
471
+ background: var(--background-primary) !important;
472
+ border: 2px solid var(--border-color) !important; /* Clear border */
473
  border-radius: 8px !important;
474
+ padding: 0.85rem 1rem !important; /* Slightly more padding */
475
  font-size: 0.98rem !important;
476
  font-family: 'Inter', sans-serif !important;
477
+ color: var(--text-primary) !important;
478
+ transition: border-color 0.2s ease, box-shadow 0.2s ease !important; /* Smooth transitions */
479
  box-shadow: var(--shadow-sm) !important;
480
  }
 
 
 
 
 
481
  .gradio-textbox textarea:focus,
482
  .gradio-textbox input:focus,
483
  .gradio-dropdown > div > input[type="text"]:focus,
484
+ .gradio-dropdown .primary-wrap.focused { /* Apply focus style to dropdown wrap */
485
  outline: none !important;
486
+ border-color: var(--border-focus) !important; /* Distinct border on focus */
487
+ box-shadow: 0 0 0 4px rgba(60, 179, 113, 0.2) !important; /* Broader, softer glow on focus */
488
  }
489
+
490
+ /* Label styling for better readability */
491
  .gradio-textbox label,
492
  .gradio-dropdown label {
493
+ font-weight: 600 !important; /* Bolder labels */
494
+ color: var(--text-primary) !important;
495
  font-size: 1rem !important;
496
  margin-bottom: 0.6rem !important;
497
  display: block !important;
498
  }
499
+ /* Info text styling below inputs */
500
  .gradio-textbox .gr-form,
501
  .gradio-dropdown .gr-form {
502
  font-size: 0.9rem !important;
503
+ color: var(--text-secondary) !important;
504
+ margin-top: 0.4rem !important; /* More space for info text */
505
  }
506
+ /* Input row layout improvements */
507
  .input-row {
508
  display: flex !important;
509
+ gap: 1.25rem !important; /* Consistent gap between query and state */
510
  margin-bottom: 0.5rem !important;
511
  }
512
  .input-field {
513
  flex: 1 !important;
514
  }
515
 
516
+ /* Button styling improvements with active state for dynamism */
517
  .button-row {
518
  display: flex !important;
519
  gap: 1rem !important;
520
+ justify-content: flex-end !important; /* Align buttons to the right */
521
+ margin-top: 1.5rem !important; /* More space above buttons */
522
  }
523
  .gradio-button {
524
+ padding: 0.85rem 1.8rem !important; /* More padding for bigger buttons */
525
+ border-radius: 9px !important; /* Slightly more rounded */
526
+ font-weight: 600 !important; /* Bolder text */
527
  font-size: 1rem !important;
528
+ transition: all 0.2s ease-out !important; /* Smooth transition for hover/active */
529
  cursor: pointer !important;
530
  border: 2px solid transparent !important;
531
+ text-align: center !important; /* Ensure button text is centered */
532
  }
533
  .gr-button-primary {
534
+ background: var(--primary-color) !important;
535
  color: white !important;
536
  box-shadow: var(--shadow-sm) !important;
537
  }
538
  .gr-button-primary:hover {
539
  background: var(--primary-hover) !important;
540
  box-shadow: var(--shadow-md) !important;
541
+ transform: translateY(-2px) !important; /* Subtle lift effect on hover */
542
  }
543
+ .gr-button-primary:active { /* Press down effect on click */
544
+ transform: translateY(1px) !important;
545
  box-shadow: none !important;
546
  }
547
  .gr-button-secondary {
548
+ background: transparent !important;
549
+ color: var(--text-primary) !important;
550
+ border-color: var(--border-color) !important;
551
  }
552
  .gr-button-secondary:hover {
553
+ background: var(--background-secondary) !important;
554
  border-color: var(--primary-color) !important;
555
  transform: translateY(-2px) !important;
556
  }
557
+ .gr-button-secondary:active { /* Press down effect on click */
558
  transform: translateY(1px) !important;
559
  box-shadow: none !important;
560
  }
561
 
562
+ /* Output styling with clear boundaries (boundeyes are clear) and dynamic fade-in */
563
  .output-content-wrapper {
564
+ background: var(--background-primary) !important;
565
+ border: 2px solid var(--border-color) !important; /* Clear border */
566
  border-radius: 8px !important;
567
  padding: 1.5rem !important;
568
+ min-height: 150px !important; /* More space for output */
569
+ color: var(--text-primary) !important;
570
+ /* Ensure the inner animated content fits well */
571
  display: flex;
572
  flex-direction: column;
573
+ justify-content: center; /* Center content vertically if small */
574
+ align-items: center; /* Center content horizontally if small */
575
  }
576
+ /* The div holding the actual response content, enabling fade-in animation */
577
  .animated-output-content {
578
  opacity: 0;
579
+ animation: fadeInAndSlideUp 0.7s ease-out forwards; /* More pronounced animation */
580
+ width: 100%; /* Take full width of parent */
581
+ /* Preserve formatting within the animated content */
582
  white-space: pre-wrap;
583
  overflow-wrap: break-word;
584
  word-break: break-word;
585
+ text-align: left !important; /* Ensure text is left-aligned within this div */
 
586
  }
587
  @keyframes fadeInAndSlideUp {
588
  from { opacity: 0; transform: translateY(15px); }
589
  to { opacity: 1; transform: translateY(0); }
590
  }
591
+
592
  .response-header {
593
  font-size: 1.3rem !important;
594
  font-weight: 700 !important;
595
+ color: var(--primary-color) !important; /* Matches primary color */
596
  margin-bottom: 0.75rem !important;
597
  display: flex !important;
598
  align-items: center !important;
 
604
  }
605
  .divider {
606
  border: none !important;
607
+ border-top: 1px dashed var(--border-color) !important; /* Dashed divider for visual separation */
608
  margin: 1rem 0 !important;
609
  }
610
+ /* Error message styling */
611
  .error-message {
612
  background: var(--error-bg) !important;
613
  border: 2px solid var(--error-border) !important;
 
620
  font-size: 0.95rem !important;
621
  font-weight: 500 !important;
622
  line-height: 1.6 !important;
623
+ text-align: left !important; /* Ensure error message text is left aligned */
624
+ width: 100%; /* Take full width of parent */
625
+ box-sizing: border-box; /* Include padding/border in width */
626
  }
627
  .error-message a {
628
  color: var(--error-text) !important;
 
639
  margin-top: 0.5rem !important;
640
  opacity: 0.8;
641
  }
642
+ /* Placeholder styling for empty output */
643
  .placeholder {
644
+ background: var(--background-secondary) !important;
645
+ border: 2px dashed var(--border-color) !important;
646
  border-radius: 8px !important;
647
  padding: 2.5rem 1.5rem !important;
648
  text-align: center !important;
649
+ color: var(--text-secondary) !important;
650
  font-style: italic !important;
651
  font-size: 1.1rem !important;
652
+ width: 100%; /* Ensure it takes full width of parent */
653
+ box-sizing: border-box; /* Include padding/border in width */
654
  }
655
 
656
+ /* Examples table styling with dynamic hover */
657
  .examples-section .gr-samples-table {
658
+ border: 2px solid var(--border-color) !important;
659
  border-radius: 8px !important;
660
  overflow: hidden !important;
661
  margin-top: 1rem !important;
 
665
  padding: 0.9rem !important;
666
  border: none !important;
667
  font-size: 0.95rem !important;
668
+ text-align: left !important; /* Ensure example text is left-aligned */
669
  }
670
  .examples-section .gr-samples-table th {
671
+ background: var(--background-secondary) !important;
672
  font-weight: 700 !important;
673
+ color: var(--text-primary) !important;
674
  }
675
  .examples-section .gr-samples-table td {
676
+ background: var(--background-primary) !important;
677
+ color: var(--text-primary) !important;
678
+ border-top: 1px solid var(--border-color) !important;
679
  cursor: pointer !important;
680
+ transition: background 0.2s ease, transform 0.1s ease !important; /* Smooth transitions */
681
  }
682
  .examples-section .gr-samples-table tr:hover td {
683
+ background: var(--background-secondary) !important;
684
+ transform: translateX(5px); /* Subtle slide on hover */
685
  }
686
+ /* Hide Gradio default elements for examples for cleaner look */
687
  .gr-examples .gr-label,
688
  .gr-examples .label-wrap,
689
  .gr-examples .gr-accordion-header {
690
  display: none !important;
691
  }
692
 
693
+ /* Footer styling - centered text */
694
  .app-footer-wrapper {
695
+ background: var(--background-secondary) !important;
696
+ border: 2px solid var(--border-color) !important;
697
  border-radius: 12px !important;
698
  padding: 1.75rem !important;
699
  margin-top: 1.5rem !important;
700
+ text-align: center !important; /* Centered footer text */
 
701
  }
702
  .app-footer p {
703
  margin: 0.6rem 0 !important;
704
  font-size: 0.95rem !important;
705
+ color: var(--text-secondary) !important;
706
  line-height: 1.6 !important;
707
  }
708
  .app-footer a {
709
+ color: var(--primary-color) !important;
710
  text-decoration: none !important;
711
  font-weight: 600 !important;
 
712
  }
713
  .app-footer a:hover {
714
  text-decoration: underline !important;
 
 
715
  }
716
 
717
+ /* Responsive design for smaller screens */
718
  @media (max-width: 768px) {
719
  .gradio-container {
720
  padding: 1rem !important;
 
729
  font-size: 1.4rem !important;
730
  }
731
  .input-row {
732
+ flex-direction: column !important; /* Stack inputs vertically */
733
  }
734
  .button-row {
735
+ flex-direction: column !important; /* Stack buttons vertically */
736
  }
737
  .gradio-button {
738
+ width: 100% !important; /* Full width buttons */
739
  }
740
  .dashboard-card-section {
741
  padding: 1.25rem !important;
 
750
  }
751
  """
752
 
753
+ # Using gr.Blocks with the specified theme and custom CSS
754
  with gr.Blocks(theme="shivi/calm_seafoam", css=custom_css, title="Landlord-Tenant Rights Assistant") as demo:
755
+ # Header Section - uses gr.Group for distinct card-like styling
756
  with gr.Group(elem_classes="app-header-wrapper"):
757
+ # Markdown used for flexible styling and auto-centering via CSS
758
  gr.Markdown(
759
  """
760
  <div class="app-header">
 
765
  """
766
  )
767
 
768
+ # Main Dashboard Container - acts as a column to stack various sections
769
  with gr.Column(elem_classes="main-dashboard-container"):
770
 
771
+ # Introduction and Disclaimer Card
772
  with gr.Group(elem_classes="dashboard-card-section"):
773
+ gr.Markdown("<h3 class='sub-section-title'>Welcome & Disclaimer</h3>") # Centered by CSS
774
  gr.Markdown(
775
  """
776
  Navigate landlord-tenant laws with ease. This assistant provides detailed, state-specific answers grounded in legal authority.
777
 
778
+ **Disclaimer:** This tool is for informational purposes only and does not constitute legal advice. For specific legal guidance, always consult a licensed attorney in your jurisdiction.
779
  """
780
  )
781
 
782
+ # OpenAI API Key Input Card
783
  with gr.Group(elem_classes="dashboard-card-section"):
784
+ gr.Markdown("<h3 class='sub-section-title'>OpenAI API Key</h3>") # Centered by CSS
785
  api_key_input = gr.Textbox(
786
  label="API Key",
787
+ type="password", # Hides the input for security
788
  placeholder="Enter your OpenAI API key (e.g., sk-...)",
789
  info="Required to process your query. Get one from OpenAI: platform.openai.com/api-keys",
790
  lines=1,
791
+ elem_classes=["input-field-group"] # Custom class for input styling
792
  )
793
 
794
+ # Query Input and State Selection Card
795
  with gr.Group(elem_classes="dashboard-card-section"):
796
+ gr.Markdown("<h3 class='sub-section-title'>Ask Your Question</h3>") # Centered by CSS
797
+ with gr.Row(elem_classes="input-row"): # Row for side-by-side query and state
798
+ with gr.Column(elem_classes="input-field", scale=3): # Query text area takes more space
799
  query_input = gr.Textbox(
800
  label="Your Question",
801
  placeholder="E.g., What are the rules for security deposit returns in my state?",
 
803
  max_lines=8,
804
  elem_classes=["input-field-group"]
805
  )
806
+ with gr.Column(elem_classes="input-field", scale=1): # State dropdown takes less space
807
  state_input = gr.Dropdown(
808
  label="Select State",
809
  choices=dropdown_choices,
 
811
  allow_custom_value=False,
812
  elem_classes=["input-field-group"]
813
  )
814
+ with gr.Row(elem_classes="button-row"): # Row for action buttons
815
  clear_button = gr.Button("Clear", variant="secondary", elem_classes=["gr-button-secondary"])
816
  submit_button = gr.Button("Submit Query", variant="primary", elem_classes=["gr-button-primary"])
817
 
818
+ # Output Display Card - Using gr.HTML for better animation control
819
  with gr.Group(elem_classes="dashboard-card-section"):
820
+ gr.Markdown("<h3 class='sub-section-title'>Legal Assistant's Response</h3>") # Centered by CSS
821
+ output = gr.HTML( # Changed to gr.HTML to wrap content with animation class
822
  value="<div class='placeholder'>The answer will appear here after submitting your query.</div>",
823
+ elem_classes="output-content-wrapper" # Custom class for output styling
824
  )
825
 
826
+ # Example Questions Section
827
  with gr.Group(elem_classes="dashboard-card-section examples-section"):
828
+ gr.Markdown("<h3 class='sub-section-title'>Example Questions</h3>") # Centered by CSS
829
  if example_queries:
830
  gr.Examples(
831
  examples=example_queries,
832
  inputs=[query_input, state_input],
833
  examples_per_page=5,
834
+ label="" # Hide default Gradio label for examples to use our custom title
835
  )
836
  else:
837
  gr.Markdown("<div class='placeholder'>Sample questions could not be loaded. Please ensure the vector database is populated.</div>")
838
 
839
+ # Footer Section - contains disclaimer and developer info
840
  with gr.Group(elem_classes="app-footer-wrapper"):
841
  gr.Markdown(
842
  """
 
845
  """
846
  )
847
 
848
+ # Event Listeners for buttons
849
  submit_button.click(
850
  fn=query_interface_wrapper,
851
  inputs=[api_key_input, query_input, state_input],
852
  outputs=output,
853
+ api_name="submit_query" # Useful for debugging / external calls
854
  )
855
 
856
  clear_button.click(
857
  fn=lambda: (
858
+ "", # Clear API key input
859
+ "", # Clear query input
860
+ initial_value, # Reset state dropdown to default prompt
861
+ "<div class='placeholder'>Inputs cleared. Ready for your next question.</div>" # Reset output message
862
  ),
863
  inputs=[],
864
  outputs=[api_key_input, query_input, state_input, output]
 
877
  PDF_PATH = os.getenv("PDF_PATH", DEFAULT_PDF_PATH)
878
  VECTOR_DB_PATH = os.getenv("VECTOR_DB_PATH", DEFAULT_DB_PATH)
879
 
880
+ # Ensure vector DB directory exists before initialization
881
  os.makedirs(os.path.dirname(VECTOR_DB_PATH), exist_ok=True)
882
 
883
  logging.info(f"Attempting to load PDF from: {PDF_PATH}")
884
  if not os.path.exists(PDF_PATH):
885
  logging.error(f"FATAL: PDF file not found at the specified path: {PDF_PATH}")
886
  print(f"\n--- CONFIGURATION ERROR ---\nPDF file ('{os.path.basename(PDF_PATH)}') not found at: {PDF_PATH}.\nPlease ensure it exists or set 'PDF_PATH' environment variable.\n---------------------------\n")
887
+ exit(1) # Correctly exits if PDF is not found
888
 
889
  if not os.access(PDF_PATH, os.R_OK):
890
  logging.error(f"FATAL: PDF file at '{PDF_PATH}' exists but is not readable. Check file permissions.")
891
  print(f"\n--- PERMISSION ERROR ---\nPDF file ('{os.path.basename(PDF_PATH)}') found but not readable at: {PDF_PATH}\nPlease check file permissions (e.g., using 'chmod +r' in terminal).\n---------------------------\n")
892
+ exit(1) # Correctly exits if PDF is unreadable
893
 
894
  logging.info(f"PDF file '{os.path.basename(PDF_PATH)}' found and is readable.")
895
 
896
+ # Initialize VectorDatabase and RAGSystem
897
  vector_db_instance = VectorDatabase(persist_directory=VECTOR_DB_PATH)
898
  rag = RAGSystem(vector_db=vector_db_instance)
899
 
900
+ # Load PDF data into the vector DB (or verify it's already loaded)
901
  rag.load_pdf(PDF_PATH)
902
 
903
+ # Get the Gradio interface object
904
  app_interface = rag.gradio_interface()
905
+ SERVER_PORT = int(os.getenv("PORT", 7860)) # Use PORT env var for Hugging Face Spaces
906
 
907
  logging.info(f"Launching Gradio app on http://0.0.0.0:{SERVER_PORT}")
908
  print(f"\n--- Gradio App Running ---\nAccess at: http://localhost:{SERVER_PORT} or your public Spaces URL\n--------------------------\n")
909
+ app_interface.launch(server_name="0.0.0.0", server_port=SERVER_PORT, share=False) # share=False is typical for Spaces
910
 
911
  except ModuleNotFoundError as e:
912
  if "vector_db" in str(e):