Nischal Subedi commited on
Commit
408ac65
·
1 Parent(s): 56f099b

updated UI

Browse files
Files changed (1) hide show
  1. app.py +690 -129
app.py CHANGED
@@ -33,7 +33,7 @@ logging.basicConfig(
33
  format='%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
34
  )
35
 
36
- # --- RAGSystem Class ---
37
  class RAGSystem:
38
  def __init__(self, vector_db: Optional[VectorDatabase] = None):
39
  logging.info("Initializing RAGSystem")
@@ -237,10 +237,10 @@ Answer:"""
237
  logging.error(f"Failed to load or process PDF '{pdf_path}': {str(e)}", exc_info=True)
238
  raise RuntimeError(f"Failed to process PDF '{pdf_path}': {e}") from e
239
 
240
- # --- GRADIO INTERFACE ---
241
  def gradio_interface(self):
242
  def query_interface_wrapper(api_key: str, query: str, state: str) -> str:
243
- # ... (validation logic remains the same)
244
  if not api_key or not api_key.strip() or not api_key.startswith("sk-"):
245
  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 here</a>.</div>"
246
  if not state or state == "Select a state..." or "Error" in state:
@@ -248,13 +248,17 @@ Answer:"""
248
  if not query or not query.strip():
249
  return "<div class='error-message'><span class='error-icon'>⚠️</span>Please enter your question in the text box.</div>"
250
 
 
251
  result = self.process_query(query=query, state=state, openai_api_key=api_key)
252
  answer = result.get("answer", "<div class='error-message'><span class='error-icon'>⚠️</span>An unexpected error occurred.</div>")
253
- if not "<div class='error-message'>" in answer:
254
- formatted_response = f"<div class='response-header'><span class='response-icon'>📜</span>Response for {state}</div><hr class='divider'>{answer}"
 
 
255
  else:
256
- formatted_response = answer
257
- return formatted_response
 
258
 
259
  try:
260
  available_states_list = self.get_states()
@@ -264,6 +268,7 @@ Answer:"""
264
  dropdown_choices = ["Error: Critical failure loading states"]
265
  initial_value = dropdown_choices[0]
266
 
 
267
  example_queries_base = [
268
  ["What are the rules for security deposit returns?", "California"],
269
  ["Can a landlord enter my apartment without notice?", "New York"],
@@ -273,133 +278,672 @@ Answer:"""
273
  if available_states_list and "Error" not in available_states_list[0] and len(available_states_list) > 0:
274
  loaded_states_set = set(available_states_list)
275
  example_queries = [ex for ex in example_queries_base if ex[1] in loaded_states_set]
276
- if not example_queries and available_states_list[0] != "Error: States unavailable": # Ensure first state is not error
277
- example_queries.append(["What basic rights do tenants have?", available_states_list[0]])
278
- elif not example_queries : # Fallback if states list is problematic
 
279
  example_queries.append(["What basic rights do tenants have?", "California"])
280
 
281
 
