taxforge 0.9.19__tar.gz → 0.9.21__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: taxforge
3
- Version: 0.9.19
3
+ Version: 0.9.21
4
4
  Summary: AI-powered tax preparation assistant
5
5
  Author: TaxForge Team
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "taxforge"
7
- version = "0.9.19"
7
+ version = "0.9.21"
8
8
  description = "AI-powered tax preparation assistant"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -1,3 +1,3 @@
1
1
  """TaxForge - AI-powered tax preparation"""
2
2
 
3
- __version__ = "0.9.19"
3
+ __version__ = "0.9.21"
@@ -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
 
@@ -658,7 +717,7 @@ def review_page() -> rx.Component:
658
717
  fintech_card(
659
718
  rx.vstack(
660
719
  rx.hstack(
661
- rx.icon("edit", size=20, color=COLORS["accent_primary"]),
720
+ rx.icon("pencil", size=20, color=COLORS["accent_primary"]),
662
721
  rx.text("Manual Entry", color=COLORS["text_primary"],
663
722
  font_size="18px", font_weight="600"),
664
723
  rx.spacer(),
@@ -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"] >= 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}",
@@ -741,7 +938,7 @@ def review_page() -> rx.Component:
741
938
  lambda b, idx: rx.hstack(
742
939
  rx.text(b["name"], flex="1"),
743
940
  rx.text(f"Net: ${b['net_profit']:,.2f}",
744
- color=rx.cond(b["net_profit"] >= 0, COLORS["success"], COLORS["error"])),
941
+ color=rx.cond(b["net_profit"].to(float) >= 0, COLORS["success"], COLORS["error"])),
745
942
  rx.icon("x", size=14, cursor="pointer",
746
943
  on_click=lambda: TaxAppState.remove_business(idx)),
747
944
  width="100%", padding="8px",
@@ -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
@@ -8,7 +8,7 @@ import shutil
8
8
  from pathlib import Path
9
9
 
10
10
 
11
- __version__ = "0.9.19"
11
+ __version__ = "0.9.21"
12
12
 
13
13
  RXCONFIG_CONTENT = '''"""Reflex config for TaxForge."""
14
14
  import reflex as rx
@@ -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
 
@@ -573,15 +573,18 @@ class TaxAppState(rx.State):
573
573
  mortgage_interest: float = 0, property_tax: float = 0,
574
574
  insurance: float = 0, repairs: float = 0,
575
575
  management: float = 0, utilities: float = 0,
576
- depreciation: float = 0, other_expenses: float = 0):
577
- """Add a rental property (Schedule E)."""
576
+ depreciation: float = 0, other_expenses: float = 0,
577
+ monthly_rent: float = 0, quarterly_tax: float = 0):
578
+ """Add a rental property (Schedule E). All values should be annual."""
578
579
  total_expenses = (mortgage_interest + property_tax + insurance +
579
580
  repairs + management + utilities + depreciation + other_expenses)
580
581
  self.rental_properties.append({
581
582
  "address": address,
582
- "rent_income": rent_income,
583
+ "monthly_rent": monthly_rent, # For display
584
+ "quarterly_tax": quarterly_tax, # For display
585
+ "rent_income": rent_income, # Annual (monthly × 12)
583
586
  "mortgage_interest": mortgage_interest,
584
- "property_tax": property_tax,
587
+ "property_tax": property_tax, # Annual (quarterly × 4)
585
588
  "insurance": insurance,
586
589
  "repairs": repairs,
587
590
  "management": management,
@@ -712,15 +715,25 @@ class TaxAppState(rx.State):
712
715
  def submit_rental_form(self):
713
716
  """Submit rental property form."""
714
717
  try:
718
+ # Monthly rent × 12 = Annual rent income
719
+ monthly_rent = float(self.rental_form_income or 0)
720
+ annual_rent = monthly_rent * 12
721
+
722
+ # Quarterly property tax × 4 = Annual
723
+ quarterly_tax = float(self.rental_form_tax or 0)
724
+ annual_tax = quarterly_tax * 4
725
+
715
726
  self.add_rental_property(
716
727
  address=self.rental_form_address or "Property",
717
- rent_income=float(self.rental_form_income or 0),
728
+ rent_income=annual_rent,
718
729
  mortgage_interest=float(self.rental_form_mortgage or 0),
719
- property_tax=float(self.rental_form_tax or 0),
730
+ property_tax=annual_tax,
720
731
  insurance=float(self.rental_form_insurance or 0),
721
732
  repairs=float(self.rental_form_repairs or 0),
722
733
  depreciation=float(self.rental_form_depreciation or 0),
723
734
  other_expenses=float(self.rental_form_other or 0),
735
+ monthly_rent=monthly_rent, # Store for display
736
+ quarterly_tax=quarterly_tax, # Store for display
724
737
  )
725
738
  # Clear form
726
739
  self.rental_form_address = ""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: taxforge
3
- Version: 0.9.19
3
+ Version: 0.9.21
4
4
  Summary: AI-powered tax preparation assistant
5
5
  Author: TaxForge Team
6
6
  License: MIT
File without changes
File without changes