Nischal Subedi commited on
Commit
c9c872c
·
1 Parent(s): 4596ac3
Files changed (1) hide show
  1. app.py +303 -715
app.py CHANGED
@@ -1,174 +1,28 @@
1
  import os
2
  import logging
3
- import re
4
- import time # Added for time.sleep in placeholder functions
5
  from typing import Dict, List, Optional
6
  from functools import lru_cache
 
7
 
8
  import gradio as gr
9
- import gradio.themes as themes # Import gradio.themes (though not explicitly used in this exact UI, it's a good practice)
10
 
11
- # --- Ensure vector_db.py is accessible ---
12
  try:
13
  # Assuming vector_db.py exists in the same directory or is installed
14
- # Placeholder for VectorDatabase if the file is not provided
15
- class VectorDatabase:
16
- def __init__(self, persist_directory: str = "chroma_db"):
17
- self.persist_directory = persist_directory
18
- self.documents = {} # Simulating document storage
19
- self.states = [] # Simulating state storage
20
- logging.info(f"VectorDatabase initialized (placeholder) at {persist_directory}")
21
-
22
- def process_and_load_pdf(self, pdf_path: str) -> int:
23
- logging.info(f"Placeholder: Processing and loading PDF '{pdf_path}'...")
24
- # Simulate parsing a PDF and extracting content
25
- # In a real scenario, this would use PyPDFLoader, RecursiveCharacterTextSplitter, Chroma.from_documents
26
- if not self.documents: # Only load once for simulation
27
- self.documents = {
28
- "doc1": "California Civil Code § 1950.5: Security deposit limit is two months' rent. Must be returned within 21 days.",
29
- "doc2": "New York Real Property Law § 235-b: Implied Warranty of Habitability. Landlord must keep premises fit for human habitation.",
30
- "doc3": "Texas Property Code § 92.056: Landlord's duty to repair or remedy. Tenant must give notice and time to repair.",
31
- "doc4": "Florida Statutes § 83.56: Termination of rental agreement. Requires specific notice periods for rent increases or lease termination.",
32
- "doc5": "Illinois Landlord and Tenant Act § 765 ILCS 705/1: Security Deposit Return Act. Landlord must return deposit within 45 days. ",
33
- "doc6": "Washington RCW 59.18.230: Tenant's right to quiet enjoyment. Landlord may not interfere with tenant's privacy.",
34
- "state_summary_ca": "California: Strong tenant protections, rent control, and strict security deposit rules.",
35
- "state_summary_ny": "New York: Extensive habitability laws, rent stabilization in some areas, and detailed eviction procedures.",
36
- "state_summary_tx": "Texas: More landlord-friendly, but still has rules on repairs, evictions, and security deposits.",
37
- "state_summary_fl": "Florida: Clear statutes on lease termination, eviction, and security deposits.",
38
- "state_summary_il": "Illinois: Rules on security deposits and landlord's duties, especially in Chicago.",
39
- "state_summary_wa": "Washington: Just cause eviction, security deposit rules, and tenant privacy laws.",
40
- }
41
- self.states = ["California", "New York", "Texas", "Florida", "Illinois", "Washington", "Massachusetts", "Colorado", "Pennsylvania", "Ohio", "Georgia", "North Carolina", "Virginia", "Michigan", "Arizona"]
42
- logging.info(f"Placeholder: Simulated loading {len(self.documents)} documents and {len(self.states)} states.")
43
- return len(self.states)
44
-
45
- def query(self, query_text: str, state: str = None, n_results: int = 5) -> Dict[str, any]:
46
- logging.info(f"Placeholder: Querying DB for '{query_text[:50]}...' in state '{state}'")
47
- # Simulate relevant document retrieval
48
- doc_matches = []
49
- for key, content in self.documents.items():
50
- if state and state.lower() in key.lower() or query_text.lower() in content.lower():
51
- doc_matches.append(content)
52
-
53
- # Simple simulation: return up to n_results relevant docs and a state summary
54
- documents_retrieved = []
55
- metadatas_retrieved = []
56
- for i, doc_content in enumerate(doc_matches):
57
- if len(documents_retrieved) >= n_results:
58
- break
59
-
60
- # Extract state from content or use provided state
61
- match_state = "Unknown"
62
- for s in self.states:
63
- if s.lower() in doc_content.lower():
64
- match_state = s
65
- break
66
- if match_state == "Unknown" and state:
67
- match_state = state # Fallback to query state if not found in content
68
-
69
- documents_retrieved.append(doc_content)
70
- metadatas_retrieved.append({"state": match_state, "chunk_id": f"sim_chunk_{i+1}"})
71
-
72
- state_summary_doc = None
73
- state_summary_metadata = None
74
- if state:
75
- for key, content in self.documents.items():
76
- if f"state_summary_{state.lower()}" in key.lower().replace(" ", "_"):
77
- state_summary_doc = content
78
- state_summary_metadata = {"state": state, "type": "summary"}
79
- break
80
-
81
- results = {
82
- "document_results": {"documents": [documents_retrieved], "metadatas": [metadatas_retrieved]},
83
- "state_results": {"documents": [[state_summary_doc]] if state_summary_doc else [[]], "metadatas": [[state_summary_metadata]] if state_summary_metadata else [[]]}
84
- }
85
- logging.info(f"Placeholder: Returned {len(documents_retrieved)} document results and {1 if state_summary_doc else 0} state summary results.")
86
- return results
87
-
88
- def get_states(self) -> List[str]:
89
- logging.info("Placeholder: Getting states from DB")
90
- # Simulate loading states or return pre-defined ones
91
- return sorted(list(set(self.states)))
92
-
93
- def document_collection(self): # Simulates Chroma collection
94
- return type('Collection', (object,), {'count': lambda: len(self.documents)})()
95
-
96
- def state_collection(self): # Simulates Chroma collection
97
- return type('Collection', (object,), {'count': lambda: len(self.states)})()
98
-
99
-
100
  except ImportError:
101
- logging.error("Error: Could not import VectorDatabase. Using a placeholder for demonstration. Please ensure vector_db.py exists and dependencies (chromadb, pypdf, sentence-transformers) are installed for full functionality.")
102
- # Define a simple placeholder if vector_db.py is missing
103
- class VectorDatabase:
104
- def __init__(self, persist_directory: str = "chroma_db"):
105
- logging.warning("Using placeholder VectorDatabase. Full functionality requires 'vector_db.py'.")
106
- self.persist_directory = persist_directory
107
- self.documents = {}
108
- self.states = []
109
-
110
- def process_and_load_pdf(self, pdf_path: str) -> int:
111
- logging.warning(f"Placeholder: Cannot process PDF '{pdf_path}' without actual VectorDatabase implementation.")
112
- self.documents = {
113
- "doc1": "California Civil Code § 1950.5: Security deposit limit is two months' rent. Must be returned within 21 days.",
114
- "doc2": "New York Real Property Law § 235-b: Implied Warranty of Habitability. Landlord must keep premises fit for human habitation.",
115
- "doc3": "Texas Property Code § 92.056: Landlord's duty to repair or remedy. Tenant must give notice and time to repair.",
116
- }
117
- self.states = ["California", "New York", "Texas", "Florida", "Illinois"]
118
- return len(self.states) # Simulate some states loaded
119
-
120
- def query(self, query_text: str, state: str = None, n_results: int = 5) -> Dict[str, any]:
121
- logging.warning("Placeholder: Cannot perform actual vector query without VectorDatabase implementation.")
122
- # Simple dummy response
123
- if state == "California":
124
- return {"answer": f"Simulated response for California: Security deposits are governed by specific statutes like Civil Code § 1950.5.", "context_used": "Simulated context for CA"}
125
- return {"answer": f"Simulated response for {state}: Landlord-tenant laws vary by state.", "context_used": "Simulated general context"}
126
-
127
- def get_states(self) -> List[str]:
128
- logging.warning("Placeholder: Getting states from dummy VectorDatabase.")
129
- return ["California", "New York", "Texas", "Florida", "Illinois"]
130
-
131
- def document_collection(self): # Simulates Chroma collection
132
- return type('Collection', (object,), {'count': lambda: len(self.documents)})()
133
-
134
- def state_collection(self): # Simulates Chroma collection
135
- return type('Collection', (object,), {'count': lambda: len(self.states)})()
136
-
137
 
138
- # --- Ensure langchain_openai is accessible ---
139
  try:
140
  from langchain_openai import ChatOpenAI
