Nischal Subedi commited on
Commit
57afd75
·
1 Parent(s): 3178581
Files changed (1) hide show
  1. app.py +180 -229
app.py CHANGED
@@ -12,14 +12,12 @@ 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 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
@@ -184,7 +182,7 @@ Answer:"""
184
  error_message = "Error: AI answer generation failed."
185
  details = f"Details: {str(e)}"
186
  if "authentication" in str(e).lower():
187
- error_message = "Error: OpenAI API Key is invalid or expired. Please check your key."
188
  details = ""
189
  elif "rate limit" in str(e).lower():
190
  error_message = "Error: You've exceeded your OpenAI API rate limit or quota. Please check your usage and plan limits, or wait and try again."
@@ -224,7 +222,6 @@ Answer:"""
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,10 +239,9 @@ Answer:"""
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:
@@ -253,29 +249,23 @@ Answer:"""
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,315 +276,294 @@ Answer:"""
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
- --header-bg-color: hsl(180, 70%, 92%); /* Distinct light seafoam for header */
 
 
 
 
 
 
 
 
321
  }
322
 
323
- /* Dark mode variables - for consistency if a dark mode toggle were present */
324
- body.dark {
325
- --background-primary: #1F303A; /* Dark blue-green */
326
- --background-secondary: #2C404B;
327
- --text-primary: #E0F2F1;
328
- --text-secondary: #A7C5C8;
329
- --border-color: #5F7C8A;
330
- --primary-color: #66BB6A; /* Brighter green for dark mode */
331
- --primary-hover: #5cb85f;
332
- --error-bg: #3F1D1D;
333
- --error-border: #5A1A1A;
334
- --error-text: #FF7070;
335
- --header-bg-color: #253842; /* Darker header for dark mode */
336
- }
337
-
338
- /* Base container improvements */
339
  .gradio-container {
340
- max-width: 900px !important; /* Slightly smaller for focused content */
341
- margin: 0 auto !important; /* Center the whole app */
342
  padding: 1.5rem !important;
343
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
344
- background-color: var(--background-secondary) !important; /* Overall background */
345
- box-shadow: none !important; /* Remove default gradio container shadow */
346
- }
347
- /* Ensure all main content sections have primary background */
348
- .main-dashboard-container > * {
349
- background-color: var(--background-primary) !important;
350
  }
351
 
352
- /* Header styling - centered and prominent */
353
  .app-header-wrapper {
354
- background: var(--header-bg-color) !important; /* Use new distinct header background color */
355
- border: 2px solid var(--border-color) !important;
356
  border-radius: 16px !important;
357
- padding: 2.5rem 1.5rem !important; /* More vertical padding */
358
  margin-bottom: 1.5rem !important;
359
- text-align: center !important; /* Center text within header */
360
  box-shadow: var(--shadow-md) !important;
361
- position: relative; /* For potential pseudo-element effects */
362
- overflow: hidden; /* For any overflow animations */
 
363
  }
364
- .app-header-wrapper::before { /* Subtle background pattern for dynamism */
365
  content: '';
366
  position: absolute;
367
  top: 0;
368
  left: 0;
369
  width: 100%;
370
  height: 100%;
371
- background: radial-gradient(circle at top left, rgba(60,179,113,0.05) 0%, transparent 40%),
372
- radial-gradient(circle at bottom right, rgba(60,179,113,0.05) 0%, transparent 40%);
373
  z-index: 0;
374
  opacity: 0.8;
375
  pointer-events: none;
376
  }
377
-
378
  .app-header-logo {
379
  font-size: 4.5rem !important; /* Larger icon */
380
  margin-bottom: 0.75rem !important;
381
  display: block !important;
382
- color: var(--primary-color) !important; /* Theme color */
383
  position: relative;
384
- z-index: 1; /* Bring icon to front of pseudo-element */
385
- /* Animation for dynamism */
386
  animation: float-icon 3s ease-in-out infinite alternate;
387
  }
388
- /* Keyframes for floating icon */
389
  @keyframes float-icon {
390
  0% { transform: translateY(0px); }
391
  50% { transform: translateY(-5px); }
392
  100% { transform: translateY(0px); }
393
  }
394
-
395
  .app-header-title {
396
  font-family: 'Poppins', sans-serif !important;
397
- font-size: 3rem !important; /* Even larger title */
398
- font-weight: 800 !important; /* Bolder */
399
- color: var(--text-primary) !important;
400
  margin: 0 0 0.75rem 0 !important;
401
  line-height: 1.1 !important;
402
- letter-spacing: -0.03em !important; /* Tighter spacing */
403
  position: relative;
404
  z-index: 1;
405
  }
406
  .app-header-tagline {
407
- font-size: 1.25rem !important; /* Slightly larger tagline */
408
- color: var(--text-secondary) !important;
409
  font-weight: 400 !important;
410
  margin: 0 !important;
411
- max-width: 700px; /* Constrain tagline width */
412
  margin-left: auto;
413
  margin-right: auto;
414
  position: relative;
415
  z-index: 1;
416
  }
417
 
418
- /* Main container with consistent spacing */
419
  .main-dashboard-container {
420
  display: flex !important;
421
  flex-direction: column !important;
422
- gap: 1.25rem !important; /* Consistent spacing between cards */
423
  }
424
- /* Card sections with clear boundaries (boundeyes) and subtle dynamic effects */
425
  .dashboard-card-section {
426
- background: var(--background-primary) !important;
427
- border: 2px solid var(--border-color) !important; /* Distinct border */
428
  border-radius: 12px !important;
429
- padding: 1.75rem !important; /* Consistent padding */
430
- box-shadow: var(--shadow-sm) !important; /* Subtle shadow */
431
- transition: all 0.3s ease-out !important; /* Smoother transition */
432
- cursor: default; /* Indicate not directly clickable (unless examples) */
 
433
  }
434
  .dashboard-card-section:hover {
435
  box-shadow: var(--shadow-md) !important;
436
- transform: translateY(-3px) !important; /* More pronounced lift */
437
  }
438
 
439
- /* Centered section titles with improved typography and consistent line */
440
  .sub-section-title {
441
  font-family: 'Poppins', sans-serif !important;
442
- font-size: 1.7rem !important; /* Slightly larger */
443
- font-weight: 700 !important; /* Bolder */
444
- color: var(--text-primary) !important;
445
- text-align: center !important; /* Centered text */
446
- margin: 0 0 1.25rem 0 !important; /* More space below title */
447
  padding-bottom: 0.75rem !important;
448
- border-bottom: 2px solid var(--border-color) !important; /* Underline effect (the "line") */
449
  display: block !important;
450
  letter-spacing: -0.01em !important;
451
  }
452
-
453
- /* Specific styling for the markdown content within cards */
454
  .dashboard-card-section p {
455
  line-height: 1.7 !important;
456
- color: var(--text-primary) !important;
457
  font-size: 1rem !important;
458
  }
459
  .dashboard-card-section strong {
460
- color: var(--primary-color) !important; /* Highlight strong text with primary color */
461
  }
462
 
463
- /* Improved input styling with clear boundaries and focus */
464
- .gradio-textbox, .gradio-dropdown {
465
- margin-bottom: 0.75rem !important;
466
- }
467
  .gradio-textbox textarea,
468
  .gradio-textbox input,
469
- .gradio-dropdown > div > input[type="text"], /* Target dropdown input for custom values */
470
- .gradio-dropdown .primary-wrap, /* Target dropdown wrapper */
471
- .gradio-dropdown .scroll-hide /* Target dropdown list container for consistency */
472
- {
473
- background: var(--background-primary) !important;
474
- border: 2px solid var(--border-color) !important; /* Clear border */
475
  border-radius: 8px !important;
476
- padding: 0.85rem 1rem !important; /* Slightly more padding */
477
  font-size: 0.98rem !important;
478
  font-family: 'Inter', sans-serif !important;
479
- color: var(--text-primary) !important;
480
- transition: border-color 0.2s ease, box-shadow 0.2s ease !important; /* Smooth transitions */
481
  box-shadow: var(--shadow-sm) !important;
482
  }
 
 
 
 
 
483
  .gradio-textbox textarea:focus,
484
  .gradio-textbox input:focus,
485
  .gradio-dropdown > div > input[type="text"]:focus,
486
- .gradio-dropdown .primary-wrap.focused { /* Apply focus style to dropdown wrap */
487
  outline: none !important;
488
- border-color: var(--border-focus) !important; /* Distinct border on focus (green) */
489
- box-shadow: 0 0 0 4px rgba(60, 179, 113, 0.2) !important; /* Broader, softer glow on focus */
490
  }
491
-
492
- /* Label styling for better readability */
493
  .gradio-textbox label,
494
  .gradio-dropdown label {
495
- font-weight: 600 !important; /* Bolder labels */
496
- color: var(--text-primary) !important;
497
  font-size: 1rem !important;
498
  margin-bottom: 0.6rem !important;
499
  display: block !important;
500
  }
501
- /* Info text styling below inputs */
502
  .gradio-textbox .gr-form,
503
  .gradio-dropdown .gr-form {
504
  font-size: 0.9rem !important;
505
- color: var(--text-secondary) !important;
506
- margin-top: 0.4rem !important; /* More space for info text */
507
  }
508
- /* Input row layout improvements */
509
  .input-row {
510
  display: flex !important;
511
- gap: 1.25rem !important; /* Consistent gap between query and state */
512
  margin-bottom: 0.5rem !important;
513
  }
514
  .input-field {
515
  flex: 1 !important;
516
  }
517
 
518
- /* Button styling improvements with active state for dynamism */
519
  .button-row {
520
  display: flex !important;
521
  gap: 1rem !important;
522
- justify-content: flex-end !important; /* Align buttons to the right */
523
- margin-top: 1.5rem !important; /* More space above buttons */
524
  }
525
  .gradio-button {
526
- padding: 0.85rem 1.8rem !important; /* More padding for bigger buttons */
527
- border-radius: 9px !important; /* Slightly more rounded */
528
- font-weight: 600 !important; /* Bolder text */
529
  font-size: 1rem !important;
530
- transition: all 0.2s ease-out !important; /* Smooth transition for hover/active */
531
  cursor: pointer !important;
532
  border: 2px solid transparent !important;
533
- text-align: center !important; /* Ensure button text is centered */
534
  }
535
  .gr-button-primary {
536
- background: var(--primary-color) !important;
537
  color: white !important;
538
  box-shadow: var(--shadow-sm) !important;
539
  }
540
  .gr-button-primary:hover {
541
  background: var(--primary-hover) !important;
542
  box-shadow: var(--shadow-md) !important;
543
- transform: translateY(-2px) !important; /* Subtle lift effect on hover */
544
  }
545
- .gr-button-primary:active { /* Press down effect on click */
546
  transform: translateY(1px) !important;
547
  box-shadow: none !important;
548
  }
549
  .gr-button-secondary {
550
- background: transparent !important;
551
- color: var(--text-primary) !important;
552
- border-color: var(--border-color) !important;
553
  }
554
  .gr-button-secondary:hover {
555
- background: var(--background-secondary) !important;
556
  border-color: var(--primary-color) !important;
557
  transform: translateY(-2px) !important;
558
  }
559
- .gr-button-secondary:active { /* Press down effect on click */
560
  transform: translateY(1px) !important;
561
  box-shadow: none !important;
562
  }
563
 
564
- /* Output styling with clear boundaries (boundeyes are clear) and dynamic fade-in */
565
  .output-content-wrapper {
566
- background: var(--background-primary) !important;
567
- border: 2px solid var(--border-color) !important; /* Clear border */
568
  border-radius: 8px !important;
569
  padding: 1.5rem !important;
570
- min-height: 150px !important; /* More space for output */
571
- color: var(--text-primary) !important;
572
- /* Ensure the inner animated content fits well */
573
  display: flex;
574
  flex-direction: column;
575
- justify-content: center; /* Center content vertically if small */
576
- align-items: center; /* Center content horizontally if small */
577
  }
578
- /* The div holding the actual response content, enabling fade-in animation */
579
  .animated-output-content {
580
  opacity: 0;
581
- animation: fadeInAndSlideUp 0.7s ease-out forwards; /* More pronounced animation */
582
- width: 100%; /* Take full width of parent */
583
- /* Preserve formatting within the animated content */
584
  white-space: pre-wrap;
585
  overflow-wrap: break-word;
586
  word-break: break-word;
587
- text-align: left !important; /* Ensure text is left-aligned within this div */
588
  }
589
  @keyframes fadeInAndSlideUp {
590
  from { opacity: 0; transform: translateY(15px); }
591
  to { opacity: 1; transform: translateY(0); }
592
  }
593
-
594
  .response-header {
595
  font-size: 1.3rem !important;
596
  font-weight: 700 !important;
597
- color: var(--primary-color) !important; /* Matches primary color */
598
  margin-bottom: 0.75rem !important;
599
  display: flex !important;
600
  align-items: center !important;
@@ -606,10 +575,9 @@ Answer:"""
606
  }
