iatoolkit 0.8.1__py3-none-any.whl → 0.63.4__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.

Potentially problematic release.


This version of iatoolkit might be problematic. Click here for more details.

Files changed (159) hide show
  1. iatoolkit/__init__.py +8 -34
  2. iatoolkit/base_company.py +14 -3
  3. iatoolkit/common/routes.py +83 -52
  4. iatoolkit/common/session_manager.py +0 -1
  5. iatoolkit/common/util.py +0 -27
  6. iatoolkit/iatoolkit.py +61 -46
  7. iatoolkit/infra/llm_client.py +7 -8
  8. iatoolkit/infra/openai_adapter.py +1 -1
  9. iatoolkit/infra/redis_session_manager.py +48 -2
  10. iatoolkit/repositories/database_manager.py +17 -2
  11. iatoolkit/repositories/models.py +31 -6
  12. iatoolkit/repositories/profile_repo.py +7 -2
  13. iatoolkit/services/auth_service.py +188 -0
  14. iatoolkit/services/branding_service.py +147 -0
  15. iatoolkit/services/dispatcher_service.py +10 -40
  16. iatoolkit/services/excel_service.py +15 -15
  17. iatoolkit/services/history_service.py +3 -12
  18. iatoolkit/services/jwt_service.py +15 -24
  19. iatoolkit/services/onboarding_service.py +43 -0
  20. iatoolkit/services/profile_service.py +97 -44
  21. iatoolkit/services/query_service.py +124 -81
  22. iatoolkit/services/tasks_service.py +1 -1
  23. iatoolkit/services/user_feedback_service.py +67 -31
  24. iatoolkit/services/user_session_context_service.py +112 -54
  25. iatoolkit/static/images/fernando.jpeg +0 -0
  26. iatoolkit/static/js/{chat_feedback.js → chat_feedback_button.js} +6 -11
  27. iatoolkit/static/js/chat_history_button.js +126 -0
  28. iatoolkit/static/js/chat_logout_button.js +36 -0
  29. iatoolkit/static/js/chat_main.js +130 -220
  30. iatoolkit/static/js/chat_onboarding_button.js +97 -0
  31. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  32. iatoolkit/static/js/chat_reload_button.js +52 -0
  33. iatoolkit/static/styles/chat_iatoolkit.css +329 -507
  34. iatoolkit/static/styles/chat_modal.css +95 -56
  35. iatoolkit/static/styles/landing_page.css +182 -0
  36. iatoolkit/static/styles/onboarding.css +169 -0
  37. iatoolkit/system_prompts/query_main.prompt +3 -12
  38. iatoolkit/templates/_company_header.html +20 -0
  39. iatoolkit/templates/_login_widget.html +40 -0
  40. iatoolkit/templates/base.html +8 -3
  41. iatoolkit/templates/change_password.html +54 -37
  42. iatoolkit/templates/chat.html +149 -66
  43. iatoolkit/templates/chat_modals.html +47 -18
  44. iatoolkit/templates/error.html +41 -8
  45. iatoolkit/templates/forgot_password.html +37 -24
  46. iatoolkit/templates/index.html +140 -0
  47. iatoolkit/templates/login_simulation.html +34 -0
  48. iatoolkit/templates/onboarding_shell.html +105 -0
  49. iatoolkit/templates/signup.html +64 -66
  50. iatoolkit/views/base_login_view.py +81 -0
  51. iatoolkit/views/change_password_view.py +23 -12
  52. iatoolkit/views/external_login_view.py +61 -28
  53. iatoolkit/views/{file_store_view.py → file_store_api_view.py} +9 -2
  54. iatoolkit/views/forgot_password_view.py +23 -13
  55. iatoolkit/views/history_api_view.py +52 -0
  56. iatoolkit/views/home_view.py +58 -25
  57. iatoolkit/views/index_view.py +14 -0
  58. iatoolkit/views/init_context_api_view.py +68 -0
  59. iatoolkit/views/llmquery_api_view.py +45 -0
  60. iatoolkit/views/login_simulation_view.py +81 -0
  61. iatoolkit/views/login_view.py +118 -34
  62. iatoolkit/views/logout_api_view.py +45 -0
  63. iatoolkit/views/{prompt_view.py → prompt_api_view.py} +7 -7
  64. iatoolkit/views/signup_view.py +38 -29
  65. iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
  66. iatoolkit/views/tasks_review_api_view.py +55 -0
  67. iatoolkit/views/{user_feedback_view.py → user_feedback_api_view.py} +16 -31
  68. iatoolkit/views/verify_user_view.py +13 -8
  69. {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/METADATA +2 -2
  70. iatoolkit-0.63.4.dist-info/RECORD +113 -0
  71. {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/top_level.txt +0 -1
  72. iatoolkit/common/auth.py +0 -200
  73. iatoolkit/static/images/arrow_up.png +0 -0
  74. iatoolkit/static/images/diagrama_iatoolkit.jpg +0 -0
  75. iatoolkit/static/images/logo_clinica.png +0 -0
  76. iatoolkit/static/images/logo_iatoolkit.png +0 -0
  77. iatoolkit/static/images/logo_maxxa.png +0 -0
  78. iatoolkit/static/images/logo_notaria.png +0 -0
  79. iatoolkit/static/images/logo_tarjeta.png +0 -0
  80. iatoolkit/static/images/logo_umayor.png +0 -0
  81. iatoolkit/static/images/upload.png +0 -0
  82. iatoolkit/static/js/chat_history.js +0 -117
  83. iatoolkit/templates/home.html +0 -201
  84. iatoolkit/templates/login.html +0 -43
  85. iatoolkit/views/chat_token_request_view.py +0 -98
  86. iatoolkit/views/chat_view.py +0 -51
  87. iatoolkit/views/download_file_view.py +0 -58
  88. iatoolkit/views/external_chat_login_view.py +0 -88
  89. iatoolkit/views/history_view.py +0 -57
  90. iatoolkit/views/llmquery_view.py +0 -65
  91. iatoolkit/views/tasks_review_view.py +0 -83
  92. iatoolkit-0.8.1.dist-info/RECORD +0 -175
  93. tests/__init__.py +0 -5
  94. tests/common/__init__.py +0 -0
  95. tests/common/test_auth.py +0 -279
  96. tests/common/test_routes.py +0 -42
  97. tests/common/test_session_manager.py +0 -59
  98. tests/common/test_util.py +0 -444
  99. tests/companies/__init__.py +0 -5
  100. tests/conftest.py +0 -36
  101. tests/infra/__init__.py +0 -5
  102. tests/infra/connectors/__init__.py +0 -5
  103. tests/infra/connectors/test_google_drive_connector.py +0 -107
  104. tests/infra/connectors/test_local_file_connector.py +0 -85
  105. tests/infra/connectors/test_s3_connector.py +0 -95
  106. tests/infra/test_call_service.py +0 -92
  107. tests/infra/test_database_manager.py +0 -59
  108. tests/infra/test_gemini_adapter.py +0 -137
  109. tests/infra/test_google_chat_app.py +0 -68
  110. tests/infra/test_llm_client.py +0 -165
  111. tests/infra/test_llm_proxy.py +0 -122
  112. tests/infra/test_mail_app.py +0 -94
  113. tests/infra/test_openai_adapter.py +0 -105
  114. tests/infra/test_redis_session_manager_service.py +0 -117
  115. tests/repositories/__init__.py +0 -5
  116. tests/repositories/test_database_manager.py +0 -87
  117. tests/repositories/test_document_repo.py +0 -76
  118. tests/repositories/test_llm_query_repo.py +0 -340
  119. tests/repositories/test_models.py +0 -38
  120. tests/repositories/test_profile_repo.py +0 -142
  121. tests/repositories/test_tasks_repo.py +0 -76
  122. tests/repositories/test_vs_repo.py +0 -107
  123. tests/services/__init__.py +0 -5
  124. tests/services/test_dispatcher_service.py +0 -274
  125. tests/services/test_document_service.py +0 -181
  126. tests/services/test_excel_service.py +0 -208
  127. tests/services/test_file_processor_service.py +0 -121
  128. tests/services/test_history_service.py +0 -164
  129. tests/services/test_jwt_service.py +0 -255
  130. tests/services/test_load_documents_service.py +0 -112
  131. tests/services/test_mail_service.py +0 -70
  132. tests/services/test_profile_service.py +0 -379
  133. tests/services/test_prompt_manager_service.py +0 -190
  134. tests/services/test_query_service.py +0 -243
  135. tests/services/test_search_service.py +0 -39
  136. tests/services/test_sql_service.py +0 -160
  137. tests/services/test_tasks_service.py +0 -252
  138. tests/services/test_user_feedback_service.py +0 -389
  139. tests/services/test_user_session_context_service.py +0 -132
  140. tests/views/__init__.py +0 -5
  141. tests/views/test_change_password_view.py +0 -191
  142. tests/views/test_chat_token_request_view.py +0 -188
  143. tests/views/test_chat_view.py +0 -98
  144. tests/views/test_download_file_view.py +0 -149
  145. tests/views/test_external_chat_login_view.py +0 -120
  146. tests/views/test_external_login_view.py +0 -102
  147. tests/views/test_file_store_view.py +0 -128
  148. tests/views/test_forgot_password_view.py +0 -142
  149. tests/views/test_history_view.py +0 -336
  150. tests/views/test_home_view.py +0 -61
  151. tests/views/test_llm_query_view.py +0 -154
  152. tests/views/test_login_view.py +0 -114
  153. tests/views/test_prompt_view.py +0 -111
  154. tests/views/test_signup_view.py +0 -140
  155. tests/views/test_tasks_review_view.py +0 -104
  156. tests/views/test_tasks_view.py +0 -130
  157. tests/views/test_user_feedback_view.py +0 -214
  158. tests/views/test_verify_user_view.py +0 -110
  159. {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/WHEEL +0 -0
@@ -0,0 +1,113 @@
1
+ iatoolkit/__init__.py,sha256=4PWjMJjktixtrxF6BY405qyA50Sv967kEP2x-oil6qk,1120
2
+ iatoolkit/base_company.py,sha256=vU4ki-wB3PWIn3_Bvehfh0TfBH_XNC614tRBKNmEd84,4718
3
+ iatoolkit/cli_commands.py,sha256=G5L9xQXZ0lVFXQWBaE_KEZHyfuiT6PL1nTQRoSdnBzc,2302
4
+ iatoolkit/company_registry.py,sha256=tduqt3oV8iDX_IB1eA7KIgvIxE4edTcy-3qZIXh3Lzw,2549
5
+ iatoolkit/iatoolkit.py,sha256=nFc0wvDX4za2R81QnF0uUMmX8MyGAF4JZWKiuLb1c_M,17616
6
+ iatoolkit/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ iatoolkit/common/exceptions.py,sha256=EXx40n5htp7UiOM6P1xfJ9U6NMcADqm62dlFaKz7ICU,1154
8
+ iatoolkit/common/routes.py,sha256=XwhbXupwnYqZ2F4Ec28o9W0LmUUHzpc0QDLQttl4Dwc,5843
9
+ iatoolkit/common/session_manager.py,sha256=UeKfD15bcEA3P5e0WSURfotLqpsiIMp3AXxAMhtgHs0,471
10
+ iatoolkit/common/util.py,sha256=dlSDxnN3g-GOjK3YsPggzQG7VsZWarIMCzUkV9FBAIw,14442
11
+ iatoolkit/infra/__init__.py,sha256=5JqK9sZ6jBuK83zDQokUhxQ0wuJJJ9DXB8pYCLkX7X4,102
12
+ iatoolkit/infra/call_service.py,sha256=iRk9VxbXaAwlLIl8fUzGDWIAdzwfsbs1MtP84YeENxg,4929
13
+ iatoolkit/infra/gemini_adapter.py,sha256=kXV-t5i9GmWBafUPX2kAyiqvcT7GPoxHylcCUWG_c_U,15051
14
+ iatoolkit/infra/google_chat_app.py,sha256=_uKWxeacHH6C5a4FVx0YZjBn1tL-x_MIQV9gqgWGAjo,1937
15
+ iatoolkit/infra/llm_client.py,sha256=clTYqV_0a2VD2vVH3j6AWqd1gVUmeg-fU3_myizmjQc,18543
16
+ iatoolkit/infra/llm_proxy.py,sha256=cHyNxUpVE4UDoWUfvSGfGCrIUFPTrpWZOixELQTsGFY,5744
17
+ iatoolkit/infra/llm_response.py,sha256=YUUQPBHzmP3Ce6-t0kKMRIpowvh7de1odSoefEByIvI,904
18
+ iatoolkit/infra/mail_app.py,sha256=PLGZdEs7LQ_9bmMRRxz0iqQdNa4xToAFyf9tg75wK8U,6103
19
+ iatoolkit/infra/openai_adapter.py,sha256=tbzd8aPAH5cQOJT-sD4ypqq2fWB6WiEIGuGesUDnQNk,3550
20
+ iatoolkit/infra/redis_session_manager.py,sha256=EPr3E_g7LHxn6U4SV5lT_L8WQsAwg8VzA_WIEZ3TwOw,3667
21
+ iatoolkit/infra/connectors/__init__.py,sha256=5JqK9sZ6jBuK83zDQokUhxQ0wuJJJ9DXB8pYCLkX7X4,102
22
+ iatoolkit/infra/connectors/file_connector.py,sha256=HOjRTFd-WfDOcFyvHncAhnGNZuFgChIwC-P6osPo9ZM,352
23
+ iatoolkit/infra/connectors/file_connector_factory.py,sha256=3qvyfH4ZHKuiMxJFkawOxhW2-TGKKtsBYHgoPpZMuKU,2118
24
+ iatoolkit/infra/connectors/google_cloud_storage_connector.py,sha256=IXpL3HTo7Ft4EQsYiQq5wXRRQK854jzOEB7ZdWjLa4U,2050
25
+ iatoolkit/infra/connectors/google_drive_connector.py,sha256=WR1AlO5-Bl3W89opdja0kKgHTJzVOjTsy3H4SlIvwVg,2537
26
+ iatoolkit/infra/connectors/local_file_connector.py,sha256=hrzIgpMJOTuwTqzlQeTIU_50ZbZ6yl8lcWPv6hMnoqI,1739
27
+ iatoolkit/infra/connectors/s3_connector.py,sha256=Nj4_YaLobjfcnbZewJf21_K2EXohgcc3mJll1Pzn4zg,1123
28
+ iatoolkit/repositories/__init__.py,sha256=5JqK9sZ6jBuK83zDQokUhxQ0wuJJJ9DXB8pYCLkX7X4,102
29
+ iatoolkit/repositories/database_manager.py,sha256=QgV8hNnVv9RmeOvUdomdj_mfk0bf3Rl8Ti41a-5zIAY,3700
30
+ iatoolkit/repositories/document_repo.py,sha256=Y7bF1kZB1HWJsAGjWdF7P2aVYeTYNufq9ngQXp7mDkY,1124
31
+ iatoolkit/repositories/llm_query_repo.py,sha256=YT_t7cYGQk8rwzH_17-28aTzO-e2jUfa2rvXy8tugvA,3612
32
+ iatoolkit/repositories/models.py,sha256=6KQpyCtp2l-ExfbeoPmoqc2V5qlTmSmEbzHYISZtu6g,14288
33
+ iatoolkit/repositories/profile_repo.py,sha256=21am3GP7XCG0nq6i3pArQ7mfGsrRn8rdcWT98fsdwlU,4397
34
+ iatoolkit/repositories/tasks_repo.py,sha256=icVO_r2oPagGnnBhwVFzznnvEEU2EAx-2dlWuWvoDC4,1745
35
+ iatoolkit/repositories/vs_repo.py,sha256=UkpmQQiocgM5IwRBmmWhw3HHzHP6zK1nN3J3TcQgjhc,5300
36
+ iatoolkit/services/__init__.py,sha256=5JqK9sZ6jBuK83zDQokUhxQ0wuJJJ9DXB8pYCLkX7X4,102
37
+ iatoolkit/services/auth_service.py,sha256=so6031zc9BTrLYskSrOpi1Cplu23_biPnTb-UgcDG4s,7495
38
+ iatoolkit/services/benchmark_service.py,sha256=CdbFYyS3FHFhNzWQEa9ZNjUlmON10DT1nKNbZQ1EUi8,5880
39
+ iatoolkit/services/branding_service.py,sha256=aCdfn6TVAYab8YcaIALGpqnvx3ObizBTEIcYWs-UVIU,7790
40
+ iatoolkit/services/dispatcher_service.py,sha256=Qdn2x4cozpgpKg2448sUxkhO6tuplzb8xPWUxdTTFBE,12772
41
+ iatoolkit/services/document_service.py,sha256=nMXrNtbHQuc9pSaten0LvKY0kT8_WngBDmZJUP3jNPw,5936
42
+ iatoolkit/services/excel_service.py,sha256=JdAcg7_vPz3J16cf2chC6j7WYVpiT55tDX9667tfMUc,3764
43
+ iatoolkit/services/file_processor_service.py,sha256=B1sUUhZNFf-rT4_1wrD38GKNoBFMp2g0dYrXYMCWe2E,4122
44
+ iatoolkit/services/history_service.py,sha256=3IxcdpKV1mHBGIiv2KIYV3LsVQJ0GPdFuCOiGYRszMU,1255
45
+ iatoolkit/services/jwt_service.py,sha256=W2kQVNQheQSLkNLS7RZ4jd3hmySBPLbHAS5hvBrUI10,3244
46
+ iatoolkit/services/load_documents_service.py,sha256=ZpB0BZ3qX1fGJGBtZtMLbFdWWx0hkPoeCS3OqJKwCTs,7291
47
+ iatoolkit/services/mail_service.py,sha256=2h-fcF3swZDya_o7IpgXkmuj3iEVHVCiHi7oVxU99sQ,2182
48
+ iatoolkit/services/onboarding_service.py,sha256=cMO2Ho1-G3wAeVNl-j25LwCMJjRwj3yKHpYKnZUFLDE,2001
49
+ iatoolkit/services/profile_service.py,sha256=cOjGJvrH9f3DRoXrhs4vZEqG7dzwrl_GnOcwxyaSX18,20301
50
+ iatoolkit/services/prompt_manager_service.py,sha256=U-XmSpkeXvv1KRN4dytdMxSYBMRSB7y-UHcb18mk0nA,8342
51
+ iatoolkit/services/query_service.py,sha256=gtMEAQ7IRVrFAMq4h_Pc_lHlMlXFI6heLSuBYHenkfs,17502
52
+ iatoolkit/services/search_service.py,sha256=i1xGWu7ORKIIDH0aAQBkF86dVVbLQ0Yrooz5TiZ6aGo,1823
53
+ iatoolkit/services/sql_service.py,sha256=MIslAtpJWnTMgSD74nnqTvQj27p-lHiyRXc6OiA2C_c,2172
54
+ iatoolkit/services/tasks_service.py,sha256=itREO5rDnUIgsqtyCOBKDtH30QL5v1egs4qPTiBK8xU,6865
55
+ iatoolkit/services/user_feedback_service.py,sha256=Bb6PVWcxzoQ3awev_k4MI0BQhkMh6iLPGC8_CK02t5g,4986
56
+ iatoolkit/services/user_session_context_service.py,sha256=vYF_vWM37tPB_ZyPBJ6f6WTJVjT2j-4L8JfZbqbI93k,6775
57
+ iatoolkit/static/images/fernando.jpeg,sha256=W68TYMuo5hZVpbP-evwH6Nu4xWFv2bc8pJzSKDoLTeQ,100612
58
+ iatoolkit/static/js/chat_feedback_button.js,sha256=j3BIyxcgyDuDDBn3vxxe0DV4evQabj3xb6XF28OWwCs,4220
59
+ iatoolkit/static/js/chat_filepond.js,sha256=mzXafm7a506EpM37KATTK3zvAswO1E0KSUY1vKbwuRc,3163
60
+ iatoolkit/static/js/chat_history_button.js,sha256=miPQOtG8m_w_TP6mhiM3PUZdIZITUJFJTchvNi7_fs4,4332
61
+ iatoolkit/static/js/chat_logout_button.js,sha256=Of9H6IbAboSBmeqRaurEVW6_dL752L0UeDcDLNBD5Z0,1335
62
+ iatoolkit/static/js/chat_main.js,sha256=TELqxzMg-qe-XbI-9-tj-CLEcrkZY8ANuQ4XggjCnow,12466
63
+ iatoolkit/static/js/chat_onboarding_button.js,sha256=vjvEloJ9PA9D7jOGca0QjS3J_7bU97R71L7kSyY3wOI,3153
64
+ iatoolkit/static/js/chat_prompt_manager.js,sha256=QYki28CpyM2Chn82dnOP2eH6FObxH8eChGFyUxukv1M,3319
65
+ iatoolkit/static/js/chat_reload_button.js,sha256=f8f_qRnZTNr_DwbcmafTHIuBLmiCoypYAKGQc472AOs,2393
66
+ iatoolkit/static/styles/chat_iatoolkit.css,sha256=sr4O4_9hpbCBtcPFaN-JQeAgEEbe0s9tEe9D7NNyKcQ,15081
67
+ iatoolkit/static/styles/chat_info.css,sha256=17DbgoNYE21VYWfb5L9-QLCpD2R1idK4imKRLwXtJLY,1058
68
+ iatoolkit/static/styles/chat_modal.css,sha256=2AAwfH88v_sqcJ_2pBrYm4_UIRsVmgf-JrVaoTkQ1no,3973
69
+ iatoolkit/static/styles/landing_page.css,sha256=E6VRI5dko_naloH_FmNAHpjzxz4NZbrbzKwYLw4fYJA,4297
70
+ iatoolkit/static/styles/llm_output.css,sha256=AlxgRSOleeCk2dLAqFWVaQ-jwZiJjcpC5rHuUv3T6VU,2312
71
+ iatoolkit/static/styles/onboarding.css,sha256=Bo0hd8ngVy404_a-gtNFi-hzljhIAnpE-1oQJGnj0F0,3655
72
+ iatoolkit/system_prompts/format_styles.prompt,sha256=MSMe1qvR3cF_0IbFshn8R0z6Wx6VCHQq1p37rpu5wwk,3576
73
+ iatoolkit/system_prompts/query_main.prompt,sha256=D2Wjf0uunQIQsQiJVrY-BTQz6PemM5En6ftmw_c5t4E,2808
74
+ iatoolkit/system_prompts/sql_rules.prompt,sha256=y4nURVnb9AyFwt-lrbMNBHHtZlhk6kC9grYoOhRnrJo,59174
75
+ iatoolkit/templates/_company_header.html,sha256=wrwDftsSVu1uMPchsweAPLupsPkmLIPQBQ0xpIIyxjA,747
76
+ iatoolkit/templates/_login_widget.html,sha256=E0ly8eGKDVfUCaOkFYYbRaPlZ4gDQUWIBTdCciN9cHw,1854
77
+ iatoolkit/templates/about.html,sha256=ciC08grUVz5qLzdzDDqDX31xirg5PrJIRYabWpV9oA8,294
78
+ iatoolkit/templates/base.html,sha256=y6YnYM1w2YXOuU0HcC9oE-o7UjFEBgusJqDxHEWazHI,2361
79
+ iatoolkit/templates/change_password.html,sha256=VObsXwjEzMkKkzIv-M2voDazPZkW_07Ye_RovTx62bw,3284
80
+ iatoolkit/templates/chat.html,sha256=6i01YQUSfWeehJ8FV1u3DDM6salIbrf3G52eDKIS6TM,12343
81
+ iatoolkit/templates/chat_modals.html,sha256=FImo_xrN3FS4xpTnWbHWde0eSOi9gAMQkAUCz6PArys,6801
82
+ iatoolkit/templates/error.html,sha256=tmN4tLx_WfgfFizvMWZJFd6_qeBTAiEnQB_Kz_AP38Q,1655
83
+ iatoolkit/templates/forgot_password.html,sha256=iJ4qgC8NMpalmO2g6sxUrPznSAy6aN0bi10EB1WldGE,2165
84
+ iatoolkit/templates/header.html,sha256=179agI7rnYwP_rvJNXIiVde5E8Ec5649_XKq6eew2Hk,1263
85
+ iatoolkit/templates/index.html,sha256=DZZ9WK0jQ9bCbCDkukNt_jWYII7ISxQtNxn4vHQNwz8,7808
86
+ iatoolkit/templates/login_simulation.html,sha256=1svwCBPrJ3Gy6bD9WMuz25NBSdFgZt4j8_sC7HE6MFU,1270
87
+ iatoolkit/templates/onboarding_shell.html,sha256=exSGckoPeE-ID9ym3B4TLh5hULpR7N1X6LeuSNmiUL0,4666
88
+ iatoolkit/templates/signup.html,sha256=ZX1Ufj-W5efOqiBG_7SIxUZcKonffsdRaWdEQYTFuqs,4262
89
+ iatoolkit/templates/test.html,sha256=rwNtxC83tbCl5COZFXYvmRBxxmgFJtPNuVBd_nq9KWY,133
90
+ iatoolkit/views/__init__.py,sha256=5JqK9sZ6jBuK83zDQokUhxQ0wuJJJ9DXB8pYCLkX7X4,102
91
+ iatoolkit/views/base_login_view.py,sha256=qoMMrAezCJvzjcfNzIbd2vwHhksALIQfNK8f_9E8m0o,3241
92
+ iatoolkit/views/change_password_view.py,sha256=NIUxvOz6rs2PpFvAjd-_UE9m4XQCrcxJicgxSXu7434,4582
93
+ iatoolkit/views/external_login_view.py,sha256=d4gUQbxFsThGbbAUdtFn7AMgPJjeb7_8KFFoH3vspVA,2853
94
+ iatoolkit/views/file_store_api_view.py,sha256=UvtZWOG-rLQMLfs8igOIYoQ-tkkEg5baMjqCJdKxaRQ,2300
95
+ iatoolkit/views/forgot_password_view.py,sha256=46AQ9bNXUESkFUYYcb_oIP1FukE5WyN2-MTwNgcqCu0,3349
96
+ iatoolkit/views/history_api_view.py,sha256=0YChbss0ae05KHzni2p3d4bGS2_yKAbjALk1OBeQk50,1867
97
+ iatoolkit/views/home_view.py,sha256=poAUAmaXNbf-bcuxmQg7XZ1yQQAbvlyCE2YkzwDS5wg,2733
98
+ iatoolkit/views/index_view.py,sha256=OsykSlXLB-TpFAPkDlsMna6bi3Ie5PXL303Hsg3WwUM,329
99
+ iatoolkit/views/init_context_api_view.py,sha256=YTjpT4xdtm1knUhelDj-VbV4EK6o_qGAgwwDhFmIOlg,2716
100
+ iatoolkit/views/llmquery_api_view.py,sha256=v_KxR6w-TrCVR2fMFHZCz3_v4o42CXb1Yvd-R1hSJHg,1662
101
+ iatoolkit/views/login_simulation_view.py,sha256=0Qt-puRnltI2HZxlfdyJmOf26-hQp3xjknGV_jkwV7E,3484
102
+ iatoolkit/views/login_view.py,sha256=XP_OkkpWEzPfY6LpPDGeZtsnNBMbliopi6k67tOwSD4,5843
103
+ iatoolkit/views/logout_api_view.py,sha256=wpiWLNkgypOOy7L75_tCJLv1gvcITgmd_hK2ipb9024,1505
104
+ iatoolkit/views/prompt_api_view.py,sha256=S_4-qAD5knh8Esae1AczEYGdXy_AuU7LMOmnUPej4jQ,1294
105
+ iatoolkit/views/signup_view.py,sha256=EWvkptFQ9g_DZZepo3lJ0-R2fgrVMq5ocxw0GFfxr9k,4112
106
+ iatoolkit/views/tasks_api_view.py,sha256=wGnuwuuL83ByQ1Yre6ytRVztA0OGQjGrwMjB1_G830U,2630
107
+ iatoolkit/views/tasks_review_api_view.py,sha256=wsCpzqyRyUdCXWAhyGlBe3eNZZ6A1DQG7TblN_GZNfM,1894
108
+ iatoolkit/views/user_feedback_api_view.py,sha256=-Ngex8SPf0mPvPNqwE_GUcRErLpOL49yJ43o5Y4Qhqo,1992
109
+ iatoolkit/views/verify_user_view.py,sha256=iFEVc-hFh5qOBy3T93ckM6-V2_mcomk-M4TVFl2D3Pw,2690
110
+ iatoolkit-0.63.4.dist-info/METADATA,sha256=9iwHUs3ntkAULZTRIUYe1NSjktISGnNrn_XCxdObwdY,9301
111
+ iatoolkit-0.63.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
112
+ iatoolkit-0.63.4.dist-info/top_level.txt,sha256=V_w4QvDx0b1RXiy8zTCrD1Bp7AZkFe3_O0-9fMiwogg,10
113
+ iatoolkit-0.63.4.dist-info/RECORD,,
iatoolkit/common/auth.py DELETED
@@ -1,200 +0,0 @@
1
- # Copyright (c) 2024 Fernando Libedinsky
2
- # Product: IAToolkit
3
- #
4
- # IAToolkit is open source software.
5
-
6
- from flask import redirect, url_for
7
- from iatoolkit.common.session_manager import SessionManager
8
- from datetime import datetime, timezone
9
- from injector import inject
10
- from iatoolkit.repositories.profile_repo import ProfileRepo
11
- from iatoolkit.services.jwt_service import JWTService
12
- import logging
13
- from flask import request
14
- from typing import Optional
15
-
16
- MAX_INACTIVITY_SECONDS = 60*30
17
-
18
- class IAuthentication:
19
- @inject
20
-
21
- def __init__(self,
22
- profile_repo: ProfileRepo,
23
- jwt_service: JWTService):
24
- self.profile_repo = profile_repo
25
- self.jwt_service = jwt_service
26
-
27
- def verify(self, company_short_name: str, body_external_user_id: str = None) -> dict:
28
- # authentication is in this orden: JWT, API Key, Sesión
29
- local_user_id = None
30
- company_id = None
31
- auth_method = None
32
- external_user_id = None # for JWT or API Key
33
-
34
- # 1. try auth via JWT
35
- jwt_company_id, jwt_external_user_id, jwt_error_info = self._authenticate_via_chat_jwt(company_short_name)
36
-
37
- if jwt_company_id is not None and jwt_external_user_id is not None:
38
- auth_method = "JWT"
39
- company_id = jwt_company_id
40
- external_user_id = jwt_external_user_id
41
- local_user_id = 0
42
- elif jwt_error_info is not None:
43
- # explicit error in JWT (inválido, expirado, etc.)
44
- logging.warning(f"Fallo de autenticación JWT: {jwt_error_info}")
45
- return {"error_message": "Fallo de autenticación JWT"}
46
- else:
47
- # 2. JWT not apply, try by API Key
48
- api_key_company_id, api_key_error_info = self._authenticate_via_api_key(company_short_name)
49
-
50
- if api_key_company_id is not None:
51
- auth_method = "API Key"
52
- company_id = api_key_company_id
53
- external_user_id = body_external_user_id # API Key usa external_user_id del body
54
- local_user_id = 0
55
- elif api_key_error_info is not None:
56
- # explicit error in API Key (inválida, incorrecta, error interno)
57
- logging.warning(f"Fallo de autenticación API Key: {api_key_error_info}")
58
- return {"error_message": "Fallo de autenticación API Key"}
59
- else:
60
- # 3. no JWT and API Key auth, try by Session
61
- self.check_if_user_is_logged_in(company_short_name) # raise exception or redirect if not logged in
62
-
63
- # In case not logged in check_if_user_is_logged_in redirects to login page
64
- auth_method = "Session"
65
- local_user_id = SessionManager.get('user_id')
66
- company_id = SessionManager.get('company_id')
67
- external_user_id = ""
68
-
69
- if not company_id or not local_user_id:
70
- logging.error(
71
- f"Sesión válida para {company_short_name} pero falta company_id o user_id en SessionManager.")
72
- return {"error_message": "Fallo interno en la autenticación o no autenticado"}
73
-
74
- # last verification of authentication
75
- if company_id is None or auth_method is None or local_user_id is None:
76
- # this condition should never happen,
77
- logging.error(
78
- f"Fallo inesperado en la lógica de autenticación para {company_short_name}. Ningún método tuvo éxito o devolvió error.")
79
- return {"error_message": "Fallo interno en la autenticación o no autenticado"}
80
-
81
- return {
82
- 'success': True,
83
- "auth_method": auth_method,
84
- "company_id": company_id,
85
- "auth_method": auth_method,
86
- "local_user_id": local_user_id,
87
- "external_user_id": external_user_id
88
- }
89
-
90
- def _authenticate_via_api_key(self, company_short_name_from_url: str):
91
- """
92
- try to authenticate using an API Key from the header 'Authorization'.
93
- Retorna (company_id, None) en éxito.
94
- Retorna (None, error_message) en fallo.
95
- """
96
- api_key_header = request.headers.get('Authorization')
97
- api_key_value = None
98
-
99
- # extract the key
100
- if api_key_header and api_key_header.startswith('Bearer '):
101
- api_key_value = api_key_header.split('Bearer ')[1]
102
- else:
103
- # there is no key in the headers expected
104
- return None, None
105
-
106
- # validate the api-key using ProfileRepo
107
- try:
108
- api_key_entry = self.profile_repo.get_active_api_key_entry(api_key_value)
109
- if not api_key_entry:
110
- logging.warning(f"Intento de acceso con API Key inválida o inactiva: {api_key_value[:5]}...")
111
- return None, "API Key inválida o inactiva"
112
-
113
- # check that the key belongs to the company
114
- # api_key_entry.company already loaded by joinedload
115
- if not api_key_entry.company or api_key_entry.company.short_name != company_short_name_from_url:
116
- return None, f"API Key no es válida para la compañía {company_short_name_from_url}"
117
-
118
- # successfull auth by API Key
119
- company_id = api_key_entry.company_id
120
-
121
- return company_id, None
122
-
123
- except Exception as e:
124
- logging.exception(f"Error interno durante validación de API Key: {e}")
125
- return None, "Error interno del servidor al validar API Key"
126
-
127
- def _authenticate_via_chat_jwt(self, company_short_name_from_url: str) -> tuple[
128
- Optional[int], Optional[str], Optional[str]]:
129
- """
130
- authenticate using an JWT chat session in the del header 'X-Chat-Token'.
131
- Return (company_id, external_user_id, None) on exit
132
- Returns (None, None, error_message) on fail.
133
- """
134
- chat_jwt = request.headers.get('X-Chat-Token')
135
- if not chat_jwt:
136
- return None, None, None
137
-
138
- # open the jwt token and retrieve the payload
139
- jwt_payload = self.jwt_service.validate_chat_jwt(chat_jwt, company_short_name_from_url)
140
- if not jwt_payload:
141
- # validation fails (token expired, incorrect signature, company , etc.)
142
- # validate_chat_jwt logs the specific failure
143
- return None, None, "Token de chat expirado, debes reingresar al chat"
144
-
145
- # JWT is validated: extract the company_id and external_user_id
146
- company_id = jwt_payload.get('company_id')
147
- external_user_id = jwt_payload.get('external_user_id')
148
-
149
- # Sanity check aditional, should never happen
150
- if not isinstance(company_id, int) or not external_user_id:
151
- logging.error(
152
- f"LLMQuery: JWT payload incompleto tras validación exitosa. CompanyID: {company_id}, UserID: {external_user_id}")
153
- return None, None, "Token de chat con formato interno incorrecto"
154
-
155
- return company_id, external_user_id, None
156
-
157
- def check_if_user_is_logged_in(self, company_short_name: str):
158
- if not SessionManager.get('user'):
159
- if company_short_name:
160
- return redirect(url_for('login', company_short_name=company_short_name))
161
- else:
162
- return redirect(url_for('home'))
163
-
164
- if company_short_name != SessionManager.get('company_short_name'):
165
- return redirect(url_for('login', company_short_name=company_short_name))
166
-
167
- # check session timeout
168
- if not self.check_session_timeout():
169
- SessionManager.clear()
170
- return redirect(url_for('login', company_short_name=company_short_name))
171
-
172
- # update last_activity
173
- SessionManager.set('last_activity', datetime.now(timezone.utc).timestamp())
174
-
175
- def check_session_timeout(self):
176
- # get last activity from session manager
177
- last_activity = SessionManager.get('last_activity')
178
- if not last_activity:
179
- return False
180
-
181
- # Tiempo actual en timestamp
182
- current_time = datetime.now(timezone.utc).timestamp()
183
-
184
- # get inactivity duration
185
- inactivity_duration = current_time - last_activity
186
-
187
- # verify if inactivity duration is greater than MAX_INACTIVITY_SECONDS
188
- if inactivity_duration > MAX_INACTIVITY_SECONDS:
189
- # close session
190
- return False
191
-
192
- # update last activity timestamp
193
- SessionManager.set('last_activity', current_time)
194
-
195
- return True # session is active
196
-
197
-
198
-
199
-
200
-
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -1,117 +0,0 @@
1
- $(document).ready(function () {
2
- // Evento para abrir el modal de historial
3
- $('#history-button').on('click', function() {
4
- loadHistory();
5
- $('#historyModal').modal('show');
6
- });
7
-
8
- // Variables globales para el historial
9
- let historyData = [];
10
-
11
- // Función para cargar el historial
12
- async function loadHistory() {
13
- const historyLoading = $('#history-loading');
14
- const historyError = $('#history-error');
15
- const historyContent = $('#history-content');
16
-
17
- // Mostrar loading
18
- historyLoading.show();
19
- historyError.hide();
20
- historyContent.hide();
21
-
22
- try {
23
- const data = {
24
- external_user_id: window.externalUserId
25
- };
26
-
27
- const responseData = await callLLMAPI("/history", data, "POST");
28
-
29
- if (responseData && responseData.history) {
30
- // Guardar datos globalmente
31
- historyData = responseData.history;
32
-
33
- // Mostrar todos los datos
34
- displayAllHistory();
35
-
36
- // Mostrar contenido
37
- historyContent.show();
38
- } else {
39
- throw new Error('No se recibieron datos del historial');
40
- }
41
- } catch (error) {
42
- console.error("Error al cargar historial:", error);
43
- const errorHtml = `
44
- <div class="alert alert-danger alert-dismissible show" role="alert">
45
- <strong>Error al cargar el historial:</strong> ${error.message}
46
- <button type="button" class="close" data-dismiss="alert">
47
- <span>&times;</span>
48
- </button>
49
- </div>
50
- `;
51
- historyError.html(errorHtml).show();
52
- } finally {
53
- historyLoading.hide();
54
- }
55
- }
56
-
57
- // Función para mostrar todo el historial
58
- function displayAllHistory() {
59
- const historyTableBody = $('#history-table-body');
60
-
61
- // Limpiar tabla
62
- historyTableBody.empty();
63
-
64
- // Filtrar solo consultas que son strings simples (no objetos JSON)
65
- const filteredHistory = historyData.filter(item => {
66
- try {
67
- // Intentar parsear como JSON
68
- const parsed = JSON.parse(item.query);
69
- // Si se puede parsear y es un objeto, filtrarlo
70
- return false;
71
- } catch (e) {
72
- // Si no se puede parsear, es un string simple, incluirlo
73
- return true;
74
- }
75
- });
76
-
77
- // Poblar tabla solo con las consultas filtradas
78
- filteredHistory.forEach((item, index) => {
79
- const row = $(`
80
- <tr>
81
- <td>${index + 1}</td>
82
- <td>${formatDate(item.created_at)}</td>
83
- <td class="query-cell" style="cursor: pointer;" title="Haz clic para copiar esta consulta al chat">${item.query}</td>
84
- </tr>
85
- `);
86
- historyTableBody.append(row);
87
- });
88
-
89
- // Agregar evento de clic a las celdas de consulta
90
- historyTableBody.on('click', '.query-cell', function() {
91
- const queryText = $(this).text();
92
-
93
- // Copiar el texto al textarea del chat
94
- $('#question').val(queryText);
95
-
96
- // Cerrar el modal
97
- $('#historyModal').modal('hide');
98
-
99
- // Hacer focus en el textarea para que el usuario pueda editar si lo desea
100
- $('#question').focus();
101
- });
102
- }
103
-
104
- // Función para formatear fecha
105
- function formatDate(dateString) {
106
- const date = new Date(dateString);
107
- return date.toLocaleDateString('es-CL', {
108
- day: '2-digit',
109
- month: '2-digit',
110
- year: 'numeric',
111
- hour: '2-digit',
112
- minute: '2-digit'
113
- });
114
- }
115
-
116
- });
117
-
@@ -1,201 +0,0 @@
1
- {% extends "base.html" %}
2
-
3
- {% block title %}Inicio - IAToolkit{% endblock %}
4
-
5
- {% block content %}
6
-
7
- <!-- Contenido principal con espaciado adicional respecto al header -->
8
- <div class="row flex-fill mt-5 flex-wrap">
9
-
10
- {% if not user or user_company != company_short_name %}
11
- <!-- Sección de login (se coloca primero en el HTML para mobile) -->
12
- <div class="col-12 col-lg-5 offset-lg-1">
13
- <div class="border rounded p-4 shadow-sm bg-light">
14
- <h4 class="text-muted fw-semibold text-start mb-3">login integrado (IAToolkit)</h4>
15
- <form id="login-form"
16
- action="{{ url_for('home', company_short_name=company_short_name) }}"
17
- method="post">
18
- <div class="mb-3">
19
- <label for="company_short_name" class="form-label d-block text-muted">Empresa</label>
20
- <select id="company_short_name" name="company_short_name" class="form-select" required>
21
- <option value="" disabled selected>Selecciona una empresa</option>
22
- {% for company in companies %}
23
- <option value="{{ company.short_name }}"
24
- {% if company.short_name == company_short_name %}selected{% endif %}>
25
- {{ company.short_name }}
26
- </option>
27
- {% endfor %}
28
- </select>
29
- </div>
30
-
31
- <div class="mb-3">
32
- <label for="email" class="form-label d-block text-muted">Correo Electrónico</label>
33
- <input type="email" id="email" name="email" class="form-control" required>
34
- </div>
35
- <div class="mb-3">
36
- <label for="password" class="form-label d-block text-muted">Contraseña</label>
37
- <input type="password" id="password" name="password" class="form-control" required>
38
- </div>
39
- <button type="submit" class="btn btn-primary w-100">
40
- Iniciar Sesión</button>
41
- </form>
42
-
43
- <div class="text-center mt-3">
44
- <a href="{% if company_short_name %}{{ url_for('signup', company_short_name=company_short_name) }}{% else %}#{% endif %}"
45
- id="signup-link"
46
- class="btn btn-outline-primary w-100">Registrarse</a>
47
- </div>
48
- <div class="text-center mt-3">
49
- <a href="{{ url_for('forgot_password', company_short_name=company_short_name) }}" class="text-decoration-none text-muted fw-semibold">
50
- ¿Olvidaste tu contraseña?
51
- </a>
52
- </div>
53
- </div>
54
- </div>
55
- {% endif %}
56
-
57
- <!-- Sección de JWT -->
58
- <div class="col-12 col-lg-5 offset-lg-1">
59
- <div class="border rounded p-4 shadow-sm bg-light">
60
- <h4 class="text-muted fw-semibold text-start mb-3">login externo (api-key)</h4>
61
- <form id="jwt-form" method="post">
62
- <div class="mb-3">
63
- <label for="company_name" class="form-label d-block text-muted">Empresa</label>
64
- <select id="company_name" name="company_short_name" class="form-select" required>
65
- <option value="" disabled selected>Selecciona una empresa</option>
66
- {% for company in companies %}
67
- {% if company.allow_jwt %}
68
- <option value="{{ company.short_name }}"> {{ company.short_name }}
69
- </option>
70
- {% endif %}
71
- {% endfor %}
72
- </select>
73
- </div>
74
-
75
- <div class="mb-3">
76
- <label for="external_user_id" class="form-label d-block text-muted">External user ID</label>
77
- <input type="text" id="external_user_id" name="external_user_id" class="form-control" required>
78
- </div>
79
-
80
- <button type="button"
81
- id="initiateJwtChatButton"
82
- class="ml-5 btn btn-primary">
83
- <span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
84
- Iniciar Sesión JWT
85
- </button>
86
- </form>
87
- </div>
88
- </div>
89
-
90
- </div>
91
-
92
- {% endblock %}
93
-
94
- {% block scripts %}
95
-
96
- <script>
97
- // Variables pasadas desde Flask (HomeView)
98
- const CHAT_TOKEN_REQUEST_URL = "{{ chat_token_request_url|safe }}";
99
- const PUBLIC_CHAT_URL_TEMPLATE = "{{ public_chat_url_template|safe }}";
100
- const API_KEY = "{{ api_key|safe }}";
101
-
102
- $(document).ready(function () {
103
- // Función para actualizar el enlace de "Registrarse" y el action del formulario "Iniciar Sesión"
104
- function updateLinksAndForm() {
105
- const selectedCompany = $('#company_short_name').val(); // Obtenemos el valor del select
106
-
107
- // Actualizar enlace "Registrarse"
108
- if (selectedCompany && selectedCompany.trim() !== '') {
109
- const signupUrl = '/' + selectedCompany + '/signup';
110
- $('#signup-link').attr('href', signupUrl); // Actualizamos el href del botón "Registrarse"
111
- } else {
112
- $('#signup-link').attr('href', '#'); // Enlace a "#" si no hay empresa seleccionada
113
- }
114
-
115
- // Actualizar action del formulario "Iniciar Sesión"
116
- if (selectedCompany && selectedCompany.trim() !== '') {
117
- const loginAction = '/' + selectedCompany + '/login';
118
- $('#login-form').attr('action', loginAction); // Actualizamos la URL del form
119
- } else {
120
- $('#login-form').attr('action', '#'); // URL genérica si no hay selección
121
- }
122
- }
123
-
124
- // Actualizamos al cargar la página
125
- updateLinksAndForm();
126
-
127
- // Escuchamos el evento de cambio en el dropdown para actualizar dinámicamente
128
- $('#company_short_name').on('change', function () {
129
- updateLinksAndForm();
130
- });
131
-
132
- // Interceptamos el click en "Registrarse"
133
- $('#signup-link').on('click', function (e) {
134
- const selectedCompany = $('#company_short_name').val();
135
-
136
- if (!selectedCompany || selectedCompany.trim() === '') {
137
- e.preventDefault(); // evitar navegación al #
138
- Swal.fire({
139
- icon: 'warning',
140
- title: 'Empresa no seleccionada',
141
- text: 'Por favor, selecciona una empresa antes de registrarte.'
142
- });
143
- }
144
- });
145
-
146
- // Event listener para el botón de "Abrir Chat (JWT)"
147
- $('#initiateJwtChatButton').on('click', function() {
148
-
149
- const selectedCompany = $('#company_name').val();
150
- const externalUserId = $('#external_user_id').val();
151
-
152
- if (!selectedCompany || !externalUserId.trim()) {
153
- Swal.fire({ icon: 'warning', title: 'Campos Requeridos', text: 'Por favor, selecciona una empresa e ingresa un ID de usuario.' });
154
- return;
155
- }
156
- if (!API_KEY || API_KEY.includes("defecto")) {
157
- Swal.fire({ icon: 'error', title: 'Error de Configuración', text: 'La API Key de la aplicación no está disponible.' });
158
- return;
159
- }
160
-
161
- const $button = $(this);
162
- const $spinner = $button.find('.spinner-border');
163
- $button.prop('disabled', true);
164
- $spinner.removeClass('d-none');
165
-
166
- fetch(`/${selectedCompany}/chat_login`, {
167
- method: 'POST',
168
- headers: {
169
- 'Content-Type': 'application/json',
170
- 'Authorization': `Bearer ${API_KEY}`
171
- },
172
- body: JSON.stringify({
173
- external_user_id: externalUserId
174
- })
175
- })
176
- .then(async response => {
177
- if (response.ok) { // Si el status es 200, la respuesta es el HTML
178
- return response.text();
179
- } else { // Si hay un error, el cuerpo es JSON
180
- const errorData = await response.json();
181
- throw new Error(errorData.error || 'Ocurrió un error desconocido.');
182
- }
183
- })
184
- .then(htmlContent => {
185
- // Éxito: Abrimos el HTML que nos devolvió el servidor en una nueva pestaña.
186
- const newTab = window.open();
187
- newTab.document.write(htmlContent);
188
- newTab.document.close();
189
- })
190
- .catch(error => {
191
- Swal.fire({ icon: 'error', title: 'Error de Inicio de Sesión', text: error.message });
192
- })
193
- .finally(() => {
194
- $button.prop('disabled', false);
195
- $spinner.addClass('d-none');
196
- });
197
-
198
- });
199
- });
200
- </script>
201
- {% endblock %}