141
- from langchain.prompts import PromptTemplate
142
- from langchain.chains import LLMChain
143
  except ImportError:
144
- logging.error("Error: langchain-openai or langchain components not found. Please install them: pip install langchain-openai langchain.")
145
- # Define placeholder classes if Langchain is missing
146
- class ChatOpenAI:
147
- def __init__(self, *args, **kwargs):
148
- logging.warning("Using placeholder ChatOpenAI. Install 'langchain-openai' for actual LLM functionality.")
149
- self.kwargs = kwargs
150
- def invoke(self, messages):
151
- if "fail" in messages.get("query", "").lower():
152
- raise Exception("Simulated LLM error.")
153
- return {"text": f"Placeholder LLM response for query: '{messages.get('query')}' in state '{messages.get('state')}'. Please install langchain-openai for real AI responses."}
154
-
155
- class PromptTemplate:
156
- def __init__(self, input_variables, template):
157
- self.input_variables = input_variables
158
- self.template = template
159
- logging.warning("Using placeholder PromptTemplate.")
160
-
161
- class LLMChain:
162
- def __init__(self, llm, prompt):
163
- self.llm = llm
164
- self.prompt = prompt
165
- logging.warning("Using placeholder LLMChain.")
166
- def invoke(self, input_data):
167
- # Simulate the prompt being filled and passed to LLM
168
- filled_prompt = self.prompt.template.format(**input_data)
169
- logging.info(f"Placeholder: LLMChain invoking with prompt: {filled_prompt[:100]}...")
170
- return self.llm.invoke(input_data)
171
 
 
 
172
 
173
  # Suppress warnings
174
  import warnings
@@ -182,7 +36,7 @@ logging.basicConfig(
182
  format='%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
183
  )
184
 
185
- # --- RAGSystem Class ---
186
  class RAGSystem:
187
  def __init__(self, vector_db: Optional[VectorDatabase] = None):
188
  logging.info("Initializing RAGSystem")
@@ -369,6 +223,7 @@ Answer:"""
369
  raise FileNotFoundError(f"PDF file not found: {pdf_path}")
370
  try:
371
  logging.info(f"Attempting to load/verify data from PDF: {pdf_path}")
 
372
  num_states_processed = self.vector_db.process_and_load_pdf(pdf_path)
373
  doc_count = self.vector_db.document_collection.count()
374
  state_count = self.vector_db.state_collection.count()
@@ -386,7 +241,7 @@ Answer:"""
386
  logging.error(f"Failed to load or process PDF '{pdf_path}': {str(e)}", exc_info=True)
387
  raise RuntimeError(f"Failed to process PDF '{pdf_path}': {e}") from e
388
 
389
- # --- GRADIO INTERFACE ---
390
  def gradio_interface(self):
391
  def query_interface_wrapper(api_key: str, query: str, state: str) -> str:
392
  # Basic client-side validation for immediate feedback (redundant but good UX)
@@ -423,13 +278,7 @@ Answer:"""
423
  ["Can a landlord enter my apartment without notice?", "New York"],
424
  ["My landlord hasn't made necessary repairs. What can I do?", "Texas"],
425
  ["How much notice must a landlord give to raise rent?", "Florida"],
426
- ["What is an implied warranty of habitability?", "Illinois"],
427
- ["Can a landlord evict a tenant for not paying rent?", "California"],
428
- ["What is a fixed-term lease?", "New York"],
429
- ["Are emotional support animals allowed?", "Texas"],
430
- ["What is a notice to quit?", "Florida"],
431
- ["How do I break my lease early?", "Illinois"],
432
- ["What are the quiet enjoyment rights?", "Washington"],
433
  ]
434
  example_queries = []
435
  if available_states_list and "Error" not in available_states_list[0] and len(available_states_list) > 0:
@@ -442,532 +291,288 @@ Answer:"""
442
  else: # Fallback if states list is problematic
443
  example_queries.append(["What basic rights do tenants have?", "California"])
444
 