607
  .divider {
608
  border: none !important;
609
- border-top: 1px dashed var(--border-color) !important; /* Dashed divider for visual separation */
610
  margin: 1rem 0 !important;
611
  }
612
- /* Error message styling */
613
  .error-message {
614
  background: var(--error-bg) !important;
615
  border: 2px solid var(--error-border) !important;
@@ -622,9 +590,9 @@ Answer:"""
622
  font-size: 0.95rem !important;
623
  font-weight: 500 !important;
624
  line-height: 1.6 !important;
625
- text-align: left !important; /* Ensure error message text is left aligned */
626
- width: 100%; /* Take full width of parent */
627
- box-sizing: border-box; /* Include padding/border in width */
628
  }
629
  .error-message a {
630
  color: var(--error-text) !important;
@@ -641,23 +609,22 @@ Answer:"""
641
  margin-top: 0.5rem !important;
642
  opacity: 0.8;
643
  }
644
- /* Placeholder styling for empty output */
645
  .placeholder {
646
- background: var(--background-secondary) !important;
647
- border: 2px dashed var(--border-color) !important;
648
  border-radius: 8px !important;
649
  padding: 2.5rem 1.5rem !important;
650
  text-align: center !important;
651
- color: var(--text-secondary) !important;
652
  font-style: italic !important;
653
  font-size: 1.1rem !important;
654
- width: 100%; /* Ensure it takes full width of parent */
655
- box-sizing: border-box; /* Include padding/border in width */
656
  }
