taxforge 0.9.20__py3-none-any.whl → 0.9.21__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 +1 -1
- aitax/aitax.py +285 -82
- aitax/cli.py +1 -1
- aitax/components.py +78 -0
- aitax/document_extractor.py +51 -14
- aitax/state.py +19 -6
- {taxforge-0.9.20.dist-info → taxforge-0.9.21.dist-info}/METADATA +1 -1
- taxforge-0.9.21.dist-info/RECORD +11 -0
- taxforge-0.9.20.dist-info/RECORD +0 -11
- {taxforge-0.9.20.dist-info → taxforge-0.9.21.dist-info}/WHEEL +0 -0
- {taxforge-0.9.20.dist-info → taxforge-0.9.21.dist-info}/entry_points.txt +0 -0
- {taxforge-0.9.20.dist-info → taxforge-0.9.21.dist-info}/top_level.txt +0 -0
aitax/__init__.py
CHANGED
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.
|
|
510
|
-
rx.
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
520
|
-
|
|
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"] == "
|
|
527
|
-
"
|
|
555
|
+
doc["status"] == "parsed",
|
|
556
|
+
f"✓ Processed - {doc['type']}",
|
|
528
557
|
rx.cond(
|
|
529
|
-
doc["status"] == "
|
|
530
|
-
"
|
|
531
|
-
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
578
|
+
align_items="start",
|
|
579
|
+
spacing="0",
|
|
580
|
+
flex="1",
|
|
545
581
|
),
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
559
|
-
padding="10px 12px",
|
|
598
|
+
padding="12px 16px",
|
|
560
599
|
background=rx.cond(
|
|
561
600
|
doc["status"] == "parsed",
|
|
562
601
|
COLORS["success_light"],
|
|
563
|
-
|
|
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
|
-
|
|
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.
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
rx.
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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(
|
|
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(
|
|
700
|
-
|
|
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(
|
|
705
|
-
|
|
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(
|
|
710
|
-
|
|
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="
|
|
720
|
-
background="
|
|
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
|
-
|
|
953
|
+
styled_input("Business Name", TaxAppState.business_form_name, TaxAppState.set_business_name),
|
|
757
954
|
rx.hstack(
|
|
758
|
-
|
|
759
|
-
|
|
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="
|
|
768
|
-
background="
|
|
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
|
-
|
|
807
|
-
|
|
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
|
-
|
|
841
|
-
|
|
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
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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="
|
|
893
|
-
background="
|
|
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
|
aitax/cli.py
CHANGED
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
|
+
)
|
aitax/document_extractor.py
CHANGED
|
@@ -10,9 +10,11 @@ from pathlib import Path
|
|
|
10
10
|
from typing import Optional
|
|
11
11
|
|
|
12
12
|
# Rate limiting settings
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/state.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
"
|
|
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=
|
|
728
|
+
rent_income=annual_rent,
|
|
718
729
|
mortgage_interest=float(self.rental_form_mortgage or 0),
|
|
719
|
-
property_tax=
|
|
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 = ""
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
aitax/__init__.py,sha256=4NZIkzaPsU2IclvaAf0AxWyjoK4jgkWut4kSZ7i9bBs,68
|
|
2
|
+
aitax/aitax.py,sha256=GfwwEY4Lb99mpMKNvBcqA0g3eZAgKPEAywfgu-53_GA,75900
|
|
3
|
+
aitax/cli.py,sha256=zKpz_3T7kJSZNJ7__LXzO6650u6Rk9tl_uMIrge5uqQ,2651
|
|
4
|
+
aitax/components.py,sha256=p3MQgKQr95zmO5Uu-jcgCBApuULoV_AD2SZvAGGT0dw,23855
|
|
5
|
+
aitax/document_extractor.py,sha256=k1mEXNp5nxM7jSLi7_okZuElu6PItrKL7K2yHRpOc6c,16130
|
|
6
|
+
aitax/state.py,sha256=69NF6QqwOd7tB1yhAUV_XAdvP1LdQLlQv7AM8TrbCAQ,46489
|
|
7
|
+
taxforge-0.9.21.dist-info/METADATA,sha256=UDdeOPJNXSBQjpfa2I9KenzDXusHN1mmHIGC3g6j8Us,4874
|
|
8
|
+
taxforge-0.9.21.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
9
|
+
taxforge-0.9.21.dist-info/entry_points.txt,sha256=2dG4U_m3yvQaVeD8LeEGm1v8szyNGQtXUZOvbwH75Gg,44
|
|
10
|
+
taxforge-0.9.21.dist-info/top_level.txt,sha256=H7DuLZSRzSwHT5STc7uQa0oTOsKcNYUz2Pn4a4C5Q0Y,6
|
|
11
|
+
taxforge-0.9.21.dist-info/RECORD,,
|
taxforge-0.9.20.dist-info/RECORD
DELETED
|
@@ -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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|