445
- # Enhanced Custom CSS optimized for Paris theme
446
- custom_css = """
447
- /* Import premium fonts for better readability */
448
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Poppins:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
449
-
450
- /* Enhanced root variables optimized for Paris theme */
451
- :root {
452
- --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
453
- --secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
454
- --accent-color: #6366f1;
455
- --accent-hover: #4f46e5;
456
- --text-contrast: #1a202c;
457
- --text-muted: #718096;
458
- --border-strong: #e2e8f0;
459
- --border-subtle: #f1f5f9;
460
- --surface-primary: #ffffff;
461
- --surface-secondary: #f7fafc;
462
- --surface-accent: #edf2f7;
463
- --shadow-soft: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
464
- --shadow-medium: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
465
- --shadow-strong: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
466
- --border-radius-sm: 8px;
467
- --border-radius-md: 12px;
468
- --border-radius-lg: 16px;
469
- --spacing-xs: 0.5rem;
470
- --spacing-sm: 0.75rem;
471
- --spacing-md: 1rem;
472
- --spacing-lg: 1.5rem;
473
- --spacing-xl: 2rem;
474
- }
475
-
476
- /* Dark mode enhancements for Paris theme */
477
- @media (prefers-color-scheme: dark) {
478
- :root {
479
- --surface-primary: #1a202c;
480
- --surface-secondary: #2d3748;
481
- --surface-accent: #4a5568;
482
- --text-contrast: #f7fafc;
483
- --text-muted: #a0aec0;
484
- --border-strong: #4a5568;
485
- --border-subtle: #2d3748;
486
- }
487
- }
488
 
489
- /* Enhanced base container for Paris theme */
490
- .gradio-container {
491
- max-width: 1100px !important;
 
 
 
 
 
 
492
  margin: 0 auto !important;
493
- padding: var(--spacing-md) !important;
494
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
495
- background: var(--surface-secondary) !important;
496
- min-height: 100vh !important;
497
  }
498
-
499
- /* Stunning header with Paris theme integration */
500
  .app-header-wrapper {
501
- background: var(--primary-gradient) !important;
502
- border: 3px solid transparent !important;
503
- background-clip: padding-box !important;
504
- border-radius: var(--border-radius-lg) !important;
505
- padding: var(--spacing-xl) !important;
506
- margin-bottom: var(--spacing-lg) !important;
507
  text-align: center !important;
508
- box-shadow: var(--shadow-strong) !important;
509
- position: relative !important;
510
- overflow: hidden !important;
511
- }
512
-
513
- .app-header-wrapper::before {
514
- content: '';
515
- position: absolute;
516
- top: 0;
517
- left: 0;
518
- right: 0;
519
- bottom: 0;
520
- background: linear-gradient(45deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
521
- pointer-events: none;
 
522
  }
523
-
524
  .app-header-logo {
525
- font-size: 3.5rem !important;
526
- margin-bottom: var(--spacing-sm) !important;
527
- display: block !important;
528
- filter: drop-shadow(0 4px 8px rgba(0,0,0,0.3)) !important;
529
- animation: float 3s ease-in-out infinite !important;
530
- }
531
-
532
- @keyframes float {
533
- 0%, 100% { transform: translateY(0px); }
534
- 50% { transform: translateY(-10px); }
535
  }
536
-
537
  .app-header-title {
538
- font-family: 'Poppins', sans-serif !important;
539
- font-size: 2.75rem !important;
540
- font-weight: 800 !important;
541
- color: white !important;
542
- margin: 0 0 var(--spacing-sm) 0 !important;
543
- line-height: 1.1 !important;
544
- text-shadow: 0 4px 8px rgba(0,0,0,0.3) !important;
545
- letter-spacing: -0.02em !important;
546
  }
547
-
548
  .app-header-tagline {
549
- font-size: 1.2rem !important;
550
- color: rgba(255,255,255,0.9) !important;
551
- font-weight: 400 !important;
552
- margin: 0 !important;
553
- text-shadow: 0 2px 4px rgba(0,0,0,0.2) !important;
554
  }
555
-
556
- /* Compact and elegant main container */
557
  .main-dashboard-container {
558
- display: flex !important;
559
- flex-direction: column !important;
560
- gap: var(--spacing-md) !important;
561
- }
562
-
563
- /* Premium card design with superior boundaries */
 
 
 
564
  .dashboard-card-section {
565
- background: var(--surface-primary) !important;
566
- border: 2px solid var(--border-strong) !important;
567
- border-radius: var(--border-radius-md) !important;
568
- padding: var(--spacing-lg) !important;
569
- box-shadow: var(--shadow-soft) !important;
570
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
571
- position: relative !important;
572
- overflow: hidden !important;
573
  }
574
-
575
- .dashboard-card-section::before {
576
- content: '';
577
- position: absolute;
578
- top: 0;
579
- left: 0;
580
- width: 100%;
581
- height: 3px;
582
- background: var(--secondary-gradient);
583
- transform: translateX(-100%);
584
- transition: transform 0.3s ease;
585
- }
586
-
587
- .dashboard-card-section:hover {
588
- box-shadow: var(--shadow-medium) !important;
589
- transform: translateY(-2px) !important;
590
- border-color: var(--accent-color) !important;
591
  }
592
-
593
- .dashboard-card-section:hover::before {
594
- transform: translateX(0);
595
- }
596
-
597
- /* Perfectly centered and styled section titles */
598
  .sub-section-title {
599
- font-family: 'Poppins', sans-serif !important;
600
- font-size: 1.6rem !important;
601
- font-weight: 700 !important;
602
- color: var(--text-contrast) !important;
603
- text-align: center !important;
604
- margin: 0 0 var(--spacing-lg) 0 !important;
605
- padding-bottom: var(--spacing-sm) !important;
606
- border-bottom: 3px solid transparent !important;
607
- background: var(--primary-gradient) !important;
608
- background-clip: text !important;
609
- -webkit-background-clip: text !important;
610
- -webkit-text-fill-color: transparent !important;
611
- position: relative !important;
612
- display: block !important;
613
- }
614
-
615
- .sub-section-title::after {
616
- content: '';
617
- position: absolute;
618
- bottom: 0;
619
- left: 50%;
620
- transform: translateX(-50%);
621
- width: 60px;
622
- height: 3px;
623
- background: var(--primary-gradient);
624
- border-radius: 2px;
625
- }
626
-
627
- /* Superior input styling with crystal clear boundaries */
628
- .gradio-textbox, .gradio-dropdown {
629
- margin-bottom: var(--spacing-sm) !important;
630
- }
631
-
632
- .gradio-textbox textarea,
633
- .gradio-textbox input,
634
- .gradio-dropdown select {
635
- background: var(--surface-primary) !important;
636
- border: 2px solid var(--border-strong) !important;
637
- border-radius: var(--border-radius-sm) !important;
638
- padding: var(--spacing-md) !important;
639
- font-size: 0.95rem !important;
640
- font-family: 'Inter', sans-serif !important;
641
- color: var(--text-contrast) !important;
642
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
643
- box-shadow: inset 0 1px 3px rgba(0,0,0,0.1) !important;
644
- line-height: 1.5 !important;
645
- }
646
-
647
- .gradio-textbox textarea:focus,
648
- .gradio-textbox input:focus,
649
- .gradio-dropdown select:focus {
650
- outline: none !important;
651
- border-color: var(--accent-color) !important;
652
- box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1), inset 0 1px 3px rgba(0,0,0,0.1) !important;
653
- transform: translateY(-1px) !important;
654
- }
655
-
656
- .gradio-textbox textarea:hover,
657
- .gradio-textbox input:hover,
658
- .gradio-dropdown select:hover {
659
- border-color: var(--accent-color) !important;
660
- box-shadow: 0 2px 4px rgba(0,0,0,0.1), inset 0 1px 3px rgba(0,0,0,0.1) !important;
661
- }
662
-
663
- /* Enhanced placeholder and label styling */
664
- .gradio-textbox textarea::placeholder,
665
- .gradio-textbox input::placeholder {
666
- color: var(--text-muted) !important;
667
- opacity: 0.8 !important;
668
- font-style: italic !important;
669
- }
670
-
671
- .gradio-textbox label,
672
- .gradio-dropdown label {
673
- font-weight: 600 !important;
674
- color: var(--text-contrast) !important;
675
- font-size: 0.9rem !important;
676
- margin-bottom: var(--spacing-xs) !important;
677
  display: block !important;
678
- text-transform: uppercase !important;
679
- letter-spacing: 0.5px !important;
680
- }
681
-
682
- /* Refined info text */
683
- .gradio-textbox .gr-form,
684
- .gradio-dropdown .gr-form {
685
- font-size: 0.85rem !important;
686
- color: var(--text-muted) !important;
687
- margin-top: var(--spacing-xs) !important;
688
- font-style: italic !important;
689
- }
690
-
691
- /* Optimized input layout */
692
- .input-row {
693
- display: flex !important;
694
- gap: var(--spacing-md) !important;
695
- margin-bottom: var(--spacing-sm) !important;
696
- align-items: flex-end !important;
697
- }
698
-
699
- .input-field {
700
- flex: 1 !important;
701
- min-width: 0 !important;
702
- }
703
-
704
- /* Premium button design */
705
- .button-row {
706
- display: flex !important;
707
- gap: var(--spacing-md) !important;
708
- justify-content: flex-end !important;
709
- margin-top: var(--spacing-lg) !important;
710
- flex-wrap: wrap !important;
711
- }
712
-
713
  .gradio-button {
714
- padding: var(--spacing-md) var(--spacing-xl) !important;
715
- border-radius: var(--border-radius-sm) !important;
 
716
  font-weight: 600 !important;
717
- font-size: 0.9rem !important;
718
- text-transform: uppercase !important;
719
- letter-spacing: 0.5px !important;
720
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
721
- cursor: pointer !important;
722
- border: 2px solid transparent !important;
723
- position: relative !important;
724
- overflow: hidden !important;
725
- }
726
-
727
- .gradio-button::before {
728
- content: '';
729
- position: absolute;
730
- top: 0;
731
- left: -100%;
732
- width: 100%;
733
- height: 100%;
734
- background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
735
- transition: left 0.5s;
736
- }
737
-
738
- .gradio-button:hover::before {
739
- left: 100%;
740
  }
741
-
742
- .gr-button-primary {
743
- background: var(--primary-gradient) !important;
744
- color: white !important;
745
- box-shadow: var(--shadow-soft) !important;
746
- border: 2px solid transparent !important;
747
  }
748
-
749
- .gr-button-primary:hover {
750
- box_shadow: var(--shadow-medium) !important;
751
- transform: translateY(-2px) scale(1.02) !important;
752
- }
753
-
754
- .gr-button-primary:active {
755
- transform: translateY(0) scale(0.98) !important;
756
- }
757
-
758
- .gr-button-secondary {
759
  background: transparent !important;
760
- color: var(--text-contrast) !important;
761
- border: 2px solid var(--border-strong) !important;
762
- backdrop-filter: blur(10px) !important;
763
- }
764
-
765
- .gr-button-secondary:hover {
766
- background: var(--surface-accent) !important;
767
- border-color: var(--accent-color) !important;
768
- transform: translateY(-1px) !important;
769
- box_shadow: var(--shadow-soft) !important;
770
- }
771
-
772
- /* Exceptional output styling */
773
- .output-content-wrapper {
774
- background: var(--surface-primary) !important;
775
- border: 2px solid var(--border-strong) !important;
776
- border-radius: var(--border-radius-sm) !important;
777
- padding: var(--spacing-lg) !important;
778
- min-height: 120px !important;
779
- font-size: 0.95rem !important;
780
- line-height: 1.6 !important;
781
- color: var(--text-contrast) !important;
782
- box-shadow: inset 0 2px 4px rgba(0,0,0,0.05) !important;
783
- font-family: 'Inter', sans-serif !important;
784
- }
785
-
786
- .response-header {
787
- font-size: 1.3rem !important;
788
- font-weight: 700 !important;
789
- color: var(--text-contrast) !important;
790
- margin-bottom: var(--spacing-md) !important;
791
- display: flex !important;
792
- align-items: center !important;
793
- gap: var(--spacing-sm) !important;
794
- background: var(--primary-gradient) !important;
795
- background-clip: text !important;
796
- -webkit-background-clip: text !important;
797
- -webkit-text-fill-color: transparent !important;
798
- }
799
-
800
- .response-icon {
801
- font-size: 1.4rem !important;
802
- background: var(--primary-gradient) !important;
803
- background-clip: text !important;
804
- -webkit-background-clip: text !important;
805
- -webkit-text-fill-color: transparent !important;
806
- }
807
-
808
- .divider {
809
- border: none !important;
810
- border-top: 2px solid var(--border-strong) !important;
811
- margin: var(--spacing-md) 0 !important;
812
- background: var(--primary-gradient) !important;
813
- height: 2px !important;
814
- border: none !important;
815
- border-radius: 1px !important;
816
- }
817
-
818
- /* Enhanced error styling */
819
- .error-message {
820
- background: linear-gradient(135deg, #fef2f2 0%, #fde8e8 100%) !important;
821
- border: 2px solid #fecaca !important;
822
- color: #dc2626 !important;
823
- padding: var(--spacing-lg) !important;
824
- border-radius: var(--border-radius-sm) !important;
 
 
 
 
 
 
 
 
825
  display: flex !important;
 
826
  align-items: flex-start !important;
827
- gap: var(--spacing-md) !important;
828
- font-size: 0.9rem !important;
829
- box_shadow: var(--shadow-soft) !important;
830
- }
831
-
832
- .error-icon {
833
- font-size: 1.3rem !important;
834
- line-height: 1 !important;
835
- margin-top: 0.1rem !important;
836
- animation: pulse 2s infinite !important;
837
  }
838
-
839
- @keyframes pulse {
840
- 0%, 100% { opacity: 1; }
841
- 50% { opacity: 0.7; }
842
- }
843
-
844
- /* Elegant placeholder */
845
- .placeholder {
846
- background: linear-gradient(135deg, var(--surface-secondary) 0%, var(--surface-accent) 100%) !important;
847
- border: 2px dashed var(--border-strong) !important;
848
- border-radius: var(--border-radius-sm) !important;
849
- padding: var(--spacing-xl) var(--spacing-lg) !important;
850
- text-align: center !important;
851
- color: var(--text-muted) !important;
852
- font-style: italic !important;
853
- font-size: 1rem !important;
854
- transition: all 0.3s ease !important;
855
- }
856
-
857
- .placeholder:hover {
858
- border-color: var(--accent-color) !important;
859
- background: linear-gradient(135deg, var(--surface-accent) 0%, var(--surface-secondary) 100%) !important;
860
- }
861
-
862
- /* Premium examples table */
863
- .examples-section .gr-samples-table {
864
- border: 2px solid var(--border-strong) !important;
865
- border-radius: var(--border-radius-sm) !important;
866
- overflow: hidden !important;
867
- margin-top: var(--spacing-lg) !important;
868
- box_shadow: var(--shadow-soft) !important;
869
- }
870
-
871
- .examples-section .gr-samples-table th,
872
- .examples-section .gr-samples-table td {
873
- padding: var(--spacing-md) !important;
874
- border: none !important;
875
- font-size: 0.9rem !important;
876
- transition: all 0.2s ease !important;
877
- }
878
-
879
- .examples-section .gr-samples-table th {
880
- background: var(--primary-gradient) !important;
881
- color: white !important;
882
- font-weight: 600 !important;
883
- text-transform: uppercase !important;
884
- letter-spacing: 0.5px !important;
885
- font-size: 0.8rem !important;
886
- }
887
-
888
- .examples-section .gr-samples-table td {
889
- background: var(--surface-primary) !important;
890
- color: var(--text-contrast) !important;
891
- border-top: 1px solid var(--border-subtle) !important;
892
- cursor: pointer !important;
893
- }
894
-
895
- .examples-section .gr-samples-table tr:hover td {
896
- background: var(--surface-accent) !important;
897
- transform: scale(1.01) !important;
898
- }
899
-
900
- /* Sophisticated footer */
901
- .app-footer-wrapper {
902
- background: linear-gradient(135deg, var(--surface-secondary) 0%, var(--surface-accent) 100%) !important;
903
- border: 2px solid var(--border-strong) !important;
904
- border-radius: var(--border-radius-md) !important;
905
- padding: var(--spacing-lg) !important;
906
- margin-top: var(--spacing-lg) !important;
907
- text-align: center !important;
908
- box_shadow: var(--shadow-soft) !important;
909
  }
910
-
911
  .app-footer p {
912
- margin: var(--spacing-sm) 0 !important;
913
- font-size: 0.9rem !important;
914
- color: var(--text-muted) !important;
915
- line-height: 1.6 !important;
916
- }
917
-
918
- .app-footer a {
919
- background: var(--primary-gradient) !important;
920
- background-clip: text !important;
921
- -webkit-background-clip: text !important;
922
- -webkit-text-fill-color: transparent !important;
923
- text-decoration: none !important;
924
- font-weight: 600 !important;
925
- transition: all 0.3s ease !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
926
  }
927
-
928
- .app-footer a:hover {
929
- text-decoration: underline !important;
930
- transform: scale(1.05) !important;
931
- display: inline-block !important;
932
- }
933
-
934
- /* Hide Gradio default elements */
935
- .gr-examples .gr-label,
936
- .gr-examples .label-wrap,
937
- .gr-examples .gr-accordion-header {
938
- display: none !important;
939
- }
940
-
941
- /* Responsive design */
942
  @media (max-width: 768px) {
943
- .gradio-container {
944
- padding: var(--spacing-sm) !important;
945
- }
946
-
947
- .app-header-title {
948
- font-size: 2rem !important;
949
- }
950
-
951
- .app-header-tagline {
952
- font-size: 1rem !important;
953
- }
954
-
955
- .input-row {
956
- flex-direction: column !important;
957
- }
958
-
959
- .button-row {
960
- flex-direction: column !important;
961
- }
962
-
963
- .gradio-button {
964
- width: 100% !important;
965
- }
 
 
 
 
 
 
 
 
 
 
 
966
  }
967
  """