657
 
658
- /* Examples table styling with dynamic hover */
659
  .examples-section .gr-samples-table {
660
- border: 2px solid var(--border-color) !important;
661
  border-radius: 8px !important;
662
  overflow: hidden !important;
663
  margin-top: 1rem !important;
@@ -667,44 +634,43 @@ Answer:"""
667
  padding: 0.9rem !important;
668
  border: none !important;
669
  font-size: 0.95rem !important;
670
- text-align: left !important; /* Ensure example text is left-aligned */
 
671
  }
672
  .examples-section .gr-samples-table th {
673
- background: var(--background-secondary) !important;
674
  font-weight: 700 !important;
675
- color: var(--text-primary) !important;
676
  }
677
  .examples-section .gr-samples-table td {
678
- background: var(--background-primary) !important;
679
- color: var(--text-primary) !important;
680
- border-top: 1px solid var(--border-color) !important;
681
  cursor: pointer !important;
682
- transition: background 0.2s ease, transform 0.1s ease !important; /* Smooth transitions */
683
  }
684
  .examples-section .gr-samples-table tr:hover td {
685
- background: var(--background-secondary) !important;
686
- transform: translateX(5px); /* Subtle slide on hover */
687
  }
688
- /* Hide Gradio default elements for examples for cleaner look */
689
  .gr-examples .gr-label,
690
  .gr-examples .label-wrap,
691
  .gr-examples .gr-accordion-header {
692
  display: none !important;
693
  }
694
 
695
- /* Footer styling - centered text and green links */
696
  .app-footer-wrapper {
697
- background: var(--background-secondary) !important;
698
- border: 2px solid var(--border-color) !important;
699
  border-radius: 12px !important;
700
  padding: 1.75rem !important;
701
  margin-top: 1.5rem !important;
702
- text-align: center !important; /* Centered footer text */
 
703
  }
704
  .app-footer p {
705
  margin: 0.6rem 0 !important;
706
  font-size: 0.95rem !important;
707
- color: var(--text-secondary) !important;
708
  line-height: 1.6 !important;
709
  }
710
  .app-footer a {
@@ -718,7 +684,7 @@ Answer:"""
718
  text-decoration-color: var(--primary-color) !important; /* Green underline on hover */
719
  }
720
 
721
- /* Responsive design for smaller screens */
722
  @media (max-width: 768px) {
723
  .gradio-container {
724
  padding: 1rem !important;
@@ -733,13 +699,13 @@ Answer:"""
733
  font-size: 1.4rem !important;
734
  }
735
  .input-row {
736
- flex-direction: column !important; /* Stack inputs vertically */
737
  }
738
  .button-row {
739
- flex-direction: column !important; /* Stack buttons vertically */
740
  }
741
  .gradio-button {
742
- width: 100% !important; /* Full width buttons */
743
  }
744
  .dashboard-card-section {
745
  padding: 1.25rem !important;
@@ -754,11 +720,8 @@ Answer:"""
754
  }
755
  """
756
 
757
- # Using gr.Blocks with the specified theme and custom CSS
758
  with gr.Blocks(theme="shivi/calm_seafoam", css=custom_css, title="Landlord-Tenant Rights Assistant") as demo:
759
- # Header Section - uses gr.Group for distinct card-like styling
760
  with gr.Group(elem_classes="app-header-wrapper"):
761
- # Markdown used for flexible styling and auto-centering via CSS
762
  gr.Markdown(
763
  """
764
  <div class="app-header">
@@ -769,12 +732,10 @@ Answer:"""
769
  """
770
  )
771
 
772
- # Main Dashboard Container - acts as a column to stack various sections
773
  with gr.Column(elem_classes="main-dashboard-container"):
774
 
775
- # Introduction and Disclaimer Card
776
  with gr.Group(elem_classes="dashboard-card-section"):
