taxforge 0.9.20__py3-none-any.whl → 0.9.22__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
aitax/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """TaxForge - AI-powered tax preparation"""
2
2
 
3
- __version__ = "0.9.20"
3
+ __version__ = "0.9.22"
aitax/aitax.py CHANGED
@@ -6,10 +6,32 @@ from .state import TaxAppState
6
6
  from .components import (
7
7
  COLORS, fintech_card, mercury_card, gradient_button, outline_button, stat_card,
8
8
  section_header, empty_state, badge, data_row, document_card,
9
- nav_bar, page_container, api_key_modal,
9
+ nav_bar, page_container, api_key_modal, processing_overlay,
10
10
  )
11
11
 
12
12
 
13
+ # ============== Styled Input Helper ==============
14
+ def styled_input(placeholder: str, value, on_change, input_type: str = "text", width: str = "100%") -> rx.Component:
15
+ """Styled input with fintech theme."""
16
+ return rx.input(
17
+ placeholder=placeholder,
18
+ value=value,
19
+ on_change=on_change,
20
+ type=input_type,
21
+ width=width,
22
+ background="white",
23
+ border=f"1px solid {COLORS['border']}",
24
+ color=COLORS["text_primary"],
25
+ _placeholder={"color": COLORS["text_muted"]},
26
+ padding="10px 12px",
27
+ border_radius="8px",
28
+ _focus={
29
+ "border_color": COLORS["accent_primary"],
30
+ "box_shadow": f"0 0 0 3px {COLORS['accent_light']}"
31
+ },
32
+ )
33
+
34
+
13
35
  # ============== Upload Zone (Compact) ==============
14
36
  def upload_zone_compact() -> rx.Component:
15
37
  """Compact file upload dropzone."""
@@ -463,6 +485,13 @@ def review_page() -> rx.Component:
463
485
  """Review and edit tax data page."""