968
 
969
  with gr.Blocks(theme="earneleh/paris", css=custom_css, title="Landlord-Tenant Rights Assistant") as demo:
970
- # Header Section
971
  with gr.Group(elem_classes="app-header-wrapper"):
972
  gr.Markdown(
973
  """
@@ -979,49 +584,42 @@ Answer:"""
979
  """
980
  )
981
 
982
- # Main Dashboard Container
983
  with gr.Column(elem_classes="main-dashboard-container"):
984
 
985
- # Introduction and Disclaimer Card
986
  with gr.Group(elem_classes="dashboard-card-section"):
987
  gr.Markdown("<h3 class='sub-section-title'>Welcome & Disclaimer</h3>")
988
  gr.Markdown(
989
  """
990
- Navigate landlord-tenant laws with ease. This assistant provides detailed, state-specific answers grounded in legal authority.
991
-
992
- **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.
993
  """
994
  )
995
 
996
- # OpenAI API Key Input Card
997
  with gr.Group(elem_classes="dashboard-card-section"):
998
  gr.Markdown("<h3 class='sub-section-title'>OpenAI API Key</h3>")
999
  api_key_input = gr.Textbox(
1000
- label="API Key",
1001
- type="password",
1002
- placeholder="Enter your API key (e.g., sk-...)",
1003
- info="Required to process your query. Get one free from OpenAI.",
1004
- lines=1,
1005
  elem_classes=["input-field-group"]
1006
  )
1007
 
1008
- # Query Input and State Selection Card
1009
  with gr.Group(elem_classes="dashboard-card-section"):
1010
  gr.Markdown("<h3 class='sub-section-title'>Ask Your Question</h3>")
1011
  with gr.Row(elem_classes="input-row"):
1012
  with gr.Column(elem_classes="input-field", scale=3):
1013
  query_input = gr.Textbox(
1014
- label="Your Question",
1015
- placeholder="E.g., What are the rules for security deposit returns in my state?",
1016
- lines=4,
1017
- max_lines=8,
1018
  elem_classes=["input-field-group"]
1019
  )
1020
  with gr.Column(elem_classes="input-field", scale=1):
1021
  state_input = gr.Dropdown(
1022
- label="Select State",
1023
- choices=dropdown_choices,
1024
- value=initial_value,
1025
  allow_custom_value=False,
1026
  elem_classes=["input-field-group"]
1027
  )
@@ -1029,59 +627,56 @@ Answer:"""
1029
  clear_button = gr.Button("Clear", variant="secondary", elem_classes=["gr-button-secondary"])
1030
  submit_button = gr.Button("Submit Query", variant="primary", elem_classes=["gr-button-primary"])
1031
 
1032
- # Output Display Card
1033
  with gr.Group(elem_classes="dashboard-card-section"):
1034
  gr.Markdown("<h3 class='sub-section-title'>Legal Assistant's Response</h3>")
1035
  output = gr.Markdown(
1036
- value="<div class='placeholder'>The answer will appear here after submitting your query.</div>",
1037
- elem_classes="output-content-wrapper"
1038
  )
1039
 
1040
- # Example Questions Section
1041
  with gr.Group(elem_classes="dashboard-card-section examples-section"):
1042
- gr.Markdown("<h3 class='sub-section-title'>Example Questions</h3>")
1043
  if example_queries:
1044
  gr.Examples(
1045
- examples=example_queries,
1046
- inputs=[query_input, state_input],
1047
  examples_per_page=5,
1048
- label="" # Hide the default "Examples" label
1049
  )
1050
  else:
1051
  gr.Markdown("<div class='placeholder'>Sample questions could not be loaded.</div>")
1052
 
1053
- # Footer Section
1054
  with gr.Group(elem_classes="app-footer-wrapper"):
1055
  gr.Markdown(
1056
  """
1057
- This tool is for informational purposes only and does not constitute legal advice. For legal guidance, always consult with a licensed attorney in your jurisdiction.
1058
-
1059
- Developed by **Nischal Subedi**. Connect on [LinkedIn](https://www.linkedin.com/in/nischal1/) or explore insights at [Substack](https://datascientistinsights.substack.com/).
 
 
 
1060
  """
1061
  )
1062
 
1063
- # Event Listeners
1064
  submit_button.click(
1065
- fn=query_interface_wrapper,
1066
- inputs=[api_key_input, query_input, state_input],
1067
- outputs=output,
1068
- api_name="submit_query" # API name for potential external calls
1069
  )
1070
-
1071
  clear_button.click(
1072
  fn=lambda: (
1073
- "", # Clear API key
1074
- "", # Clear query input
1075
- initial_value, # Reset state dropdown to initial value
1076
- "<div class='placeholder'>Inputs cleared. Ready for your next question.</div>" # Reset output
1077
  ),
1078
- inputs=[],
1079
- outputs=[api_key_input, query_input, state_input, output]
1080
  )
1081
-
1082
  return demo
1083
 
1084
- # --- Main Execution Block ---
1085
  if __name__ == "__main__":
1086
  logging.info("Starting Landlord-Tenant Rights Bot application...")
1087
  try:
@@ -1089,44 +684,37 @@ if __name__ == "__main__":
1089
  DEFAULT_PDF_PATH = os.path.join(SCRIPT_DIR, "tenant-landlord.pdf")
1090
  DEFAULT_DB_PATH = os.path.join(SCRIPT_DIR, "chroma_db")
1091
 
1092
- # Use environment variables for paths if available, otherwise use defaults
1093
  PDF_PATH = os.getenv("PDF_PATH", DEFAULT_PDF_PATH)
1094
  VECTOR_DB_PATH = os.getenv("VECTOR_DB_PATH", DEFAULT_DB_PATH)
1095
 
1096
- # Ensure the directory for the vector database exists
1097
  os.makedirs(os.path.dirname(VECTOR_DB_PATH), exist_ok=True)
1098
 
1099
  logging.info(f"Attempting to load PDF from: {PDF_PATH}")
1100
  if not os.path.exists(PDF_PATH):
1101
  logging.error(f"FATAL: PDF file not found at the specified path: {PDF_PATH}")
1102
  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")
1103
- exit(1) # Exit if PDF not found
1104
 
 
 
1105
  if not os.access(PDF_PATH, os.R_OK):
1106
  logging.error(f"FATAL: PDF file at '{PDF_PATH}' exists but is not readable. Check file permissions.")
1107
  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")
1108
- exit(1) # Exit if PDF not readable
1109
 
1110
  logging.info(f"PDF file '{os.path.basename(PDF_PATH)}' found and is readable.")
1111
 
1112
- # Initialize VectorDatabase and RAGSystem
1113
  vector_db_instance = VectorDatabase(persist_directory=VECTOR_DB_PATH)
1114
  rag = RAGSystem(vector_db=vector_db_instance)
1115
 
1116
- # Load PDF into the vector database (or verify it's loaded if already persisted)
1117
  rag.load_pdf(PDF_PATH)
1118
 
1119
- # Get the Gradio interface object from the RAGSystem instance
1120
  app_interface = rag.gradio_interface()
1121
-
1122
- # Determine server port (for Gradio Spaces compatibility)
1123
- SERVER_PORT = int(os.getenv("PORT", 7860))
1124
 
1125
  logging.info(f"Launching Gradio app on http://0.0.0.0:{SERVER_PORT}")
1126
  print(f"\n--- Gradio App Running ---\nAccess at: http://localhost:{SERVER_PORT} or your public Spaces URL\n--------------------------\n")
1127
-
1128
- # Launch the Gradio interface
1129
- app_interface.launch(server_name="0.0.0.0", server_port=SERVER_PORT, share=False)
1130
 
1131
  except ModuleNotFoundError as e:
1132
  if "vector_db" in str(e):
 
1
  import os
2
  import logging
 
 
3
  from typing import Dict, List, Optional
4
  from functools import lru_cache
5
+ import re
6
 
7
  import gradio as gr
8
+ import gradio.themes as themes # Import gradio.themes
9
 
 
10
  try:
11
  # Assuming vector_db.py exists in the same directory or is installed
12
+ from vector_db import VectorDatabase
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  except ImportError:
14
+ print("Error: Could not import VectorDatabase from vector_db.py.")
15
+ print("Please ensure vector_db.py exists in the same directory and is correctly defined.")
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(1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ from langchain.prompts import PromptTemplate
25
+ from langchain.chains import LLMChain
26
 
27
  # Suppress warnings
28
  import warnings
 
36
  format='%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
37
  )
38
 
39
+ # --- RAGSystem Class (Processing Logic - kept intact as requested) ---
40
  class RAGSystem:
41
  def __init__(self, vector_db: Optional[VectorDatabase] = None):
42
  logging.info("Initializing RAGSystem")
 
223
  raise FileNotFoundError(f"PDF file not found: {pdf_path}")
224
  try:
225
  logging.info(f"Attempting to load/verify data from PDF: {pdf_path}")
226
+ # Assuming process_and_load_pdf is part of VectorDatabase and correctly implemented
227
  num_states_processed = self.vector_db.process_and_load_pdf(pdf_path)
228
  doc_count = self.vector_db.document_collection.count()
229
  state_count = self.vector_db.state_collection.count()
 
241
  logging.error(f"Failed to load or process PDF '{pdf_path}': {str(e)}", exc_info=True)
242
  raise RuntimeError(f"Failed to process PDF '{pdf_path}': {e}") from e
243
 
244
+ # --- GRADIO INTERFACE (Refactored to use earneleh/paris theme) ---
245
  def gradio_interface(self):
246
  def query_interface_wrapper(api_key: str, query: str, state: str) -> str:
247
  # Basic client-side validation for immediate feedback (redundant but good UX)
 
278
  ["Can a landlord enter my apartment without notice?", "New York"],
279
  ["My landlord hasn't made necessary repairs. What can I do?", "Texas"],
280
  ["How much notice must a landlord give to raise rent?", "Florida"],
281
+ ["What is an implied warranty of habitability?", "Illinois"]
 
 
 
 
 
 
282
  ]