777
- gr.Markdown("<h3 class='sub-section-title'>Welcome & Disclaimer</h3>") # Centered by CSS
778
  gr.Markdown(
779
  """
780
  Navigate landlord-tenant laws with ease. This assistant provides detailed, state-specific answers grounded in legal authority.
@@ -783,23 +744,21 @@ Answer:"""
783
  """
784
  )
785
 
786
- # OpenAI API Key Input Card
787
  with gr.Group(elem_classes="dashboard-card-section"):
788
- gr.Markdown("<h3 class='sub-section-title'>OpenAI API Key</h3>") # Centered by CSS
789
  api_key_input = gr.Textbox(
790
  label="API Key",
791
- type="password", # Hides the input for security
792
  placeholder="Enter your OpenAI API key (e.g., sk-...)",
793
  info="Required to process your query. Get one from OpenAI: platform.openai.com/api-keys",
794
  lines=1,
795
- elem_classes=["input-field-group"] # Custom class for input styling
796
  )
797
 
798
- # Query Input and State Selection Card
799
  with gr.Group(elem_classes="dashboard-card-section"):
800
- gr.Markdown("<h3 class='sub-section-title'>Ask Your Question</h3>") # Centered by CSS
801
- with gr.Row(elem_classes="input-row"): # Row for side-by-side query and state
802
- with gr.Column(elem_classes="input-field", scale=3): # Query text area takes more space
803
  query_input = gr.Textbox(
804
  label="Your Question",
805
  placeholder="E.g., What are the rules for security deposit returns in my state?",
@@ -807,7 +766,7 @@ Answer:"""
807
  max_lines=8,
808
  elem_classes=["input-field-group"]
809
  )
810
- with gr.Column(elem_classes="input-field", scale=1): # State dropdown takes less space
811
  state_input = gr.Dropdown(
812
  label="Select State",
813
  choices=dropdown_choices,
@@ -815,32 +774,29 @@ Answer:"""
815
  allow_custom_value=False,
816
  elem_classes=["input-field-group"]
817
  )
818
- with gr.Row(elem_classes="button-row"): # Row for action buttons
819
  clear_button = gr.Button("Clear", variant="secondary", elem_classes=["gr-button-secondary"])
820
  submit_button = gr.Button("Submit Query", variant="primary", elem_classes=["gr-button-primary"])
821
 
822
- # Output Display Card - Using gr.HTML for better animation control
823
  with gr.Group(elem_classes="dashboard-card-section"):
824
- gr.Markdown("<h3 class='sub-section-title'>Legal Assistant's Response</h3>") # Centered by CSS
825
- output = gr.HTML( # Changed to gr.HTML to wrap content with animation class
826
  value="<div class='placeholder'>The answer will appear here after submitting your query.</div>",
827
- elem_classes="output-content-wrapper" # Custom class for output styling
828
  )
829
 
830
- # Example Questions Section
831
  with gr.Group(elem_classes="dashboard-card-section examples-section"):
832
- gr.Markdown("<h3 class='sub-section-title'>Example Questions</h3>") # Centered by CSS
833
  if example_queries:
834
  gr.Examples(
835
  examples=example_queries,
836
  inputs=[query_input, state_input],
837
  examples_per_page=5,
838
- label="" # Hide default Gradio label for examples to use our custom title
839
  )
840
  else:
841
  gr.Markdown("<div class='placeholder'>Sample questions could not be loaded. Please ensure the vector database is populated.</div>")
842
 
843
- # Footer Section - contains disclaimer and developer info
844
  with gr.Group(elem_classes="app-footer-wrapper"):
845
  gr.Markdown(
846
  """
@@ -849,20 +805,19 @@ Answer:"""
849
  """
850
  )
851
 
852
- # Event Listeners for buttons
853
  submit_button.click(
854
  fn=query_interface_wrapper,
855
  inputs=[api_key_input, query_input, state_input],
856
  outputs=output,
857
- api_name="submit_query" # Useful for debugging / external calls
858
  )
859
 
