Nischal Subedi commited on
Commit
5caafe7
Β·
1 Parent(s): 2669b75
Files changed (1) hide show
  1. app.py +617 -444
app.py CHANGED
@@ -248,11 +248,11 @@ Answer:"""
248
  def query_interface_wrapper(api_key: str, query: str, state: str) -> str:
249
  # Basic client-side validation for immediate feedback (redundant but good UX)
250
  if not api_key or not api_key.strip() or not api_key.startswith("sk-"):
251
- 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'>OpenAI</a>.</div>"
252
- if not state or state is None or "Error" in state: # Check for error states
253
- return "<div class='error-message'><span class='error-icon'></span>Please select a valid state from the list.</div>"
254
  if not query or not query.strip():
255
- return "<div class='error-message'><span class='error-icon'></span>Please enter your question in the text box.</div>"
256
 
257
  # Call the core processing logic using 'self'
258
  result = self.process_query(query=query, state=state, openai_api_key=api_key)
@@ -262,15 +262,13 @@ Answer:"""
262
  if "<div class='error-message'>" in answer:
263
  return answer
264
  else:
265
- formatted_response_content = f"<div class='response-header'><span class='response-icon'>πŸ“œ</span>Response for {state}</div><hr class='divider'>{answer}"
266
  return f"<div class='animated-output-content'>{formatted_response_content}</div>"
267
 
268
  try:
269
- available_states_list = self.get_states() # Call self.get_states() directly
270
  print(f"DEBUG: States loaded for selection: {available_states_list}")
271
- # Ensure radio_choices is not empty if there's an error and handling the error message
272
  radio_choices = available_states_list if available_states_list and "Error" not in available_states_list[0] else ["Error: States unavailable"]
273
- # Set initial value only if there are valid choices
274
  initial_value_radio = radio_choices[0] if radio_choices and "Error" not in radio_choices[0] else None
275
  except Exception as e:
276
  print(f"DEBUG: Error loading states for selection: {e}")
@@ -284,465 +282,637 @@ Answer:"""
284
  ["How much notice must a landlord give to raise rent?", "Florida"],
285
  ["What is an implied warranty of habitability?", "Illinois"]
286
  ]
 
287
  example_queries = []
288
  if radio_choices and "Error" not in radio_choices[0] and len(radio_choices) > 0:
289
  loaded_states_set = set(radio_choices)
290
- # Filter examples to only include states that were successfully loaded
291
  example_queries = [ex for ex in example_queries_base if ex[1] in loaded_states_set]
292
  if not example_queries and radio_choices[0] != "Error: States unavailable":
293
- # Fallback if no matching examples found, use the first available state
294
  example_queries.append(["What basic rights do tenants have?", radio_choices[0]])
295
- else: # If states failed to load, provide a generic example (e.g., California)
296
  example_queries.append(["What basic rights do tenants have?", "California"])
297
 
298
- custom_css = f"""
299
- /* Import legible fonts from Google Fonts */
300
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Poppins:wght@600;700;800&display=swap');
301
 