282
- # --- FINAL REFINED "Clarity & Counsel" Theme ---
283
  custom_css = """
284
- @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
 
285
 
286
  :root {
287
- --font-family-main: 'Poppins', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
288
- /* Light Theme */
289
- --app-bg-light: #F9FAFB; --surface-bg-light: #FFFFFF; --text-primary-light: #1A202C;
290
- --text-secondary-light: #718096; --accent-primary-light: #00796B; --accent-primary-hover-light: #00695C;
291
- --interactive-text-light: #00796B; --interactive-text-hover-light: #005F52; --border-light: #E2E8F0;
292
- --button-secondary-bg-light: #F1F5F9; --button-secondary-text-light: #334155; --button-secondary-hover-bg-light: #E2E8F0;
293
- --shadow-light: 0 5px 15px rgba(0,0,0,0.05); --focus-ring-light: rgba(0, 121, 107, 0.25);
294
- --error-bg-light: #FFF1F2; --error-text-light: #C81E1E; --error-border-light: #FFD0D0;
295
- --success-bg-light: #EFFCF6; --success-text-light: #15803D; --success-border-light: #B3EED1;
296
- /* Dark Theme */
297
- --app-bg-dark: #0F172A; --surface-bg-dark: #1E293B; --text-primary-dark: #F1F5F9;
298
- --text-secondary-dark: #94A3B8; --accent-primary-dark: #2DD4BF; --accent-primary-hover-dark: #14B8A6;
299
- --interactive-text-dark: #5EEAD4; --interactive-text-hover-dark: #99F6E4; --border-dark: #334155;
300
- --button-secondary-bg-dark: #334155; --button-secondary-text-dark: #CBD5E1; --button-secondary-hover-bg-dark: #475569;
301
- --shadow-dark: 0 5px 15px rgba(0,0,0,0.2); --focus-ring-dark: rgba(45, 212, 191, 0.3);
302
- --error-bg-dark: #451515; --error-text-dark: #FFD0D0; --error-border-dark: #9E2D2D;
303
- --success-bg-dark: #073D24; --success-text-dark: #B3EED1; --success-border-dark: #16653D;
304
-
305
- --radius-md: 8px; --radius-lg: 12px; --transition: 0.2s ease-in-out;
306
- }
307
-
308
- body, .gradio-container { font-family: var(--font-family-main) !important; background: var(--app-bg-light) !important; color: var(--text-primary-light) !important; margin: 0; padding: 0; min-height: 100vh; font-size: 16px; line-height: 1.7; }
309
- * { box-sizing: border-box; }
310
- @media (prefers-color-scheme: dark) { body, .gradio-container { background: var(--app-bg-dark) !important; color: var(--text-primary-dark) !important; } }
311
-
312
- .gradio-container > .flex.flex-col { max-width: 820px; margin: 0 auto !important; padding: 0 1.5rem 3rem 1.5rem !important; gap: 0 !important; /* Remove gap, manage spacing with element margins */ }
313
-
314
- .content-surface { background: var(--surface-bg-light) !important; border-radius: var(--radius-lg) !important; padding: 3rem !important; box-shadow: var(--shadow-light) !important; border: 1px solid var(--border-light) !important; margin-bottom: 3rem; }
315
- .content-surface:last-child { margin-bottom: 0; } /* No bottom margin for the last surface */
316
- @media (prefers-color-scheme: dark) { .content-surface { background: var(--surface-bg-dark) !important; box-shadow: var(--shadow-dark) !important; border: 1px solid var(--border-dark) !important; } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
- .app-header-wrapper { background: var(--accent-primary-light) !important; margin-bottom: 3rem; border-bottom-left-radius: var(--radius-lg); border-bottom-right-radius: var(--radius-lg); box-shadow: var(--shadow-light); }
319
- .app-header { color: #FFFFFF !important; padding: 3.5rem 2rem !important; text-align: center !important; display: flex; flex-direction: column; align-items: center; }
320
- .app-header-logo { font-size: 3rem; margin-bottom: 0.75rem; display: block; text-align: center !important; }
321
- .app-header-title { font-size: 2.25rem; font-weight: 600; margin: 0 0 0.5rem 0; text-align: center !important; }
322
- .app-header-tagline { font-size: 1.1rem; font-weight: 300; opacity: 0.95; text-align: center !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  @media (prefers-color-scheme: dark) {
324
- .app-header-wrapper { background: var(--accent-primary-dark) !important; box-shadow: var(--shadow-dark); }
325
- .app-header { color: var(--app-bg-dark) !important; }
 
 
 
326
  }
327
 
328
- .section-title, .input-form-card h3, .examples-card .gr-examples-header { font-size: 1.5rem !important; font-weight: 600 !important; color: var(--text-primary-light) !important; margin: 0 auto 2rem auto !important; padding-bottom: 1rem !important; border-bottom: 1px solid var(--border-light) !important; text-align: center !important; width: 100%; }
329
- @media (prefers-color-scheme: dark) { .section-title, .input-form-card h3, .examples-card .gr-examples-header { color: var(--text-primary-dark) !important; border-bottom-color: var(--border-dark) !important; } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
 
331
- .content-surface p { font-size: 1rem; line-height: 1.75; color: var(--text-secondary-light); margin-bottom: 1rem; }
332
- .content-surface a { color: var(--interactive-text-light); text-decoration: none; font-weight: 500; }
333
- .content-surface a:hover { color: var(--interactive-text-hover-light); text-decoration: underline; }
334
- .content-surface strong { font-weight: 600; color: var(--text-primary-light); }
335
- @media (prefers-color-scheme: dark) { .content-surface p { color: var(--text-secondary-dark); } .content-surface a { color: var(--interactive-text-dark); } .content-surface a:hover { color: var(--interactive-text-hover-dark); } .content-surface strong { color: var(--text-primary-dark); } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
 
337
  .input-field-group { margin-bottom: 2rem; }
338
- .input-row { display: flex; gap: 1.75rem; flex-wrap: wrap; margin-bottom: 2rem; }
339
- .input-field { flex: 1; min-width: 250px; }
340
-
341
- .gradio-input-label { font-size: 0.9rem !important; font-weight: 500 !important; color: var(--text-primary-light) !important; margin-bottom: 0.5rem !important; display: block !important; }
342
- .gradio-input-info { font-size: 0.8rem !important; color: var(--text-secondary-light) !important; margin-top: 0.35rem; }
343
- @media (prefers-color-scheme: dark) { .gradio-input-label { color: var(--text-primary-dark) !important; } .gradio-input-info { color: var(--text-secondary-dark) !important; } }
344
-
345
- .gradio-textbox textarea, .gradio-dropdown select, .gradio-textbox input[type=password] { border: 1px solid var(--border-light) !important; border-radius: var(--radius-md) !important; padding: 0.9rem 1.05rem !important; font-size: 1rem !important; background: var(--surface-bg-light) !important; color: var(--text-primary-light) !important; width: 100% !important; box-shadow: none !important; transition: border-color var(--transition), box-shadow var(--transition); }
346
- .gradio-textbox textarea { min-height: 120px; }
347
- .gradio-textbox textarea::placeholder, .gradio-textbox input[type=password]::placeholder { color: #A0AEC0 !important; }
348
- .gradio-textbox textarea:focus, .gradio-dropdown select:focus, .gradio-textbox input[type=password]:focus { border-color: var(--accent-primary-light) !important; box-shadow: 0 0 0 3px var(--focus-ring-light) !important; outline: none !important; }
349
- @media (prefers-color-scheme: dark) { .gradio-textbox textarea, .gradio-dropdown select, .gradio-textbox input[type=password] { border: 1px solid var(--border-dark) !important; background: var(--surface-bg-dark) !important; color: var(--text-primary-dark) !important; } .gradio-textbox textarea::placeholder, .gradio-textbox input[type=password]::placeholder { color: #718096 !important; } .gradio-textbox textarea:focus, .gradio-dropdown select:focus, .gradio-textbox input[type=password]:focus { border-color: var(--accent-primary-dark) !important; box-shadow: 0 0 0 3px var(--focus-ring-dark) !important; } }
350
- .gradio-dropdown select { appearance: none; -webkit-appearance: none; -moz-appearance: none; background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2020%2020%22%20fill%3D%22%236B7280%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20d%3D%22M5.293%207.293a1%201%200%20011.414%200L10%2010.586l3.293-3.293a1%201%200%20111.414%201.414l-4%204a1%201%200%2001-1.414%200l-4-4a1%201%200%20010-1.414z%22%20clip-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E'); background-repeat: no-repeat; background-position: right 1rem center; background-size: 1em; padding-right: 3rem !important; }
351
- @media (prefers-color-scheme: dark) { .gradio-dropdown select { background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2020%2020%22%20fill%3D%22%239CA3AF%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20d%3D%22M5.293%207.293a1%201%200%20011.414%200L10%2010.586l3.293-3.293a1%201%200%20111.414%201.414l-4%204a1%201%200%2001-1.414%200l-4-4a1%201%200%20010-1.414z%22%20clip-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E'); } }
352
-
353
- .button-row { display: flex; gap: 1.25rem; margin-top: 2.25rem; flex-wrap: wrap; justify-content: flex-end; }
354
- .gradio-button { border-radius: var(--radius-md) !important; padding: 0.8rem 1.85rem !important; font-size: 1rem !important; font-weight: 500 !important; border: 1px solid transparent !important; box-shadow: var(--shadow-light) !important; }
355
- .gradio-button:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 6px 12px rgba(0,0,0,0.07) !important; }
356
- .gradio-button:active:not(:disabled) { transform: translateY(-1px); }
357
- .gradio-button:disabled { background: #E5E7EB !important; color: #9CA3AF !important; box-shadow: none !important; border-color: #D1D5DB !important; }
358
- .gr-button-primary { background: var(--accent-primary-light) !important; color: #FFFFFF !important; border-color: var(--accent-primary-light) !important; }
359
- .gr-button-primary:hover:not(:disabled) { background: var(--accent-primary-hover-light) !important; border-color: var(--accent-primary-hover-light) !important;}
360
- .gr-button-secondary { background: var(--button-secondary-bg-light) !important; color: var(--button-secondary-text-light) !important; border-color: var(--border-light) !important; }
361
- .gr-button-secondary:hover:not(:disabled) { background: var(--button-secondary-hover-bg-light) !important; border-color: #CBD5E0 !important; }
362
- @media (prefers-color-scheme: dark) { .gradio-button { box-shadow: var(--shadow-dark) !important; } .gradio-button:hover:not(:disabled) { box-shadow: 0 6px 12px rgba(0,0,0,0.25) !important; } .gradio-button:disabled { background: #334155 !important; color: #6B7280 !important; border-color: #475569 !important;} .gr-button-primary { background: var(--accent-primary-dark) !important; color: var(--app-bg-dark) !important; border-color: var(--accent-primary-dark) !important; } .gr-button-primary:hover:not(:disabled) { background: var(--accent-primary-hover-dark) !important; border-color: var(--accent-primary-hover-dark) !important; } .gr-button-secondary { background: var(--button-secondary-bg-dark) !important; color: var(--button-secondary-text-dark) !important; border-color: var(--border-dark) !important; } .gr-button-secondary:hover:not(:disabled) { background: var(--button-secondary-hover-bg-dark) !important; border-color: #475569 !important; } }
363
-
364
- .output-card .response-header { font-size: 1.3rem; font-weight: 600; color: var(--text-primary-light); margin: 0 0 1rem 0; display: flex; align-items: center; gap: 0.6rem; }
365
- .output-card .response-icon { font-size: 1.4rem; color: var(--text-secondary-light); }
366
- .output-card .divider { border: none; border-top: 1px solid var(--border-light); margin: 1.5rem 0; }
367
- .output-card .output-content-wrapper { font-size: 1rem; line-height: 1.75; color: var(--text-primary-light); }
368
- .output-card .output-content-wrapper p { margin-bottom: 1rem; } .output-card .output-content-wrapper ul, .output-card .output-content-wrapper ol { margin-left: 1.5rem; margin-bottom: 1rem; padding-left: 1rem; } .output-card .output-content-wrapper li { margin-bottom: 0.5rem; }
369
- @media (prefers-color-scheme: dark) { .output-card .response-header { color: var(--text-primary-dark); } .output-card .response-icon { color: var(--text-secondary-dark); } .output-card .divider { border-top: 1px solid var(--border-dark); } .output-card .output-content-wrapper { color: var(--text-primary-dark); } }
370
-
371
- .output-card .error-message, .output-card .success-message { padding: 1rem 1.25rem; margin-top: 1.25rem; font-size: 0.95rem; border-radius: var(--radius-md);}
372
- .output-card .error-message .error-icon { font-size: 1.2rem; } .output-card .error-details { font-size: 0.85rem; }
373
- .output-card .placeholder { padding: 3rem 1.5rem; font-size: 1.1rem; border-radius: var(--radius-lg); border: 2px dashed var(--border-light); }
374
- @media (prefers-color-scheme: dark) { .output-card .placeholder { border-color: var(--border-dark); } }
375
-
376
- .examples-card .gr-examples-table { border-radius: var(--radius-lg) !important; border: 1px solid var(--border-light) !important; }
377
- .examples-card .gr-examples-table th, .examples-card .gr-examples-table td { padding: 0.9rem 1.1rem !important; font-size: 0.95rem !important; }
378
- .examples-card .gr-examples-table th { background: #F9FAFB !important; }
379
- @media (prefers-color-scheme: dark) { .examples-card .gr-examples-table { border: 1px solid var(--border-dark) !important;} .examples-card .gr-examples-table th { background: #0F172A !important; } }
380
-
381
- .app-footer-wrapper { border-top: 1px solid var(--border-light) !important; margin-top: 3rem; }
382
- .app-footer { padding: 3rem 1.5rem !important; text-align: center !important; display: flex; flex-direction: column; align-items: center; }
383
- .app-footer p { font-size: 0.9rem !important; color: var(--text-secondary-light) !important; margin-bottom: 0.75rem; text-align: center !important; max-width: 600px; /* Constrain footer text width */ }
384
- .app-footer a { color: var(--interactive-text-light) !important; font-weight: 500; }
385
- .app-footer a:hover { color: var(--interactive-text-hover-light) !important; text-decoration: underline; }
386
- @media (prefers-color-scheme: dark) { .app-footer-wrapper { border-top-color: var(--border-dark) !important; } .app-footer p { color: var(--text-secondary-dark) !important; } .app-footer a { color: var(--interactive-text-dark) !important; } .app-footer a:hover { color: var(--interactive-text-hover-dark) !important; } }
387
-
388
- :focus-visible { outline: 2px solid var(--accent-primary-light) !important; outline-offset: 2px; box-shadow: 0 0 0 3px var(--focus-ring-light) !important; }
389
- @media (prefers-color-scheme: dark) { :focus-visible { outline-color: var(--accent-primary-dark) !important; box-shadow: 0 0 0 3px var(--focus-ring-dark) !important; } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  .gradio-button span:focus { outline: none !important; }
391
 
392
- @media (max-width: 768px) { body { font-size: 15px; } .gradio-container > .flex.flex-col { padding: 0 1rem 2.5rem 1rem !important; } .content-surface { padding: 2.25rem !important; margin-bottom: 2.5rem; } .app-header-wrapper { margin-bottom: 2.5rem; } .app-header { padding: 2.75rem 1.25rem !important; } .app-header-logo { font-size: 2.6rem; } .app-header-title { font-size: 2rem; } .app-header-tagline { font-size: 1.05rem; } .input-row { flex-direction: column; gap: 1.5rem; } .input-field { min-width: 100%; } .button-row { justify-content: stretch; } .gradio-button { width: 100%; } .section-title, .input-form-card h3, .examples-card .gr-examples-header { font-size: 1.35rem !important; } .app-footer-wrapper { margin-top: 2.5rem; } .app-footer { padding: 2.5rem 1rem !important; } }
393
- @media (max-width: 480px) { .gradio-container > .flex.flex-col { padding: 0 0.75rem 2rem 0.75rem !important; } .content-surface { padding: 1.75rem !important; margin-bottom: 2rem; border-radius: var(--radius-md) !important; } .app-header-wrapper { margin-bottom: 2rem; border-bottom-left-radius: var(--radius-md); border-bottom-right-radius: var(--radius-md); } .app-header { padding: 2.25rem 1rem !important; } .app-header-logo { font-size: 2.2rem; } .app-header-title { font-size: 1.7rem; } .app-header-tagline { font-size: 0.95rem; } .section-title, .input-form-card h3, .examples-card .gr-examples-header { font-size: 1.25rem !important; margin-bottom: 1.75rem !important; padding-bottom: 0.85rem !important; } .gradio-textbox textarea, .gradio-dropdown select, .gradio-textbox input[type=password] { font-size: 0.95rem !important; padding: 0.85rem 1rem !important; } .gradio-button { padding: 0.85rem 1.5rem !important; font-size: 0.95rem !important; } .examples-card .gr-examples-table th, .examples-card .gr-examples-table td { padding: 0.75rem 0.9rem !important; font-size: 0.9rem !important; } .app-footer-wrapper { margin-top: 2rem; } .app-footer { padding: 2rem 0.75rem !important; } }
394
 
395
- .gradio-container > .flex { gap: 0 !important; } /* Main gap removed, managed by surface margins */
396
- .gradio-markdown > *:first-child { margin-top: 0; } .gradio-markdown > *:last-child { margin-bottom: 0; }
 
 
 
397
  .gradio-dropdown, .gradio-textbox { border: none !important; padding: 0 !important; background: transparent !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398
  """