283
  example_queries = []
284
  if available_states_list and "Error" not in available_states_list[0] and len(available_states_list) > 0:
 
291
  else: # Fallback if states list is problematic
292
  example_queries.append(["What basic rights do tenants have?", "California"])
293
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
 
295
+ # --- Minimal Custom CSS for structural/behavioral overrides not covered by the theme ---
296
+ # This CSS focuses on animations, specific layout tweaks, and hiding unwanted Gradio default elements.
297
+ custom_css = """
298
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:wght@700;800;900&display=swap');
299
+ /* Animation for header */
300
+ @keyframes fadeInSlideDown { from { opacity: 0; transform: translateY(-40px); } to { opacity: 1; transform: translateY(0); } }
301
+ /* General layout adjustments to Gradio's base container */
302
+ .gradio-container > .flex.flex-col {
303
+ max-width: 1120px;
304
  margin: 0 auto !important;
305
+ padding: 0 !important; /* Remove default Gradio padding */
306
+ gap: 0 !important; /* Remove default Gradio gap */
 
 
307
  }
308
+ /* Header specific styling */
 
309
  .app-header-wrapper {
310
+ padding: 4.5rem 3.5rem !important;
 
 
 
 
 
311
  text-align: center !important;
312
+ border-bottom-left-radius: 24px;
313
+ border-bottom-right-radius: 24px;
314
+ position: relative;
315
+ overflow: hidden;
316
+ z-index: 10;
317
+ margin-bottom: 2rem; /* Reduced margin */
318
+ border: 2px solid var(--border-color-primary) !important; /* Thicker border */
319
+ border-top: none;
320
+ max-width: 1120px;
321
+ margin-left: auto;
322
+ margin-right: auto;
323
+ width: 100%;
324
+ /* Enhanced background */
325
+ background-image: linear-gradient(135deg, var(--background-fill-primary) 0%, var(--background-fill-secondary) 100%);
326
+ box-shadow: 0 10px 30px rgba(0,0,0,0.4); /* More pronounced shadow */
327
  }
328
+ .app-header { display: flex; flex-direction: column; align-items: center; position: relative; z-index: 1; }
329
  .app-header-logo {
330
+ font-size: 5.5rem; margin-bottom: 0.8rem; line-height: 1;
331
+ filter: drop-shadow(0 0 15px var(--primary-500));
332
+ transform: translateY(-40px); opacity: 0;
333
+ animation: fadeInSlideDown 1.5s ease-out forwards; animation-delay: 0.3s;
 
 
 
 
 
 
334
  }
 
335
  .app-header-title {
336
+ font-family: 'Playfair Display', serif !important;
337
+ font-size: 4.2rem; font-weight: 900;
338
+ margin: 0 0 0.8rem 0; letter-spacing: -0.07em;
339
+ text-shadow: 0 8px 16px rgba(0,0,0,0.5);
340
+ transform: translateY(-40px); opacity: 0;
341
+ animation: fadeInSlideDown 1.5s ease-out forwards; animation-delay: 0.6s;
 
 
342
  }
 
343
  .app-header-tagline {
344
+ font-family: 'Inter', sans-serif !important;
345
+ font-size: 1.6rem; font-weight: 300;
346
+ opacity: 0.9; max-width: 900px;
347
+ transform: translateY(-40px); opacity: 0;
348
+ animation: fadeInSlideDown 1.5s ease-out forwards; animation-delay: 0.9s;
349
  }
350
+ /* Main dashboard container */
 
351
  .main-dashboard-container {
352
+ border-radius: 24px;
353
+ border: 2px solid var(--border-color-primary) !important; /* Thicker border */
354
+ padding: 3.5rem !important;
355
+ margin: 0 auto 0.6rem auto; /* Reduced margin-bottom */
356
+ z-index: 1; position: relative;
357
+ display: flex; flex-direction: column; gap: 2rem; /* Reduced gap between boxes */
358
+ max-width: 1120px;
359
+ }
360
+ /* Card sections within the dashboard */
361
  .dashboard-card-section {
362
+ border-radius: 12px;
363
+ border: 2px solid var(--border-color-primary); /* Thicker border */
364
+ padding: 2.5rem;
365
+ box-shadow: inset 0 0 10px rgba(0,0,0,0.2);
366
+ display: flex; flex-direction: column; gap: 1.5rem;
 
 
 
367
  }
368
+ /* Light mode specific inner shadow for card sections */
369
+ @media (prefers-color-scheme: light) {
370
+ .dashboard-card-section { box-shadow: inset 0 0 8px rgba(0,0,0,0.05); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  }
372
+ /* Section titles */
 
 
 
 
 
373
  .sub-section-title {
374
+ font-family: 'Playfair Display', serif !important;
375
+ font-size: 3.0rem !important; /* Increased font size */
376
+ font-weight: 800 !important;
377
+ text-align: center !important; /* Already centered, kept */
378
+ margin-top: 1.5rem !important;
379
+ margin-bottom: 0.8rem !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  display: block !important;
381
+ width: 100% !important;
382
+ }
383
+ /* Text styling for custom markdown within cards */
384
+ .dashboard-card-section p, .output-content-wrapper p {
385
+ font-size: 1.15rem; line-height: 1.8;
386
+ margin-bottom: 1.2rem;
387
+ }
388
+ .dashboard-card-section a, .output-content-wrapper a {
389
+ text-decoration: none; font-weight: 500;
390
+ }
391
+ .dashboard-card-section a:hover, .output-content-wrapper a:hover { text-decoration: underline; }
392
+ .dashboard-card-section strong, .output-content-wrapper strong { font-weight: 700; }
393
+ /* Input layout */
394
+ .input-field-group { margin-bottom: 1rem; }
395
+ .input-row { display: flex; gap: 1.8rem; flex-wrap: wrap; margin-bottom: 1rem; }
396
+ .input-field { flex: 1; }
397
+ /* Button layout */
398
+ .button-row { display: flex; gap: 2rem; margin-top: 2rem; flex-wrap: wrap; justify-content: flex-end; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  .gradio-button {
400
+ border-radius: 12px !important;
401
+ padding: 1.2rem 2.8rem !important;
402
+ font-size: 1.15rem !important;
403
  font-weight: 600 !important;
404
+ border: 1px solid transparent !important;
405
+ box-shadow: 0 6px 20px rgba(0,0,0,0.35);
406
+ transition: all 0.4s cubic-bezier(0.0, 0.0, 0.2, 1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  }
408
+ .gradio-button:hover:not(:disabled) { transform: translateY(-6px); box-shadow: 0 12px 28px rgba(0,0,0,0.45) !important; }
409
+ .gradio-button:active:not(:disabled) { transform: translateY(-3px); }
410
+ .gradio-button:disabled {
411
+ box-shadow: none !important;
412
+ cursor: not-allowed;
 
413
  }
414
+ .gr-button-secondary { /* Override for secondary button specific look */
 
 
 
 
 
 
 
 
 
 
415
  background: transparent !important;
416
+ border: 2px solid var(--neutral-500) !important; /* Used neutral-500 for clearer border */
417
+ box-shadow: none !important;
418
+ }
419
+ .gr-button-secondary:hover:not(:disabled) {
420
+ background: var(--button-secondary-background-hover) !important;
421
+ border-color: var(--primary-500) !important;
422
+ }
423
+ @media (prefers-color-scheme: light) {
424
+ .gradio-button { box-shadow: 0 6px 20px rgba(0,0,0,0.1); }
425
+ .gradio-button:hover:not(:disabled) { box-shadow: 0 12px 28px rgba(0,0,0,0.2) !important; }
426
+ }
427
+ /* Output card and placeholders */
428
+ .output-card { padding: 0 !important; margin-top: 0 !important; margin-bottom: 0 !important; }
429
+ .output-card .response-header {
430
+ font-size: 1.8rem; font-weight: 700;
431
+ margin: 0 0 1rem 0; display: flex; align-items: center; gap: 1.2rem;
432
+ }
433
+ .output-card .response-icon { font-size: 2rem; color: var(--primary-500); }
434
+ .output-card .divider { border: none; border-top: 1px solid var(--border-color-primary); margin: 1.5rem 0 1.8rem 0; }
435
+ .output-card .output-content-wrapper { font-size: 1.15rem; line-height: 1.8; }
436
+ .output-card .output-content-wrapper p { margin-bottom: 1rem; }
437
+ .output-card .output-content-wrapper ul, .output-card .output-content-wrapper ol { margin-left: 2.2rem; margin-bottom: 1.2rem; padding-left: 0; list-style-type: disc; }
438
+ .output-card .output-content-wrapper ol { list-style-type: decimal; }
439
+ .output-card .output-content-wrapper li { margin-bottom: 0.8rem; }
440
+ .output-card .error-message {
441
+ padding: 1.5rem 2rem; margin-top: 1.5rem; font-size: 1.1rem;
442
+ border-radius: 12px;
443
+ background: var(--error-background-fill) !important;
444
+ color: var(--error-text-color) !important;
445
+ border: 2px solid var(--error-border-color) !important;
446
+ display: flex; align-items: flex-start; gap: 1.5em;
447
+ }
448
+ .output-card .error-message .error-icon { font-size: 1.8rem; line-height: 1; padding-top: 0.1em; }
449
+ .output-card .error-details { font-size: 0.95rem; margin-top: 0.8rem; opacity: 0.9; word-break: break-word; }
450
+ .output-card .placeholder {
451
+ padding: 2.5rem 2rem; font-size: 1.2rem; border-radius: 12px;
452
+ border: 3px dashed var(--border-color-primary);
453
+ text-align: center; opacity: 0.8;
454
+ }
455
+ /* Examples table styling */
456
+ .examples-section table.gr-samples-table {
457
+ border-radius: 12px !important;
458
+ border: 2px solid var(--border-color-primary) !important; /* Thicker border */
459
+ overflow: hidden;
460
+ box-shadow: inset 0 0 10px rgba(0,0,0,0.2);
461
+ }
462
+ @media (prefers-color-scheme: light) {
463
+ .examples-section table.gr-samples-table { box-shadow: inset 0 0 8px rgba(0,0,0,0.05); }
464
+ }
465
+ .examples-section table.gr-samples-table th, .examples-section table.gr-samples-table td { padding: 1rem 1.2rem !important; font-size: 1.05rem !important; border: none !important; }
466
+ .examples-section table.gr-samples-table th {
467
+ background: var(--block-background-fill) !important;
468
+ font-weight: 600 !important; text-align: left;
469
+ }
470
+ .examples-section table.gr-samples-table td {
471
+ border-top: 1px solid var(--border-color-primary) !important;
472
+ cursor: pointer;
473
+ }
474
+ .examples-section table.gr-samples-table tr:hover td { background: var(--hover-color) !important; }
475
+ .examples-section table.gr-samples-table tr:first-child td { border-top: none !important; }
476
+ /* Footer styling */
477
+ .app-footer-wrapper {
478
+ border-top: 2px solid var(--border-color-primary) !important; /* Thicker border */
479
+ margin-top: 0.5rem;
480
+ padding-top: 2.5rem;
481
+ padding-bottom: 2.5rem;
482
+ border-top-left-radius: 24px;
483
+ border-top-right-radius: 24px;
484
+ box-shadow: inset 0 8px 15px rgba(0,0,0,0.2);
485
+ max-width: 1120px;
486
+ margin-left: auto;
487
+ margin-right: auto;
488
+ width: 100%;
489
  display: flex !important;
490
+ flex-direction: column !important;
491
  align-items: flex-start !important;
 
 
 
 
 
 
 
 
 
 
492
  }
493
+ .app-footer {
494
+ padding: 0 3.5rem !important;
495
+ display: flex;
496
+ flex-direction: column;
497
+ align-items: stretch;
498
+ width: 100%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  }
 
500
  .app-footer p {
501
+ font-size: 1.05rem !important;
502
+ text-align: left !important;
503
+ margin-bottom: 1rem;
504
+ }
505
+ .app-footer a { font-weight: 500; }
506
+ /* Hide default Gradio example labels and related elements for cleaner presentation */
507
+ .gr-examples .gr-label, .gr-examples button.gr-button-filter, .gr-examples .label-wrap,
508
+ .gr-examples div[data-testid*="label-text"], .gr-examples span[data-testid*="label-text"],
509
+ .gr-examples div[class*="label"], .gr-examples .gr-example-label,
510
+ .gr-examples .gr-box.gr-component.gradio-example > div:first-child:has(> span[data-testid]),
511
+ .gr-examples .gr-box.gr-component.gradio-example > div:first-child > span,
512
+ .gr-examples .gr-accordion-header, .gr-examples .gr-accordion-title, .gr-examples .gr-accordion-toggle-icon,
513
+ .gr-examples .gr-accordion-header button, .gr-examples .gr-button.gr-button-filter,
514
+ .gr-examples .gr-button.gr-button-primary.gr-button-filter,
515
+ .gr-examples .gr-examples-header, .gr-examples .gr-examples-header > * {
516
+ display: none !important; visibility: hidden !important; width: 0 !important; height: 0 !important;
517
+ overflow: hidden !important; margin: 0 !important; padding: 0 !important; border: 0 !important;
518
+ font-size: 0 !important; line-height: 0 !important; position: absolute !important;
519
+ pointer-events: none !important;
520
+ }
521
+ /* Responsive Adjustments */
522
+ @media (max-width: 1024px) {
523
+ .gradio-container > .flex.flex-col { max-width: 960px; padding: 0 1.5rem !important; }
524
+ .app-header-title { font-size: 3.8rem; } .app-header-tagline { font-size: 1.5rem; }
525
+ .app-header-wrapper { padding: 2.5rem 3.5rem !important; margin-bottom: 1.8rem; border-bottom-left-radius: 12px; border-bottom-right-radius: 12px; }
526
+ .main-dashboard-container { padding: 2.5rem !important; margin-bottom: 0.6rem; border-radius: 12px; gap: 1.8rem; } /* Adjusted gap */
527
+ .dashboard-card-section { padding: 1.8rem; border-radius: 8px; }
528
+ .sub-section-title { font-size: 2.5rem !important; margin-bottom: 0.7rem !important; } /* Adjusted font size */
529
+ .input-row { gap: 1.5rem; } .input-field { min-width: 280px; }
530
+ .gradio-textbox textarea { min-height: 160px; } .output-card .response-header { font-size: 1.7rem; }
531
+ .examples-section { padding-top: 1.2rem; }
532
+ .examples-section table.gr-samples-table th, .examples-section table.gr-samples-table td { padding: 0.9rem 1.1rem !important; }
533
+ .app-footer-wrapper { margin-top: 0.6rem; border-top-left-radius: 12px; border-top-right-radius: 12px; }
534
+ .app-footer { padding: 0 1.5rem !important; }
535
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
  @media (max-width: 768px) {
537
+ .gradio-container > .flex.flex-col { padding: 0 1rem !important; }
538
+ .app-header-wrapper { padding: 1.8rem 2.5rem !important; margin-bottom: 1.5rem; border-bottom-left-radius: 12px; border-bottom-right-radius: 12px; }
539
+ .app-header-logo { font-size: 4.5rem; margin-bottom: 0.6rem; } .app-header-title { font-size: 3.2rem; letter-spacing: -0.06em; }
540
+ .app-header-tagline { font-size: 1.3rem; }
541
+ .main-dashboard-container { padding: 1.8rem !important; margin-bottom: 0.5rem; border-radius: 12px; gap: 1.5rem; } /* Adjusted gap */
542
+ .dashboard-card-section { padding: 1.5rem; border-radius: 8px; }
543
+ .sub-section-title { font-size: 2.0rem !important; margin-top: 1rem !important; margin-bottom: 0.6rem !important; } /* Adjusted font size */
544
+ .input-row { flex-direction: column; gap: 1rem; } .input-field { min-width: 100%; }
545
+ .gradio-textbox textarea { min-height: 140px; } .button-row { justify-content: stretch; gap: 1rem; }
546
+ .gradio-button { width: 100%; padding: 1.1rem 2rem !important; font-size: 1.1rem !important; }
547
+ .output-card .response-header { font-size: 1.5rem; } .output-card .response-icon { font-size: 1.7rem; }
548
+ .output-card .placeholder { padding: 2.5rem 1.5rem; font-size: 1.1rem; }
549
+ .examples-section { padding-top: 1.2rem; }
550
+ .examples-section table.gr-samples-table th, .examples-section table.gr-samples-table td { padding: 0.9rem 1.1rem !important; font-size: 1.0rem !important; }
551
+ .app-footer-wrapper { margin-top: 0.6rem; border-top-left-radius: 12px; border-top-right-radius: 12px; padding-top: 2rem; padding-bottom: 2rem; }
552
+ .app-footer { padding: 0 1rem !important; }
553
+ }
554
+ @media (max-width: 480px) {
555
+ .gradio-container > .flex.flex-col { padding: 0 0.8rem !important; }
556
+ .app-header-wrapper { padding: 1.2rem 1rem !important; margin-bottom: 1.2rem; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; }
557
+ .app-header-logo { font-size: 3.8rem; margin-bottom: 0.5rem; } .app-header-title { font-size: 2.8rem; }
558
+ .app-header-tagline { font-size: 1.1rem; }
559
+ .main-dashboard-container { padding: 1.2rem !important; margin-bottom: 0.4rem; border-radius: 8px; gap: 1.2rem; } /* Adjusted gap */
560
+ .dashboard-card-section { padding: 1rem; border-radius: 4px; }
561
+ .sub-section-title { font-size: 1.8rem !important; margin-top: 0.8rem !important; margin-bottom: 0.5rem !important; } /* Adjusted font size */
562
+ .gradio-textbox textarea, .gradio-dropdown select, .gradio-textbox input[type=password] { font-size: 1.05rem !important; padding: 1rem 1.2rem !important; }
563
+ .gradio-textbox textarea { min-height: 120px; }
564
+ .gradio-button { padding: 1rem 1.5rem !important; font-size: 1.0rem !important; }
565
+ .output-card .response-header { font-size: 1.4rem; } .output-card .response-icon { font-size: 1.5rem; }
566
+ .output-card .placeholder { padding: 2rem 1rem; font-size: 1.05rem; }
567
+ .examples-section { padding-top: 0.8rem; }
568
+ .examples-section table.gr-samples-table th, .examples-section table.gr-samples-table td { padding: 0.6rem 0.8rem !important; font-size: 0.95rem !important; }
569
+ .app-footer-wrapper { margin-top: 0.4rem; border-top-left-radius: 8px; border-top-right-radius: 8px; padding-top: 1.5rem; padding-bottom: 1.5rem; }
570
+ .app-footer { padding: 0 0.8rem !important; }
571
  }
572
  """