302
- /* Force light theme globally */
303
- :root, html, body, .gradio-app, .dark, html.dark, body.dark {{
304
- color-scheme: light only !important; /* Prevent dark mode */
305
- background-color: var(--custom-background-secondary) !important;
306
- color: var(--custom-text-primary) !important;
307
- transition: none !important; /* Prevent theme transition flickering */
308
- }}
309
-
310
- /* Define custom light theme variables */
311
- :root {{
312
- --custom-primary-color: #FF8C00;
313
- --custom-primary-hover: #E07B00;
314
- --custom-background-primary: hsl(30, 100%, 99.9%); /* Near white, slightly warm */
315
- --custom-background-secondary: hsl(30, 100%, 96%); /* Slightly darker background */
316
- --custom-text-primary: #4A3C32; /* Dark brown/off-black for main text */
317
- --custom-text-secondary: #8C7B6F; /* Lighter brown/gray for subdued text */
318
- --custom-border-color: hsl(30, 70%, 85%); /* Light brown/peach border */
319
- --custom-border-focus: #FF8C00; /* Primary color for focus */
320
- --custom-shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
321
- --custom-shadow-md: 0 4px 10px rgba(0,0,0,0.1);
322
- --custom-shadow-lg: 0 10px 20px rgba(0,0,0,0.15);
323
- --custom-error-bg: #FFF0E0; /* Light peach error background */
324
- --custom-error-border: #FFD2B2; /* Peach error border */
325
- --custom-error-text: #E05C00; /* Orange-red error text */
326
- }}
327
-
328
- /* Override dark mode for all Gradio components */
329
- :root, html.dark, body.dark, .dark, .gradio-container, .gradio-app {{
330
- /* General backgrounds */
331
- --background-fill-primary: var(--custom-background-primary) !important;
332
- --background-fill-secondary: var(--custom-background-secondary) !important;
333
- --background-fill-tertiary: var(--custom-background-secondary) !important;
334
- --panel-background: var(--custom-background-primary) !important;
335
- --block-background: var(--custom-background-primary) !important;
336
- --color-background-primary: var(--custom-background-primary) !important;
337
- --color-background-secondary: var(--custom-background-secondary) !important;
338
-
339
  /* Text colors */
340
- --text-color-body: var(--custom-text-primary) !important;
341
- --text-color-subdued: var(--custom-text-secondary) !important;
342
- --text-color-highlight: var(--custom-primary-color) !important;
343
- --text-color-placeholder: var(--custom-text-secondary) !important;
344
- --text-color-link: var(--custom-primary-color) !important;
345
- --color-text-primary: var(--custom-text-primary) !important;
346
- --color-text-secondary: var(--custom-text-secondary) !important;
347
- --color-text-placeholder: var(--custom-text-secondary) !important;
348
-
349
  /* Border colors */
350
- --border-color-primary: var(--custom-border-color) !important;
351
- --border-color-accent: var(--custom-border-focus) !important;
352
- --border-color-secondary: var(--custom-border-color) !important;
353
- --border-color-tertiary: var(--custom-border-color) !important;
354
- --color-border-primary: var(--custom-border-color) !important;
355
- --color-border-secondary: var(--custom-border-color) !important;
356
-
357
- /* Button colors */
358
- --button-primary-background: var(--custom-primary-color) !important;
359
- --button-primary-background-hover: var(--custom-primary-hover) !important;
360
- --button-primary-text-color: white !important;
361
- --button-secondary-background: transparent !important;
362
- --button-secondary-background-hover: var(--custom-background-secondary) !important;
363
- --button-secondary-border-color: var(--custom-border-color) !important;
364
- --button-secondary-border-color-hover: var(--custom-primary-color) !important;
365
- --button-secondary-text-color: var(--custom-text-primary) !important;
366
- --color-button-primary-background: var(--custom-primary-color) !important;
367
- --color-button-primary-background-hover: var(--custom-primary-hover) !important;
368
- --color-button-primary-text: white !important;
369
- --color-button-secondary-text: var(--custom-text-primary) !important;
370
-
371
- /* Input specific */
372
- --input-background-fill: var(--custom-background-primary) !important;
373
- --input-border-color: var(--custom-border-color) !important;
374
- --input-border-color-focus: var(--custom-border-focus) !important;
375
- --input-shadow: var(--custom-shadow-sm) !important;
376
- --input-shadow-focus: 0 0 0 4px rgba(255, 140, 0, 0.2) !important;
377
- --input-text-color: var(--custom-text-primary) !important;
378
-
379
  /* Shadows */
380
- --shadow-s: var(--custom-shadow-sm) !important;
381
- --shadow-m: var(--custom-shadow-md) !important;
382
- --shadow-l: var(--custom-shadow-lg) !important;
383
-
384
- /* Misc */
385
- --color-accent-soft: rgba(255, 140, 0, 0.2) !important;
386
- --color-error: var(--custom-error-text) !important;
387
- --color-error-background: var(--custom-error-bg) !important;
388
- --color-error-border: var(--custom-error-border) !important;
389
-
390
- /* Specific overrides */
391
- --block-border-color: var(--custom-border-color) !important;
392
- --input-border-color: var(--custom-border-color) !important;
393
- --input-label-color: var(--custom-text-primary) !important;
394
- --link-text-color: var(--custom-primary-color) !important;
395
- --link-text-color-hover: var(--custom-primary-hover) !important;
396
-
397
- /* Scrollbar */
398
- --scrollbar-thumb-color: var(--custom-border-color) !important;
399
- --scrollbar-track-color: var(--custom-background-secondary) !important;
400
- }}
401
-
402
- /* Override dark mode media query */
403
- @media (prefers-color-scheme: dark) {{
404
- :root, html, body, .gradio-app, .dark, .gradio-container {{
405
- color-scheme: light only !important;
406
- background-color: var(--custom-background-secondary) !important;
407
- color: var(--custom-text-primary) !important;
408
- }}
409
- }}
410
-
411
- /* Base styles for html and body */
412
- body, html {{
413
- background-color: var(--custom-background-secondary) !important;
414
- color: var(--custom-text-primary) !important;
415
- transition: none !important;
416
- }}
417
-
418
- /* Target the main Gradio application container */
419
- .gradio-app {{
420
- background-color: var(--custom-background-secondary) !important;
421
- color: var(--custom-text-primary) !important;
422
- }}
423
-
424
- /* Ensure all core Gradio blocks and components use our defined colors */
425
- .gr-block, .gr-box, .gr-panel, .gr-form, .gr-columns, .gr-column,
426
- .gradio-html, .gradio-markdown, .gradio-prose {{
427
- background-color: var(--custom-background-primary) !important;
428
- color: var(--custom-text-primary) !important;
429
- border-color: var(--custom-border-color) !important;
430
- --text-color-body: var(--custom-text-primary) !important;
431
- --text-color-subdued: var(--custom-text-secondary) !important;
432
- }}
433
-
434
- /* Centering styles for title and headers */
435
- .app-header-title, .section-title {{
436
- text-align: center !important;
437
- width: 100% !important;
 
 
438
  margin: 0 auto !important;
439
- }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
 
441
- .app-header-wrapper, .dashboard-card-section {{
 
442
  display: flex;
 
 
443
  justify-content: center;
444
- align-items: center;
445
- flex-direction: column;
446
- }}
447
-
448
- /* Specific overrides for input fields */
449
- .gradio-textbox textarea,
450
- .gradio-textbox input {{
451
- background-color: var(--custom-background-primary) !important;
452
- color: var(--custom-text-primary) !important;
453
- border: 2px solid var(--custom-border-color) !important;
454
- box-shadow: var(--custom-shadow-sm) !important;
455
- }}
456
- .gradio-textbox textarea:focus,
457
- .gradio-textbox input:focus {{
458
- border-color: var(--custom-border-focus) !important;
459
- box-shadow: 0 0 0 4px rgba(255, 140, 0, 0.2) !important;
460
- outline: none !important;
461
- }}
462
-
463
- /* Specific overrides for radio buttons */
464
- .gradio-radio label {{
465
- background-color: var(--custom-background-primary) !important;
466
- color: var(--custom-text-primary) !important;
467
- border: 2px solid var(--custom-border-color) !important;
468
- box-shadow: var(--custom-shadow-sm) !important;
469
- }}
470
- .gradio-radio label span.text-lg {{
471
- color: var(--custom-text-primary) !important;
472
- }}
473
- .gradio-radio label:hover {{
474
- background-color: var(--custom-background-secondary) !important;
475
- border-color: var(--custom-primary-color) !important;
476
- box-shadow: var(--custom-shadow-md) !important;
477
- transform: translateY(-2px) !important;
478
- }}
479
- .gradio-radio input[type="radio"]:checked + label {{
480
- background-color: var(--custom-primary-color) !important;
481
- color: white !important;
482
- border-color: var(--custom-primary-hover) !important;
483
- box-shadow: var(--custom-shadow-md) !important;
484
- }}
485
- .gradio-radio input[type="radio"]:checked + label span.text-lg {{
486
  color: white !important;
487
- }}
 
488
 
489
- /* Overrides for buttons */
490
- .gr-button-primary {{
491
- background-color: var(--custom-primary-color) !important;
492
- color: white !important;
493
- }}
494
- .gr-button-primary:hover {{
495
- background-color: var(--custom-primary-hover) !important;
496
- }}
497
- .gr-button-secondary {{
498
  background-color: transparent !important;
499
- color: var(--custom-text-primary) !important;
500
- border-color: var(--custom-border-color) !important;
501
- }}
502
- .gr-button-secondary:hover {{
503
- background-color: var(--custom-background-secondary) !important;
504
- border-color: var(--custom-primary-color) !important;
505
- }}
506
-
507
- /* Placeholders */
508
- .placeholder {{
509
- background-color: var(--custom-background-primary) !important;
510
- border: 2px dashed var(--custom-border-color) !important;
511
- color: var(--custom-text-secondary) !important;
512
- }}
513
-
514
- /* Example table */
515
- .examples-section .gr-samples-table {{
516
- border: 2px solid var(--custom-border-color) !important;
517
- background-color: var(--custom-background-primary) !important;
518
- color: var(--custom-text-primary) !important;
519
- }}
520
- .examples-section .gr-samples-table th {{
521
- background-color: var(--custom-background-secondary) !important;
522
- color: var(--custom-text-primary) !important;
523
- text-align: center !important;
524
- }}
525
- .examples-section .gr-samples-table td {{
526
- background-color: var(--custom-background-primary) !important;
527
- color: var(--custom-text-primary) !important;
528
- border-top: 1px solid var(--custom-border-color) !important;
529
- }}
530
- .examples-section .gr-samples-table tr:hover td {{
531
- background-color: var(--custom-background-secondary) !important;
532
- transform: translateX(5px);
533
- }}
534
- .examples-section .gr-samples-table th, .examples-section .gr-samples-table td {{
535
- border-color: var(--custom-border-color) !important;
536
- }}
537
-
538
- /* Error messages */
539
- .error-message {{
540
- background-color: var(--custom-error-bg) !important;
541
- border: 2px solid var(--custom-error-border) !important;
542
- color: var(--custom-error-text) !important;
543
- }}
544
- .error-message a {{
545
- color: var(--custom-error-text) !important;
546
- }}
547
-
548
- /* Footer */
549
- .app-footer-wrapper {{
550
- background: linear-gradient(145deg, var(--custom-background-primary) 0%, var(--custom-background-secondary) 100%) !important;
551
- border: 2px solid var(--custom-border-color) !important;
552
- color: var(--custom-text-primary) !important;
553
- }}
554
- .app-footer p {{
555
- color: var(--custom-text-secondary) !important;
556
- }}
557
- .app-footer strong, .app-footer b {{
558
- color: var(--custom-primary-color) !important;
559
- }}
560
- .app-footer a {{
561
- color: var(--custom-primary-color) !important;
562
- }}
563
-
564
- /* General element classes */
565
- .gr-text, .gr-label, .gr-checkbox-group, .gr-radio-group,
566
- .gr-slider, .gr-number, .gr-dropdown {{
567
- color: var(--custom-text-primary) !important;
568
- }}
569
-
570
- /* Ensure all text within any Gradio element defaults to primary text color */
571
- .gradio-container * {{
572
- color: var(--custom-text-primary) !important;
573
- }}
574
- /* Re-override for specific cases that need secondary text color */
575
- .gradio-container .gr-prose,
576
- .gradio-container .app-header-tagline,
577
- .gradio-container .placeholder,
578
- .gradio-container .app-footer p {{
579
- color: var(--custom-text-secondary) !important;
580
- }}
581
- /* Re-override for primary color elements */
582
- .gradio-container .app-header-logo,
583
- .gradio-container .response-header,
584
- .gradio-container .response-icon,
585
- .gradio-container .custom-link {{
586
- color: var(--custom-primary-color) !important;
587
- }}
588
- .gradio-container .gr-button-primary {{
589
- color: white !important;
590
- }}
591
- /* Ensure inputs specifically use primary text color */
592
- .gradio-textbox textarea, .gradio-textbox input,
593
- .gradio-radio label span.text-lg {{
594
- color: var(--custom-text-primary) !important;
595
- }}
596
-
597
- /* Media Queries */
598
- @media (max-width: 768px) {{
599
- .gradio-container {{
600
- padding: 1rem !important;
601
- }}
602
- .app-header-title {{
603
- font-size: 2.2rem !important;
604
- }}
605
- .app-header-tagline {{
606
- font-size: 1rem !important;
607
- }}
608
- .section-title {{
609
- font-size: 1.4rem !important;
610
- }}
611
- .input-column {{
612
- flex-direction: column !important;
613
- }}
614
- .button-row {{
615
- flex-direction: column !important;
616
- }}
617
- .gradio-button {{
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
618
  width: 100% !important;
619
- }}
620
- .dashboard-card-section {{
621
- }}
622
- .section-title-gradient-bar {{
623
- padding: 0.1rem 1rem !important;
624
- }}
625
- .dashboard-card-content-area {{
626
- padding: 0 1rem 1rem 1rem !important;
627
- }}
628
- .output-content-wrapper {{
629
- min-height: 120px !important;
630
- }}
631
- .placeholder {{
632
- padding: 1.5rem 1rem !important;
633
- font-size: 1rem !important;
634
- }}
635
- }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
636
  """
637
 
638
- with gr.Blocks(css=custom_css, title="Landlord-Tenant Rights Assistant") as demo:
639
- with gr.Group(elem_classes="app-header-wrapper"):
640
- gr.Markdown(
641
- """
642
- <span class='app-header-logo'>βš–οΈ</span>
643
- <h1 class='app-header-title'>Landlord-Tenant Rights Assistant</h1>
644
- <p class='app-header-tagline'>Empowering You with State-Specific Legal Insights</p>
645
- """,
646
- elem_classes="full-width-center"
647
- )
648
-
649
- with gr.Column(elem_classes="main-dashboard-container"):
650
- # How This Assistant Works Box
651
- with gr.Group(elem_classes="dashboard-card-section"):
652
- gr.Markdown("<h3 class='section-title'>How This Assistant Works</h3>", elem_classes="full-width-center section-title-gradient-bar")
653
- with gr.Column(elem_classes="dashboard-card-content-area"):
654
- gr.Markdown(
655
- """
656
- This AI-powered assistant helps navigate complex landlord-tenant laws. Simply ask a question about your state's regulations, and it will provide detailed, legally-grounded insights.
657
- """
658
- )
659
-
660
- # OpenAI API Key Input Card
661
- with gr.Group(elem_classes="dashboard-card-section"):
662
- gr.Markdown("<h3 class='section-title'>OpenAI API Key</h3>", elem_classes="full-width-center section-title-gradient-bar")
663
- with gr.Column(elem_classes="dashboard-card-content-area"):
664
- api_key_input = gr.Textbox(
665
- label="API Key",
666
- type="password",
667
- placeholder="Enter your OpenAI API key (e.g., sk-...)",
668
- lines=1,
669
- elem_classes=["input-field-group"]
670
- )
671
- gr.Markdown(
672
- "Required to process your query. Get one from OpenAI: [platform.openai.com/api-keys](https://platform.openai.com/api-keys)",
673
- elem_classes="gr-prose"
674
- )
675
-
676
- # Query Input and State Selection Card
677
- with gr.Group(elem_classes="dashboard-card-section"):
678
- gr.Markdown("<h3 class='section-title'>Ask Your Question</h3>", elem_classes="full-width-center section-title-gradient-bar")
679
- with gr.Column(elem_classes="dashboard-card-content-area"):
680
- with gr.Column(elem_classes="input-column"):
681
- with gr.Column(elem_classes="input-field", scale=1):
682
- query_input = gr.Textbox(
683
- label="Your Question",
684
- placeholder="E.g., What are the rules for security deposit returns in my state?",
685
- lines=8,
686
- max_lines=15,
687
- elem_classes=["input-field-group"]
688
- )
689
- with gr.Column(elem_classes="input-field", scale=1):
690
- state_input = gr.Radio(
691
- label="Select State",
692
- choices=radio_choices,
693
- value=initial_value_radio,
694
- elem_classes=["input-field-group", "gradio-radio-custom"],
695
- interactive=True
696
- )
697
- with gr.Row(elem_classes="button-row"):
698
- clear_button = gr.Button("Clear", variant="secondary", elem_classes=["gr-button-secondary"])
699
- submit_button = gr.Button("Submit Query", variant="primary", elem_classes=["gr-button-primary"])
700
-
701
- # Output Display Card
702
- with gr.Group(elem_classes="dashboard-card-section"):
703
- gr.Markdown("<h3 class='section-title'>Legal Assistant's Response</h3>", elem_classes="full-width-center section-title-gradient-bar")
704
- with gr.Column(elem_classes="dashboard-card-content-area"):
705
- output = gr.HTML(
706
- value="<div class='placeholder'>The answer will appear here after submitting your query.</div>",
707
- elem_classes="output-content-wrapper"
708
- )
709
-
710
- # Example Questions Section
711
- with gr.Group(elem_classes="dashboard-card-section examples-section"):
712
- gr.Markdown("<h3 class='section-title'>Example Questions</h3>", elem_classes="full-width-center section-title-gradient-bar")
713
- with gr.Column(elem_classes="dashboard-card-content-area"):
714
- if example_queries:
715
- gr.Examples(
716
- examples=example_queries,
717
- inputs=[query_input, state_input],
718
- examples_per_page=5,
719
- label=""
720
  )
721
- else:
722
- gr.Markdown("<div class='placeholder'>Sample questions could not be loaded. Please ensure the vector database is populated.</div>")
723
-
724
- # Footer Section
725
- with gr.Group(elem_classes="app-footer-wrapper"):
726
- gr.Markdown(
727
- f"""
728
- <style>
729
- .custom-link {{
730
- font-weight: bold !important;
731
- color: var(--custom-primary-color) ! includes;
732
- text-decoration: underline;
733
- }}
734
- .app-footer p {{
735
- color: var(--custom-text-secondary) !important;
736
- }}
737
- .app-footer strong, .app-footer b {{
738
- color: var(--custom-primary-color) !important;
739
- }}
740
- </style>
741
- <p><strong>Disclaimer:</strong> This tool is for informational purposes only and does not constitute legal advice. For specific legal guidance, always consult with a licensed attorney in your jurisdiction.</p>
742
- <p>Developed by <strong>Nischal Subedi</strong>. Connect on <a href="https://www.linkedin.com/in/nischal1/" target="_blank" class="custom-link">LinkedIn</a> or explore insights at <a href="https://datascientistinsights.substack.com/" target="_blank" class="custom-link">Substack</a>.</p>
743
- """
744
- )
745
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
746
  submit_button.click(
747
  fn=query_interface_wrapper,
748
  inputs=[api_key_input, query_input, state_input],
@@ -752,7 +922,10 @@ Answer:"""
752
 
753
  clear_button.click(
754
  fn=lambda: (
755
- "", "", initial_value_radio, "<div class='placeholder'>Inputs cleared. Ready for your next question.</div>"
 
 
 
756
  ),
757
  inputs=[],
758
  outputs=[api_key_input, query_input, state_input, output]
 
248
  def query_interface_wrapper(api_key: str, query: str, state: str) -> str:
249
  # Basic client-side validation for immediate feedback (redundant but good UX)
250
  if not api_key or not api_key.strip() or not api_key.startswith("sk-"):
251
+ 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 yours here</a>.</div>"
252
+ if not state or state is None or "Error" in state:
253
+ return "<div class='error-message'><span class='error-icon'>⚠️</span>Please select a valid state from the list.</div>"
254
  if not query or not query.strip():
255
+ return "<div class='error-message'><span class='error-icon'>⚠️</span>Please enter your question in the text box.</div>"
256
 
257
  # Call the core processing logic using 'self'
258
  result = self.process_query(query=query, state=state, openai_api_key=api_key)
 
262
  if "<div class='error-message'>" in answer:
263
  return answer
264
  else:
265
+ formatted_response_content = f"<div class='response-header'><span class='response-icon'>πŸ“œ</span>Response for {state}</div><div class='response-divider'></div><div class='response-content'>{answer}</div>"
266
  return f"<div class='animated-output-content'>{formatted_response_content}</div>"
267
 
268
  try:
269
+ available_states_list = self.get_states()
270
  print(f"DEBUG: States loaded for selection: {available_states_list}")
 
271
  radio_choices = available_states_list if available_states_list and "Error" not in available_states_list[0] else ["Error: States unavailable"]
 
272
  initial_value_radio = radio_choices[0] if radio_choices and "Error" not in radio_choices[0] else None
273
  except Exception as e:
274
  print(f"DEBUG: Error loading states for selection: {e}")
 
282
  ["How much notice must a landlord give to raise rent?", "Florida"],
283
  ["What is an implied warranty of habitability?", "Illinois"]
284
  ]
285
+
286
  example_queries = []
287
  if radio_choices and "Error" not in radio_choices[0] and len(radio_choices) > 0:
288
  loaded_states_set = set(radio_choices)
 
289
  example_queries = [ex for ex in example_queries_base if ex[1] in loaded_states_set]
290
  if not example_queries and radio_choices[0] != "Error: States unavailable":
 
291
  example_queries.append(["What basic rights do tenants have?", radio_choices[0]])
292
+ else:
293
  example_queries.append(["What basic rights do tenants have?", "California"])
294
 
295
+ custom_css = """
296
+ /* Import modern fonts */
297
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Poppins:wght@600;700;800&display=swap');
298
 
299
+ /* CSS Variables for theme consistency */
300
+ :root {
301
+ /* Light theme colors */
302
+ --primary-color: #FF8C00;
303
+ --primary-hover: #E07B00;
304
+ --primary-light: rgba(255, 140, 0, 0.1);
305
+ --primary-border: rgba(255, 140, 0, 0.3);
306
+
307
+ /* Backgrounds */
308
+ --bg-primary: #ffffff;
309
+ --bg-secondary: #f8f9fa;
310
+ --bg-tertiary: #f1f3f4;
311
+ --bg-card: #ffffff;
312
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  /* Text colors */
314
+ --text-primary: #1a1a1a;
315
+ --text-secondary: #6c757d;
316
+ --text-muted: #adb5bd;
317
+
 
 
 
 
 
318
  /* Border colors */
319
+ --border-light: #e9ecef;
320
+ --border-medium: #dee2e6;
321
+ --border-strong: #adb5bd;
322
+
323
+ /* Status colors */
324
+ --success-bg: #d1edff;
325
+ --success-border: #0ea5e9;
326
+ --success-text: #0369a1;
327
+ --error-bg: #fef2f2;
328
+ --error-border: #fca5a5;
329
+ --error-text: #dc2626;
330
+ --warning-bg: #fffbeb;
331
+ --warning-border: #fbbf24;
332
+ --warning-text: #d97706;
333
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  /* Shadows */
335
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
336
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
337
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
338
+ --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
339
+ }
340
+
341
+ /* Dark theme colors */
342
+ @media (prefers-color-scheme: dark) {
343
+ :root {
344
+ --bg-primary: #1a1a1a;
345
+ --bg-secondary: #2d2d2d;
346
+ --bg-tertiary: #404040;
347
+ --bg-card: #2d2d2d;
348
+
349
+ --text-primary: #ffffff;
350
+ --text-secondary: #a1a1aa;
351
+ --text-muted: #71717a;
352
+
353
+ --border-light: #404040;
354
+ --border-medium: #525252;
355
+ --border-strong: #737373;
356
+
357
+ --success-bg: rgba(14, 165, 233, 0.1);
358
+ --error-bg: rgba(220, 38, 38, 0.1);
359
+ --warning-bg: rgba(217, 119, 6, 0.1);
360
+ }
361
+ }
362
+
363
+ /* Force dark theme when .dark class is present */
364
+ .dark {
365
+ --bg-primary: #1a1a1a !important;
366
+ --bg-secondary: #2d2d2d !important;
367
+ --bg-tertiary: #404040 !important;
368
+ --bg-card: #2d2d2d !important;
369
+
370
+ --text-primary: #ffffff !important;
371
+ --text-secondary: #a1a1aa !important;
372
+ --text-muted: #71717a !important;
373
+
374
+ --border-light: #404040 !important;
375
+ --border-medium: #525252 !important;
376
+ --border-strong: #737373 !important;
377
+
378
+ --success-bg: rgba(14, 165, 233, 0.1) !important;
379
+ --error-bg: rgba(220, 38, 38, 0.1) !important;
380
+ --warning-bg: rgba(217, 119, 6, 0.1) !important;
381
+ }
382
+
383
+ /* Base styles */
384
+ body, html {
385
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
386
+ background-color: var(--bg-secondary) !important;
387
+ color: var(--text-primary) !important;
388
+ line-height: 1.6;
389
+ }
390
+
391
+ .gradio-container {
392
+ background-color: var(--bg-secondary) !important;
393
+ color: var(--text-primary) !important;
394
+ max-width: 1200px !important;
395
  margin: 0 auto !important;
396
+ padding: 1rem !important;
397
+ }
398
+
399
+ /* Header styles */
400
+ .app-header {
401
+ text-align: center;
402
+ padding: 2rem 1rem;
403
+ background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-tertiary) 100%);
404
+ border: 1px solid var(--border-light);
405
+ border-radius: 16px;
406
+ margin-bottom: 2rem;
407
+ box-shadow: var(--shadow-md);
408
+ }
409
+
410
+ .app-header-icon {
411
+ font-size: 3rem;
412
+ margin-bottom: 1rem;
413
+ display: block;
414
+ }
415
+
416
+ .app-header-title {
417
+ font-family: 'Poppins', sans-serif;
418
+ font-size: 2.5rem;
419
+ font-weight: 700;
420
+ color: var(--text-primary);
421
+ margin: 0 0 0.5rem 0;
422
+ background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
423
+ -webkit-background-clip: text;
424
+ background-clip: text;
425
+ -webkit-text-fill-color: transparent;
426
+ }
427
+
428
+ .app-header-tagline {
429
+ font-size: 1.1rem;
430
+ color: var(--text-secondary);
431
+ margin: 0;
432
+ font-weight: 400;
433
+ }
434
+
435
+ /* Card styles */
436
+ .dashboard-card {
437
+ background-color: var(--bg-card);
438
+ border: 1px solid var(--border-light);
439
+ border-radius: 12px;
440
+ margin-bottom: 1.5rem;
441
+ box-shadow: var(--shadow-sm);
442
+ overflow: hidden;
443
+ transition: all 0.2s ease;
444
+ }
445
+
446
+ .dashboard-card:hover {
447
+ box-shadow: var(--shadow-md);
448
+ transform: translateY(-1px);
449
+ }
450
+
451
+ .card-header {
452
+ background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
453
+ color: white;
454
+ padding: 1rem 1.5rem;
455
+ border-bottom: none;
456
+ }
457
+
458
+ .card-title {
459
+ font-family: 'Poppins', sans-serif;
460
+ font-size: 1.25rem;
461
+ font-weight: 600;
462
+ margin: 0;
463
+ text-align: center;
464
+ }
465
+
466
+ .card-content {
467
+ padding: 1.5rem;
468
+ background-color: var(--bg-card);
469
+ }
470
+
471
+ /* Input styles */
472
+ .input-group {
473
+ margin-bottom: 1.5rem;
474
+ }
475
+
476
+ .input-group:last-child {
477
+ margin-bottom: 0;
478
+ }
479
+
480
+ .gradio-textbox, .gradio-radio {
481
+ margin-bottom: 1rem;
482
+ }
483
+
484
+ .gradio-textbox label, .gradio-radio legend {
485
+ font-weight: 600;
486
+ color: var(--text-primary) !important;
487
+ margin-bottom: 0.5rem;
488
+ display: block;
489
+ }
490
+
491
+ .gradio-textbox textarea, .gradio-textbox input {
492
+ background-color: var(--bg-primary) !important;
493
+ border: 2px solid var(--border-medium) !important;
494
+ border-radius: 8px !important;
495
+ color: var(--text-primary) !important;
496
+ font-size: 0.95rem !important;
497
+ padding: 0.75rem !important;
498
+ transition: all 0.2s ease !important;
499
+ box-shadow: var(--shadow-sm) !important;
500
+ }
501
+
502
+ .gradio-textbox textarea:focus, .gradio-textbox input:focus {
503
+ border-color: var(--primary-color) !important;
504
+ box-shadow: 0 0 0 3px var(--primary-light) !important;
505
+ outline: none !important;
506
+ }
507
+
508
+ /* Radio button styles */
509
+ .gradio-radio .wrap {
510
+ display: grid !important;
511
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)) !important;
512
+ gap: 0.5rem !important;
513
+ margin-top: 0.5rem !important;
514
+ }
515
+
516
+ .gradio-radio label {
517
+ background-color: var(--bg-primary) !important;
518
+ border: 2px solid var(--border-medium) !important;
519
+ border-radius: 8px !important;
520
+ padding: 0.75rem 1rem !important;
521
+ cursor: pointer !important;
522
+ transition: all 0.2s ease !important;
523
+ text-align: center !important;
524
+ font-weight: 500 !important;
525
+ color: var(--text-primary) !important;
526
+ box-shadow: var(--shadow-sm) !important;
527
+ }
528
+
529
+ .gradio-radio label:hover {
530
+ border-color: var(--primary-color) !important;
531
+ background-color: var(--primary-light) !important;
532
+ transform: translateY(-1px) !important;
533
+ box-shadow: var(--shadow-md) !important;
534
+ }
535
+
536
+ .gradio-radio input[type="radio"]:checked + label {
537
+ background-color: var(--primary-color) !important;
538
+ border-color: var(--primary-hover) !important;
539
+ color: white !important;
540
+ box-shadow: var(--shadow-md) !important;
541
+ }
542
 
543
+ /* Button styles */
544
+ .button-row {
545
  display: flex;
546
+ gap: 1rem;
547
+ margin-top: 1.5rem;
548
  justify-content: center;
549
+ }
550
+
551
+ .gradio-button {
552
+ padding: 0.75rem 2rem !important;
553
+ border-radius: 8px !important;
554
+ font-weight: 600 !important;
555
+ font-size: 0.95rem !important;
556
+ transition: all 0.2s ease !important;
557
+ border: 2px solid transparent !important;
558
+ cursor: pointer !important;
559
+ min-width: 120px !important;
560
+ }
561
+
562
+ .gradio-button.primary {
563
+ background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
  color: white !important;
565
+ box-shadow: var(--shadow-md) !important;
566
+ }
567
 
568
+ .gradio-button.primary:hover {
569
+ transform: translateY(-2px) !important;
570
+ box-shadow: var(--shadow-lg) !important;
571
+ }
572
+
573
+ .gradio-button.secondary {
 
 
 
574
  background-color: transparent !important;
575
+ color: var(--text-primary) !important;
576
+ border-color: var(--border-medium) !important;
577
+ }
578
+
579
+ .gradio-button.secondary:hover {
580
+ background-color: var(--bg-tertiary) !important;
581
+ border-color: var(--primary-color) !important;
582
+ transform: translateY(-1px) !important;
583
+ }
584
+
585
+ /* Output styles */
586
+ .output-container {
587
+ min-height: 200px;
588
+ background-color: var(--bg-primary);
589
+ border: 2px solid var(--border-light);
590
+ border-radius: 8px;
591
+ padding: 1.5rem;
592
+ box-shadow: var(--shadow-sm);
593
+ }
594
+
595
+ .placeholder {
596
+ text-align: center;
597
+ color: var(--text-muted);
598
+ font-style: italic;
599
+ padding: 2rem;
600
+ border: 2px dashed var(--border-medium);
601
+ border-radius: 8px;
602
+ background-color: var(--bg-tertiary);
603
+ }
604
+
605
+ .animated-output-content {
606
+ animation: fadeInUp 0.3s ease-out;
607
+ }
608
+
609
+ @keyframes fadeInUp {
610
+ from {
611
+ opacity: 0;
612
+ transform: translateY(10px);
613
+ }
614
+ to {
615
+ opacity: 1;
616
+ transform: translateY(0);
617
+ }
618
+ }
619
+
620
+ .response-header {
621
+ display: flex;
622
+ align-items: center;
623
+ gap: 0.5rem;
624
+ font-weight: 600;
625
+ color: var(--primary-color);
626
+ margin-bottom: 1rem;
627
+ font-size: 1.1rem;
628
+ }
629
+
630
+ .response-divider {
631
+ height: 2px;
632
+ background: linear-gradient(90deg, var(--primary-color), transparent);
633
+ margin: 1rem 0;
634
+ border-radius: 1px;
635
+ }
636
+
637
+ .response-content {
638
+ color: var(--text-primary);
639
+ line-height: 1.7;
640
+ }
641
+
642
+ /* Error message styles */
643
+ .error-message {
644
+ background-color: var(--error-bg);
645
+ border: 2px solid var(--error-border);
646
+ border-radius: 8px;
647
+ padding: 1rem;
648
+ color: var(--error-text);
649
+ display: flex;
650
+ align-items: center;
651
+ gap: 0.5rem;
652
+ font-weight: 500;
653
+ }
654
+
655
+ .error-icon {
656
+ font-size: 1.2rem;
657
+ }
658
+
659
+ .error-message a {
660
+ color: var(--error-text);
661
+ text-decoration: underline;
662
+ font-weight: 600;
663
+ }
664
+
665
+ /* Examples section */
666
+ .examples-section {
667
+ margin-top: 1rem;
668
+ }
669
+
670
+ .examples-section .gradio-examples {
671
+ border: 1px solid var(--border-light);
672
+ border-radius: 8px;
673
+ overflow: hidden;
674
+ box-shadow: var(--shadow-sm);
675
+ }
676
+
677
+ .examples-section table {
678
+ width: 100%;
679
+ border-collapse: collapse;
680
+ background-color: var(--bg-card);
681
+ }
682
+
683
+ .examples-section th {
684
+ background-color: var(--bg-tertiary);
685
+ color: var(--text-primary);
686
+ font-weight: 600;
687
+ padding: 1rem;
688
+ text-align: left;
689
+ border-bottom: 2px solid var(--border-medium);
690
+ }
691
+
692
+ .examples-section td {
693
+ padding: 1rem;
694
+ border-bottom: 1px solid var(--border-light);
695
+ color: var(--text-primary);
696
+ cursor: pointer;
697
+ transition: all 0.2s ease;
698
+ }
699
+
700
+ .examples-section tr:hover td {
701
+ background-color: var(--primary-light);
702
+ transform: translateX(4px);
703
+ }
704
+
705
+ /* Footer styles */
706
+ .app-footer {
707
+ margin-top: 3rem;
708
+ padding: 2rem;
709
+ background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-tertiary) 100%);
710
+ border: 1px solid var(--border-light);
711
+ border-radius: 12px;
712
+ text-align: center;
713
+ box-shadow: var(--shadow-sm);
714
+ }
715
+
716
+ .app-footer p {
717
+ margin: 0.5rem 0;
718
+ color: var(--text-secondary);
719
+ line-height: 1.6;
720
+ }
721
+
722
+ .app-footer strong {
723
+ color: var(--primary-color);
724
+ }
725
+
726
+ .app-footer a {
727
+ color: var(--primary-color);
728
+ text-decoration: none;
729
+ font-weight: 600;
730
+ transition: all 0.2s ease;
731
+ }
732
+
733
+ .app-footer a:hover {
734
+ text-decoration: underline;
735
+ color: var(--primary-hover);
736
+ }
737
+
738
+ /* Responsive design */
739
+ @media (max-width: 768px) {
740
+ .gradio-container {
741
+ padding: 0.5rem !important;
742
+ }
743
+
744
+ .app-header {
745
+ padding: 1.5rem 1rem;
746
+ }
747
+
748
+ .app-header-title {
749
+ font-size: 2rem;
750
+ }
751
+
752
+ .card-content {
753
+ padding: 1rem;
754
+ }
755
+
756
+ .button-row {
757
+ flex-direction: column;
758
+ }
759
+
760
+ .gradio-button {
761
  width: 100% !important;
762
+ }
763
+
764
+ .gradio-radio .wrap {
765
+ grid-template-columns: 1fr !important;
766
+ }
767
+ }
768
+
769
+ @media (max-width: 480px) {
770
+ .app-header-title {
771
+ font-size: 1.8rem;
772
+ }
773
+
774
+ .app-header-tagline {
775
+ font-size: 1rem;
776
+ }
777
+
778
+ .card-header {
779
+ padding: 0.75rem 1rem;
780
+ }
781
+
782
+ .card-title {
783
+ font-size: 1.1rem;
784
+ }
785
+ }
786
+
787
+ /* Accessibility improvements */
788
+ .gradio-button:focus,
789
+ .gradio-textbox input:focus,
790
+ .gradio-textbox textarea:focus,
791
+ .gradio-radio label:focus {
792
+ outline: 2px solid var(--primary-color);
793
+ outline-offset: 2px;
794
+ }
795
+
796
+ /* Animation for loading states */
797
+ .loading {
798
+ opacity: 0.7;
799
+ pointer-events: none;
800
+ }
801
+
802
+ .loading::after {
803
+ content: '';
804
+ position: absolute;
805
+ top: 50%;
806
+ left: 50%;
807
+ width: 20px;
808
+ height: 20px;
809
+ margin: -10px 0 0 -10px;
810
+ border: 2px solid var(--primary-color);
811
+ border-radius: 50%;
812
+ border-top-color: transparent;
813
+ animation: spin 1s linear infinite;
814
+ }
815
+
816
+ @keyframes spin {
817
+ to {
818
+ transform: rotate(360deg);
819
+ }
820
+ }
821
  """
822
 
823
+ with gr.Blocks(css=custom_css, title="Landlord-Tenant Rights Assistant", theme="default") as demo:
824
+ # Header Section
825
+ with gr.Group(elem_classes="app-header"):
826
+ gr.HTML("""
827
+ <div class="app-header-icon">βš–οΈ</div>
828
+ <h1 class="app-header-title">Landlord-Tenant Rights Assistant</h1>
829
+ <p class="app-header-tagline">Empowering You with State-Specific Legal Insights</p>
830
+ """)
831
+
832
+ # How It Works Card
833
+ with gr.Group(elem_classes="dashboard-card"):
834
+ gr.HTML('<div class="card-header"><h3 class="card-title">How This Assistant Works</h3></div>')
835
+ with gr.Column(elem_classes="card-content"):
836
+ gr.Markdown("""
837
+ This AI-powered assistant helps navigate complex landlord-tenant laws. Simply ask a question about your state's regulations, and it will provide detailed, legally-grounded insights based on current legal statutes and precedents.
838
+ """)
839
+
840
+ # API Key Input Card
841
+ with gr.Group(elem_classes="dashboard-card"):
842
+ gr.HTML('<div class="card-header"><h3 class="card-title">πŸ”‘ OpenAI API Key</h3></div>')
843
+ with gr.Column(elem_classes="card-content"):
844
+ api_key_input = gr.Textbox(
845
+ label="API Key",
846
+ type="password",
847
+ placeholder="Enter your OpenAI API key (e.g., sk-...)",
848
+ lines=1,
849
+ elem_classes=["input-group"]
850
+ )
851
+ gr.Markdown("""
852
+ **Required to process your query.** Get your API key from [OpenAI Platform](https://platform.openai.com/api-keys).
853
+ Your key is processed securely and not stored.
854
+ """)
855
+
856
+ # Query Input Card
857
+ with gr.Group(elem_classes="dashboard-card"):
858
+ gr.HTML('<div class="card-header"><h3 class="card-title">πŸ’¬ Ask Your Question</h3></div>')
859
+ with gr.Column(elem_classes="card-content"):
860
+ with gr.Row():
861
+ with gr.Column(scale=2):
862
+ query_input = gr.Textbox(
863
+ label="Your Question",
864
+ placeholder="E.g., What are the rules for security deposit returns in my state?",
865
+ lines=6,
866
+ max_lines=10,
867
+ elem_classes=["input-group"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
868
  )
869
+ with gr.Column(scale=1):
870
+ state_input = gr.Radio(
871
+ label="Select State",
872
+ choices=radio_choices,
873
+ value=initial_value_radio,
874
+ elem_classes=["input-group"],
875
+ interactive=True
876
+ )
877
+
878
+ with gr.Row(elem_classes="button-row"):
879
+ clear_button = gr.Button("Clear", variant="secondary", elem_classes=["secondary"])
880
+ submit_button = gr.Button("Submit Query", variant="primary", elem_classes=["primary"])
881
+
882
+ # Output Card
883
+ with gr.Group(elem_classes="dashboard-card"):
884
+ gr.HTML('<div class="card-header"><h3 class="card-title">πŸ“‹ Legal Assistant\'s Response</h3></div>')
885
+ with gr.Column(elem_classes="card-content"):
886
+ output = gr.HTML(
887
+ value='<div class="placeholder">πŸ’‘ The answer will appear here after submitting your query.</div>',
888
+ elem_classes=["output-container"]
889
+ )
890
+
891
+ # Examples Card
892
+ with gr.Group(elem_classes="dashboard-card examples-section"):
893
+ gr.HTML('<div class="card-header"><h3 class="card-title">πŸ’‘ Example Questions</h3></div>')
894
+ with gr.Column(elem_classes="card-content"):
895
+ if example_queries:
896
+ gr.Examples(
897
+ examples=example_queries,
898
+ inputs=[query_input, state_input],
899
+ examples_per_page=5,
900
+ label=""
901
+ )
902
+ else:
903
+ gr.HTML('<div class="placeholder">⚠️ Sample questions could not be loaded. Please ensure the vector database is populated.</div>')
904
+
905
+ # Footer
906
+ with gr.Group(elem_classes="app-footer"):
907
+ gr.HTML("""
908
+ <p><strong>⚠️ Disclaimer:</strong> This tool is for informational purposes only and does not constitute legal advice.
909
+ For specific legal guidance, always consult with a licensed attorney in your jurisdiction.</p>
910
+ <p>Developed by <strong>Nischal Subedi</strong>.
911
+ Connect on <a href="https://www.linkedin.com/in/nischal1/" target="_blank">LinkedIn</a> or
912
+ explore insights at <a href="https://datascientistinsights.substack.com/" target="_blank">Substack</a>.</p>
913
+ """)
914
+
915
+ # Event handlers
916
  submit_button.click(
917
  fn=query_interface_wrapper,
918
  inputs=[api_key_input, query_input, state_input],
 
922
 
923
  clear_button.click(
924
  fn=lambda: (
925
+ "",
926
+ "",
927
+ initial_value_radio,
928
+ '<div class="placeholder">✨ Inputs cleared. Ready for your next question.</div>'
929
  ),
930
  inputs=[],
931
  outputs=[api_key_input, query_input, state_input, output]