399
 
400
  with gr.Blocks(theme=None, css=custom_css, title="Landlord-Tenant Rights Assistant") as demo:
401
- # --- Header Section ---
402
- # We'll wrap the Markdown in a gr.Group to apply wrapper styles if needed
403
  with gr.Group(elem_classes="app-header-wrapper"):
404
  gr.Markdown(
405
  """
@@ -411,17 +955,18 @@ Answer:"""
411
  """
412
  )
413
 
414
- # --- Main Content Sections ---
415
  with gr.Group(elem_classes="content-surface"):
416
  gr.Markdown("<h3 class='section-title'>Know Your Rights</h3>")
417
  gr.Markdown(
418
  """
419
  <p>Navigate landlord-tenant laws with ease. Enter your <strong>OpenAI API key</strong>, select your state, and ask your question to get detailed, state-specific answers.</p>
420
  <p>Don't have an API key? <a href='https://platform.openai.com/api-keys' target='_blank'>Get one free from OpenAI</a>.</p>
421
- <p><strong>Disclaimer:</strong> This tool provides information only, not legal advice. For legal guidance, consult a licensed attorney.</p>
422
  """
423
  )
424
 
 
425
  with gr.Group(elem_classes="content-surface input-form-card"):
426
  gr.Markdown("<h3>Ask Your Question</h3>")
427
  with gr.Column(elem_classes="input-field-group"):
@@ -430,12 +975,12 @@ Answer:"""
430
  info="Required to process your query. Securely used per request, not stored.", lines=1
431
  )
432
  with gr.Row(elem_classes="input-row"):
433
- with gr.Column(elem_classes="input-field", min_width="58%"):
434
  query_input = gr.Textbox(
435
  label="Your Question", placeholder="E.g., What are the rules for security deposit returns in my state?",
436
  lines=5, max_lines=10
437
  )