464
486
  return page_container(
465
487
  api_modal(),
488
+ # Processing overlay - shows full screen modal during AI processing
489
+ processing_overlay(
490
+ is_processing=TaxAppState.processing,
491
+ current_file=TaxAppState.processing_current_file,
492
+ file_index=TaxAppState.processing_file_index,
493
+ total_files=TaxAppState.processing_total_files,
494
+ ),
466
495
  rx.vstack(
467
496
  # Header
468
497
  rx.hstack(
@@ -503,67 +532,97 @@ def review_page() -> rx.Component:
503
532
  ),
504
533
  rx.divider(border_color=COLORS["border"]),
505
534
 
506
- # File list with checkboxes
535
+ # File list with checkboxes - ENTIRE ROW CLICKABLE
507
536
  rx.foreach(
508
537
  TaxAppState.parsed_documents,
509
- lambda doc: rx.hstack(
510
- rx.cond(
511
- doc["status"] == "parsed",
512
- rx.icon("circle-check", size=18, color=COLORS["success"]),
513
- rx.checkbox(
514
- checked=TaxAppState.selected_files.contains(doc["name"]),
515
- on_change=lambda: TaxAppState.toggle_file_selection(doc["name"]),
516
- disabled=doc["status"] == "processing",
538
+ lambda doc: rx.box(
539
+ rx.hstack(
540
+ rx.cond(
541
+ doc["status"] == "parsed",
542
+ rx.icon("circle-check", size=18, color=COLORS["success"]),
543
+ rx.box(
544
+ rx.cond(
545
+ TaxAppState.selected_files.contains(doc["name"]),
546
+ rx.icon("square-check", size=20, color=COLORS["accent_primary"]),
547
+ rx.icon("square", size=20, color=COLORS["text_muted"]),
548
+ ),
549
+ ),
517
550
  ),
518
- ),
519
- rx.vstack(
520
- rx.text(doc["name"], color=COLORS["text_primary"], font_size="14px"),
521
- rx.text(
522
- rx.cond(
523
- doc["status"] == "parsed",
524
- f"✓ Processed - {doc['type']}",
551
+ rx.vstack(
552
+ rx.text(doc["name"], color=COLORS["text_primary"], font_size="14px"),
553
+ rx.text(
525
554
  rx.cond(
526
- doc["status"] == "error",
527
- " Error - click to retry",
555
+ doc["status"] == "parsed",
556
+ f" Processed - {doc['type']}",
528
557
  rx.cond(
529
- doc["status"] == "processing",
530
- " Processing...",
531
- f"Pending - {doc['type']}",
558
+ doc["status"] == "error",
559
+ " Error - click to retry",
560
+ rx.cond(
561
+ doc["status"] == "processing",
562
+ "⏳ Processing...",
563
+ f"Pending - {doc['type']}",
564
+ ),
532
565
  ),
533
566
  ),
534
- ),
535
- color=rx.cond(
536
- doc["status"] == "parsed",
537
- COLORS["success"],
538
- rx.cond(
539
- doc["status"] == "error",
540
- COLORS["warning"],
541
- COLORS["text_muted"],
567
+ color=rx.cond(
568
+ doc["status"] == "parsed",
569
+ COLORS["success"],
570
+ rx.cond(
571
+ doc["status"] == "error",
572
+ COLORS["warning"],
573
+ COLORS["text_muted"],
574
+ ),
542
575
  ),
576
+ font_size="12px",
543
577
  ),
544
- font_size="12px",
578
+ align_items="start",
579
+ spacing="0",
580
+ flex="1",
545
581
  ),
546
- align_items="start",
547
- spacing="0",
548
- flex="1",
549
- ),
550
- rx.icon(
551
- "x",
552
- size=16,
553
- color=COLORS["text_muted"],
554
- cursor="pointer",
555
- _hover={"color": COLORS["error"]},
556
- on_click=lambda: TaxAppState.remove_file(doc["name"]),
582
+ rx.box(
583
+ rx.icon(
584
+ "x",
585
+ size=16,
586
+ color=COLORS["text_muted"],
587
+ _hover={"color": COLORS["error"]},
588
+ ),
589
+ cursor="pointer",
590
+ padding="4px",
591
+ border_radius="4px",
592
+ _hover={"background": COLORS["error_light"]},
593
+ on_click=lambda: TaxAppState.remove_file(doc["name"]),
594
+ ),
595
+ width="100%",
596
+ spacing="3",
557
597
  ),
558
- width="100%",
559
- padding="10px 12px",
598
+ padding="12px 16px",
560
599
  background=rx.cond(
561
600
  doc["status"] == "parsed",
562
601
  COLORS["success_light"],
563
- COLORS["bg_hover"],
602
+ rx.cond(
603
+ TaxAppState.selected_files.contains(doc["name"]),
604
+ "#EEF2FF", # Light indigo when selected
605
+ COLORS["bg_hover"],
606
+ ),
564
607
  ),
565
608
  border_radius="8px",
566
- spacing="3",
609
+ border=rx.cond(
610
+ TaxAppState.selected_files.contains(doc["name"]),
611
+ f"2px solid {COLORS['accent_primary']}",
612
+ "2px solid transparent",
613
+ ),
614
+ cursor=rx.cond(doc["status"] == "parsed", "default", "pointer"),
615
+ _hover=rx.cond(
616
+ doc["status"] != "parsed",
617
+ {"background": "#E8EDFF"},
618
+ {},
619
+ ),
620
+ on_click=rx.cond(
621
+ doc["status"] != "parsed",
622
+ lambda: TaxAppState.toggle_file_selection(doc["name"]),
623
+ lambda: None,
624
+ ),
625
+ transition="all 0.15s ease",
567
626
  ),
568
627
  ),
569
628
 
@@ -677,16 +736,54 @@ def review_page() -> rx.Component:
677
736
  ),
678
737
  rx.cond(
679
738
  TaxAppState.rental_properties.length() > 0,
680
- rx.foreach(
681
- TaxAppState.rental_properties,
682
- lambda p, idx: rx.hstack(
683
- rx.text(p["address"], flex="1"),
684
- rx.text(f"Net: ${p['net_income']:,.2f}",
685
- color=rx.cond(p["net_income"].to(float) >= 0, COLORS["success"], COLORS["error"])),
686
- rx.icon("x", size=14, cursor="pointer",
687
- on_click=lambda: TaxAppState.remove_rental_property(idx)),
688
- width="100%", padding="8px",
739
+ rx.vstack(
740
+ rx.foreach(
741
+ TaxAppState.rental_properties,
742
+ lambda p, idx: rx.box(
743
+ rx.hstack(
744
+ rx.vstack(
745
+ rx.text(p["address"], color=COLORS["text_primary"], font_weight="600", font_size="15px"),
746
+ rx.hstack(
747
+ rx.text(f"Rent: ${p['monthly_rent']:,.0f}/mo", color=COLORS["accent_primary"], font_size="13px"),
748
+ rx.text("→", color=COLORS["text_muted"], font_size="13px"),
749
+ rx.text(f"${p['rent_income']:,.0f}/yr", color=COLORS["success"], font_size="13px", font_weight="500"),
750
+ spacing="1",
751
+ ),
752
+ rx.text(f"Expenses: ${p['total_expenses']:,.0f}/yr",
753
+ color=COLORS["text_muted"], font_size="12px"),
754
+ align_items="start",
755
+ spacing="1",
756
+ flex="1",
757
+ ),
758
+ rx.vstack(
759
+ rx.text("Net Income", color=COLORS["text_muted"], font_size="11px"),
760
+ rx.text(f"${p['net_income']:,.0f}",
761
+ color=rx.cond(p["net_income"].to(float) >= 0, COLORS["success"], COLORS["error"]),
762
+ font_weight="700",
763
+ font_size="18px"),
764
+ rx.text("/year", color=COLORS["text_muted"], font_size="11px"),
765
+ align_items="center",
766
+ spacing="0",
767
+ ),
768
+ rx.box(
769
+ rx.icon("x", size=16, color=COLORS["text_muted"]),
770
+ cursor="pointer",
771
+ padding="6px",
772
+ border_radius="6px",
773
+ _hover={"background": COLORS["error_light"], "color": COLORS["error"]},
774
+ on_click=lambda: TaxAppState.remove_rental_property(idx),
775
+ ),
776
+ width="100%", spacing="4", align_items="center",
777
+ ),
778
+ padding="16px",
779
+ background="white",
780
+ border_radius="10px",
781
+ border=f"1px solid {COLORS['border']}",
782
+ box_shadow="0 1px 3px rgba(0,0,0,0.08)",
783
+ ),
689
784
  ),
785
+ spacing="3",
786
+ width="100%",
690
787
  ),
691
788
  rx.text("No rental properties added", color=COLORS["text_muted"], font_style="italic"),
692
789
  ),
@@ -694,30 +791,130 @@ def review_page() -> rx.Component:
694
791
  rx.cond(
695
792
  TaxAppState.show_rental_form,
696
793
  rx.vstack(
697
- rx.input(placeholder="Address", value=TaxAppState.rental_form_address, on_change=TaxAppState.set_rental_address, width="100%"),
794
+ rx.input(
795
+ placeholder="Address",
796
+ value=TaxAppState.rental_form_address,
797
+ on_change=TaxAppState.set_rental_address,
798
+ width="100%",
799
+ background="white",
800
+ border=f"1px solid {COLORS['border']}",
801
+ color=COLORS["text_primary"],
802
+ _placeholder={"color": COLORS["text_muted"]},
803
+ padding="10px 12px",
804
+ border_radius="8px",
805
+ _focus={"border_color": COLORS["accent_primary"], "box_shadow": f"0 0 0 3px {COLORS['accent_light']}"},
806
+ ),
698
807
  rx.hstack(
699
- rx.input(placeholder="Rent Income", value=TaxAppState.rental_form_income, on_change=TaxAppState.set_rental_income, type="number"),
700
- rx.input(placeholder="Mortgage Int", value=TaxAppState.rental_form_mortgage, on_change=TaxAppState.set_rental_mortgage, type="number"),
808
+ rx.input(
809
+ placeholder="Monthly Rent ($)",
810
+ value=TaxAppState.rental_form_income,
811
+ on_change=TaxAppState.set_rental_income,
812
+ type="number",
813
+ background="white",
814
+ border=f"1px solid {COLORS['border']}",
815
+ color=COLORS["text_primary"],
816
+ _placeholder={"color": COLORS["text_muted"]},
817
+ padding="10px 12px",
818
+ border_radius="8px",
819
+ _focus={"border_color": COLORS["accent_primary"], "box_shadow": f"0 0 0 3px {COLORS['accent_light']}"},
820
+ ),
821
+ rx.input(
822
+ placeholder="Mortgage Interest ($)",
823
+ value=TaxAppState.rental_form_mortgage,
824
+ on_change=TaxAppState.set_rental_mortgage,
825
+ type="number",
826
+ background="white",
827
+ border=f"1px solid {COLORS['border']}",
828
+ color=COLORS["text_primary"],
829
+ _placeholder={"color": COLORS["text_muted"]},
830
+ padding="10px 12px",
831
+ border_radius="8px",
832
+ _focus={"border_color": COLORS["accent_primary"], "box_shadow": f"0 0 0 3px {COLORS['accent_light']}"},
833
+ ),
701
834
  width="100%",
835
+ spacing="3",
702
836
  ),
703
837
  rx.hstack(
704
- rx.input(placeholder="Property Tax", value=TaxAppState.rental_form_tax, on_change=TaxAppState.set_rental_tax, type="number"),
705
- rx.input(placeholder="Insurance", value=TaxAppState.rental_form_insurance, on_change=TaxAppState.set_rental_insurance, type="number"),
838
+ rx.input(
839
+ placeholder="Property Tax (Quarterly $)",
840
+ value=TaxAppState.rental_form_tax,
841
+ on_change=TaxAppState.set_rental_tax,
842
+ type="number",
843
+ background="white",
844
+ border=f"1px solid {COLORS['border']}",
845
+ color=COLORS["text_primary"],
846
+ _placeholder={"color": COLORS["text_muted"]},
847
+ padding="10px 12px",
848
+ border_radius="8px",
849
+ _focus={"border_color": COLORS["accent_primary"], "box_shadow": f"0 0 0 3px {COLORS['accent_light']}"},
850
+ ),
851
+ rx.input(
852
+ placeholder="Insurance ($)",
853
+ value=TaxAppState.rental_form_insurance,
854
+ on_change=TaxAppState.set_rental_insurance,
855
+ type="number",
856
+ background="white",
857
+ border=f"1px solid {COLORS['border']}",
858
+ color=COLORS["text_primary"],
859
+ _placeholder={"color": COLORS["text_muted"]},
860
+ padding="10px 12px",
861
+ border_radius="8px",
862
+ _focus={"border_color": COLORS["accent_primary"], "box_shadow": f"0 0 0 3px {COLORS['accent_light']}"},
863
+ ),
706
864
  width="100%",
865
+ spacing="3",
707
866
  ),
708
867
  rx.hstack(
709
- rx.input(placeholder="Repairs", value=TaxAppState.rental_form_repairs, on_change=TaxAppState.set_rental_repairs, type="number"),
710
- rx.input(placeholder="Depreciation", value=TaxAppState.rental_form_depreciation, on_change=TaxAppState.set_rental_depreciation, type="number"),
868
+ rx.input(
869
+ placeholder="Repairs ($)",
870
+ value=TaxAppState.rental_form_repairs,
871
+ on_change=TaxAppState.set_rental_repairs,
872
+ type="number",
873
+ background="white",
874
+ border=f"1px solid {COLORS['border']}",
875
+ color=COLORS["text_primary"],
876
+ _placeholder={"color": COLORS["text_muted"]},
877
+ padding="10px 12px",
878
+ border_radius="8px",
879
+ _focus={"border_color": COLORS["accent_primary"], "box_shadow": f"0 0 0 3px {COLORS['accent_light']}"},
880
+ ),
881
+ rx.input(
882
+ placeholder="Depreciation ($)",
883
+ value=TaxAppState.rental_form_depreciation,
884
+ on_change=TaxAppState.set_rental_depreciation,
885
+ type="number",
886
+ background="white",
887
+ border=f"1px solid {COLORS['border']}",
888
+ color=COLORS["text_primary"],
889
+ _placeholder={"color": COLORS["text_muted"]},
890
+ padding="10px 12px",
891
+ border_radius="8px",
892
+ _focus={"border_color": COLORS["accent_primary"], "box_shadow": f"0 0 0 3px {COLORS['accent_light']}"},
893
+ ),
711
894
  width="100%",
895
+ spacing="3",
896
+ ),
897
+ rx.input(
898
+ placeholder="Other Expenses ($)",
899
+ value=TaxAppState.rental_form_other,
900
+ on_change=TaxAppState.set_rental_other,
901
+ type="number",
902
+ width="100%",
903
+ background="white",
904
+ border=f"1px solid {COLORS['border']}",
905
+ color=COLORS["text_primary"],
906
+ _placeholder={"color": COLORS["text_muted"]},
907
+ padding="10px 12px",
908
+ border_radius="8px",
909
+ _focus={"border_color": COLORS["accent_primary"], "box_shadow": f"0 0 0 3px {COLORS['accent_light']}"},
712
910
  ),
713
- rx.input(placeholder="Other Expenses", value=TaxAppState.rental_form_other, on_change=TaxAppState.set_rental_other, type="number", width="100%"),
714
911
  rx.hstack(
715
912
  rx.button("Cancel", variant="outline", on_click=TaxAppState.toggle_rental_form),
716
913
  rx.button("Add Property", on_click=TaxAppState.submit_rental_form),
717
- width="100%", justify="end",
914
+ width="100%", justify="end", spacing="3",
718
915
  ),
719
- width="100%", spacing="2", padding="12px",
720
- background="white", border_radius="8px", border=f"1px solid {COLORS['border']}",
916
+ width="100%", spacing="3", padding="16px",
917
+ background=COLORS["bg_page"], border_radius="8px", border=f"1px solid {COLORS['border']}",
721
918
  ),
722
919
  ),
723
920
  rx.text(f"Total Rental Income: ${TaxAppState.total_rental_income:,.2f}",
@@ -753,19 +950,20 @@ def review_page() -> rx.Component:
753
950
  rx.cond(
754
951
  TaxAppState.show_business_form,
755
952
  rx.vstack(
756
- rx.input(placeholder="Business Name", value=TaxAppState.business_form_name, on_change=TaxAppState.set_business_name, width="100%"),
953
+ styled_input("Business Name", TaxAppState.business_form_name, TaxAppState.set_business_name),
757
954
  rx.hstack(
758
- rx.input(placeholder="Gross Income", value=TaxAppState.business_form_income, on_change=TaxAppState.set_business_income, type="number"),
759
- rx.input(placeholder="Total Expenses", value=TaxAppState.business_form_expenses, on_change=TaxAppState.set_business_expenses, type="number"),
955
+ styled_input("Gross Income ($)", TaxAppState.business_form_income, TaxAppState.set_business_income, "number"),
956
+ styled_input("Total Expenses ($)", TaxAppState.business_form_expenses, TaxAppState.set_business_expenses, "number"),
760
957
  width="100%",
958
+ spacing="3",
761
959
  ),
762
960
  rx.hstack(
763
961
  rx.button("Cancel", variant="outline", on_click=TaxAppState.toggle_business_form),
764
962
  rx.button("Add Business", on_click=TaxAppState.submit_business_form),
765
- width="100%", justify="end",
963
+ width="100%", justify="end", spacing="3",
766
964
  ),
767
- width="100%", spacing="2", padding="12px",
768
- background="white", border_radius="8px", border=f"1px solid {COLORS['border']}",
965
+ width="100%", spacing="3", padding="16px",
966
+ background=COLORS["bg_page"], border_radius="8px", border=f"1px solid {COLORS['border']}",
769
967
  ),
770
968
  ),
771
969
  rx.text(f"Total Business Income: ${TaxAppState.total_business_income:,.2f}",
@@ -803,10 +1001,12 @@ def review_page() -> rx.Component:
803
1001
  rx.cond(
804
1002
  TaxAppState.show_other_income_form,
805
1003
  rx.hstack(
806
- rx.input(placeholder="Description", value=TaxAppState.other_income_form_desc, on_change=TaxAppState.set_other_income_desc, flex="1"),
807
- rx.input(placeholder="Amount", value=TaxAppState.other_income_form_amount, on_change=TaxAppState.set_other_income_amount, type="number", width="120px"),
1004
+ styled_input("Description", TaxAppState.other_income_form_desc, TaxAppState.set_other_income_desc),
1005
+ styled_input("Amount ($)", TaxAppState.other_income_form_amount, TaxAppState.set_other_income_amount, "number", "140px"),
808
1006
  rx.button("Add", on_click=TaxAppState.submit_other_income_form),
809
1007
  width="100%",
1008
+ spacing="3",
1009
+ padding="8px 0",
810
1010
  ),
811
1011
  ),
812
1012
  width="100%", spacing="2", padding="12px",
@@ -837,10 +1037,12 @@ def review_page() -> rx.Component:
837
1037
  rx.cond(
838
1038
  TaxAppState.show_other_deduction_form,
839
1039
  rx.hstack(
840
- rx.input(placeholder="Description", value=TaxAppState.other_deduction_form_desc, on_change=TaxAppState.set_other_deduction_desc, flex="1"),
841
- rx.input(placeholder="Amount", value=TaxAppState.other_deduction_form_amount, on_change=TaxAppState.set_other_deduction_amount, type="number", width="120px"),
1040
+ styled_input("Description", TaxAppState.other_deduction_form_desc, TaxAppState.set_other_deduction_desc),
1041
+ styled_input("Amount ($)", TaxAppState.other_deduction_form_amount, TaxAppState.set_other_deduction_amount, "number", "140px"),
842
1042
  rx.button("Add", on_click=TaxAppState.submit_other_deduction_form),
843
1043
  width="100%",
1044
+ spacing="3",
1045
+ padding="8px 0",
844
1046
  ),
845
1047
  ),
846
1048
  width="100%", spacing="2", padding="12px",
@@ -877,20 +1079,21 @@ def review_page() -> rx.Component:
877
1079
  TaxAppState.show_dependent_form,
878
1080
  rx.vstack(
879
1081
  rx.hstack(
880
- rx.input(placeholder="Name", value=TaxAppState.dependent_form_name, on_change=TaxAppState.set_dependent_name, flex="1"),
881
- rx.input(placeholder="Relationship", value=TaxAppState.dependent_form_relationship, on_change=TaxAppState.set_dependent_relationship, width="120px"),
882
- rx.input(placeholder="Age", value=TaxAppState.dependent_form_age, on_change=TaxAppState.set_dependent_age, type="number", width="80px"),
1082
+ styled_input("Name", TaxAppState.dependent_form_name, TaxAppState.set_dependent_name),
1083
+ styled_input("Relationship", TaxAppState.dependent_form_relationship, TaxAppState.set_dependent_relationship, "text", "140px"),
1084
+ styled_input("Age", TaxAppState.dependent_form_age, TaxAppState.set_dependent_age, "number", "80px"),
883
1085
  width="100%",
1086
+ spacing="3",
884
1087
  ),
885
1088
  rx.text("Under 17 = $2,000 Child Tax Credit | 17+ = $500 Other Dependent Credit",
886
1089
  color=COLORS["text_muted"], font_size="12px"),
887
1090
  rx.hstack(
888
1091
  rx.button("Cancel", variant="outline", on_click=TaxAppState.toggle_dependent_form),
889
1092
  rx.button("Add Dependent", on_click=TaxAppState.submit_dependent_form),
890
- width="100%", justify="end",
1093
+ width="100%", justify="end", spacing="3",
891
1094
  ),
892
- width="100%", spacing="2", padding="12px",
893
- background="white", border_radius="8px", border=f"1px solid {COLORS['border']}",
1095
+ width="100%", spacing="3", padding="16px",
1096
+ background=COLORS["bg_page"], border_radius="8px", border=f"1px solid {COLORS['border']}",
894
1097
  ),
895
1098
  ),
896
1099
  # Credits Summary
@@ -1309,7 +1512,7 @@ app = rx.App(
1309
1512
  )
1310
1513
 
1311
1514
  # Register pages
1312
- app.add_page(dashboard_page, route="/", title="TaxForge - Dashboard")
1515
+ app.add_page(dashboard_page, route="/", title="TaxForge - Dashboard", on_load=TaxAppState.load_saved_data)
1313
1516
  app.add_page(upload_page, route="/upload", title="TaxForge - Upload")
1314
1517
  app.add_page(review_page, route="/review", title="TaxForge - Review")
1315
1518
  app.add_page(settings_page, route="/settings", title="TaxForge - Settings")
aitax/cli.py CHANGED
@@ -8,7 +8,7 @@ import shutil
8
8
  from pathlib import Path
9
9
 
10
10
 
11
- __version__ = "0.9.20"
11
+ __version__ = "0.9.22"
12
12
 
13
13
  RXCONFIG_CONTENT = '''"""Reflex config for TaxForge."""
14
14
  import reflex as rx
aitax/components.py CHANGED
@@ -562,3 +562,81 @@ def api_key_modal(is_open: bool, on_close: Callable, api_key_value: str,
562
562
  ),
563
563
  ),
564
564
  )
565
+
566
+
567
+ def processing_overlay(
568
+ is_processing: bool,
569
+ current_file: str,
570
+ file_index: int,
571
+ total_files: int,
572
+ ) -> rx.Component:
573
+ """Full-screen processing overlay with progress."""
574
+ return rx.cond(
575
+ is_processing,
576
+ rx.box(
577
+ # Backdrop
578
+ rx.box(
579
+ position="fixed",
580
+ top="0",
581
+ left="0",
582
+ right="0",
583
+ bottom="0",
584
+ background="rgba(0, 0, 0, 0.5)",
585
+ z_index="300",
586
+ ),
587
+ # Modal
588
+ rx.box(
589
+ rx.vstack(
590
+ rx.box(
591
+ rx.spinner(size="3", color=COLORS["accent_primary"]),
592
+ padding="16px",
593
+ background=COLORS["accent_light"],
594
+ border_radius="50%",
595
+ ),
596
+ rx.text("Processing Documents",
597
+ color=COLORS["text_primary"],
598
+ font_size="20px",
599
+ font_weight="600"),
600
+ rx.text(f"File {file_index} of {total_files}",
601
+ color=COLORS["text_secondary"],
602
+ font_size="14px"),
603
+ rx.text(current_file,
604
+ color=COLORS["accent_primary"],
605
+ font_size="14px",
606
+ font_weight="500",
607
+ max_width="280px",
608
+ overflow="hidden",
609
+ text_overflow="ellipsis",
610
+ white_space="nowrap"),
611
+ rx.box(
612
+ rx.progress(
613
+ value=file_index,
614
+ max=total_files,
615
+ width="100%",
616
+ color_scheme="indigo",
617
+ ),
618
+ width="100%",
619
+ padding="8px 0",
620
+ ),
621
+ rx.text("Please wait while AI extracts data...",
622
+ color=COLORS["text_muted"],
623
+ font_size="13px"),
624
+ spacing="3",
625
+ align_items="center",
626
+ width="100%",
627
+ ),
628
+ background="white",
629
+ border_radius="16px",
630
+ padding="32px",
631
+ box_shadow="0 25px 50px -12px rgba(0, 0, 0, 0.25)",
632
+ position="fixed",
633
+ top="50%",
634
+ left="50%",
635
+ transform="translate(-50%, -50%)",
636
+ z_index="301",
637
+ width="90%",
638
+ max_width="360px",
639
+ text_align="center",
640
+ ),
641
+ ),
642
+ )
@@ -10,9 +10,11 @@ from pathlib import Path
10
10
  from typing import Optional
11
11
 
12
12
  # Rate limiting settings
13
- MAX_RETRIES = 3
14
- RETRY_DELAY_SECONDS = 5
15
- REQUEST_DELAY_SECONDS = 2 # Delay between requests to avoid rate limiting
13
+ # Gemini free tier: 15 RPM (requests per minute) = 1 request per 4 seconds
14
+ # Being conservative with 5 seconds to avoid hitting limits
15
+ MAX_RETRIES = 5
16
+ RETRY_DELAY_SECONDS = 10 # Initial retry delay (will exponentially backoff)
17
+ REQUEST_DELAY_SECONDS = 5 # Delay between requests to avoid rate limiting
16
18
 
17
19
 
18
20
  # Prompt to extract tax document data
@@ -36,12 +38,13 @@ For 1099-INT forms, extract:
36
38
  - interest_income: Box 1 - Interest income
37
39
  - federal_withheld: Box 4 - Federal income tax withheld
38
40
 
39
- For 1099-DIV forms, extract:
40
- - payer_name: Institution name
41
- - ordinary_dividends: Box 1a - Total ordinary dividends
41
+ For 1099-DIV forms (Dividend Income), extract:
42
+ - payer_name: Institution/broker name (e.g., Fidelity, Schwab, Vanguard)
43
+ - ordinary_dividends: Box 1a - Total ordinary dividends (THIS IS THE MAIN AMOUNT - look for it carefully!)
42
44
  - qualified_dividends: Box 1b - Qualified dividends
43
45
  - capital_gain: Box 2a - Total capital gain distributions
44
46
  - federal_withheld: Box 4 - Federal income tax withheld
47
+ NOTE: On consolidated 1099s, dividend info may be labeled "1099-DIV" in a section. Look for dollar amounts next to Box 1a.
45
48
 
46
49
  For 1099-B forms, extract:
47
50
  - payer_name: Broker name
@@ -268,28 +271,47 @@ async def extract_with_retry(
268
271
  api_key: Optional[str] = None,
269
272
  ) -> dict:
270
273
  """
271
- Extract with automatic retry on rate limiting.
274
+ Extract with automatic retry on rate limiting and transient errors.
272
275
  """
276
+ last_result = None
277
+
273
278
  for attempt in range(MAX_RETRIES):
274
279
  result = await extract_from_document(file_path, api_key)
280
+ last_result = result
281
+
282
+ error = result.get("error", "")
283
+
284
+ # Check for retryable errors
285
+ is_rate_limited = error == "RATE_LIMITED"
286
+ is_transient = any(x in str(error).lower() for x in [
287
+ "rate", "quota", "limit", "429", "500", "502", "503", "504",
288
+ "timeout", "overloaded", "capacity", "try again"
289
+ ])
275
290
 
276
- if result.get("error") == "RATE_LIMITED":
291
+ if is_rate_limited or is_transient:
277
292
  if attempt < MAX_RETRIES - 1:
278
293
  # Wait and retry with exponential backoff
279
294
  wait_time = RETRY_DELAY_SECONDS * (2 ** attempt)
295
+ print(f"[TaxForge] Rate limited, waiting {wait_time}s before retry {attempt + 2}/{MAX_RETRIES}...")
280
296
  await asyncio.sleep(wait_time)
281
297
  continue
282
298
  else:
283
299
  return {
284
- "error": "API quota exceeded after retries. Please wait a minute and try again.",
300
+ "error": f"API quota exceeded after {MAX_RETRIES} retries. Please wait a minute and try again.",
285
301
  "document_type": "UNKNOWN",
286
302
  "confidence": 0.0,
287
303
  "extracted_data": {}
288
304
  }
289
305
 
306
+ # Non-retryable error or success
290
307
  return result
291
308
 
292
- return result
309
+ return last_result or {
310
+ "error": "Max retries exceeded",
311
+ "document_type": "UNKNOWN",
312
+ "confidence": 0.0,
313
+ "extracted_data": {}
314
+ }
293
315
 
294
316
 
295
317
  def convert_to_w2(extracted_data: dict) -> dict:
@@ -304,17 +326,32 @@ def convert_to_w2(extracted_data: dict) -> dict:
304
326
  def convert_to_1099(extracted_data: dict, form_type: str) -> dict:
305
327
  """Convert extracted data to 1099 format for state."""
306
328
  amount = 0.0
329
+
307
330
  if form_type == "1099-INT":
308
- amount = float(extracted_data.get("interest_income", 0))
331
+ # Try multiple field names that LLM might use
332
+ amount = float(extracted_data.get("interest_income", 0) or
333
+ extracted_data.get("interest", 0) or
334
+ extracted_data.get("box_1", 0) or
335
+ extracted_data.get("taxable_interest", 0) or 0)
309
336
  elif form_type == "1099-DIV":
310
- amount = float(extracted_data.get("ordinary_dividends", 0))
337
+ # Try multiple field names that LLM might use
338
+ amount = float(extracted_data.get("ordinary_dividends", 0) or
339
+ extracted_data.get("total_ordinary_dividends", 0) or
340
+ extracted_data.get("dividends", 0) or
341
+ extracted_data.get("box_1a", 0) or
342
+ extracted_data.get("total_dividends", 0) or 0)
311
343
  elif form_type == "1099-B":
312
- amount = float(extracted_data.get("gain_loss", extracted_data.get("proceeds", 0)))
344
+ # Try multiple field names
345
+ amount = float(extracted_data.get("gain_loss", 0) or
346
+ extracted_data.get("net_gain_loss", 0) or
347
+ extracted_data.get("proceeds", 0) or
348
+ extracted_data.get("total_proceeds", 0) or 0)
313
349
 
314
350
  return {
315
- "payer_name": extracted_data.get("payer_name", extracted_data.get("lender_name", "Unknown Payer")),
351
+ "payer_name": extracted_data.get("payer_name", extracted_data.get("lender_name", extracted_data.get("institution", "Unknown Payer"))),
316
352
  "amount": amount,
317
353
  "form_type": form_type,
354
+ "raw_data": extracted_data, # Keep raw for debugging
318
355
  }
319
356
 
320
357
 
aitax/persistence.py ADDED
@@ -0,0 +1,197 @@
1
+ """
2
+ TaxForge - Data Persistence
3
+ Save and load tax data to/from local JSON file.
4
+ """
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Dict, Any, Optional
9
+ from datetime import datetime
10
+ import base64
11
+ import hashlib
12
+
13
+ # Data directory
14
+ DATA_DIR = Path.home() / ".taxforge"
15
+ DATA_FILE = DATA_DIR / "tax_data.json"
16
+ BACKUP_DIR = DATA_DIR / "backups"
17
+
18
+ # Fields to persist (from TaxAppState)
19
+ PERSIST_FIELDS = [
20
+ # Personal info
21
+ "first_name", "last_name", "filing_status",
22
+ # API key (will be obfuscated)
23
+ "openai_api_key",
24
+ # Documents
25
+ "uploaded_files", "parsed_documents",
26
+ # Tax data
27
+ "w2_list", "form_1099_list", "form_1098_list", "form_5498_list",
28
+ # Manual entries
29
+ "rental_properties", "business_income", "other_income", "other_deductions",
30
+ "dependents", "child_care_expenses",
31
+ # Calculated values we want to persist
32
+ "total_wages", "total_interest", "total_dividends", "total_capital_gains",
33
+ "total_rental_income", "total_business_income", "total_other_income",
34
+ "hsa_deduction", "self_employment_tax", "mortgage_interest_deduction",
35
+ "adjusted_gross_income", "itemized_deductions", "standard_deduction",
36
+ "total_deductions", "taxable_income", "total_tax", "total_withholding",
37
+ "other_withholding", "refund_or_owed", "is_refund",
38
+ "child_tax_credit", "other_dependent_credit", "child_care_credit", "total_credits",
39
+ # Status
40
+ "return_status", "return_generated",
41
+ ]
42
+
43
+ # Fields that should be obfuscated (not encrypted, just not plain text)
44
+ SENSITIVE_FIELDS = ["openai_api_key"]
45
+
46
+
47
+ def _obfuscate(value: str) -> str:
48
+ """Simple obfuscation for sensitive values (not real encryption)."""
49
+ if not value:
50
+ return ""
51
+ return base64.b64encode(value.encode()).decode()
52
+
53
+
54
+ def _deobfuscate(value: str) -> str:
55
+ """Reverse obfuscation."""
56
+ if not value:
57
+ return ""
58
+ try:
59
+ return base64.b64decode(value.encode()).decode()
60
+ except:
61
+ return value # Return as-is if can't decode
62
+
63
+
64
+ def ensure_data_dir():
65
+ """Ensure data directory exists."""
66
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
67
+ BACKUP_DIR.mkdir(parents=True, exist_ok=True)
68
+
69
+
70
+ def save_state(state) -> bool:
71
+ """
72
+ Save state to JSON file.
73
+
74
+ Args:
75
+ state: TaxAppState instance
76
+
77
+ Returns:
78
+ True if successful, False otherwise
79
+ """
80
+ try:
81
+ ensure_data_dir()
82
+
83
+ # Extract fields to save
84
+ data = {
85
+ "_saved_at": datetime.now().isoformat(),
86
+ "_version": "0.9.21",
87
+ }
88
+
89
+ for field in PERSIST_FIELDS:
90
+ if hasattr(state, field):
91
+ value = getattr(state, field)
92
+ # Handle special types
93
+ if isinstance(value, (list, dict)):
94
+ data[field] = value
95
+ else:
96
+ data[field] = value
97
+
98
+ # Obfuscate sensitive fields
99
+ if field in SENSITIVE_FIELDS and value:
100
+ data[field] = _obfuscate(str(value))
101
+
102
+ # Create backup of existing file
103
+ if DATA_FILE.exists():
104
+ backup_name = f"tax_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
105
+ backup_path = BACKUP_DIR / backup_name
106
+ # Keep only last 5 backups
107
+ existing_backups = sorted(BACKUP_DIR.glob("tax_data_*.json"))
108
+ if len(existing_backups) >= 5:
109
+ for old_backup in existing_backups[:-4]:
110
+ old_backup.unlink()
111
+
112
+ # Write to file
113
+ with open(DATA_FILE, 'w') as f:
114
+ json.dump(data, f, indent=2, default=str)
115
+
116
+ return True
117
+
118
+ except Exception as e:
119
+ print(f"[TaxForge] Error saving state: {e}")
120
+ return False
121
+
122
+
123
+ def load_state(state) -> bool:
124
+ """
125
+ Load state from JSON file.
126
+
127
+ Args:
128
+ state: TaxAppState instance to populate
129
+
130
+ Returns:
131
+ True if data was loaded, False if no data or error
132
+ """
133
+ try:
134
+ if not DATA_FILE.exists():
135
+ print("[TaxForge] No saved data found, starting fresh")
136
+ return False
137
+
138
+ with open(DATA_FILE, 'r') as f:
139
+ data = json.load(f)
140
+
141
+ print(f"[TaxForge] Loading saved data from {data.get('_saved_at', 'unknown')}")
142
+
143
+ # Restore fields
144
+ for field in PERSIST_FIELDS:
145
+ if field in data:
146
+ value = data[field]
147
+
148
+ # Deobfuscate sensitive fields
149
+ if field in SENSITIVE_FIELDS and value:
150
+ value = _deobfuscate(value)
151
+
152
+ # Set the attribute
153
+ if hasattr(state, field):
154
+ setattr(state, field, value)
155
+
156
+ print(f"[TaxForge] Loaded: {len(data.get('w2_list', []))} W-2s, "
157
+ f"{len(data.get('form_1099_list', []))} 1099s, "
158
+ f"{len(data.get('rental_properties', []))} rentals")
159
+
160
+ return True
161
+
162
+ except Exception as e:
163
+ print(f"[TaxForge] Error loading state: {e}")
164
+ return False
165
+
166
+
167
+ def clear_saved_data() -> bool:
168
+ """Clear all saved data."""
169
+ try:
170
+ if DATA_FILE.exists():
171
+ DATA_FILE.unlink()
172
+ return True
173
+ except Exception as e:
174
+ print(f"[TaxForge] Error clearing data: {e}")
175
+ return False
176
+
177
+
178
+ def get_data_info() -> Dict[str, Any]:
179
+ """Get info about saved data."""
180
+ info = {
181
+ "exists": DATA_FILE.exists(),
182
+ "path": str(DATA_FILE),
183
+ "size": 0,
184
+ "saved_at": None,
185
+ }
186
+
187
+ if DATA_FILE.exists():
188
+ info["size"] = DATA_FILE.stat().st_size
189
+ try:
190
+ with open(DATA_FILE, 'r') as f:
191
+ data = json.load(f)
192
+ info["saved_at"] = data.get("_saved_at")
193
+ info["version"] = data.get("_version")
194
+ except:
195
+ pass
196
+
197
+ return info
aitax/state.py CHANGED
@@ -15,6 +15,7 @@ from .document_extractor import (
15
15
  convert_1099_misc_to_other_income,
16
16
  REQUEST_DELAY_SECONDS
17
17
  )
18
+ from .persistence import save_state, load_state, clear_saved_data
18
19
  import asyncio
19
20
 
20
21
 
@@ -269,6 +270,7 @@ class TaxAppState(rx.State):
269
270
  self.openai_api_key = self.temp_api_key
270
271
  self.show_api_modal = False
271
272
  self.success_message = "API key saved!"
273
+ self._auto_save() # Persist API key
272
274
 
273
275
  def check_api_key_for_upload(self):
274
276
  """Check if API key is set, open modal if not."""
@@ -328,6 +330,7 @@ class TaxAppState(rx.State):
328
330
  if new_files > 0:
329
331
  self.success_message = f"Uploaded {new_files} file(s). Go to Review to process with AI."
330
332
  self.return_status = "in_progress"
333
+ self._auto_save() # Persist uploaded files list
331
334
  except Exception as e:
332
335
  self.error_message = f"Upload failed: {str(e)}"
333
336
 
@@ -573,15 +576,18 @@ class TaxAppState(rx.State):
573
576
  mortgage_interest: float = 0, property_tax: float = 0,
574
577
  insurance: float = 0, repairs: float = 0,
575
578
  management: float = 0, utilities: float = 0,
576
- depreciation: float = 0, other_expenses: float = 0):
577
- """Add a rental property (Schedule E)."""
579
+ depreciation: float = 0, other_expenses: float = 0,
580
+ monthly_rent: float = 0, quarterly_tax: float = 0):
581
+ """Add a rental property (Schedule E). All values should be annual."""
578
582
  total_expenses = (mortgage_interest + property_tax + insurance +
579
583
  repairs + management + utilities + depreciation + other_expenses)
580
584
  self.rental_properties.append({
581
585
  "address": address,
582
- "rent_income": rent_income,
586
+ "monthly_rent": monthly_rent, # For display
587
+ "quarterly_tax": quarterly_tax, # For display
588
+ "rent_income": rent_income, # Annual (monthly × 12)
583
589
  "mortgage_interest": mortgage_interest,
584
- "property_tax": property_tax,
590
+ "property_tax": property_tax, # Annual (quarterly × 4)
585
591
  "insurance": insurance,
586
592
  "repairs": repairs,
587
593
  "management": management,
@@ -712,15 +718,25 @@ class TaxAppState(rx.State):
712
718
  def submit_rental_form(self):
713
719
  """Submit rental property form."""
714
720
  try:
721
+ # Monthly rent × 12 = Annual rent income
722
+ monthly_rent = float(self.rental_form_income or 0)
723
+ annual_rent = monthly_rent * 12
724
+
725
+ # Quarterly property tax × 4 = Annual
726
+ quarterly_tax = float(self.rental_form_tax or 0)
727
+ annual_tax = quarterly_tax * 4
728
+
715
729
  self.add_rental_property(
716
730
  address=self.rental_form_address or "Property",
717
- rent_income=float(self.rental_form_income or 0),
731
+ rent_income=annual_rent,
718
732
  mortgage_interest=float(self.rental_form_mortgage or 0),
719
- property_tax=float(self.rental_form_tax or 0),
733
+ property_tax=annual_tax,
720
734
  insurance=float(self.rental_form_insurance or 0),
721
735
  repairs=float(self.rental_form_repairs or 0),
722
736
  depreciation=float(self.rental_form_depreciation or 0),
723
737
  other_expenses=float(self.rental_form_other or 0),
738
+ monthly_rent=monthly_rent, # Store for display
739
+ quarterly_tax=quarterly_tax, # Store for display
724
740
  )
725
741
  # Clear form
726
742
  self.rental_form_address = ""
@@ -1073,6 +1089,16 @@ class TaxAppState(rx.State):
1073
1089
 
1074
1090
  if self.total_wages > 0 or total_income > 0:
1075
1091
  self.return_status = "in_progress"
1092
+
1093
+ # Auto-save after recalculation
1094
+ self._auto_save()
1095
+
1096
+ def _auto_save(self):
1097
+ """Auto-save state to disk."""
1098
+ try:
1099
+ save_state(self)
1100
+ except Exception as e:
1101
+ print(f"[TaxForge] Auto-save failed: {e}")
1076
1102
 
1077
1103
  def _calculate_tax(self, income: float, brackets: list) -> float:
1078
1104
  """Calculate tax from brackets."""
@@ -1140,6 +1166,15 @@ class TaxAppState(rx.State):
1140
1166
  self.return_generated = False
1141
1167
  self.error_message = ""
1142
1168
  self.success_message = ""
1169
+ # Also clear persisted data
1170
+ clear_saved_data()
1171
+
1172
+ def load_saved_data(self):
1173
+ """Load previously saved data from disk."""
1174
+ if load_state(self):
1175
+ self.success_message = "Previous session restored!"
1176
+ else:
1177
+ self.success_message = ""
1143
1178
 
1144
1179
  # ===== Computed Properties =====
1145
1180
  @rx.var
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: taxforge
3
- Version: 0.9.20
3
+ Version: 0.9.22
4
4
  Summary: AI-powered tax preparation assistant
5
5
  Author: TaxForge Team
6
6
  License: MIT
@@ -0,0 +1,12 @@
1
+ aitax/__init__.py,sha256=nyfwdgcJkm0IZZxEUPth1pyolrF5U_FCMcunvZ63w6k,68
2
+ aitax/aitax.py,sha256=6XY7MM5iKMh9PWpjajIwh5fDMR-zjLGXBn8Vjk9w9ZY,75937
3
+ aitax/cli.py,sha256=rTFcb_CCd0cM_KJjolruBk_2aH0k8MJrnRpyowkBEFE,2651
4
+ aitax/components.py,sha256=p3MQgKQr95zmO5Uu-jcgCBApuULoV_AD2SZvAGGT0dw,23855
5
+ aitax/document_extractor.py,sha256=k1mEXNp5nxM7jSLi7_okZuElu6PItrKL7K2yHRpOc6c,16130
6
+ aitax/persistence.py,sha256=m9Bel2wrgDaduNQyRxyfXRYEsw_z_83xc2oK2I94FXI,5932
7
+ aitax/state.py,sha256=D85kA3mcUUP7YuDARfstTPjSPilVv2HAKDYD3K2jIAI,47234
8
+ taxforge-0.9.22.dist-info/METADATA,sha256=_DcfKQ9V9ze5feHr8D2bla7pEPoSbgGkfsDInYLBIDA,4874
9
+ taxforge-0.9.22.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
10
+ taxforge-0.9.22.dist-info/entry_points.txt,sha256=2dG4U_m3yvQaVeD8LeEGm1v8szyNGQtXUZOvbwH75Gg,44
11
+ taxforge-0.9.22.dist-info/top_level.txt,sha256=H7DuLZSRzSwHT5STc7uQa0oTOsKcNYUz2Pn4a4C5Q0Y,6
12
+ taxforge-0.9.22.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- aitax/__init__.py,sha256=hV9XKDmqYNgMHDYg2q_S1YEwZ4owPZh93d_XtJ8_PVM,68
2
- aitax/aitax.py,sha256=l0lSEz81q8A2__gpmmIFvn-8eol1cSFU8QQzhNtrbcU,63645
3
- aitax/cli.py,sha256=dm3M-sQ506Q5_hJRh_fOeosPF-ufY43BT46AeeOUwJU,2651
4
- aitax/components.py,sha256=pi4wKOtNsw0ju1cN3ivY7UWF0Q32rshijeklFnxVt5U,21056
5
- aitax/document_extractor.py,sha256=gArprV9JkIXCUsjUdbmbBobcAIzhHPZMcpUY4VKcGtA,14104
6
- aitax/state.py,sha256=vr5hlK2Dv3a4fhMntK6NUTnQdBjILpewoWGaq8DRQAw,45779
7
- taxforge-0.9.20.dist-info/METADATA,sha256=YGHJYtiUf7B6d7v8TBUo3gmCaoxOlknUkdJhSbdeFOg,4874
8
- taxforge-0.9.20.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
9
- taxforge-0.9.20.dist-info/entry_points.txt,sha256=2dG4U_m3yvQaVeD8LeEGm1v8szyNGQtXUZOvbwH75Gg,44
10
- taxforge-0.9.20.dist-info/top_level.txt,sha256=H7DuLZSRzSwHT5STc7uQa0oTOsKcNYUz2Pn4a4C5Q0Y,6
11
- taxforge-0.9.20.dist-info/RECORD,,