573
 
574
  with gr.Blocks(theme="earneleh/paris", css=custom_css, title="Landlord-Tenant Rights Assistant") as demo:
575
+ # --- Header Section ---
576
  with gr.Group(elem_classes="app-header-wrapper"):
577
  gr.Markdown(
578
  """
 
584
  """
585
  )
586
 
587
+ # --- Main Dashboard Console Container ---
588
  with gr.Column(elem_classes="main-dashboard-container"):
589
 
590
+ # --- Section 1: Introduction and Disclaimer Card ---
591
  with gr.Group(elem_classes="dashboard-card-section"):
592
  gr.Markdown("<h3 class='sub-section-title'>Welcome & Disclaimer</h3>")
593
  gr.Markdown(
594
  """
595
+ <p>Navigate landlord-tenant laws with ease. This assistant provides detailed, state-specific answers grounded in legal authority.</p>
596
+ <p><strong>Disclaimer:</strong> 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.</p>
 
597
  """
598
  )
599
 
600
+ # --- Section 2: OpenAI API Key Input Card ---
601
  with gr.Group(elem_classes="dashboard-card-section"):
602
  gr.Markdown("<h3 class='sub-section-title'>OpenAI API Key</h3>")
603
  api_key_input = gr.Textbox(
604
+ label="OpenAI API Key", # Keep label for theme compatibility
605
+ type="password", placeholder="Enter your API key (e.g., sk-...)",
606
+ info="Required to process your query. Securely used per request, not stored. <a href='https://platform.openai.com/api-keys' target='_blank'>Get one free from OpenAI</a>.", lines=1,
 
 
607
  elem_classes=["input-field-group"]
608
  )