438
- with gr.Column(elem_classes="input-field", min_width="38%"):
439
  state_input = gr.Dropdown(
440
  label="Select State", choices=dropdown_choices, value=initial_value,
441
  allow_custom_value=False
@@ -444,12 +989,14 @@ Answer:"""
444
  clear_button = gr.Button("Clear", variant="secondary", elem_classes=["gr-button-secondary"])
445
  submit_button = gr.Button("Submit Query", variant="primary", elem_classes=["gr-button-primary"])
446
 
 
447
  with gr.Group(elem_classes="content-surface output-card"):
448
  output = gr.Markdown(
449
  value="<div class='placeholder'>Your answer will appear here after submitting your query.</div>",
450
- elem_classes="output-content-wrapper"
451
  )
452
 
 
453
  if example_queries:
454
  with gr.Group(elem_classes="content-surface examples-card"):
455
  gr.Examples(
@@ -460,7 +1007,7 @@ Answer:"""
460
  with gr.Group(elem_classes="content-surface"):
461
  gr.Markdown("<div class='placeholder'>Sample questions could not be loaded.</div>")
462
 
463
- # --- Footer Section ---
464
  with gr.Group(elem_classes="app-footer-wrapper"):
465
  gr.Markdown(
466
  """
@@ -473,46 +1020,60 @@ Answer:"""
473
  """
474
  )
475
 
 
476
  submit_button.click(
477
  fn=query_interface_wrapper, inputs=[api_key_input, query_input, state_input], outputs=output, api_name="submit_query"
478
  )
479
  clear_button.click(
480
- fn=lambda: ("", "", initial_value, "<div class='placeholder'>Inputs cleared. Ready for your next question.</div>"),
 
 
 
 
 
481
  inputs=[], outputs=[api_key_input, query_input, state_input, output]
482
  )
483
- logging.info("Final refined Clarity & Counsel theme Gradio interface created.")
484
  return demo
485
 
486
- # --- Main Execution Block (remains the same) ---
487
  if __name__ == "__main__":
488
  logging.info("Starting Landlord-Tenant Rights Bot application...")
489
  try:
490
  SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
491
- DEFAULT_PDF_PATH = os.path.join(SCRIPT_DIR, "tenant-landlord.pdf")
492
- DEFAULT_DB_PATH = os.path.join(SCRIPT_DIR, "chroma_db")
493
 
 
494
  PDF_PATH = os.getenv("PDF_PATH", DEFAULT_PDF_PATH)
495
  VECTOR_DB_PATH = os.getenv("VECTOR_DB_PATH", DEFAULT_DB_PATH)
496
 
 
497
  os.makedirs(os.path.dirname(VECTOR_DB_PATH), exist_ok=True)
498
- os.makedirs(os.path.dirname(PDF_PATH), exist_ok=True)
 
 
 
499
 
 
500
  if not os.path.exists(PDF_PATH):
501
  logging.error(f"FATAL: PDF file not found at the specified path: {PDF_PATH}")
502
  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")
503
  exit(1)
504
 
 
505
  vector_db_instance = VectorDatabase(persist_directory=VECTOR_DB_PATH)
506
  rag = RAGSystem(vector_db=vector_db_instance)
507
- rag.load_pdf(PDF_PATH)
508
 
 
509
  app_interface = rag.gradio_interface()
510
  SERVER_PORT = 7860
511
  logging.info(f"Launching Gradio app on http://0.0.0.0:{SERVER_PORT}")
512
- print(f"\n--- Gradio App Running ---\nAccess at: http://localhost:{SERVER_PORT}\n--------------------------\n")
513
  app_interface.launch(server_name="0.0.0.0", server_port=SERVER_PORT, share=True)
514
 
515
  except Exception as e:
516
  logging.error(f"Application startup failed: {str(e)}", exc_info=True)
517
- print(f"\n--- FATAL STARTUP ERROR ---\n{str(e)}\nCheck logs for details.\n---------------------------\n")
518
  exit(1)
 
33
  format='%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
34
  )
35
 
36
+ # --- RAGSystem Class (Processing Logic - kept intact as requested) ---
37
  class RAGSystem:
38
  def __init__(self, vector_db: Optional[VectorDatabase] = None):
39
  logging.info("Initializing RAGSystem")
 
237
  logging.error(f"Failed to load or process PDF '{pdf_path}': {str(e)}", exc_info=True)
238
  raise RuntimeError(f"Failed to process PDF '{pdf_path}': {e}") from e
239
 
240
+ # --- GRADIO INTERFACE (Completely Redesigned UI) ---
241
  def gradio_interface(self):
242
  def query_interface_wrapper(api_key: str, query: str, state: str) -> str:
243
+ # Basic client-side validation for immediate feedback
244
  if not api_key or not api_key.strip() or not api_key.startswith("sk-"):
245
  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 here</a>.</div>"
246
  if not state or state == "Select a state..." or "Error" in state:
 
248
  if not query or not query.strip():
249
  return "<div class='error-message'><span class='error-icon'>⚠️</span>Please enter your question in the text box.</div>"
250
 
251
+ # Call the core processing logic
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
+ # Check if the answer already contains an error message (from deeper within process_query)
256
+ if "<div class='error-message'>" in answer:
257
+ return answer # Return the pre-formatted error message directly
258
  else:
259
+ # Format the successful response with the new UI structure
260
+ formatted_response = f"<div class='response-header'><span class='response-icon'>📜</span>Response for {state}</div><hr class='divider'>{answer}"
261
+ return formatted_response
262
 
263
  try:
264
  available_states_list = self.get_states()
 
268
  dropdown_choices = ["Error: Critical failure loading states"]
269
  initial_value = dropdown_choices[0]
270
 
271
+ # Define example queries, filtering based on available states
272
  example_queries_base = [
273
  ["What are the rules for security deposit returns?", "California"],
274
  ["Can a landlord enter my apartment without notice?", "New York"],
 
278
  if available_states_list and "Error" not in available_states_list[0] and len(available_states_list) > 0:
279
  loaded_states_set = set(available_states_list)
280
  example_queries = [ex for ex in example_queries_base if ex[1] in loaded_states_set]
281
+ # Add a generic example if no specific state examples match or if list is empty
282
+ if not example_queries:
283
+ example_queries.append(["What basic rights do tenants have?", available_states_list[0] if available_states_list else "California"])
284
+ else: # Fallback if states list is problematic
285
  example_queries.append(["What basic rights do tenants have?", "California"])
286
 
287
 
288
+ # --- Custom CSS for "Legal Lumen" Theme ---
289
  custom_css = """
290
+ /* New Theme: Legal Lumen */
291
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Montserrat:wght@600;700;800&display=swap');
292
 
293
  :root {
294
+ /* Light Theme Colors */
295
+ --app-bg-light: #F8F9FA; /* Very light gray */
296
+ --surface-bg-light: #FFFFFF; /* Pure white for cards */
297
+ --text-primary-light: #212529; /* Dark charcoal */
298
+ --text-secondary-light: #6C757D; /* Muted gray */
299
+ --accent-main-light: #007bff; /* Vibrant professional blue */
300
+ --accent-hover-light: #0056b3; /* Darker blue on hover */
301
+ --accent-secondary-light: #6C757D; /* For secondary buttons, similar to text secondary */
302
+ --accent-secondary-hover-light: #5A6268; /* Darker secondary hover */
303
+ --border-light: #DEE2E6; /* Light border */
304
+ --shadow-light: 0 0.5rem 1rem rgba(0,0,0,0.08); /* Subtle shadow */
305
+ --focus-ring-light: rgba(0, 123, 255, 0.25); /* Blue focus ring */
306
+ --error-bg-light: #F8D7DA; /* Light red for error backgrounds */
307
+ --error-text-light: #721C24; /* Dark red for error text */
308
+ --error-border-light: #F5C6CB; /* Red border for errors */
309
+
310
+ /* Dark Theme Colors */
311
+ --app-bg-dark: #212529; /* Deep dark gray */
312
+ --surface-bg-dark: #343A40; /* Slightly lighter dark gray for cards */
313
+ --text-primary-dark: #F8F9FA; /* Off-white */
314
+ --text-secondary-dark: #ADB5BD; /* Muted light gray */
315
+ --accent-main-dark: #8AB4F8; /* Lighter blue for dark mode */
316
+ --accent-hover-dark: #669DF6; /* Darker blue on hover for dark mode */
317
+ --accent-secondary-dark: #ADB5BD; /* For secondary buttons in dark mode */
318
+ --accent-secondary-hover-dark: #97A0A8; /* Darker secondary hover in dark mode */
319
+ --border-dark: #495057; /* Dark border */
320
+ --shadow-dark: 0 0.5rem 1rem rgba(0,0,0,0.3); /* Darker shadow */
321
+ --focus-ring-dark: rgba(138, 180, 248, 0.35); /* Muted blue focus ring for dark mode */
322
+ --error-bg-dark: #721C24; /* Dark red for error backgrounds */
323
+ --error-text-dark: #F8D7DA; /* Light red for error text */
324
+ --error-border-dark: #F5C6CB; /* Red border for errors */
325
+
326
+ /* General Styling Variables */
327
+ --font-family-main: 'Inter', sans-serif;
328
+ --font-family-header: 'Montserrat', sans-serif;
329
+ --radius-sm: 4px; /* Small border radius */
330
+ --radius-md: 8px; /* Medium border radius */
331
+ --radius-lg: 12px; /* Large border radius */
332
+ --transition-speed: 0.25s; /* Standard transition duration */
333
+ }
334
+
335
+ /* Base styles for the entire app */
336
+ body, .gradio-container {
337
+ font-family: var(--font-family-main) !important;
338
+ background: var(--app-bg-light) !important;
339
+ color: var(--text-primary-light) !important;
340
+ margin: 0;
341
+ padding: 0;
342
+ min-height: 100vh;
343
+ font-size: 16px;
344
+ line-height: 1.6; /* Improved readability */
345
+ -webkit-font-smoothing: antialiased; /* Smoother fonts */
346
+ -moz-osx-font-smoothing: grayscale;
347
+ }
348
+ * { box-sizing: border-box; } /* Ensure consistent box model */
349
+
350
+ /* Dark mode preference (applies styles when user's system is in dark mode) */
351
+ @media (prefers-color-scheme: dark) {
352
+ body, .gradio-container {
353
+ background: var(--app-bg-dark) !important;
354
+ color: var(--text-primary-dark) !important;
355
+ }
356
+ }
357
+
358
+ /* Global container styling: max-width for content, centering, and padding */
359
+ .gradio-container > .flex.flex-col {
360
+ max-width: 900px; /* Slightly wider for better content flow */
361
+ margin: 0 auto !important; /* Center the container */
362
+ padding: 2rem 1.5rem 4rem 1.5rem !important; /* Generous top, side, and bottom padding */
363
+ gap: 0 !important; /* Remove Gradio's default gap, manage spacing with element margins */
364
+ }
365
+
366
+ /* Header section styling: prominent, rounded banner */
367
+ .app-header-wrapper {
368
+ background: linear-gradient(135deg, var(--accent-main-light) 0%, var(--accent-hover-light) 100%); /* Gradient background for depth */
369
+ color: #FFFFFF !important; /* White text for light theme header */
370
+ padding: 3rem 2rem 4rem 2rem !important; /* More vertical padding for impact */
371
+ text-align: center !important;
372
+ border-radius: var(--radius-lg); /* Rounded corners for the header card */
373
+ box-shadow: var(--shadow-light);
374
+ margin-bottom: 3.5rem; /* Spacing between header and first content card */
375
+ position: relative; /* For animation context */
376
+ overflow: hidden; /* Ensure content stays within rounded corners */
377
+ }
378
+ .app-header {
379
+ display: flex;
380
+ flex-direction: column;
381
+ align-items: center;
382
+ position: relative;
383
+ z-index: 1; /* Keep content above any potential background effects */
384
+ }
385
+ .app-header-logo {
386
+ font-size: 4rem; /* Larger icon */
387
+ margin-bottom: 1rem;
388
+ display: block;
389
+ line-height: 1; /* Prevent extra space below emoji */
390
+ animation: bounceIn 0.8s ease-out; /* Simple entry animation for logo */
391
+ }
392
+ .app-header-title {
393
+ font-family: var(--font-family-header) !important;
394
+ font-size: 2.8rem; /* Much larger and bolder */
395
+ font-weight: 800; /* Extra bold */
396
+ margin: 0 0 0.75rem 0;
397
+ letter-spacing: -0.04em; /* Tighter letter spacing for a modern look */
398
+ text-shadow: 0 2px 4px rgba(0,0,0,0.2); /* Subtle text shadow for depth */
399
+ animation: fadeInDown 1s ease-out; /* Entry animation for title */
400
+ }
401
+ .app-header-tagline {
402
+ font-family: var(--font-family-main) !important;
403
+ font-size: 1.25rem; /* Slightly larger tagline */
404
+ font-weight: 300;
405
+ opacity: 0.9;
406
+ max-width: 600px;
407
+ animation: fadeInUp 1s ease-out; /* Entry animation for tagline */
408
+ }
409
+
410
+ /* Dark mode header adjustments */
411
+ @media (prefers-color-scheme: dark) {
412
+ .app-header-wrapper {
413
+ background: linear-gradient(135deg, var(--accent-main-dark) 0%, var(--accent-hover-dark) 100%);
414
+ box-shadow: var(--shadow-dark);
415
+ }
416
+ .app-header-title { color: var(--text-primary-dark) !important; } /* Ensure title color adjusts for dark theme */
417
+ }
418
+
419
+ /* Keyframe animations for header elements */
420
+ @keyframes bounceIn {
421
+ 0%, 20%, 40%, 60%, 80%, 100% {
422
+ -webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
423
+ transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
424
+ }
425
+ 0% { opacity: 0; -webkit-transform: scale3d(0.3, 0.3, 0.3); transform: scale3d(0.3, 0.3, 0.3); }
426
+ 20% { -webkit-transform: scale3d(1.1, 1.1, 1.1); transform: scale3d(1.1, 1.1, 1.1); }
427
+ 40% { -webkit-transform: scale3d(0.9, 0.9, 0.9); transform: scale3d(0.9, 0.9, 0.9); }
428
+ 60% { opacity: 1; -webkit-transform: scale3d(1.03, 1.03, 1.03); transform: scale3d(1.03, 1.03, 1.03); }
429
+ 80% { -webkit-transform: scale3d(0.97, 0.97, 0.97); transform: scale3d(0.97, 0.97, 0.97); }
430
+ 100% { opacity: 1; -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); }
431
+ }
432
 
433
+ @keyframes fadeInDown {
434
+ from { opacity: 0; transform: translate3d(0, -20px, 0); }
435
+ to { opacity: 1; transform: translate3d(0, 0, 0); }
436
+ }
437
+
438
+ @keyframes fadeInUp {
439
+ from { opacity: 0; transform: translate3d(0, 20px, 0); }
440
+ to { opacity: 1; transform: translate3d(0, 0, 0); }
441
+ }
442
+
443
+
444
+ /* Styling for general content cards (surface elements) */
445
+ .content-surface {
446
+ background: var(--surface-bg-light) !important;
447
+ border-radius: var(--radius-lg) !important;
448
+ padding: 3rem !important; /* Consistent padding */
449
+ box-shadow: var(--shadow-light) !important;
450
+ border: 1px solid var(--border-light) !important;
451
+ margin-bottom: 3rem; /* Spacing between cards */
452
+ transition: background var(--transition-speed), border-color var(--transition-speed), box-shadow var(--transition-speed);
453
+ }
454
+ .content-surface:last-child { margin-bottom: 0; } /* No bottom margin for the last surface */
455
  @media (prefers-color-scheme: dark) {
456
+ .content-surface {
457
+ background: var(--surface-bg-dark) !important;
458
+ box-shadow: var(--shadow-dark) !important;
459
+ border: 1px solid var(--border-dark) !important;
460
+ }
461
  }
462
 
463
+ /* Section titles within content cards: centered, bold, with a clear separator */
464
+ .section-title, .input-form-card h3, .examples-card .gr-examples-header {
465
+ font-family: var(--font-family-header) !important;
466
+ font-size: 1.65rem !important; /* Slightly larger than before */
467
+ font-weight: 600 !important;
468
+ color: var(--text-primary-light) !important;
469
+ margin: 0 auto 2.2rem auto !important; /* Centered, with more space below title */
470
+ padding-bottom: 1rem !important;
471
+ border-bottom: 2px solid var(--border-light) !important; /* Thicker separator for emphasis */
472
+ text-align: center !important;
473
+ width: 100%;
474
+ }
475
+ @media (prefers-color-scheme: dark) {
476
+ .section-title, .input-form-card h3, .examples-card .gr-examples-header {
477
+ color: var(--text-primary-dark) !important;
478
+ border-bottom-color: var(--border-dark) !important;
479
+ }
480
+ }
481
 
482
+ /* General text styling within content surfaces */
483
+ .content-surface p {
484
+ font-size: 1.05rem; /* Slightly larger body text for readability */
485
+ line-height: 1.7; /* Generous line height */
486
+ color: var(--text-secondary-light);
487
+ margin-bottom: 1rem;
488
+ }
489
+ .content-surface a {
490
+ color: var(--accent-main-light);
491
+ text-decoration: none;
492
+ font-weight: 500;
493
+ transition: color var(--transition-speed), text-decoration var(--transition-speed);
494
+ }
495
+ .content-surface a:hover {
496
+ color: var(--accent-hover-light);
497
+ text-decoration: underline;
498
+ }
499
+ .content-surface strong {
500
+ font-weight: 600;
501
+ color: var(--text-primary-light);
502
+ }
503
+ @media (prefers-color-scheme: dark) {
504
+ .content-surface p { color: var(--text-secondary-dark); }
505
+ .content-surface a { color: var(--accent-main-dark); }
506
+ .content-surface a:hover { color: var(--accent-hover-dark); }
507
+ .content-surface strong { color: var(--text-primary-dark); }
508
+ }
509
 
510
+ /* Input field group and layout: organized and symmetric */
511
  .input-field-group { margin-bottom: 2rem; }
512
+ .input-row {
513
+ display: flex;
514
+ gap: 2rem; /* Increased gap for better spacing */
515
+ flex-wrap: wrap; /* Allow wrapping on smaller screens */
516
+ margin-bottom: 2.5rem; /* More space before buttons */
517
+ }
518
+ .input-field {
519
+ flex: 1; /* Distribute space evenly */
520
+ min-width: 280px; /* Adjusted minimum width before wrapping */
521
+ }
522
+
523
+ /* Input labels and info text */
524
+ .gradio-input-label {
525
+ font-size: 1rem !important; /* Slightly larger label */
526
+ font-weight: 500 !important;
527
+ color: var(--text-primary-light) !important;
528
+ margin-bottom: 0.6rem !important;
529
+ display: block !important;
530
+ }
531
+ .gradio-input-info {
532
+ font-size: 0.85rem !important;
533
+ color: var(--text-secondary-light) !important;
534
+ margin-top: 0.4rem;
535
+ }
536
+ @media (prefers-color-scheme: dark) {
537
+ .gradio-input-label { color: var(--text-primary-dark) !important; }
538
+ .gradio-input-info { color: var(--text-secondary-dark) !important; }
539
+ }
540
+
541
+ /* Textbox, Dropdown, Password input styling: larger and more padded */
542
+ .gradio-textbox textarea,
543
+ .gradio-dropdown select,
544
+ .gradio-textbox input[type=password] {
545
+ border: 1px solid var(--border-light) !important;
546
+ border-radius: var(--radius-md) !important;
547
+ padding: 1rem 1.2rem !important; /* More padding */
548
+ font-size: 1.05rem !important; /* Larger font size */
549
+ background: var(--surface-bg-light) !important;
550
+ color: var(--text-primary-light) !important;
551
+ width: 100% !important;
552
+ box-shadow: none !important;
553
+ transition: border-color var(--transition-speed), box-shadow var(--transition-speed);
554
+ }
555
+ .gradio-textbox textarea { min-height: 140px; } /* Taller textarea for more input visibility */
556
+ .gradio-textbox textarea::placeholder,
557
+ .gradio-textbox input[type=password]::placeholder {
558
+ color: #A0AEC0 !important; /* Consistent placeholder color */
559
+ }
560
+ .gradio-textbox textarea:focus,
561
+ .gradio-dropdown select:focus,
562
+ .gradio-textbox input[type=password]:focus {
563
+ border-color: var(--accent-main-light) !important;
564
+ box-shadow: 0 0 0 3px var(--focus-ring-light) !important; /* Accent color focus ring */
565
+ outline: none !important;
566
+ }
567
+ @media (prefers-color-scheme: dark) {
568
+ .gradio-textbox textarea,
569
+ .gradio-dropdown select,
570
+ .gradio-textbox input[type=password] {
571
+ border: 1px solid var(--border-dark) !important;
572
+ background: var(--surface-bg-dark) !important;
573
+ color: var(--text-primary-dark) !important;
574
+ }
575
+ .gradio-textbox textarea::placeholder,
576
+ .gradio-textbox input[type=password]::placeholder {
577
+ color: #718096 !important;
578
+ }
579
+ .gradio-textbox textarea:focus,
580
+ .gradio-dropdown select:focus,
581
+ .gradio-textbox input[type=password]:focus {
582
+ border-color: var(--accent-main-dark) !important;
583
+ box-shadow: 0 0 0 3px var(--focus-ring-dark) !important;
584
+ }
585
+ }
586
+
587
+ /* Custom dropdown arrow for consistent look (re-applied for new colors) */
588
+ .gradio-dropdown select {
589
+ appearance: none; /* Remove native arrow */
590
+ -webkit-appearance: none;
591
+ -moz-appearance: none;
592
+ background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2020%2020%22%20fill%3D%22%236C757D%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20d%3D%22M5.293%207.293a1%201%200%20011.414%200L10%2010.586l3.293-3.293a1%201%200%20111.414%201.414l-4%204a1%201%200%2001-1.414%200l-4-4a1%201%200%20010-1.414z%22%20clip-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E');
593
+ background-repeat: no-repeat;
594
+ background-position: right 1.2rem center; /* Adjusted position */
595
+ background-size: 1.1em; /* Slightly larger arrow */
596
+ padding-right: 3.5rem !important; /* Make space for arrow */
597
+ }
598
+ @media (prefers-color-scheme: dark) {
599
+ .gradio-dropdown select {
600
+ background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2020%2020%22%20fill%3D%22%23ADB5BD%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20d%3D%22M5.293%207.293a1%201%200%20011.414%200L10%2010.586l3.293-3.293a1%201%200%20111.414%201.414l-4%204a1%201%200%2001-1.414%200l-4-4a1%201%200%20010-1.414z%22%20clip-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E');
601
+ }
602
+ }
603
+
604
+ /* Button group and styling: dynamic and clear actions */
605
+ .button-row {
606
+ display: flex;
607
+ gap: 1.5rem; /* More space between buttons */
608
+ margin-top: 2rem;
609
+ flex-wrap: wrap;
610
+ justify-content: flex-end; /* Align buttons to the right */
611
+ }
612
+ .gradio-button {
613
+ border-radius: var(--radius-md) !important;
614
+ padding: 0.9rem 2rem !important; /* More padding */
615
+ font-size: 1.05rem !important; /* Larger font */
616
+ font-weight: 500 !important;
617
+ border: 1px solid transparent !important;
618
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1); /* Subtle shadow for primary */
619
+ transition: all var(--transition-speed) ease; /* Smooth transition for hover/active states */
620
+ }
621
+ .gradio-button:hover:not(:disabled) {
622
+ transform: translateY(-3px); /* More pronounced lift on hover */
623
+ box-shadow: 0 6px 12px rgba(0,0,0,0.15) !important;
624
+ }
625
+ .gradio-button:active:not(:disabled) { transform: translateY(-1px); } /* Slight press effect */
626
+ .gradio-button:disabled {
627
+ background: #E9ECEF !important;
628
+ color: #ADB5BD !important;
629
+ box-shadow: none !important;
630
+ border-color: #DEE2E6 !important;
631
+ cursor: not-allowed;
632
+ }
633
+
634
+ /* Primary button (Submit Query) */
635
+ .gr-button-primary {
636
+ background: var(--accent-main-light) !important;
637
+ color: #FFFFFF !important;
638
+ border-color: var(--accent-main-light) !important;
639
+ }
640
+ .gr-button-primary:hover:not(:disabled) {
641
+ background: var(--accent-hover-light) !important;
642
+ border-color: var(--accent-hover-light) !important;
643
+ }
644
+
645
+ /* Secondary button (Clear) */
646
+ .gr-button-secondary {
647
+ background: transparent !important; /* Transparent background */
648
+ color: var(--accent-secondary-light) !important;
649
+ border-color: var(--accent-secondary-light) !important; /* Border in accent color */
650
+ box-shadow: none !important; /* No shadow for secondary button */
651
+ }
652
+ .gr-button-secondary:hover:not(:disabled) {
653
+ background: rgba(0, 123, 255, 0.05) !important; /* Very light hover background */
654
+ color: var(--accent-hover-light) !important;
655
+ border-color: var(--accent-hover-light) !important;
656
+ box-shadow: none !important;
657
+ }
658
+
659
+ @media (prefers-color-scheme: dark) {
660
+ .gradio-button { box-shadow: 0 2px 5px rgba(0,0,0,0.2) !important; }
661
+ .gradio-button:hover:not(:disabled) { box-shadow: 0 6px 12px rgba(0,0,0,0.3) !important; }
662
+ .gradio-button:disabled {
663
+ background: #495057 !important;
664
+ color: #6C757D !important;
665
+ border-color: #6C757D !important;
666
+ }
667
+ .gr-button-primary {
668
+ background: var(--accent-main-dark) !important;
669
+ color: var(--text-primary-dark) !important; /* Text color contrasts with dark accent */
670
+ border-color: var(--accent-main-dark) !important;
671
+ }
672
+ .gr-button-primary:hover:not(:disabled) {
673
+ background: var(--accent-hover-dark) !important;
674
+ border-color: var(--accent-hover-dark) !important;
675
+ }
676
+ .gr-button-secondary {
677
+ background: transparent !important;
678
+ color: var(--accent-secondary-dark) !important;
679
+ border-color: var(--accent-secondary-dark) !important;
680
+ }
681
+ .gr-button-secondary:hover:not(:disabled) {
682
+ background: rgba(138, 180, 248, 0.1) !important; /* Light blue hover for dark secondary */
683
+ color: var(--accent-hover-dark) !important;
684
+ border-color: var(--accent-hover-dark) !important;
685
+ }
686
+ }
687
+
688
+ /* Output Card Styling: clear structure for answers */
689
+ .output-card .response-header {
690
+ font-size: 1.4rem; /* Larger header */
691
+ font-weight: 600;
692
+ color: var(--text-primary-light);
693
+ margin: 0 0 1.2rem 0;
694
+ display: flex;
695
+ align-items: center;
696
+ gap: 0.8rem; /* Space between icon and text */
697
+ }
698
+ .output-card .response-icon {
699
+ font-size: 1.6rem;
700
+ color: var(--accent-main-light); /* Accent color for icon */
701
+ }
702
+ .output-card .divider {
703
+ border: none;
704
+ border-top: 1px solid var(--border-light);
705
+ margin: 1.5rem 0 2rem 0; /* More spacing around divider */
706
+ }
707
+ .output-card .output-content-wrapper {
708
+ font-size: 1.05rem;
709
+ line-height: 1.7;
710
+ color: var(--text-primary-light);
711
+ }
712
+ .output-card .output-content-wrapper p { margin-bottom: 1rem; }
713
+ .output-card .output-content-wrapper ul,
714
+ .output-card .output-content-wrapper ol {
715
+ margin-left: 1.8rem; /* More indentation for lists */
716
+ margin-bottom: 1rem;
717
+ padding-left: 0;
718
+ list-style-type: disc; /* Ensure consistent bullet style */
719
+ }
720
+ .output-card .output-content-wrapper ol { list-style-type: decimal; } /* Numbered lists */
721
+ .output-card .output-content-wrapper li { margin-bottom: 0.6rem; }
722
+ .output-card .output-content-wrapper strong { font-weight: 700; } /* Bolder strong text within output */
723
+ .output-card .output-content-wrapper a {
724
+ color: var(--accent-main-light);
725
+ text-decoration: underline; /* Links in output should be underlined for clarity */
726
+ }
727
+ .output-card .output-content-wrapper a:hover {
728
+ color: var(--accent-hover-light);
729
+ }
730
+
731
+ @media (prefers-color-scheme: dark) {
732
+ .output-card .response-header { color: var(--text-primary-dark); }
733
+ .output-card .response-icon { color: var(--accent-main-dark); }
734
+ .output-card .divider { border-top: 1px solid var(--border-dark); }
735
+ .output-card .output-content-wrapper { color: var(--text-primary-dark); }
736
+ .output-card .output-content-wrapper a { color: var(--accent-main-dark); }
737
+ .output-card .output-content-wrapper a:hover { color: var(--accent-hover-dark); }
738
+ }
739
+
740
+ /* Error and Success Messages: visually distinct and informative */
741
+ .output-card .error-message {
742
+ padding: 1rem 1.5rem;
743
+ margin-top: 1.5rem;
744
+ font-size: 1rem;
745
+ border-radius: var(--radius-md);
746
+ background: var(--error-bg-light);
747
+ color: var(--error-text-light);
748
+ border: 1px solid var(--error-border-light);
749
+ display: flex; /* Use flexbox for icon and text alignment */
750
+ align-items: flex-start; /* Align text to top if icon is taller */
751
+ gap: 0.8em;
752
+ }
753
+ .output-card .error-message .error-icon {
754
+ font-size: 1.4rem;
755
+ line-height: 1; /* Ensure icon is vertically centered */
756
+ padding-top: 0.1em; /* Fine tune vertical alignment */
757
+ }
758
+ .output-card .error-details {
759
+ font-size: 0.9rem;
760
+ margin-top: 0.6rem;
761
+ opacity: 0.9;
762
+ word-break: break-word; /* Prevent long URLs/messages from overflowing */
763
+ }
764
+ @media (prefers-color-scheme: dark) {
765
+ .output-card .error-message {
766
+ background: var(--error-bg-dark);
767
+ color: var(--error-text-dark);
768
+ border: 1px solid var(--error-border-dark);
769
+ }
770
+ }
771
+
772
+ /* Placeholder styling for empty output area */
773
+ .output-card .placeholder {
774
+ padding: 3.5rem 1.5rem; /* More vertical padding */
775
+ font-size: 1.15rem; /* Larger placeholder text */
776
+ border-radius: var(--radius-lg);
777
+ border: 2px dashed var(--border-light);
778
+ color: var(--text-secondary-light);
779
+ text-align: center;
780
+ opacity: 0.8;
781
+ }
782
+ @media (prefers-color-scheme: dark) {
783
+ .output-card .placeholder {
784
+ border-color: var(--border-dark);
785
+ color: var(--text-secondary-dark);
786
+ }
787
+ }
788
+
789
+ /* Examples table styling: clean and interactive */
790
+ .examples-card .gr-examples-table {
791
+ border-radius: var(--radius-lg) !important;
792
+ border: 1px solid var(--border-light) !important;
793
+ overflow: hidden; /* Ensures border-radius is applied to table */
794
+ }
795
+ .examples-card .gr-examples-table th,
796
+ .examples-card .gr-examples-table td {
797
+ padding: 1rem 1.2rem !important; /* More padding */
798
+ font-size: 0.98rem !important;
799
+ border: none !important; /* Remove individual cell borders */
800
+ }
801
+ .examples-card .gr-examples-table th {
802
+ background: var(--app-bg-light) !important; /* Match app background for table header for seamless look */
803
+ color: var(--text-primary-light) !important;
804
+ font-weight: 600 !important;
805
+ text-align: left;
806
+ }
807
+ .examples-card .gr-examples-table td {
808
+ background: var(--surface-bg-light) !important;
809
+ color: var(--text-primary-light) !important;
810
+ border-top: 1px solid var(--border-light) !important; /* Add horizontal rule between rows */
811
+ cursor: pointer; /* Indicate clickable rows */
812
+ transition: background var(--transition-speed);
813
+ }
814
+ .examples-card .gr-examples-table tr:hover td {
815
+ background: rgba(0, 123, 255, 0.03) !important; /* Very light hover effect */
816
+ }
817
+ .examples-card .gr-examples-table tr:first-child td { border-top: none !important; } /* No top border for first row's cells */
818
+
819
+ @media (prefers-color-scheme: dark) {
820
+ .examples-card .gr-examples-table { border: 1px solid var(--border-dark) !important;}
821
+ .examples-card .gr-examples-table th {
822
+ background: var(--app-bg-dark) !important;
823
+ color: var(--text-primary-dark) !important;
824
+ }
825
+ .examples-card .gr-examples-table td {
826
+ background: var(--surface-bg-dark) !important;
827
+ color: var(--text-primary-dark) !important;
828
+ border-top: 1px solid var(--border-dark) !important;
829
+ }
830
+ .examples-card .gr-examples-table tr:hover td {
831
+ background: rgba(138, 180, 248, 0.1) !important; /* Light blue hover for dark table */
832
+ }
833
+ }
834
+
835
+ /* Footer styling: clean and informative */
836
+ .app-footer-wrapper {
837
+ border-top: 1px solid var(--border-light) !important;
838
+ margin-top: 3.5rem; /* Spacing from last content card */
839
+ padding-top: 3rem; /* Padding above footer content */
840
+ }
841
+ .app-footer {
842
+ padding: 0 1.5rem !important;
843
+ text-align: center !important;
844
+ display: flex;
845
+ flex-direction: column;
846
+ align-items: center;
847
+ }
848
+ .app-footer p {
849
+ font-size: 0.95rem !important;
850
+ color: var(--text-secondary-light) !important;
851
+ margin-bottom: 0.8rem;
852
+ max-width: 650px; /* Constrain text width for readability */
853
+ }
854
+ .app-footer a {
855
+ color: var(--accent-main-light) !important;
856
+ font-weight: 500;
857
+ transition: color var(--transition-speed), text-decoration var(--transition-speed);
858
+ }
859
+ .app-footer a:hover {
860
+ color: var(--accent-hover-light) !important;
861
+ text-decoration: underline;
862
+ }
863
+ @media (prefers-color-scheme: dark) {
864
+ .app-footer-wrapper { border-top-color: var(--border-dark) !important; }
865
+ .app-footer p { color: var(--text-secondary-dark) !important; }
866
+ .app-footer a { color: var(--accent-main-dark) !important; }
867
+ .app-footer a:hover { color: var(--accent-hover-dark) !important; }
868
+ }
869
+
870
+ /* Accessibility: Focus styles for keyboard navigation */
871
+ :focus-visible {
872
+ outline: 2px solid var(--accent-main-light) !important;
873
+ outline-offset: 2px;
874
+ box-shadow: 0 0 0 4px var(--focus-ring-light) !important; /* Thicker, more visible focus ring */
875
+ }
876
+ @media (prefers-color-scheme: dark) {
877
+ :focus-visible {
878
+ outline-color: var(--accent-main-dark) !important;
879
+ box-shadow: 0 0 0 4px var(--focus-ring-dark) !important;
880
+ }
881
+ }
882
+ /* Prevent double focus outline on Gradio buttons' internal span */
883
  .gradio-button span:focus { outline: none !important; }