860
  clear_button.click(
861
  fn=lambda: (
862
- "", # Clear API key input
863
- "", # Clear query input
864
- initial_value, # Reset state dropdown to default prompt
865
- "<div class='placeholder'>Inputs cleared. Ready for your next question.</div>" # Reset output message
866
  ),
867
  inputs=[],
868
  outputs=[api_key_input, query_input, state_input, output]
@@ -881,36 +836,32 @@ if __name__ == "__main__":
881
  PDF_PATH = os.getenv("PDF_PATH", DEFAULT_PDF_PATH)
882
  VECTOR_DB_PATH = os.getenv("VECTOR_DB_PATH", DEFAULT_DB_PATH)
883
 
884
- # Ensure vector DB directory exists before initialization
885
  os.makedirs(os.path.dirname(VECTOR_DB_PATH), exist_ok=True)
886
 
887
  logging.info(f"Attempting to load PDF from: {PDF_PATH}")
888
  if not os.path.exists(PDF_PATH):
889
  logging.error(f"FATAL: PDF file not found at the specified path: {PDF_PATH}")
890
  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")
891
- exit(1) # Correctly exits if PDF is not found
892
 
893
  if not os.access(PDF_PATH, os.R_OK):
894
  logging.error(f"FATAL: PDF file at '{PDF_PATH}' exists but is not readable. Check file permissions.")
895
  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")
896
- exit(1) # Correctly exits if PDF is unreadable
897
 
898
  logging.info(f"PDF file '{os.path.basename(PDF_PATH)}' found and is readable.")
899
 
900
- # Initialize VectorDatabase and RAGSystem
901
  vector_db_instance = VectorDatabase(persist_directory=VECTOR_DB_PATH)
902
  rag = RAGSystem(vector_db=vector_db_instance)
903
 
904
- # Load PDF data into the vector DB (or verify it's already loaded)
905
  rag.load_pdf(PDF_PATH)
906
 
907
- # Get the Gradio interface object
908
  app_interface = rag.gradio_interface()
909
- SERVER_PORT = int(os.getenv("PORT", 7860)) # Use PORT env var for Hugging Face Spaces
910
 
911
  logging.info(f"Launching Gradio app on http://0.0.0.0:{SERVER_PORT}")
912
  print(f"\n--- Gradio App Running ---\nAccess at: http://localhost:{SERVER_PORT} or your public Spaces URL\n--------------------------\n")
913
- app_interface.launch(server_name="0.0.0.0", server_port=SERVER_PORT, share=False) # share=False is typical for Spaces
914
 
915
  except ModuleNotFoundError as e:
916
  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(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
 
182
  error_message = "Error: AI answer generation failed."
183
  details = f"Details: {str(e)}"
184
  if "authentication" in str(e).lower():
185
+ error_message = "Error: Authentication failed. Please double-check your OpenAI API key."
186
  details = ""
187
  elif "rate limit" in str(e).lower():
188
  error_message = "Error: You've exceeded your OpenAI API rate limit or quota. Please check your usage and plan limits, or wait and try again."
 
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
  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 (NEW UI DESIGN BASED ON IMAGE) ---
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'><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>"
247
  if not state or state == "Select a state..." or "Error" in state:
 
249
  if not query or not query.strip():
250
  return "<div class='error-message'><span class='error-icon'>⚠️</span>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'><span class='error-icon'>⚠️</span>An unexpected error occurred.</div>")
254
 
 
255
  if "<div class='error-message'>" in answer:
 
256
  return answer
257
  else:
 
258
  formatted_response_content = f"<div class='response-header'><span class='response-icon'>📜</span>Response for {state}</div><hr class='divider'>{answer}"
259
  return f"<div class='animated-output-content'>{formatted_response_content}</div>"
260
 
261
  try:
262
  available_states_list = self.get_states()
 
263
  dropdown_choices = ["Select a state..."] + (available_states_list if available_states_list and "Error" not in available_states_list[0] else ["Error: States unavailable"])
264
+ initial_value = dropdown_choices[0]
265
+ except Exception:
266
  dropdown_choices = ["Error: Critical failure loading states"]
267
  initial_value = dropdown_choices[0]
268
 
 
269
  example_queries_base = [
270
  ["What are the rules for security deposit returns?", "California"],
271
  ["Can a landlord enter my apartment without notice?", "New York"],
 
276
  example_queries = []
277
  if available_states_list and "Error" not in available_states_list[0] and len(available_states_list) > 0:
278
  loaded_states_set = set(available_states_list)
 
279
  example_queries = [ex for ex in example_queries_base if ex[1] in loaded_states_set]
 
280
  if not example_queries:
 
281
  example_queries.append(["What basic rights do tenants have?", available_states_list[0] if available_states_list else "California"])
282
+ else:
283
  example_queries.append(["What basic rights do tenants have?", "California"])
284
 
285
 
286
+ # Custom CSS for the dark theme based on the provided image
287
  custom_css = """
288
  /* Import legible fonts from Google Fonts */
289
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Poppins:wght@600;700;800&display=swap');
290
 
291
+ /* Root variables for consistent theming - based on image's dark theme */
292
  :root {
293
+ --primary-color: hsl(150, 60%, 50%); /* Vibrant Seafoam Green for highlights */
294
+ --primary-hover: hsl(150, 60%, 45%);
295
+
296
+ --background-app: hsl(210, 15%, 15%); /* Dark charcoal/navy for overall background */
297
+ --background-card: hsl(210, 30%, 20%); /* Deep blue for content cards */
298
+ --background-input-output: hsl(210, 25%, 28%); /* Slightly lighter blue for input/output elements */
299
+
300
+ --text-light: hsl(0, 0%, 95%); /* Very light text for dark backgrounds */
301
+ --text-muted-light: hsl(210, 10%, 70%); /* Muted light text for secondary info */
302
+
303
+ --border-light: hsl(210, 10%, 65%); /* Light gray for borders */
304
+ --border-input: hsl(210, 15%, 45%); /* Mid-tone blue-gray for input borders */
305
+
306
+ --shadow-sm: 0 1px 3px rgba(0,0,0,0.15);
307
+ --shadow-md: 0 4px 10px rgba(0,0,0,0.3);
308
+ --shadow-lg: 0 10px 20px rgba(0,0,0,0.4);
309
+
310
+ --error-bg: hsl(0, 70%, 20%); /* Dark red for error background */
311
+ --error-border: hsl(0, 70%, 35%);
312
+ --error-text: hsl(0, 90%, 80%); /* Light red for error text */
313
+
314
+ --button-secondary-bg: hsl(0, 0%, 85%); /* Light gray for secondary button */
315
+ --button-secondary-text: hsl(210, 30%, 20%); /* Dark blue for secondary button text */
316
  }
317
 
318
+ /* Body and Gradio Container */
319
+ body {
320
+ background-color: var(--background-app) !important;
321
+ }
 
 
 
 
 
 
 
 
 
 
 
 
322
  .gradio-container {
323
+ max-width: 900px !important;
324
+ margin: 0 auto !important;
325
  padding: 1.5rem !important;
326
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
327
+ background-color: var(--background-app) !important; /* Overall background */
328
+ box-shadow: none !important;
329
+ border: 2px solid var(--border-light) !important; /* Outer border as per image */
330
+ border-radius: 16px !important;
 
 
331
  }
332
 
333
+ /* Header Styling */
334
  .app-header-wrapper {
335
+ background: var(--background-card) !important; /* Consistent dark blue with other cards */
336
+ border: 2px solid var(--border-light) !important;
337
  border-radius: 16px !important;
338
+ padding: 2.5rem 1.5rem !important;
339
  margin-bottom: 1.5rem !important;
340
+ text-align: center !important;
341
  box-shadow: var(--shadow-md) !important;
342
+ position: relative;
343
+ overflow: hidden;
344
+ color: var(--text-light) !important; /* Ensure header text is light */
345
  }
346
+ .app-header-wrapper::before { /* Subtle background pattern */
347
  content: '';
348
  position: absolute;
349
  top: 0;
350
  left: 0;
351
  width: 100%;
352
  height: 100%;
353
+ background: radial-gradient(circle at top left, rgba(60,179,113,0.08) 0%, transparent 40%),
354
+ radial-gradient(circle at bottom right, rgba(60,179,113,0.08) 0%, transparent 40%);
355
  z-index: 0;
356
  opacity: 0.8;
357
  pointer-events: none;
358
  }
 
359
  .app-header-logo {
360
  font-size: 4.5rem !important; /* Larger icon */
361
  margin-bottom: 0.75rem !important;
362
  display: block !important;
363
+ color: var(--border-light) !important; /* Light gray for icon as per image */
364
  position: relative;
365
+ z-index: 1;
 
366
  animation: float-icon 3s ease-in-out infinite alternate;
367
  }
 
368
  @keyframes float-icon {
369
  0% { transform: translateY(0px); }
370
  50% { transform: translateY(-5px); }
371
  100% { transform: translateY(0px); }
372
  }
 
373
  .app-header-title {
374
  font-family: 'Poppins', sans-serif !important;
375
+ font-size: 3rem !important;
376
+ font-weight: 800 !important;
377
+ color: var(--text-light) !important; /* White for title */
378
  margin: 0 0 0.75rem 0 !important;
379
  line-height: 1.1 !important;
380
+ letter-spacing: -0.03em !important;
381
  position: relative;
382
  z-index: 1;
383
  }
384
  .app-header-tagline {
385
+ font-size: 1.25rem !important;
386
+ color: var(--text-light) !important; /* White for tagline */
387
  font-weight: 400 !important;
388
  margin: 0 !important;
389
+ max-width: 700px;
390
  margin-left: auto;
391
  margin-right: auto;
392
  position: relative;
393
  z-index: 1;
394
  }
395
 
396
+ /* Main Dashboard Container & Card Sections */
397
  .main-dashboard-container {
398
  display: flex !important;
399
  flex-direction: column !important;
400
+ gap: 1.25rem !important;
401
  }
 
402
  .dashboard-card-section {
403
+ background: var(--background-card) !important; /* Deep blue for content cards */
404
+ border: 2px solid var(--border-light) !important;
405
  border-radius: 12px !important;
406
+ padding: 1.75rem !important;
407
+ box-shadow: var(--shadow-sm) !important;
408
+ transition: all 0.3s ease-out !important;
409
+ cursor: default;
410
+ color: var(--text-light) !important; /* Ensure card content text is light */
411
  }
412
  .dashboard-card-section:hover {
413
  box-shadow: var(--shadow-md) !important;
414
+ transform: translateY(-3px) !important;
415
  }
416
 
417
+ /* Section Titles (e.g., "Welcome & Disclaimer") */
418
  .sub-section-title {
419
  font-family: 'Poppins', sans-serif !important;
420
+ font-size: 1.7rem !important;
421
+ font-weight: 700 !important;
422
+ color: var(--text-light) !important; /* White for titles */
423
+ text-align: left !important; /* LEFT-ALIGNED as per image */
424
+ margin: 0 0 1.25rem 0 !important;
425
  padding-bottom: 0.75rem !important;
426
+ border-bottom: 1px dashed rgba(255, 255, 255, 0.15) !important; /* Subtle light dashed line as separator */
427
  display: block !important;
428
  letter-spacing: -0.01em !important;
429
  }
 
 
430
  .dashboard-card-section p {
431
  line-height: 1.7 !important;
432
+ color: var(--text-light) !important;
433
  font-size: 1rem !important;
434
  }
435
  .dashboard-card-section strong {
436
+ color: var(--primary-color) !important; /* Green for strong text */
437
  }
438
 
439
+ /* Input Styling */
 
 
 
440
  .gradio-textbox textarea,
441
  .gradio-textbox input,
442
+ .gradio-dropdown > div > input[type="text"],
443
+ .gradio-dropdown .primary-wrap,
444
+ .gradio-dropdown .scroll-hide {
445
+ background: var(--background-input-output) !important; /* Lighter blue for inputs */
446
+ border: 2px solid var(--border-input) !important; /* Mid-tone blue-gray border */
 
447
  border-radius: 8px !important;
448
+ padding: 0.85rem 1rem !important;
449
  font-size: 0.98rem !important;
450
  font-family: 'Inter', sans-serif !important;
451
+ color: var(--text-light) !important; /* White text for inputs */
452
+ transition: border-color 0.2s ease, box-shadow 0.2s ease !important;
453
  box-shadow: var(--shadow-sm) !important;
454
  }
455
+ .gradio-textbox textarea::placeholder,
456
+ .gradio-textbox input::placeholder {
457
+ color: var(--text-muted-light) !important; /* Muted light placeholder text */
458
+ opacity: 0.7;
459
+ }
460
  .gradio-textbox textarea:focus,
461
  .gradio-textbox input:focus,
462
  .gradio-dropdown > div > input[type="text"]:focus,
463
+ .gradio-dropdown .primary-wrap.focused {
464
  outline: none !important;
465
+ border-color: var(--primary-color) !important; /* Green border on focus */
466
+ box-shadow: 0 0 0 4px rgba(60, 179, 113, 0.3) !important; /* Green glow on focus */
467
  }
 
 
468
  .gradio-textbox label,
469
  .gradio-dropdown label {
470
+ font-weight: 600 !important;
471
+ color: var(--text-light) !important; /* White labels */
472
  font-size: 1rem !important;
473
  margin-bottom: 0.6rem !important;
474
  display: block !important;
475
  }
 
476
  .gradio-textbox .gr-form,
477
  .gradio-dropdown .gr-form {
478
  font-size: 0.9rem !important;
479
+ color: var(--text-muted-light) !important; /* Muted light info text */
480
+ margin-top: 0.4rem !important;
481
  }
 
482
  .input-row {
483
  display: flex !important;
484
+ gap: 1.25rem !important;
485
  margin-bottom: 0.5rem !important;
486
  }
487
  .input-field {
488
  flex: 1 !important;
489
  }
490
 
491
+ /* Button Styling */
492
  .button-row {
493
  display: flex !important;
494
  gap: 1rem !important;
495
+ justify-content: flex-end !important;
496
+ margin-top: 1.5rem !important;
497
  }
498
  .gradio-button {
499
+ padding: 0.85rem 1.8rem !important;
500
+ border-radius: 9px !important;
501
+ font-weight: 600 !important;
502
  font-size: 1rem !important;
503
+ transition: all 0.2s ease-out !important;
504
  cursor: pointer !important;
505
  border: 2px solid transparent !important;
506
+ text-align: center !important;
507
  }
508
  .gr-button-primary {
509
+ background: var(--primary-color) !important; /* Green button */
510
  color: white !important;
511
  box-shadow: var(--shadow-sm) !important;
512
  }
513
  .gr-button-primary:hover {
514
  background: var(--primary-hover) !important;
515
  box-shadow: var(--shadow-md) !important;
516
+ transform: translateY(-2px) !important;
517
  }
518
+ .gr-button-primary:active {
519
  transform: translateY(1px) !important;
520
  box-shadow: none !important;
521
  }
522
  .gr-button-secondary {
523
+ background: var(--button-secondary-bg) !important; /* Light gray button */
524
+ color: var(--button-secondary-text) !important; /* Dark text */
525
+ border-color: var(--border-light) !important;
526
  }
527
  .gr-button-secondary:hover {
528
+ background: hsl(0, 0%, 75%) !important; /* Darker gray on hover */
529
  border-color: var(--primary-color) !important;
530
  transform: translateY(-2px) !important;
531
  }
532
+ .gr-button-secondary:active {
533
  transform: translateY(1px) !important;
534
  box-shadow: none !important;
535
  }
536
 
537
+ /* Output Styling */
538
  .output-content-wrapper {
539
+ background: var(--background-input-output) !important; /* Lighter blue for output box */
540
+ border: 2px dashed var(--border-input) !important; /* Dashed border as per image */
541
  border-radius: 8px !important;
542
  padding: 1.5rem !important;
543
+ min-height: 150px !important;
544
+ color: var(--text-light) !important; /* White text for output */
 
545
  display: flex;
546
  flex-direction: column;
547
+ justify-content: center;
548
+ align-items: center;
549
  }
 
550
  .animated-output-content {
551
  opacity: 0;
552
+ animation: fadeInAndSlideUp 0.7s ease-out forwards;
553
+ width: 100%;
 
554
  white-space: pre-wrap;
555
  overflow-wrap: break-word;
556
  word-break: break-word;
557
+ text-align: left !important;
558
  }
559
  @keyframes fadeInAndSlideUp {
560
  from { opacity: 0; transform: translateY(15px); }
561
  to { opacity: 1; transform: translateY(0); }
562
  }
 
563
  .response-header {
564
  font-size: 1.3rem !important;
565
  font-weight: 700 !important;
566
+ color: var(--primary-color) !important;
567
  margin-bottom: 0.75rem !important;
568
  display: flex !important;
569
  align-items: center !important;
 
575
  }
576
  .divider {
577
  border: none !important;
578
+ border-top: 1px dashed rgba(255, 255, 255, 0.15) !important; /* Light dashed divider */
579
  margin: 1rem 0 !important;
580
  }
 
581
  .error-message {
582
  background: var(--error-bg) !important;
583
  border: 2px solid var(--error-border) !important;
 
590
  font-size: 0.95rem !important;
591
  font-weight: 500 !important;
592
  line-height: 1.6 !important;
593
+ text-align: left !important;
594
+ width: 100%;
595
+ box-sizing: border-box;
596
  }
597
  .error-message a {
598
  color: var(--error-text) !important;
 
609
  margin-top: 0.5rem !important;
610
  opacity: 0.8;
611
  }
 
612
  .placeholder {
613
+ background: var(--background-input-output) !important; /* Lighter blue for placeholder */
614
+ border: 2px dashed var(--border-input) !important; /* Dashed border */
615
  border-radius: 8px !important;
616
  padding: 2.5rem 1.5rem !important;
617
  text-align: center !important;
618
+ color: var(--text-muted-light) !important; /* Muted light text */
619
  font-style: italic !important;
620
  font-size: 1.1rem !important;
621
+ width: 100%;
622
+ box-sizing: border-box;
623
  }
624
 
625
+ /* Examples Table Styling */
626
  .examples-section .gr-samples-table {
627
+ border: 2px solid var(--border-light) !important;
628
  border-radius: 8px !important;
629
  overflow: hidden !important;
630
  margin-top: 1rem !important;
 
634
  padding: 0.9rem !important;
635
  border: none !important;
636
  font-size: 0.95rem !important;
637
+ text-align: left !important;
638
+ color: var(--text-light) !important; /* White text for examples */
639
  }
640
  .examples-section .gr-samples-table th {
641
+ background: var(--background-card) !important; /* Deep blue for example header */
642
  font-weight: 700 !important;
 
643
  }
644
  .examples-section .gr-samples-table td {
645
+ background: var(--background-input-output) !important; /* Lighter blue for example rows */
646
+ border-top: 1px solid var(--border-input) !important; /* Mid-tone blue-gray border */
 
647
  cursor: pointer !important;
648
+ transition: background 0.2s ease, transform 0.1s ease !important;
649
  }
650
  .examples-section .gr-samples-table tr:hover td {
651
+ background: hsl(210, 28%, 32%) !important; /* Slightly darker light blue on hover */
652
+ transform: translateX(5px);
653
  }
 
654
  .gr-examples .gr-label,
655
  .gr-examples .label-wrap,
656
  .gr-examples .gr-accordion-header {
657
  display: none !important;
658
  }
659
 
660
+ /* Footer Styling */
661
  .app-footer-wrapper {
662
+ background: var(--background-card) !important; /* Deep blue for footer */
663
+ border: 2px solid var(--border-light) !important;
664
  border-radius: 12px !important;
665
  padding: 1.75rem !important;
666
  margin-top: 1.5rem !important;
667
+ text-align: center !important;
668
+ color: var(--text-light) !important; /* White text for footer */
669
  }
670
  .app-footer p {
671
  margin: 0.6rem 0 !important;
672
  font-size: 0.95rem !important;
673
+ color: var(--text-muted-light) !important; /* Muted light text */
674
  line-height: 1.6 !important;
675
  }
676
  .app-footer a {
 
684
  text-decoration-color: var(--primary-color) !important; /* Green underline on hover */
685
  }
686
 
687
+ /* Responsive Design */
688
  @media (max-width: 768px) {
689
  .gradio-container {
690
  padding: 1rem !important;
 
699
  font-size: 1.4rem !important;
700
  }
701
  .input-row {
702
+ flex-direction: column !important;
703
  }
704
  .button-row {
705
+ flex-direction: column !important;
706
  }
707
  .gradio-button {
708
+ width: 100% !important;
709
  }
710
  .dashboard-card-section {
711
  padding: 1.25rem !important;
 
720
  }
721
  """
722
 
 
723
  with gr.Blocks(theme="shivi/calm_seafoam", css=custom_css, title="Landlord-Tenant Rights Assistant") as demo:
 
724
  with gr.Group(elem_classes="app-header-wrapper"):
 
725
  gr.Markdown(
726
  """
727
  <div class="app-header">
 
732
  """
733
  )
734
 
 
735
  with gr.Column(elem_classes="main-dashboard-container"):
736
 
 
737
  with gr.Group(elem_classes="dashboard-card-section"):
738
+ gr.Markdown("<h3 class='sub-section-title'>Welcome & Disclaimer</h3>")
739
  gr.Markdown(
740
  """
741
  Navigate landlord-tenant laws with ease. This assistant provides detailed, state-specific answers grounded in legal authority.
 
744
  """
745
  )
746
 
 
747
  with gr.Group(elem_classes="dashboard-card-section"):
748
+ gr.Markdown("<h3 class='sub-section-title'>OpenAI API Key</h3>")
749
  api_key_input = gr.Textbox(
750
  label="API Key",
751
+ type="password",
752
  placeholder="Enter your OpenAI API key (e.g., sk-...)",
753
  info="Required to process your query. Get one from OpenAI: platform.openai.com/api-keys",
754
  lines=1,
755
+ elem_classes=["input-field-group"]
756
  )
757
 
 
758
  with gr.Group(elem_classes="dashboard-card-section"):
759
+ gr.Markdown("<h3 class='sub-section-title'>Ask Your Question</h3>")
760
+ with gr.Row(elem_classes="input-row"):
761
+ with gr.Column(elem_classes="input-field", scale=3):
762
  query_input = gr.Textbox(
763
  label="Your Question",
764
  placeholder="E.g., What are the rules for security deposit returns in my state?",
 
766
  max_lines=8,
767
  elem_classes=["input-field-group"]
768
  )
769
+ with gr.Column(elem_classes="input-field", scale=1):
770
  state_input = gr.Dropdown(
771
  label="Select State",
772
  choices=dropdown_choices,
 
774
  allow_custom_value=False,
775
  elem_classes=["input-field-group"]
776
  )
777
+ with gr.Row(elem_classes="button-row"):
778
  clear_button = gr.Button("Clear", variant="secondary", elem_classes=["gr-button-secondary"])
779
  submit_button = gr.Button("Submit Query", variant="primary", elem_classes=["gr-button-primary"])
780
 
 
781
  with gr.Group(elem_classes="dashboard-card-section"):
782
+ gr.Markdown("<h3 class='sub-section-title'>Legal Assistant's Response</h3>")
783
+ output = gr.HTML(
784
  value="<div class='placeholder'>The answer will appear here after submitting your query.</div>",
785
+ elem_classes="output-content-wrapper"
786
  )
787
 
 
788
  with gr.Group(elem_classes="dashboard-card-section examples-section"):
789
+ gr.Markdown("<h3 class='sub-section-title'>Example Questions</h3>")
790
  if example_queries:
791
  gr.Examples(
792
  examples=example_queries,
793
  inputs=[query_input, state_input],
794
  examples_per_page=5,
795
+ label=""
796
  )
797
  else:
798
  gr.Markdown("<div class='placeholder'>Sample questions could not be loaded. Please ensure the vector database is populated.</div>")
799
 
 
800
  with gr.Group(elem_classes="app-footer-wrapper"):
801
  gr.Markdown(
802
  """
 
805
  """
806
  )
807
 
 
808
  submit_button.click(
809
  fn=query_interface_wrapper,
810
  inputs=[api_key_input, query_input, state_input],
811
  outputs=output,
812
+ api_name="submit_query"
813
  )
814
 
815
  clear_button.click(
816
  fn=lambda: (
817
+ "",
818
+ "",
819
+ initial_value,
820
+ "<div class='placeholder'>Inputs cleared. Ready for your next question.</div>"
821
  ),
822
  inputs=[],
823
  outputs=[api_key_input, query_input, state_input, output]
 
836
  PDF_PATH = os.getenv("PDF_PATH", DEFAULT_PDF_PATH)
837
  VECTOR_DB_PATH = os.getenv("VECTOR_DB_PATH", DEFAULT_DB_PATH)
838
 
 
839
  os.makedirs(os.path.dirname(VECTOR_DB_PATH), exist_ok=True)
840
 
841
  logging.info(f"Attempting to load PDF from: {PDF_PATH}")
842
  if not os.path.exists(PDF_PATH):
843
  logging.error(f"FATAL: PDF file not found at the specified path: {PDF_PATH}")
844
  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")
845
+ exit(1)
846
 
847
  if not os.access(PDF_PATH, os.R_OK):
848
  logging.error(f"FATAL: PDF file at '{PDF_PATH}' exists but is not readable. Check file permissions.")
849
  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")
850
+ exit(1)
851
 
852
  logging.info(f"PDF file '{os.path.basename(PDF_PATH)}' found and is readable.")
853
 
 
854
  vector_db_instance = VectorDatabase(persist_directory=VECTOR_DB_PATH)
855
  rag = RAGSystem(vector_db=vector_db_instance)
856
 
 
857
  rag.load_pdf(PDF_PATH)
858
 
 
859
  app_interface = rag.gradio_interface()
860
+ SERVER_PORT = int(os.getenv("PORT", 7860))
861
 
862
  logging.info(f"Launching Gradio app on http://0.0.0.0:{SERVER_PORT}")
863
  print(f"\n--- Gradio App Running ---\nAccess at: http://localhost:{SERVER_PORT} or your public Spaces URL\n--------------------------\n")
864
+ app_interface.launch(server_name="0.0.0.0", server_port=SERVER_PORT, share=False)
865
 
866
  except ModuleNotFoundError as e:
867
  if "vector_db" in str(e):