609
 
610
+ # --- Section 3: Query Input and State Selection Card ---
611
  with gr.Group(elem_classes="dashboard-card-section"):
612
  gr.Markdown("<h3 class='sub-section-title'>Ask Your Question</h3>")
613
  with gr.Row(elem_classes="input-row"):
614
  with gr.Column(elem_classes="input-field", scale=3):
615
  query_input = gr.Textbox(
616
+ label="Question", placeholder="E.g., What are the rules for security deposit returns in my state?",
617
+ lines=5, max_lines=10,
 
 
618
  elem_classes=["input-field-group"]
619
  )
620
  with gr.Column(elem_classes="input-field", scale=1):
621
  state_input = gr.Dropdown(
622
+ label="Select State", choices=dropdown_choices, value=initial_value,
 
 
623
  allow_custom_value=False,
624
  elem_classes=["input-field-group"]
625
  )
 
627
  clear_button = gr.Button("Clear", variant="secondary", elem_classes=["gr-button-secondary"])
628
  submit_button = gr.Button("Submit Query", variant="primary", elem_classes=["gr-button-primary"])
629
 
630
+ # --- Section 4: Output Display Card ---
631
  with gr.Group(elem_classes="dashboard-card-section"):
632
  gr.Markdown("<h3 class='sub-section-title'>Legal Assistant's Response</h3>")