884
 
 
 
885
 
886
+ /* Essential overrides to ensure custom styles take precedence over Gradio defaults */
887
+ .gradio-container > .flex { gap: 0 !important; } /* Remove main container gap */
888
+ .gradio-markdown > *:first-child { margin-top: 0; } /* Remove top margin from first element in Markdown */
889
+ .gradio-markdown > *:last-child { margin-bottom: 0; } /* Remove bottom margin from last element in Markdown */
890
+ /* Remove default Gradio component borders/padding that interfere with custom styling */
891
  .gradio-dropdown, .gradio-textbox { border: none !important; padding: 0 !important; background: transparent !important; }
892
+
893
+ /* Responsive adjustments for various screen sizes */
894
+ @media (max-width: 992px) {
895
+ .gradio-container > .flex.flex-col { max-width: 760px; padding: 1.5rem 1.25rem 3.5rem 1.25rem !important; }
896
+ .content-surface { padding: 2.5rem !important; margin-bottom: 2.5rem; }
897
+ .app-header-wrapper { padding: 2.5rem 1.5rem 3.5rem 1.5rem !important; margin-bottom: 3rem; }
898
+ .app-header-title { font-size: 2.5rem; }
899
+ .app-header-tagline { font-size: 1.15rem; }
900
+ .section-title, .input-form-card h3, .examples-card .gr-examples-header { font-size: 1.5rem !important; margin-bottom: 2rem !important; }
901
+ .input-row { gap: 1.5rem; }
902
+ .gradio-textbox textarea { min-height: 120px; }
903
+ }
904
+
905
+ @media (max-width: 768px) {
906
+ .gradio-container > .flex.flex-col { padding: 1rem 1rem 3rem 1rem !important; }
907
+ .content-surface { padding: 2rem !important; margin-bottom: 2rem; }
908
+ .app-header-wrapper { padding: 2rem 1.25rem 3rem 1.25rem !important; margin-bottom: 2.5rem; }
909
+ .app-header-logo { font-size: 3.5rem; }
910
+ .app-header-title { font-size: 2.2rem; letter-spacing: -0.03em; }
911
+ .app-header-tagline { font-size: 1.05rem; }
912
+ .section-title, .input-form-card h3, .examples-card .gr-examples-header { font-size: 1.4rem !important; margin-bottom: 1.8rem !important; }
913
+ .input-row { flex-direction: column; gap: 1.25rem; } /* Stack inputs vertically */
914
+ .input-field { min-width: 100%; }
915
+ .button-row { justify-content: stretch; } /* Stretch buttons to full width */
916
+ .gradio-button { width: 100%; }
917
+ .output-card .response-header { font-size: 1.25rem; }
918
+ .output-card .response-icon { font-size: 1.4rem; }
919
+ .output-card .placeholder { padding: 3rem 1rem; font-size: 1.1rem; }
920
+ .examples-card .gr-examples-table th, .examples-card .gr-examples-table td { padding: 0.8rem 1rem !important; font-size: 0.95rem !important; }
921
+ .app-footer-wrapper { margin-top: 3rem; padding-top: 2.5rem; }
922
+ .app-footer p { font-size: 0.9rem !important; }
923
+ }
924
+
925
+ @media (max-width: 480px) {
926
+ .gradio-container > .flex.flex-col { padding: 0.75rem 0.75rem 2.5rem 0.75rem !important; }
927
+ .content-surface { padding: 1.5rem !important; margin-bottom: 1.5rem; border-radius: var(--radius-md) !important; }
928
+ .app-header-wrapper { padding: 1.5rem 1rem 2rem 1rem !important; margin-bottom: 2rem; border-radius: var(--radius-md); }
929
+ .app-header-logo { font-size: 3rem; margin-bottom: 0.75rem; }
930
+ .app-header-title { font-size: 2rem; letter-spacing: -0.02em; }
931
+ .app-header-tagline { font-size: 0.95rem; }
932
+ .section-title, .input-form-card h3, .examples-card .gr-examples-header { font-size: 1.25rem !important; margin-bottom: 1.5rem !important; padding-bottom: 0.75rem !important; }
933
+ .gradio-textbox textarea, .gradio-dropdown select, .gradio-textbox input[type=password] { font-size: 0.95rem !important; padding: 0.8rem 1rem !important; }
934
+ .gradio-textbox textarea { min-height: 100px; }
935
+ .gradio-button { padding: 0.8rem 1.2rem !important; font-size: 0.95rem !important; }
936
+ .output-card .response-header { font-size: 1.15rem; }
937
+ .output-card .response-icon { font-size: 1.3rem; }
938
+ .output-card .placeholder { padding: 2.5rem 0.8rem; font-size: 1rem; }
939
+ .examples-card .gr-examples-table th, .examples-card .gr-examples-table td { padding: 0.6rem 0.8rem !important; font-size: 0.85rem !important; }
940
+ .app-footer-wrapper { margin-top: 2.5rem; padding-top: 2rem; }
941
+ .app-footer p { font-size: 0.85rem !important; }
942
+ }
943
  """
944
 
945
  with gr.Blocks(theme=None, css=custom_css, title="Landlord-Tenant Rights Assistant") as demo:
946
+ # --- Header Section: Prominent, dynamic banner ---
 
947
  with gr.Group(elem_classes="app-header-wrapper"):
948
  gr.Markdown(
949
  """
 
955
  """
956
  )
957
 
958
+ # --- "Know Your Rights" Section: Informational card ---
959
  with gr.Group(elem_classes="content-surface"):
960
  gr.Markdown("<h3 class='section-title'>Know Your Rights</h3>")
961
  gr.Markdown(
962
  """
963
  <p>Navigate landlord-tenant laws with ease. Enter your <strong>OpenAI API key</strong>, select your state, and ask your question to get detailed, state-specific answers.</p>
964
  <p>Don't have an API key? <a href='https://platform.openai.com/api-keys' target='_blank'>Get one free from OpenAI</a>.</p>
965
+ <p><strong>Disclaimer:</strong> This tool provides information only, not legal advice. For legal guidance, always consult a licensed attorney.</p>
966
  """
967
  )
968
 
969
+ # --- "Ask Your Question" Section (Input Form): Structured and clear inputs ---
970
  with gr.Group(elem_classes="content-surface input-form-card"):
971
  gr.Markdown("<h3>Ask Your Question</h3>")
972
  with gr.Column(elem_classes="input-field-group"):
 
975
  info="Required to process your query. Securely used per request, not stored.", lines=1
976
  )
977
  with gr.Row(elem_classes="input-row"):
978
+ with gr.Column(elem_classes="input-field", min_width="58%"): # Allocate more width for the question
979
  query_input = gr.Textbox(
980
  label="Your Question", placeholder="E.g., What are the rules for security deposit returns in my state?",
981
  lines=5, max_lines=10
982
  )
983
+ with gr.Column(elem_classes="input-field", min_width="38%"): # Allocate less width for the dropdown
984
  state_input = gr.Dropdown(
985
  label="Select State", choices=dropdown_choices, value=initial_value,
986
  allow_custom_value=False
 
989
  clear_button = gr.Button("Clear", variant="secondary", elem_classes=["gr-button-secondary"])
990
  submit_button = gr.Button("Submit Query", variant="primary", elem_classes=["gr-button-primary"])
991
 
992
+ # --- Output Section: Clearly formatted answers or messages ---
993
  with gr.Group(elem_classes="content-surface output-card"):
994
  output = gr.Markdown(
995
  value="<div class='placeholder'>Your answer will appear here after submitting your query.</div>",
996
+ elem_classes="output-content-wrapper" # This div will be replaced by the answer HTML
997
  )
998
 
999
+ # --- "Explore Sample Questions" Section: Interactive examples ---
1000
  if example_queries:
1001
  with gr.Group(elem_classes="content-surface examples-card"):
1002
  gr.Examples(
 
1007
  with gr.Group(elem_classes="content-surface"):
1008
  gr.Markdown("<div class='placeholder'>Sample questions could not be loaded.</div>")
1009
 
1010
+ # --- Footer Section: Disclaimer and author info ---
1011
  with gr.Group(elem_classes="app-footer-wrapper"):
1012
  gr.Markdown(
1013
  """
 