633
  output = gr.Markdown(
634
+ value="<div class='placeholder output-card'>The answer will appear here after submitting your query.</div>",
635
+ elem_classes="output-content-wrapper output-card"
636
  )
637
 
638
+ # --- Section 5: Example Questions Section ---
639
  with gr.Group(elem_classes="dashboard-card-section examples-section"):
640
+ gr.Markdown("<h3 class='sub-section-title'>Example Questions to Ask</h3>")
641
  if example_queries:
642
  gr.Examples(
643
+ examples=example_queries, inputs=[query_input, state_input],
 
644
  examples_per_page=5,
645
+ label="" # Hide default Examples label
646
  )
647
  else:
648
  gr.Markdown("<div class='placeholder'>Sample questions could not be loaded.</div>")
649
 
650
+ # --- Footer Section ---
651
  with gr.Group(elem_classes="app-footer-wrapper"):
652
  gr.Markdown(
653
  """
654
+ <div class="app-footer">
655
+ <p>This tool is for informational purposes only and does not constitute legal advice. For legal guidance, always consult with a licensed attorney in your jurisdiction.</p>
656
+ <p>Developed by <strong>Nischal Subedi</strong>.
657
+ Connect on <a href="https://www.linkedin.com/in/nischal1/" target='_blank'>LinkedIn</a>
658
+ or explore insights at <a href="https://datascientistinsights.substack.com/" target='_blank'>Substack</a>.</p>
659
+ </div>
660
  """
661
  )
662
 
663
+ # --- Event Listeners (Logic remains identical) ---
664
  submit_button.click(
665
+ fn=query_interface_wrapper, inputs=[api_key_input, query_input, state_input], outputs=output, api_name="submit_query"
 
 
 
666
  )
 
667
  clear_button.click(
668
  fn=lambda: (
669
+ "",
670
+ "",
671
+ initial_value,
672
+ "<div class='placeholder output-card'>Inputs cleared. Ready for your next question.</div>"
673
  ),
674
+ inputs=[], outputs=[api_key_input, query_input, state_input, output]
 
675
  )
676
+ logging.info("Gradio interface created with earneleh/paris theme and refined custom CSS.")
677
  return demo
678
 
679
+ # --- Main Execution Block (remains untouched from original logic, just fixed the exit) ---
680
  if __name__ == "__main__":
681
  logging.info("Starting Landlord-Tenant Rights Bot application...")
682
  try:
 
684
  DEFAULT_PDF_PATH = os.path.join(SCRIPT_DIR, "tenant-landlord.pdf")
685
  DEFAULT_DB_PATH = os.path.join(SCRIPT_DIR, "chroma_db")
686
 
 
687
  PDF_PATH = os.getenv("PDF_PATH", DEFAULT_PDF_PATH)
688
  VECTOR_DB_PATH = os.getenv("VECTOR_DB_PATH", DEFAULT_DB_PATH)
689
 
 
690
  os.makedirs(os.path.dirname(VECTOR_DB_PATH), exist_ok=True)
691
 
692
  logging.info(f"Attempting to load PDF from: {PDF_PATH}")
693
  if not os.path.exists(PDF_PATH):
694
  logging.error(f"FATAL: PDF file not found at the specified path: {PDF_PATH}")
695
  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")
696
+ exit(1) # This exit is correct: if file not found
697
 
698
+ # This `if` block checks for readability *only if the file exists*.
699
+ # The erroneous `exit(1)` that was likely at line 701 has been removed.
700
  if not os.access(PDF_PATH, os.R_OK):
701
  logging.error(f"FATAL: PDF file at '{PDF_PATH}' exists but is not readable. Check file permissions.")
702
  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")
703
+ exit(1) # This exit is correct: if file exists but is not readable
704
 
705
  logging.info(f"PDF file '{os.path.basename(PDF_PATH)}' found and is readable.")
706
 
 
707
  vector_db_instance = VectorDatabase(persist_directory=VECTOR_DB_PATH)
708
  rag = RAGSystem(vector_db=vector_db_instance)
709
 
 
710
  rag.load_pdf(PDF_PATH)
711
 
 
712
  app_interface = rag.gradio_interface()
713
+ SERVER_PORT = int(os.getenv("PORT", 7860)) # Use PORT env var if on Spaces/Cloud, else 7860
 
 
714
 
715
  logging.info(f"Launching Gradio app on http://0.0.0.0:{SERVER_PORT}")
716
  print(f"\n--- Gradio App Running ---\nAccess at: http://localhost:{SERVER_PORT} or your public Spaces URL\n--------------------------\n")
717
+ app_interface.launch(server_name="0.0.0.0", server_port=SERVER_PORT, share=False) # share=False is typical for Spaces
 
 
718
 
719
  except ModuleNotFoundError as e:
720
  if "vector_db" in str(e):