1020
  """
1021
  )
1022
 
1023
+ # --- Event Listeners ---
1024
  submit_button.click(
1025
  fn=query_interface_wrapper, inputs=[api_key_input, query_input, state_input], outputs=output, api_name="submit_query"
1026
  )
1027
  clear_button.click(
1028
+ fn=lambda: (
1029
+ "", # Clear API key input
1030
+ "", # Clear query input
1031
+ initial_value, # Reset state dropdown to initial value
1032
+ "<div class='placeholder'>Inputs cleared. Ready for your next question.</div>" # Reset output to placeholder
1033
+ ),
1034
  inputs=[], outputs=[api_key_input, query_input, state_input, output]
1035
  )
1036
+ logging.info("Advanced UI (Legal Lumen theme) Gradio interface created.")
1037
  return demo
1038
 
1039
+ # --- Main Execution Block (remains untouched from original logic) ---
1040
  if __name__ == "__main__":
1041
  logging.info("Starting Landlord-Tenant Rights Bot application...")
1042
  try:
1043
  SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
1044
+ DEFAULT_PDF_PATH = os.path.join(SCRIPT_DIR, "data/tenant-landlord.pdf")
1045
+ DEFAULT_DB_PATH = os.path.join(SCRIPT_DIR, "data/chroma_db")
1046
 
1047
+ # Use environment variables if set, otherwise default paths
1048
  PDF_PATH = os.getenv("PDF_PATH", DEFAULT_PDF_PATH)
1049
  VECTOR_DB_PATH = os.getenv("VECTOR_DB_PATH", DEFAULT_DB_PATH)
1050
 
1051
+ # Ensure directories exist
1052
  os.makedirs(os.path.dirname(VECTOR_DB_PATH), exist_ok=True)
1053
+ # If PDF_PATH could point to a file in a non-existent subdirectory, uncomment:
1054
+ # if os.path.dirname(PDF_PATH):
1055
+ # os.makedirs(os.path.dirname(PDF_PATH), exist_ok=True)
1056
+
1057
 
1058
+ # Validate PDF file existence before proceeding
1059
  if not os.path.exists(PDF_PATH):
1060
  logging.error(f"FATAL: PDF file not found at the specified path: {PDF_PATH}")
1061
  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")
1062
  exit(1)
1063
 
1064
+ # Initialize Vector Database and RAG System
1065
  vector_db_instance = VectorDatabase(persist_directory=VECTOR_DB_PATH)
1066
  rag = RAGSystem(vector_db=vector_db_instance)
1067
+ rag.load_pdf(PDF_PATH) # Load/process PDF data into the vector DB
1068
 
1069
+ # Create and launch the Gradio interface
1070
  app_interface = rag.gradio_interface()
1071
  SERVER_PORT = 7860
1072
  logging.info(f"Launching Gradio app on http://0.0.0.0:{SERVER_PORT}")
1073
+ print(f"\n--- Gradio App Running ---\nAccess at: http://localhost:{SERVER_PORT}\n(Share link will be available if you use share=True)\n--------------------------\n")
1074
  app_interface.launch(server_name="0.0.0.0", server_port=SERVER_PORT, share=True)
1075
 
1076
  except Exception as e:
1077
  logging.error(f"Application startup failed: {str(e)}", exc_info=True)
1078
+ print(f"\n--- FATAL STARTUP ERROR ---\n{str(e)}\nCheck logs for more details.\n---------------------------\n")
1079
  exit(1)