hazo_collab_forms 5.7.0 → 6.0.0

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.
Files changed (362) hide show
  1. package/CHANGE_LOG.md +115 -0
  2. package/README.md +203 -111
  3. package/dist/audit/built_in_actions.d.ts +6 -0
  4. package/dist/audit/built_in_actions.d.ts.map +1 -0
  5. package/dist/audit/built_in_actions.js +23 -0
  6. package/dist/audit/built_in_actions.js.map +1 -0
  7. package/dist/audit/intent_emitter.d.ts +4 -0
  8. package/dist/audit/intent_emitter.d.ts.map +1 -0
  9. package/dist/audit/intent_emitter.js +25 -0
  10. package/dist/audit/intent_emitter.js.map +1 -0
  11. package/dist/audit/lazy_audit_icon.d.ts +8 -0
  12. package/dist/audit/lazy_audit_icon.d.ts.map +1 -0
  13. package/dist/audit/lazy_audit_icon.js +20 -0
  14. package/dist/audit/lazy_audit_icon.js.map +1 -0
  15. package/dist/components/_internal_form_set.d.ts +6 -0
  16. package/dist/components/_internal_form_set.d.ts.map +1 -1
  17. package/dist/components/_internal_form_set.js +48 -51
  18. package/dist/components/_internal_form_set.js.map +1 -1
  19. package/dist/components/clarification/clarification_item_body.d.ts +1 -1
  20. package/dist/components/clarification/clarification_item_body.d.ts.map +1 -1
  21. package/dist/components/clarification/resolution_status_strip.d.ts +1 -1
  22. package/dist/components/clarification/resolution_status_strip.d.ts.map +1 -1
  23. package/dist/components/field_audit/auditor.d.ts +30 -0
  24. package/dist/components/field_audit/auditor.d.ts.map +1 -0
  25. package/dist/components/field_audit/auditor.js +91 -0
  26. package/dist/components/field_audit/auditor.js.map +1 -0
  27. package/dist/components/field_audit/context.d.ts +29 -0
  28. package/dist/components/field_audit/context.d.ts.map +1 -0
  29. package/dist/components/field_audit/context.js +123 -0
  30. package/dist/components/field_audit/context.js.map +1 -0
  31. package/dist/components/field_audit/field_audit_icon.d.ts +12 -0
  32. package/dist/components/field_audit/field_audit_icon.d.ts.map +1 -0
  33. package/dist/components/field_audit/field_audit_icon.js +23 -0
  34. package/dist/components/field_audit/field_audit_icon.js.map +1 -0
  35. package/dist/components/field_audit/field_audit_panel.d.ts +9 -0
  36. package/dist/components/field_audit/field_audit_panel.d.ts.map +1 -0
  37. package/dist/components/field_audit/field_audit_panel.js +54 -0
  38. package/dist/components/field_audit/field_audit_panel.js.map +1 -0
  39. package/dist/components/field_audit/index.d.ts +33 -0
  40. package/dist/components/field_audit/index.d.ts.map +1 -0
  41. package/dist/components/field_audit/index.js +29 -0
  42. package/dist/components/field_audit/index.js.map +1 -0
  43. package/dist/components/field_audit/types.d.ts +75 -0
  44. package/dist/components/field_audit/types.d.ts.map +1 -0
  45. package/dist/components/field_audit/types.js +10 -0
  46. package/dist/components/field_audit/types.js.map +1 -0
  47. package/dist/components/field_audit/with_field_audit.d.ts +32 -0
  48. package/dist/components/field_audit/with_field_audit.d.ts.map +1 -0
  49. package/dist/components/field_audit/with_field_audit.js +42 -0
  50. package/dist/components/field_audit/with_field_audit.js.map +1 -0
  51. package/dist/components/hazo_collab_form_checkbox.d.ts.map +1 -1
  52. package/dist/components/hazo_collab_form_checkbox.js +3 -1
  53. package/dist/components/hazo_collab_form_checkbox.js.map +1 -1
  54. package/dist/components/hazo_collab_form_doc.d.ts.map +1 -1
  55. package/dist/components/hazo_collab_form_doc.js +4 -1
  56. package/dist/components/hazo_collab_form_doc.js.map +1 -1
  57. package/dist/components/hazo_collab_form_radio.d.ts.map +1 -1
  58. package/dist/components/hazo_collab_form_radio.js +4 -2
  59. package/dist/components/hazo_collab_form_radio.js.map +1 -1
  60. package/dist/components/hazo_collab_form_view/context.d.ts +7 -0
  61. package/dist/components/hazo_collab_form_view/context.d.ts.map +1 -1
  62. package/dist/components/hazo_collab_form_view/context.js +46 -0
  63. package/dist/components/hazo_collab_form_view/context.js.map +1 -1
  64. package/dist/components/hazo_collab_form_view/hooks/use_view_callbacks.d.ts +8 -1
  65. package/dist/components/hazo_collab_form_view/hooks/use_view_callbacks.d.ts.map +1 -1
  66. package/dist/components/hazo_collab_form_view/hooks/use_view_callbacks.js +4 -2
  67. package/dist/components/hazo_collab_form_view/hooks/use_view_callbacks.js.map +1 -1
  68. package/dist/components/hazo_collab_form_view/index.d.ts +1 -1
  69. package/dist/components/hazo_collab_form_view/index.d.ts.map +1 -1
  70. package/dist/components/hazo_collab_form_view/index.js +59 -3
  71. package/dist/components/hazo_collab_form_view/index.js.map +1 -1
  72. package/dist/components/hazo_collab_form_view/types.d.ts +134 -0
  73. package/dist/components/hazo_collab_form_view/types.d.ts.map +1 -1
  74. package/dist/components/hazo_collab_form_view/views/approval_view.d.ts.map +1 -1
  75. package/dist/components/hazo_collab_form_view/views/approval_view.js +3 -1
  76. package/dist/components/hazo_collab_form_view/views/approval_view.js.map +1 -1
  77. package/dist/components/hazo_collab_form_view/views/edit_view.d.ts.map +1 -1
  78. package/dist/components/hazo_collab_form_view/views/edit_view.js +8 -3
  79. package/dist/components/hazo_collab_form_view/views/edit_view.js.map +1 -1
  80. package/dist/components/hazo_collab_form_view/views/print_view.d.ts.map +1 -1
  81. package/dist/components/hazo_collab_form_view/views/print_view.js +3 -1
  82. package/dist/components/hazo_collab_form_view/views/print_view.js.map +1 -1
  83. package/dist/components/hazo_collab_form_view/views/summary_view.d.ts.map +1 -1
  84. package/dist/components/hazo_collab_form_view/views/summary_view.js +4 -2
  85. package/dist/components/hazo_collab_form_view/views/summary_view.js.map +1 -1
  86. package/dist/components/hazo_data_form/group_renderer.d.ts +8 -2
  87. package/dist/components/hazo_data_form/group_renderer.d.ts.map +1 -1
  88. package/dist/components/hazo_data_form/group_renderer.js +3 -3
  89. package/dist/components/hazo_data_form/group_renderer.js.map +1 -1
  90. package/dist/components/hazo_data_form/hazo_data_form.d.ts +2 -1
  91. package/dist/components/hazo_data_form/hazo_data_form.d.ts.map +1 -1
  92. package/dist/components/hazo_data_form/hazo_data_form.js +47 -6
  93. package/dist/components/hazo_data_form/hazo_data_form.js.map +1 -1
  94. package/dist/components/hazo_data_form/section_renderer.d.ts +4 -2
  95. package/dist/components/hazo_data_form/section_renderer.d.ts.map +1 -1
  96. package/dist/components/hazo_data_form/section_renderer.js +2 -2
  97. package/dist/components/hazo_data_form/section_renderer.js.map +1 -1
  98. package/dist/components/hazo_data_form/shared/data_form_field_layout.d.ts +4 -1
  99. package/dist/components/hazo_data_form/shared/data_form_field_layout.d.ts.map +1 -1
  100. package/dist/components/hazo_data_form/shared/data_form_field_layout.js +34 -8
  101. package/dist/components/hazo_data_form/shared/data_form_field_layout.js.map +1 -1
  102. package/dist/components/hazo_data_form/types.d.ts +56 -1
  103. package/dist/components/hazo_data_form/types.d.ts.map +1 -1
  104. package/dist/components/index.d.ts +5 -5
  105. package/dist/components/index.d.ts.map +1 -1
  106. package/dist/components/index.js +4 -4
  107. package/dist/components/index.js.map +1 -1
  108. package/dist/components/shared/base_field_layout.d.ts.map +1 -1
  109. package/dist/components/shared/base_field_layout.js +5 -1
  110. package/dist/components/shared/base_field_layout.js.map +1 -1
  111. package/dist/components/shared/field_action_array_slot.d.ts +10 -0
  112. package/dist/components/shared/field_action_array_slot.d.ts.map +1 -0
  113. package/dist/components/shared/field_action_array_slot.js +33 -0
  114. package/dist/components/shared/field_action_array_slot.js.map +1 -0
  115. package/dist/components/shared/field_action_slot.d.ts +22 -0
  116. package/dist/components/shared/field_action_slot.d.ts.map +1 -0
  117. package/dist/components/shared/field_action_slot.js +20 -0
  118. package/dist/components/shared/field_action_slot.js.map +1 -0
  119. package/dist/components/shared/ihelp_icon.d.ts +1 -1
  120. package/dist/components/shared/ihelp_icon.js +1 -1
  121. package/dist/components/shared/rule_result_card.js +1 -1
  122. package/dist/components/shared/rule_result_card.js.map +1 -1
  123. package/dist/components/shared/use_field_action_slot.d.ts +37 -0
  124. package/dist/components/shared/use_field_action_slot.d.ts.map +1 -0
  125. package/dist/components/shared/use_field_action_slot.js +77 -0
  126. package/dist/components/shared/use_field_action_slot.js.map +1 -0
  127. package/dist/components/thread_form/components/key_info_drawer.d.ts +7 -1
  128. package/dist/components/thread_form/components/key_info_drawer.d.ts.map +1 -1
  129. package/dist/components/thread_form/components/key_info_drawer.js +2 -2
  130. package/dist/components/thread_form/components/key_info_drawer.js.map +1 -1
  131. package/dist/components/thread_form/components/send_back_message.d.ts +1 -1
  132. package/dist/components/thread_form/components/send_back_message.d.ts.map +1 -1
  133. package/dist/components/thread_form/hooks/use_file_pipeline.d.ts +1 -1
  134. package/dist/components/thread_form/hooks/use_file_pipeline.d.ts.map +1 -1
  135. package/dist/components/thread_form/hooks/use_file_pipeline.js +1 -1
  136. package/dist/components/thread_form/hooks/use_file_pipeline.js.map +1 -1
  137. package/dist/components/thread_form/index.d.ts +1 -1
  138. package/dist/components/thread_form/index.d.ts.map +1 -1
  139. package/dist/components/thread_form/index.js.map +1 -1
  140. package/dist/components/thread_form/thread_form.d.ts.map +1 -1
  141. package/dist/components/thread_form/thread_form.js +3 -3
  142. package/dist/components/thread_form/thread_form.js.map +1 -1
  143. package/dist/components/thread_form/types.d.ts +32 -4
  144. package/dist/components/thread_form/types.d.ts.map +1 -1
  145. package/dist/components/thread_form/types.js.map +1 -1
  146. package/dist/lib/index.d.ts +0 -2
  147. package/dist/lib/index.d.ts.map +1 -1
  148. package/dist/lib/index.js +0 -2
  149. package/dist/lib/index.js.map +1 -1
  150. package/dist/lib/resolution_handler.d.ts +1 -1
  151. package/dist/lib/resolution_handler.d.ts.map +1 -1
  152. package/dist/lib/resolve_variable.d.ts +1 -1
  153. package/dist/lib/resolve_variable.d.ts.map +1 -1
  154. package/dist/types/clarification.d.ts +1 -1
  155. package/dist/types/clarification.d.ts.map +1 -1
  156. package/dist/types/field_action.d.ts +25 -0
  157. package/dist/types/field_action.d.ts.map +1 -0
  158. package/dist/types/field_action.js +8 -0
  159. package/dist/types/field_action.js.map +1 -0
  160. package/dist/types/index.d.ts +3 -6
  161. package/dist/types/index.d.ts.map +1 -1
  162. package/dist/types/index.js +1 -3
  163. package/dist/types/index.js.map +1 -1
  164. package/dist/types/{fb_form_data.d.ts → shared_data.d.ts} +1 -3
  165. package/dist/types/shared_data.d.ts.map +1 -0
  166. package/dist/types/{fb_form_data.js → shared_data.js} +1 -2
  167. package/dist/types/shared_data.js.map +1 -0
  168. package/dist/utils/dev_file_manager.d.ts +1 -1
  169. package/dist/utils/dev_file_manager.js +1 -1
  170. package/dist/{components/hazo_fb_form/shared → utils}/format.d.ts +2 -2
  171. package/dist/utils/format.d.ts.map +1 -0
  172. package/dist/{components/hazo_fb_form/shared → utils}/format.js +1 -1
  173. package/dist/utils/format.js.map +1 -0
  174. package/dist/utils/index.d.ts +1 -9
  175. package/dist/utils/index.d.ts.map +1 -1
  176. package/dist/utils/index.js +3 -15
  177. package/dist/utils/index.js.map +1 -1
  178. package/package.json +6 -1
  179. package/dist/components/hazo_fb_form/components/backoffice_run_button.d.ts +0 -18
  180. package/dist/components/hazo_fb_form/components/backoffice_run_button.d.ts.map +0 -1
  181. package/dist/components/hazo_fb_form/components/backoffice_run_button.js +0 -23
  182. package/dist/components/hazo_fb_form/components/backoffice_run_button.js.map +0 -1
  183. package/dist/components/hazo_fb_form/components/draft_clarification_card.d.ts +0 -39
  184. package/dist/components/hazo_fb_form/components/draft_clarification_card.d.ts.map +0 -1
  185. package/dist/components/hazo_fb_form/components/draft_clarification_card.js +0 -94
  186. package/dist/components/hazo_fb_form/components/draft_clarification_card.js.map +0 -1
  187. package/dist/components/hazo_fb_form/components/fb_document_type_editor.d.ts +0 -11
  188. package/dist/components/hazo_fb_form/components/fb_document_type_editor.d.ts.map +0 -1
  189. package/dist/components/hazo_fb_form/components/fb_document_type_editor.js +0 -82
  190. package/dist/components/hazo_fb_form/components/fb_document_type_editor.js.map +0 -1
  191. package/dist/components/hazo_fb_form/components/fb_tag_editor.d.ts +0 -11
  192. package/dist/components/hazo_fb_form/components/fb_tag_editor.d.ts.map +0 -1
  193. package/dist/components/hazo_fb_form/components/fb_tag_editor.js +0 -107
  194. package/dist/components/hazo_fb_form/components/fb_tag_editor.js.map +0 -1
  195. package/dist/components/hazo_fb_form/components/front_office_stepper.d.ts +0 -15
  196. package/dist/components/hazo_fb_form/components/front_office_stepper.d.ts.map +0 -1
  197. package/dist/components/hazo_fb_form/components/front_office_stepper.js +0 -21
  198. package/dist/components/hazo_fb_form/components/front_office_stepper.js.map +0 -1
  199. package/dist/components/hazo_fb_form/components/instance_sidebar.d.ts +0 -21
  200. package/dist/components/hazo_fb_form/components/instance_sidebar.d.ts.map +0 -1
  201. package/dist/components/hazo_fb_form/components/instance_sidebar.js +0 -58
  202. package/dist/components/hazo_fb_form/components/instance_sidebar.js.map +0 -1
  203. package/dist/components/hazo_fb_form/components/reject_clarification_dialog.d.ts +0 -15
  204. package/dist/components/hazo_fb_form/components/reject_clarification_dialog.d.ts.map +0 -1
  205. package/dist/components/hazo_fb_form/components/reject_clarification_dialog.js +0 -26
  206. package/dist/components/hazo_fb_form/components/reject_clarification_dialog.js.map +0 -1
  207. package/dist/components/hazo_fb_form/components/run_button.d.ts +0 -19
  208. package/dist/components/hazo_fb_form/components/run_button.d.ts.map +0 -1
  209. package/dist/components/hazo_fb_form/components/run_button.js +0 -38
  210. package/dist/components/hazo_fb_form/components/run_button.js.map +0 -1
  211. package/dist/components/hazo_fb_form/components/run_details_dialog.d.ts +0 -17
  212. package/dist/components/hazo_fb_form/components/run_details_dialog.d.ts.map +0 -1
  213. package/dist/components/hazo_fb_form/components/run_details_dialog.js +0 -35
  214. package/dist/components/hazo_fb_form/components/run_details_dialog.js.map +0 -1
  215. package/dist/components/hazo_fb_form/components/sent_clarification_group.d.ts +0 -30
  216. package/dist/components/hazo_fb_form/components/sent_clarification_group.d.ts.map +0 -1
  217. package/dist/components/hazo_fb_form/components/sent_clarification_group.js +0 -76
  218. package/dist/components/hazo_fb_form/components/sent_clarification_group.js.map +0 -1
  219. package/dist/components/hazo_fb_form/components/tag_pill.d.ts +0 -15
  220. package/dist/components/hazo_fb_form/components/tag_pill.d.ts.map +0 -1
  221. package/dist/components/hazo_fb_form/components/tag_pill.js +0 -15
  222. package/dist/components/hazo_fb_form/components/tag_pill.js.map +0 -1
  223. package/dist/components/hazo_fb_form/context.d.ts +0 -135
  224. package/dist/components/hazo_fb_form/context.d.ts.map +0 -1
  225. package/dist/components/hazo_fb_form/context.js +0 -13
  226. package/dist/components/hazo_fb_form/context.js.map +0 -1
  227. package/dist/components/hazo_fb_form/hazo_fb_form.d.ts +0 -13
  228. package/dist/components/hazo_fb_form/hazo_fb_form.d.ts.map +0 -1
  229. package/dist/components/hazo_fb_form/hazo_fb_form.js +0 -1188
  230. package/dist/components/hazo_fb_form/hazo_fb_form.js.map +0 -1
  231. package/dist/components/hazo_fb_form/hooks/use_fb_form_state.d.ts +0 -58
  232. package/dist/components/hazo_fb_form/hooks/use_fb_form_state.d.ts.map +0 -1
  233. package/dist/components/hazo_fb_form/hooks/use_fb_form_state.js +0 -919
  234. package/dist/components/hazo_fb_form/hooks/use_fb_form_state.js.map +0 -1
  235. package/dist/components/hazo_fb_form/hooks/use_llm_run.d.ts +0 -52
  236. package/dist/components/hazo_fb_form/hooks/use_llm_run.d.ts.map +0 -1
  237. package/dist/components/hazo_fb_form/hooks/use_llm_run.js +0 -1863
  238. package/dist/components/hazo_fb_form/hooks/use_llm_run.js.map +0 -1
  239. package/dist/components/hazo_fb_form/index.d.ts +0 -29
  240. package/dist/components/hazo_fb_form/index.d.ts.map +0 -1
  241. package/dist/components/hazo_fb_form/index.js +0 -19
  242. package/dist/components/hazo_fb_form/index.js.map +0 -1
  243. package/dist/components/hazo_fb_form/shared/agent_stepper.d.ts +0 -9
  244. package/dist/components/hazo_fb_form/shared/agent_stepper.d.ts.map +0 -1
  245. package/dist/components/hazo_fb_form/shared/agent_stepper.js +0 -17
  246. package/dist/components/hazo_fb_form/shared/agent_stepper.js.map +0 -1
  247. package/dist/components/hazo_fb_form/shared/clarification_helpers.d.ts +0 -15
  248. package/dist/components/hazo_fb_form/shared/clarification_helpers.d.ts.map +0 -1
  249. package/dist/components/hazo_fb_form/shared/clarification_helpers.js +0 -23
  250. package/dist/components/hazo_fb_form/shared/clarification_helpers.js.map +0 -1
  251. package/dist/components/hazo_fb_form/shared/file_status_accordion.d.ts +0 -9
  252. package/dist/components/hazo_fb_form/shared/file_status_accordion.d.ts.map +0 -1
  253. package/dist/components/hazo_fb_form/shared/file_status_accordion.js +0 -39
  254. package/dist/components/hazo_fb_form/shared/file_status_accordion.js.map +0 -1
  255. package/dist/components/hazo_fb_form/shared/file_utils.d.ts +0 -9
  256. package/dist/components/hazo_fb_form/shared/file_utils.d.ts.map +0 -1
  257. package/dist/components/hazo_fb_form/shared/file_utils.js +0 -31
  258. package/dist/components/hazo_fb_form/shared/file_utils.js.map +0 -1
  259. package/dist/components/hazo_fb_form/shared/format.d.ts.map +0 -1
  260. package/dist/components/hazo_fb_form/shared/format.js.map +0 -1
  261. package/dist/components/hazo_fb_form/shared/group_debug_icon.d.ts +0 -15
  262. package/dist/components/hazo_fb_form/shared/group_debug_icon.d.ts.map +0 -1
  263. package/dist/components/hazo_fb_form/shared/group_debug_icon.js +0 -48
  264. package/dist/components/hazo_fb_form/shared/group_debug_icon.js.map +0 -1
  265. package/dist/components/hazo_fb_form/shared/index.d.ts +0 -10
  266. package/dist/components/hazo_fb_form/shared/index.d.ts.map +0 -1
  267. package/dist/components/hazo_fb_form/shared/index.js +0 -9
  268. package/dist/components/hazo_fb_form/shared/index.js.map +0 -1
  269. package/dist/components/hazo_fb_form/shared/pdf_side_panel.d.ts +0 -22
  270. package/dist/components/hazo_fb_form/shared/pdf_side_panel.d.ts.map +0 -1
  271. package/dist/components/hazo_fb_form/shared/pdf_side_panel.js +0 -10
  272. package/dist/components/hazo_fb_form/shared/pdf_side_panel.js.map +0 -1
  273. package/dist/components/hazo_fb_form/shared/send_back_item_card.d.ts +0 -44
  274. package/dist/components/hazo_fb_form/shared/send_back_item_card.d.ts.map +0 -1
  275. package/dist/components/hazo_fb_form/shared/send_back_item_card.js +0 -80
  276. package/dist/components/hazo_fb_form/shared/send_back_item_card.js.map +0 -1
  277. package/dist/components/hazo_fb_form/shared/use_pdf_viewer.d.ts +0 -28
  278. package/dist/components/hazo_fb_form/shared/use_pdf_viewer.d.ts.map +0 -1
  279. package/dist/components/hazo_fb_form/shared/use_pdf_viewer.js +0 -46
  280. package/dist/components/hazo_fb_form/shared/use_pdf_viewer.js.map +0 -1
  281. package/dist/components/hazo_fb_form/types.d.ts +0 -372
  282. package/dist/components/hazo_fb_form/types.d.ts.map +0 -1
  283. package/dist/components/hazo_fb_form/types.js +0 -5
  284. package/dist/components/hazo_fb_form/types.js.map +0 -1
  285. package/dist/components/hazo_fb_form/views/back_office_view.d.ts +0 -7
  286. package/dist/components/hazo_fb_form/views/back_office_view.d.ts.map +0 -1
  287. package/dist/components/hazo_fb_form/views/back_office_view.js +0 -425
  288. package/dist/components/hazo_fb_form/views/back_office_view.js.map +0 -1
  289. package/dist/components/hazo_fb_form/views/clarifications_view.d.ts +0 -16
  290. package/dist/components/hazo_fb_form/views/clarifications_view.d.ts.map +0 -1
  291. package/dist/components/hazo_fb_form/views/clarifications_view.js +0 -291
  292. package/dist/components/hazo_fb_form/views/clarifications_view.js.map +0 -1
  293. package/dist/components/hazo_fb_form/views/client_data_view.d.ts +0 -6
  294. package/dist/components/hazo_fb_form/views/client_data_view.d.ts.map +0 -1
  295. package/dist/components/hazo_fb_form/views/client_data_view.js +0 -39
  296. package/dist/components/hazo_fb_form/views/client_data_view.js.map +0 -1
  297. package/dist/components/hazo_fb_form/views/front_office_view.d.ts +0 -6
  298. package/dist/components/hazo_fb_form/views/front_office_view.d.ts.map +0 -1
  299. package/dist/components/hazo_fb_form/views/front_office_view.js +0 -1351
  300. package/dist/components/hazo_fb_form/views/front_office_view.js.map +0 -1
  301. package/dist/components/hazo_fb_form/views/interim_view.d.ts +0 -8
  302. package/dist/components/hazo_fb_form/views/interim_view.d.ts.map +0 -1
  303. package/dist/components/hazo_fb_form/views/interim_view.js +0 -535
  304. package/dist/components/hazo_fb_form/views/interim_view.js.map +0 -1
  305. package/dist/components/hazo_fb_form/views/review_queue_view.d.ts +0 -14
  306. package/dist/components/hazo_fb_form/views/review_queue_view.d.ts.map +0 -1
  307. package/dist/components/hazo_fb_form/views/review_queue_view.js +0 -230
  308. package/dist/components/hazo_fb_form/views/review_queue_view.js.map +0 -1
  309. package/dist/components/hazo_fb_form/views/send_back_view.d.ts +0 -13
  310. package/dist/components/hazo_fb_form/views/send_back_view.d.ts.map +0 -1
  311. package/dist/components/hazo_fb_form/views/send_back_view.js +0 -258
  312. package/dist/components/hazo_fb_form/views/send_back_view.js.map +0 -1
  313. package/dist/components/hazo_fb_form/views/summary_review_view.d.ts +0 -17
  314. package/dist/components/hazo_fb_form/views/summary_review_view.d.ts.map +0 -1
  315. package/dist/components/hazo_fb_form/views/summary_review_view.js +0 -258
  316. package/dist/components/hazo_fb_form/views/summary_review_view.js.map +0 -1
  317. package/dist/components/shared/json_data_panel/index.d.ts +0 -3
  318. package/dist/components/shared/json_data_panel/index.d.ts.map +0 -1
  319. package/dist/components/shared/json_data_panel/index.js +0 -2
  320. package/dist/components/shared/json_data_panel/index.js.map +0 -1
  321. package/dist/components/shared/json_data_panel/json_data_panel.d.ts +0 -28
  322. package/dist/components/shared/json_data_panel/json_data_panel.d.ts.map +0 -1
  323. package/dist/components/shared/json_data_panel/json_data_panel.js +0 -156
  324. package/dist/components/shared/json_data_panel/json_data_panel.js.map +0 -1
  325. package/dist/lib/fb_form_handler.d.ts +0 -63
  326. package/dist/lib/fb_form_handler.d.ts.map +0 -1
  327. package/dist/lib/fb_form_handler.js +0 -425
  328. package/dist/lib/fb_form_handler.js.map +0 -1
  329. package/dist/types/fb_form_data.d.ts.map +0 -1
  330. package/dist/types/fb_form_data.js.map +0 -1
  331. package/dist/types/fb_form_data_v1.d.ts +0 -250
  332. package/dist/types/fb_form_data_v1.d.ts.map +0 -1
  333. package/dist/types/fb_form_data_v1.js +0 -117
  334. package/dist/types/fb_form_data_v1.js.map +0 -1
  335. package/dist/types/fb_form_instance.d.ts +0 -49
  336. package/dist/types/fb_form_instance.d.ts.map +0 -1
  337. package/dist/types/fb_form_instance.js +0 -10
  338. package/dist/types/fb_form_instance.js.map +0 -1
  339. package/dist/utils/fb_data_adapter.d.ts +0 -33
  340. package/dist/utils/fb_data_adapter.d.ts.map +0 -1
  341. package/dist/utils/fb_data_adapter.js +0 -436
  342. package/dist/utils/fb_data_adapter.js.map +0 -1
  343. package/dist/utils/fb_data_adapter_v2.d.ts +0 -17
  344. package/dist/utils/fb_data_adapter_v2.d.ts.map +0 -1
  345. package/dist/utils/fb_data_adapter_v2.js +0 -483
  346. package/dist/utils/fb_data_adapter_v2.js.map +0 -1
  347. package/dist/utils/fb_data_helpers.d.ts +0 -86
  348. package/dist/utils/fb_data_helpers.d.ts.map +0 -1
  349. package/dist/utils/fb_data_helpers.js +0 -269
  350. package/dist/utils/fb_data_helpers.js.map +0 -1
  351. package/dist/utils/fb_data_mutations.d.ts +0 -43
  352. package/dist/utils/fb_data_mutations.d.ts.map +0 -1
  353. package/dist/utils/fb_data_mutations.js +0 -379
  354. package/dist/utils/fb_data_mutations.js.map +0 -1
  355. package/dist/utils/fb_data_mutations_v2.d.ts +0 -46
  356. package/dist/utils/fb_data_mutations_v2.d.ts.map +0 -1
  357. package/dist/utils/fb_data_mutations_v2.js +0 -341
  358. package/dist/utils/fb_data_mutations_v2.js.map +0 -1
  359. package/dist/utils/fb_data_queries.d.ts +0 -81
  360. package/dist/utils/fb_data_queries.d.ts.map +0 -1
  361. package/dist/utils/fb_data_queries.js +0 -354
  362. package/dist/utils/fb_data_queries.js.map +0 -1
@@ -1,1863 +0,0 @@
1
- /**
2
- * LLM "Run" pipeline hook - handles per-file classification + file routing to back-office.
3
- *
4
- * After classification, files are routed to matching back-office groups by tag_id,
5
- * then autofill is triggered automatically on each group.
6
- */
7
- 'use client';
8
- import { useCallback, useRef } from 'react';
9
- import { use_logger } from '../../../logger/index.js';
10
- import { add_data_entry } from '../../../utils/fb_data_mutations.js';
11
- import { add_autofill_run } from '../../../utils/fb_data_mutations_v2.js';
12
- import { get_autofilled_file_groups } from '../../../utils/fb_data_queries.js';
13
- import { next_ie_id, next_pd_id, next_act_id } from '../../../types/fb_form_data_v1.js';
14
- import { rule_to_fb_execution } from '../../../utils/rule_to_execution.js';
15
- /** Extract file info from a FileTextboxValue */
16
- function get_files_from_value(value) {
17
- if (!Array.isArray(value))
18
- return [];
19
- return value
20
- .filter((b) => b.type === 'file' && b.attachment?.file_id)
21
- .map((b) => ({ file_id: b.attachment.file_id, file_name: b.attachment.file_name, attachment: b.attachment }));
22
- }
23
- /** Infer MIME type from file extension */
24
- function mime_from_name(name) {
25
- const ext = name.split('.').pop()?.toLowerCase();
26
- switch (ext) {
27
- case 'pdf': return 'application/pdf';
28
- case 'png': return 'image/png';
29
- case 'jpg':
30
- case 'jpeg': return 'image/jpeg';
31
- case 'gif': return 'image/gif';
32
- case 'webp': return 'image/webp';
33
- case 'svg': return 'image/svg+xml';
34
- case 'csv': return 'text/csv';
35
- case 'txt': return 'text/plain';
36
- case 'xlsx': return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
37
- case 'xls': return 'application/vnd.ms-excel';
38
- case 'docx': return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
39
- case 'doc': return 'application/msword';
40
- default: return 'application/octet-stream';
41
- }
42
- }
43
- /** Extract text content from a FileTextboxValue */
44
- function get_text_from_value(value) {
45
- if (typeof value === 'string')
46
- return value;
47
- if (!Array.isArray(value))
48
- return '';
49
- return value
50
- .filter((b) => b.type === 'text' && b.content)
51
- .map((b) => b.content)
52
- .join('\n');
53
- }
54
- /** Build a map of tag_id -> group configs from back_sections */
55
- function build_tag_group_map(back_sections) {
56
- const map = new Map();
57
- for (const section of back_sections) {
58
- for (const group of section.groups ?? []) {
59
- const tag_id = group.tag_id;
60
- if (tag_id) {
61
- const existing = map.get(tag_id) ?? [];
62
- existing.push(group);
63
- map.set(tag_id, existing);
64
- }
65
- }
66
- }
67
- return map;
68
- }
69
- /** Route classified files to back-office groups and trigger autofill */
70
- async function route_files_to_back_office(options) {
71
- const { classifications, back_sections, front_form_data, on_back_change, back_form_data, autofill_api_endpoint, file_manager, update_progress, set_group_autofill_log, run_log, tracker, on_autofill_file, on_queue_files, force_autofill, on_autofill_activity, autofilled_file_groups, on_autofill_run } = options;
72
- const tag_group_map = build_tag_group_map(back_sections);
73
- const errors = [];
74
- const unassigned = [];
75
- // Collect all classified files across all fields
76
- const all_files = [];
77
- for (const cls of classifications) {
78
- // Check both field value and __files_ prefix (Instance 2+ may only have __files_ populated)
79
- const field_files = get_files_from_value(front_form_data[cls.field_id]);
80
- const prefixed_files = get_files_from_value(front_form_data[`__files_${cls.field_id}`]);
81
- const combined_files = field_files.length > 0 ? field_files : prefixed_files;
82
- for (const fc of cls.file_classifications) {
83
- const file_info = combined_files.find((f) => f.file_id === fc.file_id);
84
- // Build attachment from classification data if not found in front_form_data
85
- const attachment = file_info?.attachment ?? {
86
- file_id: fc.file_id,
87
- ref_id: `ref_${fc.file_id}`,
88
- file_name: fc.file_name,
89
- file_size: 0,
90
- mime_type: mime_from_name(fc.file_name),
91
- visibility: 'public',
92
- attached_at: new Date().toISOString(),
93
- };
94
- all_files.push({ ...fc, source_field_id: cls.field_id, attachment });
95
- }
96
- }
97
- // Track accumulated files per group locally to avoid stale state reads.
98
- // React state updates from on_back_change are async, so reading back_form_data
99
- // for the same key multiple times would miss prior additions within this loop.
100
- const accumulated_files = new Map();
101
- // Track which files are NEWLY routed (not already in back_form_data) — only these need autofill
102
- const newly_routed_files = new Map();
103
- const groups_to_autofill = new Set();
104
- for (const file of all_files) {
105
- let matched = false;
106
- // Route each file to only ONE group — the first matching tag wins.
107
- // Tags are ordered by priority (LLM returns best-match first), so
108
- // iterating in order and breaking after the first match prevents a
109
- // multi-tagged file from being duplicated across every matching group.
110
- for (const tag of file.tags) {
111
- const groups = tag_group_map.get(tag);
112
- if (groups) {
113
- matched = true;
114
- for (const group of groups) {
115
- if (on_back_change && file.attachment) {
116
- const file_key = `__files_${group.id}`;
117
- // Seed from back_form_data on first access, then use local accumulator
118
- if (!accumulated_files.has(file_key)) {
119
- accumulated_files.set(file_key, [...(back_form_data[file_key] ?? [])]);
120
- }
121
- const current = accumulated_files.get(file_key);
122
- const is_new = !current.some((f) => f.file_id === file.file_id);
123
- if (is_new) {
124
- current.push(file.attachment);
125
- }
126
- // Track for autofill: new files always, existing files only when force_autofill
127
- if (is_new || force_autofill) {
128
- if (!newly_routed_files.has(file_key)) {
129
- newly_routed_files.set(file_key, []);
130
- }
131
- // Avoid duplicates in the autofill list
132
- const autofill_list = newly_routed_files.get(file_key);
133
- if (!autofill_list.some((f) => f.file_id === file.file_id)) {
134
- autofill_list.push(file.attachment);
135
- }
136
- groups_to_autofill.add(group.id);
137
- }
138
- }
139
- }
140
- break; // First matching tag wins — don't duplicate to other groups
141
- }
142
- }
143
- // Persist tags to hazo_files
144
- if (file.tags.length > 0 && file_manager?.callbacks?.update_tags) {
145
- try {
146
- await file_manager.callbacks.update_tags(file.file_id, file.tags);
147
- }
148
- catch (err) {
149
- errors.push({ step: `update_tags:${file.file_id}`, error: err instanceof Error ? err.message : 'Failed to update tags' });
150
- }
151
- }
152
- if (!matched) {
153
- unassigned.push({
154
- file_id: file.file_id,
155
- file_name: file.file_name,
156
- tags: file.tags,
157
- document_date: file.document_date,
158
- document_nature: file.document_nature,
159
- source_field_id: file.source_field_id,
160
- });
161
- }
162
- }
163
- // Flush accumulated files to state in one call per group
164
- if (on_back_change) {
165
- for (const [file_key, files] of accumulated_files) {
166
- on_back_change(file_key, files);
167
- }
168
- }
169
- // Trigger autofill only for NEWLY routed files (not files already in the group)
170
- if (autofill_api_endpoint && groups_to_autofill.size > 0) {
171
- let autofill_done = 0;
172
- // Count total NEW files across all groups for progress
173
- let total_autofills = 0;
174
- const all_autofill_file_ids = [];
175
- for (const gid of groups_to_autofill) {
176
- const fk = `__files_${gid}`;
177
- const new_files = newly_routed_files.get(fk) ?? [];
178
- total_autofills += Math.max(new_files.length, 1);
179
- for (const f of new_files)
180
- all_autofill_file_ids.push(f.file_id);
181
- }
182
- // Mark all files as queued before autofill starts
183
- if (all_autofill_file_ids.length > 0) {
184
- on_queue_files?.(all_autofill_file_ids, true);
185
- }
186
- // Add autofill actions to the pipeline tracker (now that we know the count)
187
- if (tracker) {
188
- tracker.total += total_autofills;
189
- }
190
- for (const group_id of groups_to_autofill) {
191
- // Find the group config to get prompt_area/prompt_key
192
- let group_config;
193
- for (const section of back_sections) {
194
- group_config = section.groups?.find((g) => g.id === group_id);
195
- if (group_config)
196
- break;
197
- }
198
- if (group_config?.prompt_area && group_config?.prompt_key) {
199
- const file_key = `__files_${group_id}`;
200
- // Only autofill newly routed files, not files already processed in prior runs
201
- const group_files = newly_routed_files.get(file_key) ?? [];
202
- const fields_schema = group_config.fields?.map((f) => ({
203
- field_id: f.id,
204
- label: f.label,
205
- field_type: f.field_type,
206
- component_type: f.component_type,
207
- table_config: f.table_config,
208
- })) ?? [];
209
- let group_fields_populated = 0;
210
- let group_last_file_name = '';
211
- let group_error = '';
212
- const details = [];
213
- // Track accumulated values per field to merge array data (tables) across files
214
- const accumulated_field_values = new Map();
215
- // Autofill API expects one file per request (matches AutofillRequest shape)
216
- for (let fi = 0; fi < group_files.length; fi++) {
217
- const f = group_files[fi];
218
- // Skip files already successfully autofilled for this group (prevents duplicate data on refresh)
219
- if (autofilled_file_groups?.has(`${f.file_id}:${group_id}`)) {
220
- autofill_done++;
221
- if (tracker)
222
- tracker.completed++;
223
- on_queue_files?.([f.file_id], false);
224
- continue;
225
- }
226
- group_last_file_name = f.file_name;
227
- autofill_done++;
228
- if (tracker)
229
- tracker.completed++;
230
- const pct = tracker ? `${Math.round((tracker.completed / tracker.total) * 100)}% ` : '';
231
- update_progress({
232
- current_step: `${pct}Auto-filling ${group_id} — file ${fi + 1}/${group_files.length} (${autofill_done}/${total_autofills})`,
233
- overall_percent: tracker ? Math.round((tracker.completed / tracker.total) * 100) : undefined,
234
- });
235
- on_queue_files?.([f.file_id], false);
236
- on_autofill_file?.(f.file_id, true);
237
- const autofill_start = Date.now();
238
- try {
239
- const download_url = file_manager?.callbacks?.get_download_url?.(f.file_id, 'public') ?? '';
240
- const autofill_body = {
241
- file_id: f.file_id,
242
- file_name: f.file_name,
243
- mime_type: f.mime_type ?? 'application/octet-stream',
244
- download_url,
245
- group_id,
246
- prompt_area: group_config.prompt_area,
247
- prompt_key: group_config.prompt_key,
248
- fields: fields_schema,
249
- };
250
- const response = await fetch(autofill_api_endpoint, {
251
- method: 'POST',
252
- headers: { 'Content-Type': 'application/json' },
253
- body: JSON.stringify(autofill_body),
254
- });
255
- const result = await response.json();
256
- run_log.push({
257
- step: 'autofill',
258
- label: `Autofill: ${group_id} — ${f.file_name}`,
259
- prompt_area: group_config.prompt_area,
260
- prompt_key: group_config.prompt_key,
261
- request: autofill_body,
262
- response: result,
263
- timestamp: autofill_start,
264
- duration_ms: Date.now() - autofill_start,
265
- });
266
- // Per-file details for the autofill activity
267
- const file_details = [];
268
- let file_field_count = 0;
269
- let file_status = 'empty';
270
- let file_error;
271
- if (result.success && result.data && on_back_change) {
272
- const field_count = Object.keys(result.data).length;
273
- file_field_count = field_count;
274
- group_fields_populated += field_count;
275
- file_status = field_count > 0 ? 'success' : 'empty';
276
- for (const [field_id, value] of Object.entries(result.data)) {
277
- // Capture detail entry
278
- const field_cfg = group_config.fields?.find((fld) => fld.id === field_id);
279
- const value_summary = Array.isArray(value)
280
- ? `${value.length} row(s)`
281
- : String(value ?? '').slice(0, 50);
282
- const detail = {
283
- field_id,
284
- field_label: field_cfg?.label ?? field_id,
285
- file_name: f.file_name,
286
- value_summary,
287
- };
288
- details.push(detail);
289
- file_details.push({ field_id, field_label: detail.field_label, value_summary });
290
- // For arrays (data tables), append rows across files with source file metadata
291
- if (Array.isArray(value)) {
292
- const tagged_rows = value.map((row) => ({
293
- ...row,
294
- _source_file_id: f.file_id,
295
- _source_file_name: f.file_name,
296
- }));
297
- // Seed from existing back_form_data on first access so prior instance data is preserved
298
- const existing = accumulated_field_values.get(field_id)
299
- ?? (Array.isArray(back_form_data[field_id]) ? back_form_data[field_id] : undefined);
300
- const merged = Array.isArray(existing) ? [...existing, ...tagged_rows] : tagged_rows;
301
- accumulated_field_values.set(field_id, merged);
302
- on_back_change(field_id, merged);
303
- }
304
- else {
305
- // Scalar: last file wins
306
- accumulated_field_values.set(field_id, value);
307
- on_back_change(field_id, value);
308
- }
309
- }
310
- }
311
- else if (!result.success) {
312
- file_status = 'error';
313
- file_error = result.error || 'Autofill failed';
314
- group_error = file_error ?? 'Autofill failed';
315
- errors.push({ step: `autofill:${group_id}`, error: group_error });
316
- }
317
- else if (result.success && (!result.data || Object.keys(result.data).length === 0)) {
318
- file_status = 'empty';
319
- // Success but no data extracted
320
- if (!group_error && group_fields_populated === 0) {
321
- group_error = result.message || 'No matching data found in document';
322
- }
323
- }
324
- // Persist autofill activity to hierarchical data model
325
- if (on_autofill_activity) {
326
- const autofill_activity = {
327
- activity_info: {
328
- activity_no: `act_autofill_${group_id}_${f.file_id}`,
329
- activity_type: 'autofill',
330
- activity_location: 'back_office',
331
- activity_time: new Date().toISOString(),
332
- },
333
- autofill_result: {
334
- file_id: f.file_id,
335
- file_name: f.file_name,
336
- group_id,
337
- prompt_area: group_config.prompt_area || '',
338
- prompt_key: group_config.prompt_key || '',
339
- fields_populated: file_field_count,
340
- details: file_details,
341
- duration_ms: Date.now() - autofill_start,
342
- status: file_status,
343
- error: file_error,
344
- },
345
- };
346
- on_autofill_activity(autofill_activity);
347
- }
348
- // Record autofill run in v2 data model (prevents re-run on refresh)
349
- on_autofill_run?.({
350
- file_id: f.file_id,
351
- file_name: f.file_name,
352
- group_id,
353
- status: file_status,
354
- fields_populated: file_field_count,
355
- time: new Date().toISOString(),
356
- form_instance: 1, // Will be overridden by caller if needed
357
- error: file_error,
358
- });
359
- }
360
- catch (err) {
361
- const caught_error = err instanceof Error ? err.message : 'Autofill request failed';
362
- group_error = caught_error;
363
- errors.push({ step: `autofill:${group_id}:${f.file_id}`, error: group_error });
364
- // Persist error activity to hierarchical data model
365
- if (on_autofill_activity) {
366
- const error_activity = {
367
- activity_info: {
368
- activity_no: `act_autofill_${group_id}_${f.file_id}`,
369
- activity_type: 'autofill',
370
- activity_location: 'back_office',
371
- activity_time: new Date().toISOString(),
372
- },
373
- autofill_result: {
374
- file_id: f.file_id,
375
- file_name: f.file_name,
376
- group_id,
377
- prompt_area: group_config.prompt_area || '',
378
- prompt_key: group_config.prompt_key || '',
379
- fields_populated: 0,
380
- details: [],
381
- duration_ms: Date.now() - autofill_start,
382
- status: 'error',
383
- error: caught_error,
384
- },
385
- };
386
- on_autofill_activity(error_activity);
387
- }
388
- // Record error run in v2 data model
389
- on_autofill_run?.({
390
- file_id: f.file_id,
391
- file_name: f.file_name,
392
- group_id,
393
- status: 'error',
394
- fields_populated: 0,
395
- time: new Date().toISOString(),
396
- form_instance: 1,
397
- error: caught_error,
398
- });
399
- }
400
- finally {
401
- on_autofill_file?.(f.file_id, false);
402
- }
403
- }
404
- // Collect run_log entries for this group
405
- const group_run_log = run_log.filter((entry) => entry.step === 'autofill' && entry.label.startsWith(`Autofill: ${group_id}`));
406
- // Set autofill log for this group
407
- if (group_error && group_fields_populated === 0) {
408
- // If there was an error and no fields were populated, check if it's a real error or just empty
409
- const is_empty = !errors.some((e) => e.step.startsWith(`autofill:${group_id}`));
410
- set_group_autofill_log((prev) => ({
411
- ...prev,
412
- [group_id]: {
413
- status: is_empty ? 'empty' : 'error',
414
- message: group_error,
415
- timestamp: Date.now(),
416
- run_log: group_run_log.length > 0 ? group_run_log : undefined,
417
- },
418
- }));
419
- }
420
- else if (group_fields_populated > 0) {
421
- const file_label = group_files.length === 1 ? group_last_file_name : `${group_files.length} file(s)`;
422
- set_group_autofill_log((prev) => ({
423
- ...prev,
424
- [group_id]: {
425
- status: 'success',
426
- message: `Populated ${group_fields_populated} field(s) from ${file_label}`,
427
- timestamp: Date.now(),
428
- details,
429
- run_log: group_run_log.length > 0 ? group_run_log : undefined,
430
- },
431
- }));
432
- }
433
- else {
434
- set_group_autofill_log((prev) => ({
435
- ...prev,
436
- [group_id]: {
437
- status: 'empty',
438
- message: 'No matching data found in document',
439
- timestamp: Date.now(),
440
- run_log: group_run_log.length > 0 ? group_run_log : undefined,
441
- },
442
- }));
443
- }
444
- }
445
- else {
446
- autofill_done++;
447
- }
448
- }
449
- }
450
- return { unassigned, errors };
451
- }
452
- /** Build a lookup of user-resolved rule+file combinations from responded clarifications */
453
- function build_resolved_rules_map(clarifications) {
454
- const map = new Map();
455
- for (const item of clarifications) {
456
- if (!item.rule_id)
457
- continue;
458
- if (item.status !== 'responded' && item.status !== 'resolved')
459
- continue;
460
- for (const ref of item.doc_references ?? []) {
461
- if (!ref.file_id)
462
- continue;
463
- const key = `${item.rule_id}::${ref.file_id}`;
464
- map.set(key, {
465
- response_choice: item.response_choice,
466
- user_comment: item.user_comment,
467
- });
468
- }
469
- }
470
- return map;
471
- }
472
- /** Validate classified files against matching validation rules */
473
- async function validate_classified_files(options) {
474
- const { classifications, validation_api_endpoint, validation_rules, file_manager, front_form_data, update_progress, set_file_validation_results, run_log, tracker } = options;
475
- const errors = [];
476
- const all_validation_results = [];
477
- const failed_file_ids = new Set();
478
- // Collect all files with their document types
479
- const files_to_validate = [];
480
- for (const cls of classifications) {
481
- // Look up file attachments from form data to get actual mime_type
482
- // Check both field value and __files_ prefix (Instance 2+ may only have __files_ populated)
483
- const field_files = get_files_from_value(front_form_data[cls.field_id]);
484
- const prefixed_files = get_files_from_value(front_form_data[`__files_${cls.field_id}`]);
485
- const combined = field_files.length > 0 ? field_files : prefixed_files;
486
- const file_lookup = new Map(combined.map(f => [f.file_id, f]));
487
- for (const fc of cls.file_classifications) {
488
- const source_file = file_lookup.get(fc.file_id);
489
- files_to_validate.push({
490
- file_id: fc.file_id,
491
- file_name: fc.file_name,
492
- mime_type: source_file?.attachment?.mime_type ?? mime_from_name(fc.file_name),
493
- document_types: fc.document_type ?? [],
494
- source_field_id: cls.field_id,
495
- });
496
- }
497
- }
498
- // Validation logging moved to hazo_logs (consumer configures via LoggerProvider)
499
- const manual_review = options.manual_review_file_ids;
500
- // Validate each file
501
- for (let i = 0; i < files_to_validate.length; i++) {
502
- const file = files_to_validate[i];
503
- if (tracker)
504
- tracker.completed++;
505
- const pct = tracker ? `${Math.round((tracker.completed / tracker.total) * 100)}% ` : '';
506
- update_progress({
507
- current_step: `${pct}Validating ${file.file_name} (${i + 1}/${files_to_validate.length})`,
508
- overall_percent: tracker ? Math.round((tracker.completed / tracker.total) * 100) : undefined,
509
- });
510
- // Skip validation for files marked for manual review by client
511
- if (manual_review?.has(file.file_id)) {
512
- const result = {
513
- file_id: file.file_id,
514
- file_name: file.file_name,
515
- status: 'manual_review',
516
- errors: [],
517
- document_types: file.document_types,
518
- };
519
- all_validation_results.push(result);
520
- set_file_validation_results((prev) => ({ ...prev, [file.file_id]: result }));
521
- continue;
522
- }
523
- // Find matching rules by document_type.
524
- // A rule matches if:
525
- // 1. Its document_type is in the file's document_types, OR
526
- // 2. Its document_type is 'general' (catch-all — always matches), OR
527
- // 3. The file has no document_types (match all enabled rules)
528
- const check_type_filter = options.check_type_filter;
529
- // check_type filter: rules without check_type default to 'immediate' context.
530
- // When filtering for 'backoffice', only explicitly backoffice rules run.
531
- // check_type filter: rules without check_type run in ALL contexts (immediate + backoffice).
532
- // Only rules with an explicit check_type are restricted to that context.
533
- const matching_rules = validation_rules.filter((r) => r.enabled && (file.document_types.includes(r.document_type) ||
534
- r.document_type === 'general' ||
535
- file.document_types.length === 0) && (!check_type_filter || !r.check_type || r.check_type === check_type_filter));
536
- if (matching_rules.length === 0) {
537
- // No rules match — preserve existing result if available (e.g. immediate passed),
538
- // otherwise mark as skipped
539
- set_file_validation_results((prev) => {
540
- if (prev[file.file_id])
541
- return prev; // keep existing result
542
- const result = {
543
- file_id: file.file_id,
544
- file_name: file.file_name,
545
- status: 'skipped',
546
- errors: [],
547
- document_types: file.document_types,
548
- };
549
- return { ...prev, [file.file_id]: result };
550
- });
551
- continue;
552
- }
553
- // Separate rules into user-resolved (skip LLM) and rules to execute
554
- const resolved_map = options.resolved_rules;
555
- const resolved_rule_results = [];
556
- const rules_needing_execution = [];
557
- for (const rule of matching_rules) {
558
- const key = `${rule.rule_id}::${file.file_id}`;
559
- const resolution = resolved_map?.get(key);
560
- if (resolution) {
561
- // User already resolved this rule for this file — skip LLM call
562
- resolved_rule_results.push({
563
- rule_id: rule.rule_id,
564
- rule_name: rule.name,
565
- issues: [{
566
- issue_id: '0',
567
- issue_description: 'User-resolved issue',
568
- }],
569
- user_resolved: true,
570
- user_resolution: {
571
- response_choice: resolution.response_choice,
572
- user_comment: resolution.user_comment,
573
- },
574
- });
575
- }
576
- else {
577
- rules_needing_execution.push(rule);
578
- }
579
- }
580
- // If all rules are user-resolved, no need for LLM call
581
- if (rules_needing_execution.length === 0) {
582
- const validation_result = {
583
- file_id: file.file_id,
584
- file_name: file.file_name,
585
- status: 'passed',
586
- errors: [],
587
- document_types: file.document_types,
588
- rule_results: resolved_rule_results,
589
- };
590
- all_validation_results.push(validation_result);
591
- set_file_validation_results((prev) => ({ ...prev, [file.file_id]: validation_result }));
592
- continue;
593
- }
594
- // Mark as validating
595
- set_file_validation_results((prev) => ({
596
- ...prev,
597
- [file.file_id]: {
598
- file_id: file.file_id,
599
- file_name: file.file_name,
600
- status: 'validating',
601
- errors: [],
602
- document_types: file.document_types,
603
- },
604
- }));
605
- try {
606
- const download_url = file_manager?.callbacks?.get_download_url?.(file.file_id, 'public') ?? '';
607
- const rules_to_execute = rules_needing_execution.map(rule_to_fb_execution);
608
- const validate_body = {
609
- file_name: file.file_name,
610
- mime_type: file.mime_type,
611
- download_url,
612
- rules: rules_to_execute,
613
- };
614
- const validate_start = Date.now();
615
- const response = await fetch(validation_api_endpoint, {
616
- method: 'POST',
617
- headers: { 'Content-Type': 'application/json' },
618
- body: JSON.stringify(validate_body),
619
- });
620
- const result = await response.json();
621
- run_log.push({
622
- step: 'validate',
623
- label: `Validate: ${file.file_name}`,
624
- request: validate_body,
625
- response: result,
626
- timestamp: validate_start,
627
- duration_ms: Date.now() - validate_start,
628
- });
629
- // Enrich rule_results with human-readable rule names from rules_needing_execution
630
- const rule_name_lookup = new Map(rules_needing_execution.map(r => [r.rule_id, r.name]));
631
- const enriched_rule_results = result.rule_results?.map(rr => ({
632
- ...rr,
633
- rule_name: rr.rule_name ?? rule_name_lookup.get(rr.rule_id),
634
- })) ?? [];
635
- // Merge user-resolved results with API results
636
- const all_rule_results = [...resolved_rule_results, ...enriched_rule_results];
637
- // Enrich clarification doc_references with actual file_id
638
- const enriched_clarifications = (result.clarifications ?? []).map(c => ({
639
- ...c,
640
- doc_references: c.doc_references.map(ref => ({
641
- ...ref,
642
- file_id: ref.file_id || file.file_id,
643
- })),
644
- }));
645
- const has_issues = enriched_clarifications.length > 0;
646
- const validation_result = {
647
- file_id: file.file_id,
648
- file_name: file.file_name,
649
- status: has_issues ? 'failed' : 'passed',
650
- errors: enriched_clarifications,
651
- document_types: file.document_types,
652
- rule_results: all_rule_results,
653
- };
654
- all_validation_results.push(validation_result);
655
- set_file_validation_results((prev) => ({ ...prev, [file.file_id]: validation_result }));
656
- if (has_issues) {
657
- failed_file_ids.add(file.file_id);
658
- }
659
- }
660
- catch (err) {
661
- errors.push({ step: `validate:${file.file_id}`, error: err instanceof Error ? err.message : 'Validation request failed' });
662
- const error_result = {
663
- file_id: file.file_id,
664
- file_name: file.file_name,
665
- status: 'failed',
666
- errors: [],
667
- document_types: file.document_types,
668
- rule_results: resolved_rule_results.length > 0 ? resolved_rule_results : undefined,
669
- };
670
- all_validation_results.push(error_result);
671
- set_file_validation_results((prev) => ({ ...prev, [file.file_id]: error_result }));
672
- failed_file_ids.add(file.file_id);
673
- }
674
- }
675
- // Build passed_classifications by filtering out failed files
676
- const passed_classifications = classifications.map((cls) => {
677
- const passed_file_cls = cls.file_classifications.filter((fc) => !failed_file_ids.has(fc.file_id));
678
- const passed_tags = [...new Set(passed_file_cls.flatMap((fc) => fc.tags))];
679
- return {
680
- field_id: cls.field_id,
681
- tags: passed_tags,
682
- file_classifications: passed_file_cls,
683
- };
684
- }).filter((cls) => cls.file_classifications.length > 0);
685
- return { passed_classifications, all_validation_results, errors };
686
- }
687
- export function use_llm_run({ props, update_progress, classification_results, set_classification_results, set_active_tab, add_classifying_files, remove_classifying_files, add_queued_files, remove_queued_files, set_unassigned_files, set_group_autofill_log, set_file_validation_results, manual_review_file_ids, sent_clarifications, pending_clarification_responses, draft_clarifications, file_validation_results, set_autofilling_file_ids, set_validating_file_ids, skipped_file_ids, update_form_data, form_data_entries }) {
688
- const logger = use_logger();
689
- // Refs to always read the latest values inside trigger_run,
690
- // avoiding stale closures when submit flushes responses then immediately triggers a run.
691
- const sent_clarifications_ref = useRef(sent_clarifications);
692
- sent_clarifications_ref.current = sent_clarifications;
693
- const pending_responses_ref = useRef(pending_clarification_responses);
694
- pending_responses_ref.current = pending_clarification_responses;
695
- const draft_clarifications_ref = useRef(draft_clarifications);
696
- draft_clarifications_ref.current = draft_clarifications;
697
- const skipped_file_ids_ref = useRef(skipped_file_ids);
698
- skipped_file_ids_ref.current = skipped_file_ids;
699
- const update_form_data_ref = useRef(update_form_data);
700
- update_form_data_ref.current = update_form_data;
701
- const form_data_entries_ref = useRef(form_data_entries);
702
- form_data_entries_ref.current = form_data_entries;
703
- /** Record an autofill run in the v2 data model (prevents re-run on refresh) */
704
- const record_autofill_run = useCallback((run) => {
705
- const updater = update_form_data_ref.current;
706
- if (!updater)
707
- return;
708
- updater((prev) => add_autofill_run(prev, run));
709
- }, []);
710
- /** Callback to persist an autofill activity to the hierarchical data model.
711
- * Creates a standalone data entry with the activity (not attached to an existing pd node)
712
- * since autofill activities are back-office operations that don't correspond to client inputs. */
713
- const emit_autofill_activity = useCallback((activity) => {
714
- const updater = update_form_data_ref.current;
715
- if (!updater)
716
- return;
717
- updater((prev) => {
718
- // Create a standalone entry for the autofill activity:
719
- // data_entry → client_data (ie) → client_input → processed_data (pd) → activity
720
- const ie_id = next_ie_id(prev);
721
- const ie_num = parseInt(ie_id.replace('ie_', ''), 10);
722
- const pd_id = next_pd_id(ie_num, []);
723
- const file_ref = activity.autofill_result;
724
- const act_id = next_act_id(pd_id, []);
725
- const activity_with_id = {
726
- ...activity,
727
- activity_info: { ...activity.activity_info, activity_no: act_id },
728
- };
729
- const new_entry = {
730
- question: {
731
- source_id: null,
732
- question_field_id: `autofill_${file_ref?.group_id}_${file_ref?.file_id}`,
733
- question_type: 'data_request',
734
- form_instance: 1,
735
- component_type: 'autofill',
736
- description: `Autofill: ${file_ref?.file_name ?? 'unknown'} → ${file_ref?.group_id ?? 'unknown'}`,
737
- },
738
- client_data: [{
739
- input_elt_id: ie_id,
740
- client_input: {
741
- data_type: 'file',
742
- data_files: file_ref ? { file_id: file_ref.file_id, file_name: file_ref.file_name } : null,
743
- input_status: 'complete',
744
- processed_data: [{
745
- processed_data_id: pd_id,
746
- processed_data_type: 'file',
747
- processed_data_files: file_ref ? { file_id: file_ref.file_id, file_name: file_ref.file_name } : undefined,
748
- activities: [activity_with_id],
749
- }],
750
- },
751
- }],
752
- };
753
- return add_data_entry(prev, new_entry);
754
- });
755
- }, []);
756
- /** Classify a single file within a field and update results */
757
- const trigger_classify_file = useCallback(async (field_id, file_id, file_name) => {
758
- const { field_textbox_configs, llm_api_endpoint, front_form_data } = props;
759
- logger.info('[use_llm_run] trigger_classify_file', { field_id, file_id, file_name, has_endpoint: !!llm_api_endpoint });
760
- if (!field_textbox_configs || !llm_api_endpoint) {
761
- logger.warn('[use_llm_run] classify_file_skipped: missing config', { has_configs: !!field_textbox_configs, has_endpoint: !!llm_api_endpoint });
762
- return;
763
- }
764
- const config = field_textbox_configs[field_id];
765
- if (!config)
766
- return;
767
- const client_text = get_text_from_value(front_form_data[field_id]);
768
- add_classifying_files([file_id]);
769
- update_progress({ status: 'classifying', total_steps: 1, completed_steps: 0, current_step: `Classifying ${file_name}`, error: undefined });
770
- try {
771
- const response = await fetch(llm_api_endpoint, {
772
- method: 'POST',
773
- headers: { 'Content-Type': 'application/json' },
774
- body: JSON.stringify({
775
- action: 'classify_files',
776
- field_id,
777
- files: [{ file_id, file_name }],
778
- client_text,
779
- prompt_area: config.classification.prompt_area,
780
- prompt_key: config.classification.prompt_key,
781
- available_tags: config.classification.available_tags ?? props.available_tags ?? [],
782
- ...(props.available_document_types?.length ? { available_document_types: props.available_document_types } : {}),
783
- }),
784
- });
785
- const result = await response.json();
786
- logger.debug('[use_llm_run] classify_file_response', { file_id, success: result.success, classification_count: result.file_classifications?.length ?? 0 });
787
- if (result.success && result.file_classifications) {
788
- const new_file_cls = result.file_classifications;
789
- // Merge: replace this file's classification, keep others.
790
- // Use functional update to avoid stale closure when multiple files classify concurrently.
791
- set_classification_results((prev_results) => {
792
- const prev = prev_results.find((c) => c.field_id === field_id);
793
- const existing_file_cls = prev?.file_classifications?.filter((fc) => fc.file_id !== file_id) ?? [];
794
- const all_file_cls = [...existing_file_cls, ...new_file_cls];
795
- const all_tags = [...new Set(all_file_cls.flatMap((fc) => fc.tags))];
796
- const updated_result = { field_id, tags: all_tags, file_classifications: all_file_cls };
797
- return [
798
- ...prev_results.filter((c) => c.field_id !== field_id),
799
- updated_result,
800
- ];
801
- });
802
- }
803
- // Validate the re-classified file (if configured)
804
- if (result.success && result.file_classifications) {
805
- const new_file_cls = result.file_classifications;
806
- const single_classification = {
807
- field_id,
808
- tags: [...new Set(new_file_cls.flatMap((fc) => fc.tags))],
809
- file_classifications: new_file_cls,
810
- };
811
- if (props.validation_api_endpoint && props.validation_rules?.length) {
812
- // Classification done — switch from classifying to validating state
813
- remove_classifying_files([file_id]);
814
- set_validating_file_ids(prev => { const s = new Set(prev); s.add(file_id); return s; });
815
- update_progress({ status: 'validating', total_steps: 2, completed_steps: 1, current_step: `Validating ${file_name}...` });
816
- const run_log = [];
817
- await validate_classified_files({
818
- classifications: [single_classification],
819
- validation_api_endpoint: props.validation_api_endpoint,
820
- validation_rules: props.validation_rules,
821
- file_manager: props.file_manager,
822
- front_form_data: props.front_form_data,
823
- update_progress,
824
- set_file_validation_results,
825
- run_log,
826
- manual_review_file_ids,
827
- check_type_filter: 'immediate',
828
- });
829
- }
830
- }
831
- // Phase 1 complete — routing deferred to trigger_complete
832
- update_progress({ status: 'validated', total_steps: 2, completed_steps: 2, current_step: undefined, error: undefined });
833
- }
834
- catch (err) {
835
- logger.error('[use_llm_run] classify_file_error', { file_id, file_name, error: err instanceof Error ? err.message : String(err) });
836
- update_progress({
837
- status: 'error',
838
- total_steps: 1,
839
- completed_steps: 0,
840
- current_step: undefined,
841
- error: err instanceof Error ? err.message : 'Classification failed',
842
- });
843
- }
844
- finally {
845
- remove_classifying_files([file_id]);
846
- set_validating_file_ids(prev => { const s = new Set(prev); s.delete(file_id); return s; });
847
- }
848
- }, [props, classification_results, update_progress, set_classification_results, add_classifying_files, remove_classifying_files, set_unassigned_files, set_group_autofill_log, set_file_validation_results, manual_review_file_ids, set_validating_file_ids]);
849
- /** Manually assign a file to a tag and route it to matching back-office group */
850
- const trigger_assign_file = useCallback(async (file_id, tag_id) => {
851
- const { back_sections, on_back_change, back_form_data, autofill_api_endpoint, file_manager, front_form_data } = props;
852
- // Find the file attachment from front_form_data
853
- let attachment;
854
- let file_name = '';
855
- for (const [_field_id, value] of Object.entries(front_form_data)) {
856
- const files = get_files_from_value(value);
857
- const found = files.find((f) => f.file_id === file_id);
858
- if (found?.attachment) {
859
- attachment = found.attachment;
860
- file_name = found.file_name;
861
- break;
862
- }
863
- }
864
- if (!attachment)
865
- return;
866
- const tag_group_map = build_tag_group_map(back_sections);
867
- const groups = tag_group_map.get(tag_id);
868
- if (groups && on_back_change) {
869
- for (const group of groups) {
870
- const file_key = `__files_${group.id}`;
871
- const existing_files = back_form_data[file_key] ?? [];
872
- if (!existing_files.some((f) => f.file_id === file_id)) {
873
- on_back_change(file_key, [...existing_files, attachment]);
874
- }
875
- // Trigger autofill for this group (single file, matches AutofillRequest shape)
876
- if (autofill_api_endpoint && group.prompt_area && group.prompt_key) {
877
- try {
878
- const download_url = file_manager?.callbacks?.get_download_url?.(attachment.file_id, 'public') ?? '';
879
- const response = await fetch(autofill_api_endpoint, {
880
- method: 'POST',
881
- headers: { 'Content-Type': 'application/json' },
882
- body: JSON.stringify({
883
- file_id: attachment.file_id,
884
- file_name: attachment.file_name,
885
- mime_type: attachment.mime_type ?? 'application/octet-stream',
886
- download_url,
887
- group_id: group.id,
888
- prompt_area: group.prompt_area,
889
- prompt_key: group.prompt_key,
890
- fields: group.fields?.map((f) => ({
891
- field_id: f.id,
892
- label: f.label,
893
- field_type: f.field_type,
894
- component_type: f.component_type,
895
- table_config: f.table_config,
896
- })) ?? [],
897
- }),
898
- });
899
- const result = await response.json();
900
- if (result.success && result.data) {
901
- const field_count = Object.keys(result.data).length;
902
- for (const [field_id, value] of Object.entries(result.data)) {
903
- on_back_change(field_id, value);
904
- }
905
- set_group_autofill_log((prev) => ({
906
- ...prev,
907
- [group.id]: {
908
- status: field_count > 0 ? 'success' : 'empty',
909
- message: field_count > 0
910
- ? `Populated ${field_count} field(s) from ${file_name}`
911
- : result.message || 'No matching data found in document',
912
- timestamp: Date.now(),
913
- },
914
- }));
915
- }
916
- else if (!result.success) {
917
- set_group_autofill_log((prev) => ({
918
- ...prev,
919
- [group.id]: {
920
- status: 'error',
921
- message: result.error || 'Autofill failed',
922
- timestamp: Date.now(),
923
- },
924
- }));
925
- }
926
- }
927
- catch (err) {
928
- // Autofill error logged via group_autofill_log
929
- set_group_autofill_log((prev) => ({
930
- ...prev,
931
- [group.id]: {
932
- status: 'error',
933
- message: err instanceof Error ? err.message : 'Autofill request failed',
934
- timestamp: Date.now(),
935
- },
936
- }));
937
- }
938
- }
939
- }
940
- }
941
- // Persist tags
942
- if (file_manager?.callbacks?.update_tags) {
943
- try {
944
- await file_manager.callbacks.update_tags(file_id, [tag_id]);
945
- }
946
- catch (err) {
947
- // Tag update error — non-critical, file_manager may not support update_tags
948
- }
949
- }
950
- // Remove from unassigned files list (handled by caller via set_unassigned_files)
951
- }, [props, set_group_autofill_log]);
952
- const trigger_run = useCallback(async (mode = 'new_only') => {
953
- const { field_textbox_configs, llm_api_endpoint, front_form_data, on_back_change, back_form_data, back_sections, autofill_api_endpoint, file_manager, on_run_complete } = props;
954
- const run_start = Date.now();
955
- logger.info('[use_llm_run] trigger_run_start', {
956
- mode,
957
- has_configs: !!field_textbox_configs,
958
- has_llm_endpoint: !!llm_api_endpoint,
959
- has_autofill_endpoint: !!autofill_api_endpoint,
960
- has_validation_endpoint: !!props.validation_api_endpoint,
961
- field_count: field_textbox_configs ? Object.keys(field_textbox_configs).length : 0,
962
- validation_rule_count: props.validation_rules?.length ?? 0,
963
- });
964
- if (!field_textbox_configs || !llm_api_endpoint) {
965
- logger.warn('[use_llm_run] trigger_run_aborted: missing config', { has_configs: !!field_textbox_configs, has_endpoint: !!llm_api_endpoint });
966
- return;
967
- }
968
- set_group_autofill_log({});
969
- // When running all, clear existing classification results (tags + metadata) upfront
970
- if (mode === 'all') {
971
- set_classification_results([]);
972
- }
973
- const all_field_ids = Object.keys(field_textbox_configs);
974
- // Build list of fields that need classification and which files are new
975
- const fields_to_process = [];
976
- for (const field_id of all_field_ids) {
977
- const current_files = get_files_from_value(front_form_data[field_id]).length > 0
978
- ? get_files_from_value(front_form_data[field_id])
979
- : get_files_from_value(front_form_data[`__files_${field_id}`]);
980
- if (current_files.length === 0)
981
- continue;
982
- const prev = classification_results.find((c) => c.field_id === field_id);
983
- if (mode === 'all' || !prev) {
984
- // Classify all files
985
- fields_to_process.push({
986
- field_id,
987
- files_to_classify: current_files,
988
- existing_file_classifications: [],
989
- });
990
- }
991
- else {
992
- // Only classify new files (not previously classified)
993
- const prev_file_cls = prev.file_classifications ?? [];
994
- const classified_ids = new Set(prev_file_cls.map((fc) => fc.file_id));
995
- const new_files = current_files.filter((f) => !classified_ids.has(f.file_id));
996
- if (new_files.length > 0) {
997
- // Keep existing classifications for files still present
998
- const still_present_ids = new Set(current_files.map((f) => f.file_id));
999
- const kept = prev_file_cls.filter((fc) => still_present_ids.has(fc.file_id));
1000
- fields_to_process.push({
1001
- field_id,
1002
- files_to_classify: new_files,
1003
- existing_file_classifications: kept,
1004
- });
1005
- }
1006
- }
1007
- }
1008
- logger.info('[use_llm_run] fields_to_process', {
1009
- count: fields_to_process.length,
1010
- total_files: fields_to_process.reduce((sum, f) => sum + f.files_to_classify.length, 0),
1011
- fields: fields_to_process.map(f => ({ field_id: f.field_id, file_count: f.files_to_classify.length, existing_count: f.existing_file_classifications.length })),
1012
- });
1013
- if (fields_to_process.length === 0) {
1014
- // No new files to classify — but still run backoffice validation if configured
1015
- // (immediate-only validation may have skipped backoffice-only rules)
1016
- const has_backoffice_validation = !!(props.validation_api_endpoint && props.validation_rules?.length);
1017
- const has_existing_classifications = classification_results.length > 0;
1018
- if (has_backoffice_validation && has_existing_classifications) {
1019
- update_progress({ status: 'validating', current_step: 'Running back-office validation...' });
1020
- const run_log = [];
1021
- const current_sent = sent_clarifications_ref.current;
1022
- const current_pending = pending_responses_ref.current;
1023
- const current_drafts = draft_clarifications_ref.current;
1024
- let effective_clarifications = current_sent;
1025
- if (current_pending.size > 0) {
1026
- const pending_items = [];
1027
- for (const [clar_id, response] of current_pending) {
1028
- if (!response.response_choice)
1029
- continue;
1030
- if (current_sent.some(c => c.id === clar_id && (c.status === 'responded' || c.status === 'resolved')))
1031
- continue;
1032
- const original = current_drafts.find(c => c.id === clar_id) ?? current_sent.find(c => c.id === clar_id);
1033
- if (original) {
1034
- pending_items.push({ ...original, status: 'responded', response_choice: response.response_choice, user_comment: response.user_comment });
1035
- }
1036
- }
1037
- if (pending_items.length > 0) {
1038
- effective_clarifications = [...current_sent, ...pending_items];
1039
- }
1040
- }
1041
- const resolved_rules = build_resolved_rules_map(effective_clarifications);
1042
- const vr = await validate_classified_files({
1043
- classifications: classification_results,
1044
- validation_api_endpoint: props.validation_api_endpoint,
1045
- validation_rules: props.validation_rules,
1046
- file_manager: props.file_manager,
1047
- front_form_data,
1048
- update_progress,
1049
- set_file_validation_results,
1050
- run_log,
1051
- manual_review_file_ids,
1052
- check_type_filter: 'backoffice',
1053
- resolved_rules,
1054
- });
1055
- // Trigger completion with existing classifications + new validation results
1056
- const run_result = {
1057
- classifications: classification_results,
1058
- unassigned_files: [],
1059
- validation_results: vr.all_validation_results,
1060
- errors: vr.errors,
1061
- };
1062
- update_progress({ status: 'validated', total_steps: 0, completed_steps: 0, current_step: undefined, error: undefined });
1063
- return;
1064
- }
1065
- update_progress({ status: 'validated', total_steps: 0, completed_steps: 0, current_step: undefined, error: undefined });
1066
- return;
1067
- }
1068
- // Build overall pipeline tracker: classify + validate + autofill (autofill count added dynamically)
1069
- const total_files_for_validation = fields_to_process.flatMap((f) => f.files_to_classify).length;
1070
- const has_validation = !!(props.validation_api_endpoint && props.validation_rules?.length);
1071
- const tracker = {
1072
- completed: 0,
1073
- total: fields_to_process.length + (has_validation ? total_files_for_validation : 0),
1074
- // autofill actions are added to total in route_files_to_back_office once known
1075
- };
1076
- update_progress({ status: 'classifying', total_steps: fields_to_process.length, completed_steps: 0, overall_percent: 0, error: undefined });
1077
- const new_classifications = [];
1078
- const errors = [];
1079
- const run_log = [];
1080
- // Mark all files as queued initially
1081
- const all_file_ids = fields_to_process.flatMap((f) => f.files_to_classify.map((file) => file.file_id));
1082
- add_queued_files(all_file_ids);
1083
- // Step 1: Per-field classification with batch support
1084
- const batch_size = props.classification_batch_size ?? 5;
1085
- for (let i = 0; i < fields_to_process.length; i++) {
1086
- const { field_id, files_to_classify, existing_file_classifications } = fields_to_process[i];
1087
- const config = field_textbox_configs[field_id];
1088
- // Move this field's files from queued → classifying
1089
- const field_file_ids = files_to_classify.map((f) => f.file_id);
1090
- remove_queued_files(field_file_ids);
1091
- add_classifying_files(field_file_ids);
1092
- const client_text = get_text_from_value(front_form_data[field_id]);
1093
- tracker.completed++;
1094
- const pct = Math.round((tracker.completed / tracker.total) * 100);
1095
- update_progress({
1096
- current_step: `${pct}% Classifying ${files_to_classify.length} file(s) in ${field_id} (${i + 1}/${fields_to_process.length})`,
1097
- completed_steps: i,
1098
- overall_percent: pct,
1099
- });
1100
- // Split files into batches to reduce per-call overhead
1101
- const batches = [];
1102
- for (let b = 0; b < files_to_classify.length; b += batch_size) {
1103
- batches.push(files_to_classify.slice(b, b + batch_size));
1104
- }
1105
- const batch_file_cls = [];
1106
- let batch_failed = false;
1107
- for (let bi = 0; bi < batches.length; bi++) {
1108
- const batch = batches[bi];
1109
- try {
1110
- const classify_body = {
1111
- action: 'classify_files',
1112
- field_id,
1113
- files: batch,
1114
- client_text,
1115
- prompt_area: config.classification.prompt_area,
1116
- prompt_key: config.classification.prompt_key,
1117
- available_tags: config.classification.available_tags ?? props.available_tags ?? [],
1118
- ...(props.available_document_types?.length ? { available_document_types: props.available_document_types } : {}),
1119
- };
1120
- const classify_start = Date.now();
1121
- const response = await fetch(llm_api_endpoint, {
1122
- method: 'POST',
1123
- headers: { 'Content-Type': 'application/json' },
1124
- body: JSON.stringify(classify_body),
1125
- });
1126
- const result = await response.json();
1127
- run_log.push({
1128
- step: 'classify',
1129
- label: `Classification: ${field_id}${batches.length > 1 ? ` (batch ${bi + 1}/${batches.length})` : ''}`,
1130
- prompt_area: config.classification.prompt_area,
1131
- prompt_key: config.classification.prompt_key,
1132
- request: classify_body,
1133
- response: result,
1134
- timestamp: classify_start,
1135
- duration_ms: Date.now() - classify_start,
1136
- });
1137
- if (result.success && result.file_classifications) {
1138
- batch_file_cls.push(...result.file_classifications);
1139
- }
1140
- else {
1141
- errors.push({ step: `classify:${field_id}`, error: result.error || 'Classification failed' });
1142
- batch_failed = true;
1143
- }
1144
- }
1145
- catch (err) {
1146
- errors.push({ step: `classify:${field_id}`, error: err instanceof Error ? err.message : 'Unknown error' });
1147
- batch_failed = true;
1148
- }
1149
- }
1150
- if (!batch_failed || batch_file_cls.length > 0) {
1151
- const all_file_cls = [...existing_file_classifications, ...batch_file_cls];
1152
- const all_tags = [...new Set(all_file_cls.flatMap((fc) => fc.tags))];
1153
- new_classifications.push({
1154
- field_id,
1155
- tags: all_tags,
1156
- file_classifications: all_file_cls,
1157
- });
1158
- }
1159
- // Clear classifying state for this field's files
1160
- remove_classifying_files(field_file_ids);
1161
- }
1162
- logger.info('[use_llm_run] classification_phase_complete', {
1163
- new_classification_count: new_classifications.length,
1164
- total_file_classifications: new_classifications.reduce((sum, c) => sum + c.file_classifications.length, 0),
1165
- error_count: errors.length,
1166
- duration_ms: Date.now() - run_start,
1167
- });
1168
- // Merge with existing results for fields that weren't re-processed
1169
- const processed_field_ids = new Set(fields_to_process.map((f) => f.field_id));
1170
- const prior_results = mode === 'all' ? [] : classification_results;
1171
- const merged_classifications = [
1172
- ...prior_results.filter((c) => !processed_field_ids.has(c.field_id)),
1173
- ...new_classifications,
1174
- ];
1175
- set_classification_results(merged_classifications);
1176
- // Step 2: Validate (if configured)
1177
- let all_validation_results = [];
1178
- if (props.validation_api_endpoint && props.validation_rules?.length) {
1179
- update_progress({ status: 'validating', current_step: 'Validating documents...' });
1180
- // Build resolved rules map from sent clarifications + pending responses.
1181
- // Use refs to get latest values — avoids stale closure when submit flushes responses then runs pipeline.
1182
- // Merge pending responses with sent clarifications so we don't re-validate already-resolved issues
1183
- // even if the parent component hasn't re-rendered with updated props yet.
1184
- const current_sent = sent_clarifications_ref.current;
1185
- const current_pending = pending_responses_ref.current;
1186
- const current_drafts = draft_clarifications_ref.current;
1187
- let effective_clarifications = current_sent;
1188
- if (current_pending.size > 0) {
1189
- // Create synthetic responded items from pending responses
1190
- const pending_items = [];
1191
- for (const [clar_id, response] of current_pending) {
1192
- if (!response.response_choice)
1193
- continue;
1194
- // Skip if already in sent_clarifications as responded
1195
- if (current_sent.some(c => c.id === clar_id && (c.status === 'responded' || c.status === 'resolved')))
1196
- continue;
1197
- const original = current_drafts.find(c => c.id === clar_id) ?? current_sent.find(c => c.id === clar_id);
1198
- if (original) {
1199
- pending_items.push({
1200
- ...original,
1201
- status: 'responded',
1202
- response_choice: response.response_choice,
1203
- user_comment: response.user_comment,
1204
- });
1205
- }
1206
- }
1207
- if (pending_items.length > 0) {
1208
- effective_clarifications = [...current_sent, ...pending_items];
1209
- }
1210
- }
1211
- const resolved_rules = build_resolved_rules_map(effective_clarifications);
1212
- const vr = await validate_classified_files({
1213
- classifications: new_classifications,
1214
- validation_api_endpoint: props.validation_api_endpoint,
1215
- validation_rules: props.validation_rules,
1216
- file_manager: props.file_manager,
1217
- front_form_data,
1218
- update_progress,
1219
- set_file_validation_results,
1220
- run_log,
1221
- tracker,
1222
- manual_review_file_ids,
1223
- check_type_filter: 'backoffice',
1224
- resolved_rules,
1225
- });
1226
- all_validation_results = vr.all_validation_results;
1227
- errors.push(...vr.errors);
1228
- logger.info('[use_llm_run] validation_phase_complete', {
1229
- total_results: vr.all_validation_results.length,
1230
- passed: vr.all_validation_results.filter(r => r.status === 'passed').length,
1231
- failed: vr.all_validation_results.filter(r => r.status === 'failed').length,
1232
- skipped: vr.all_validation_results.filter(r => r.status === 'skipped').length,
1233
- validation_errors: vr.errors.length,
1234
- });
1235
- }
1236
- // Phase 1 complete: classification + validation done.
1237
- // Routing + autofill deferred to trigger_complete (Phase 2).
1238
- const run_result = {
1239
- classifications: new_classifications,
1240
- unassigned_files: [],
1241
- validation_results: all_validation_results,
1242
- errors,
1243
- };
1244
- update_progress({
1245
- status: errors.length > 0 ? 'error' : 'validated',
1246
- completed_steps: fields_to_process.length,
1247
- total_steps: fields_to_process.length,
1248
- current_step: undefined,
1249
- overall_percent: 100,
1250
- error: errors.length > 0 ? `${errors.length} error(s) during processing` : undefined,
1251
- error_details: errors.length > 0 ? errors : undefined,
1252
- run_log,
1253
- });
1254
- logger.info('[use_llm_run] trigger_run_complete', {
1255
- mode,
1256
- total_duration_ms: Date.now() - run_start,
1257
- classification_count: new_classifications.length,
1258
- validation_count: all_validation_results.length,
1259
- error_count: errors.length,
1260
- run_log_entries: run_log.length,
1261
- });
1262
- on_run_complete?.(run_result);
1263
- // Switch to clarifications tab so user can review validation results
1264
- set_active_tab('clarifications');
1265
- // Note: sent_clarifications is read via ref (sent_clarifications_ref.current) to avoid stale closures
1266
- // eslint-disable-next-line react-hooks/exhaustive-deps
1267
- }, [props, classification_results, update_progress, set_classification_results, set_active_tab, add_classifying_files, remove_classifying_files, add_queued_files, remove_queued_files, set_unassigned_files, set_group_autofill_log, set_file_validation_results, manual_review_file_ids]);
1268
- /** Route specific files (by file_id) to back-office groups using their classification tags + trigger autofill.
1269
- * Used by "skip validation and process" flow in clarifications view and review queue accept flow. */
1270
- const route_skipped_files = useCallback(async (file_ids) => {
1271
- if (file_ids.length === 0)
1272
- return;
1273
- const { back_sections, front_form_data, on_back_change, back_form_data, autofill_api_endpoint, file_manager } = props;
1274
- // Build synthetic passed_classifications containing only the specified files
1275
- const file_id_set = new Set(file_ids);
1276
- const synthetic_classifications = classification_results
1277
- .map((cls) => ({
1278
- field_id: cls.field_id,
1279
- tags: cls.tags,
1280
- file_classifications: cls.file_classifications.filter((fc) => file_id_set.has(fc.file_id)),
1281
- }))
1282
- .filter((cls) => cls.file_classifications.length > 0);
1283
- if (synthetic_classifications.length === 0)
1284
- return;
1285
- // Show queued state immediately so Tax Data view reflects pending files
1286
- add_queued_files(file_ids);
1287
- const handle_autofill_file = (file_id, active) => {
1288
- set_autofilling_file_ids(prev => {
1289
- const next = new Set(prev);
1290
- if (active)
1291
- next.add(file_id);
1292
- else
1293
- next.delete(file_id);
1294
- return next;
1295
- });
1296
- };
1297
- const run_log = [];
1298
- await route_files_to_back_office({
1299
- classifications: synthetic_classifications,
1300
- back_sections,
1301
- front_form_data,
1302
- on_back_change,
1303
- back_form_data,
1304
- autofill_api_endpoint,
1305
- file_manager,
1306
- update_progress,
1307
- set_group_autofill_log,
1308
- run_log,
1309
- force_autofill: true,
1310
- on_autofill_file: handle_autofill_file,
1311
- on_queue_files: (fids, queued) => {
1312
- if (queued)
1313
- add_queued_files(fids);
1314
- else
1315
- remove_queued_files(fids);
1316
- },
1317
- on_autofill_activity: emit_autofill_activity,
1318
- autofilled_file_groups: get_autofilled_file_groups(form_data_entries_ref.current ?? []),
1319
- on_autofill_run: record_autofill_run,
1320
- });
1321
- }, [props, classification_results, update_progress, set_group_autofill_log, add_queued_files, remove_queued_files, set_autofilling_file_ids, emit_autofill_activity, record_autofill_run]);
1322
- /** Phase 2: Route eligible files to back-office groups and trigger autofill.
1323
- * Called when user clicks "Complete" after classification + validation (Phase 1).
1324
- * Includes files that passed validation AND files whose validation issues were resolved
1325
- * via clarifications. For resolved clarifications with response files, both the original
1326
- * file and response files are routed using the original file's tags. */
1327
- const trigger_complete = useCallback(async () => {
1328
- const { on_back_change, back_form_data, back_sections, front_form_data, autofill_api_endpoint, file_manager, on_run_complete } = props;
1329
- const complete_start = Date.now();
1330
- logger.info('[use_llm_run] trigger_complete_start', {
1331
- classification_count: classification_results.length,
1332
- total_file_classifications: classification_results.reduce((sum, c) => sum + c.file_classifications.length, 0),
1333
- validation_result_count: Object.keys(file_validation_results).length,
1334
- has_on_back_change: !!on_back_change,
1335
- has_autofill_endpoint: !!autofill_api_endpoint,
1336
- });
1337
- if (!on_back_change)
1338
- return;
1339
- set_group_autofill_log({});
1340
- update_progress({ status: 'routing', total_steps: 1, completed_steps: 0, current_step: 'Routing files to back-office...', error: undefined });
1341
- const current_sent = sent_clarifications_ref.current;
1342
- const current_pending = pending_responses_ref.current;
1343
- const current_drafts = draft_clarifications_ref.current;
1344
- // Build a unified view of all clarification items with their latest status.
1345
- // Clarifications may be in drafts (auto-populated from validation), sent (approved by agent),
1346
- // or have pending responses (client responded but not yet flushed). Merge them all.
1347
- const all_clarification_items = new Map();
1348
- for (const item of current_drafts)
1349
- all_clarification_items.set(item.id, item);
1350
- for (const item of current_sent)
1351
- all_clarification_items.set(item.id, item);
1352
- // Apply pending responses on top
1353
- for (const [clar_id, response] of current_pending) {
1354
- const existing = all_clarification_items.get(clar_id);
1355
- if (existing && response.response_choice) {
1356
- all_clarification_items.set(clar_id, {
1357
- ...existing,
1358
- status: 'responded',
1359
- response_choice: response.response_choice,
1360
- user_comment: response.user_comment,
1361
- response_files: response.response_files ?? existing.response_files ?? [],
1362
- });
1363
- }
1364
- }
1365
- // Build a lookup of all file attachments from front_form_data
1366
- const all_attachments = new Map();
1367
- for (const value of Object.values(front_form_data)) {
1368
- if (!Array.isArray(value))
1369
- continue;
1370
- for (const block of value) {
1371
- if (block?.type === 'file' && block?.attachment?.file_id) {
1372
- all_attachments.set(block.attachment.file_id, block.attachment);
1373
- }
1374
- }
1375
- }
1376
- // Determine eligible files: passed/skipped/pending/validating, OR failed but all clarifications resolved
1377
- const eligible_classifications = [];
1378
- for (const cls of classification_results) {
1379
- const eligible_file_cls = [];
1380
- for (const fc of cls.file_classifications) {
1381
- const vr = file_validation_results[fc.file_id];
1382
- if (!vr || vr.status === 'passed' || vr.status === 'skipped' || vr.status === 'pending' || vr.status === 'validating' || vr.status === 'manual_review') {
1383
- // No validation, passed, skipped, manual_review, or validation not yet complete — eligible
1384
- eligible_file_cls.push(fc);
1385
- }
1386
- else if (vr.status === 'failed') {
1387
- // Failed — check if ALL its clarification errors are resolved (in sent, drafts, or pending).
1388
- // 'ignore' response means the user wants to skip this file entirely — treat as NOT resolved.
1389
- const all_resolved = vr.errors.length === 0 || vr.errors.every(err => {
1390
- const item = all_clarification_items.get(err.id);
1391
- if (!item || (item.status !== 'responded' && item.status !== 'resolved'))
1392
- return false;
1393
- return item.response_choice !== 'ignore';
1394
- });
1395
- if (all_resolved) {
1396
- eligible_file_cls.push(fc);
1397
- }
1398
- else {
1399
- logger.warn('[use_llm_run] file ineligible: failed validation with unresolved errors', { file_id: fc.file_id, file_name: fc.file_name, error_count: vr.errors.length });
1400
- }
1401
- }
1402
- else if (vr.status === 'manual_review') {
1403
- // Client requested manual review — still eligible for routing/autofill
1404
- eligible_file_cls.push(fc);
1405
- }
1406
- else {
1407
- logger.warn('[use_llm_run] file ineligible: unexpected validation status', { file_id: fc.file_id, status: vr.status });
1408
- }
1409
- }
1410
- if (eligible_file_cls.length > 0) {
1411
- eligible_classifications.push({
1412
- field_id: cls.field_id,
1413
- tags: [...new Set(eligible_file_cls.flatMap(fc => fc.tags))],
1414
- file_classifications: eligible_file_cls,
1415
- });
1416
- }
1417
- }
1418
- // Also include response files from resolved clarifications.
1419
- // These inherit the original file's tags and get routed to the same groups.
1420
- const routed_file_ids = new Set(eligible_classifications.flatMap(c => c.file_classifications.map(f => f.file_id)));
1421
- for (const item of all_clarification_items.values()) {
1422
- if ((item.status !== 'responded' && item.status !== 'resolved') || !item.response_files?.length)
1423
- continue;
1424
- const source_file_id = item.doc_references?.[0]?.file_id;
1425
- if (!source_file_id)
1426
- continue;
1427
- // Find the original file's tags
1428
- let source_tags = [];
1429
- let source_field_id = '';
1430
- for (const cls of classification_results) {
1431
- const fc = cls.file_classifications.find(f => f.file_id === source_file_id);
1432
- if (fc && fc.tags.length > 0) {
1433
- source_tags = fc.tags;
1434
- source_field_id = cls.field_id;
1435
- break;
1436
- }
1437
- }
1438
- if (source_tags.length === 0)
1439
- continue;
1440
- const new_file_cls = [];
1441
- for (const att of item.response_files) {
1442
- if (!routed_file_ids.has(att.file_id)) {
1443
- new_file_cls.push({ file_id: att.file_id, file_name: att.file_name, tags: source_tags });
1444
- routed_file_ids.add(att.file_id);
1445
- }
1446
- }
1447
- if (new_file_cls.length > 0) {
1448
- eligible_classifications.push({
1449
- field_id: source_field_id,
1450
- tags: source_tags,
1451
- file_classifications: new_file_cls,
1452
- });
1453
- }
1454
- }
1455
- // Route + autofill
1456
- const handle_autofill_file = (file_id, active) => {
1457
- set_autofilling_file_ids(prev => {
1458
- const next = new Set(prev);
1459
- if (active)
1460
- next.add(file_id);
1461
- else
1462
- next.delete(file_id);
1463
- return next;
1464
- });
1465
- };
1466
- const run_log = [];
1467
- const routing_result = await route_files_to_back_office({
1468
- classifications: eligible_classifications,
1469
- back_sections,
1470
- front_form_data,
1471
- on_back_change,
1472
- back_form_data,
1473
- autofill_api_endpoint,
1474
- file_manager,
1475
- update_progress,
1476
- set_group_autofill_log,
1477
- run_log,
1478
- on_autofill_file: handle_autofill_file,
1479
- on_queue_files: (file_ids, queued) => {
1480
- if (queued)
1481
- add_queued_files(file_ids);
1482
- else
1483
- remove_queued_files(file_ids);
1484
- },
1485
- on_autofill_activity: emit_autofill_activity,
1486
- autofilled_file_groups: get_autofilled_file_groups(form_data_entries_ref.current ?? []),
1487
- on_autofill_run: record_autofill_run,
1488
- });
1489
- set_unassigned_files(routing_result.unassigned);
1490
- logger.info('[use_llm_run] trigger_complete_done', {
1491
- duration_ms: Date.now() - complete_start,
1492
- eligible_file_count: eligible_classifications.reduce((sum, c) => sum + c.file_classifications.length, 0),
1493
- unassigned_count: routing_result.unassigned.length,
1494
- routing_errors: routing_result.errors.length,
1495
- });
1496
- update_progress({
1497
- status: routing_result.errors.length > 0 ? 'error' : 'done',
1498
- completed_steps: 1,
1499
- total_steps: 1,
1500
- current_step: undefined,
1501
- overall_percent: 100,
1502
- error: routing_result.errors.length > 0 ? `${routing_result.errors.length} error(s) during routing` : undefined,
1503
- run_log,
1504
- });
1505
- set_active_tab('back');
1506
- }, [props, classification_results, file_validation_results, update_progress, set_active_tab, set_unassigned_files, set_group_autofill_log]);
1507
- /** Run the back-office pipeline: backoffice validation → routing → autofill.
1508
- * Classification is NOT re-run — it's already done per-file in the front office.
1509
- * This is triggered manually by the "Run on Backoffice" button. */
1510
- const trigger_backoffice_run = useCallback(async () => {
1511
- const { on_back_change, back_form_data, back_sections, front_form_data, autofill_api_endpoint, file_manager, on_run_complete, validation_api_endpoint, validation_rules } = props;
1512
- const bo_start = Date.now();
1513
- logger.info('[use_llm_run] trigger_backoffice_run_start', {
1514
- classification_count: classification_results.length,
1515
- has_validation: !!(validation_api_endpoint && validation_rules?.length),
1516
- rule_count: validation_rules?.length ?? 0,
1517
- has_on_back_change: !!on_back_change,
1518
- has_autofill_endpoint: !!autofill_api_endpoint,
1519
- back_sections_count: back_sections.length,
1520
- back_section_groups: back_sections.map(s => (s.groups ?? []).map((g) => ({ id: g.id, tag_id: g.tag_id, prompt_area: g.prompt_area, prompt_key: g.prompt_key }))),
1521
- });
1522
- if (!on_back_change) {
1523
- logger.warn('[use_llm_run] trigger_backoffice_run: no on_back_change — aborting');
1524
- return;
1525
- }
1526
- if (classification_results.length === 0) {
1527
- logger.warn('[use_llm_run] trigger_backoffice_run: no classification_results — aborting');
1528
- return;
1529
- }
1530
- set_group_autofill_log({});
1531
- const run_log = [];
1532
- const errors = [];
1533
- // ── Queue all classified files as "queued" so UI shows pending state ──
1534
- const all_classified_file_ids = classification_results.flatMap(c => c.file_classifications.map(fc => fc.file_id));
1535
- if (all_classified_file_ids.length > 0) {
1536
- add_queued_files(all_classified_file_ids);
1537
- }
1538
- // ── Snapshot refs BEFORE backoffice validation ──
1539
- // Backoffice validation triggers set_file_validation_results → useEffect clears/replaces
1540
- // draft_clarifications via React state flush during await. Capture stable copies now.
1541
- const snapshot_sent = [...sent_clarifications_ref.current];
1542
- const snapshot_pending = new Map(pending_responses_ref.current);
1543
- const snapshot_drafts = [...draft_clarifications_ref.current];
1544
- // Build unified clarification items view from the snapshot
1545
- const all_clarification_items = new Map();
1546
- for (const item of snapshot_drafts)
1547
- all_clarification_items.set(item.id, item);
1548
- for (const item of snapshot_sent)
1549
- all_clarification_items.set(item.id, item);
1550
- for (const [clar_id, response] of snapshot_pending) {
1551
- const existing = all_clarification_items.get(clar_id);
1552
- if (existing && response.response_choice) {
1553
- all_clarification_items.set(clar_id, {
1554
- ...existing,
1555
- status: 'responded',
1556
- response_choice: response.response_choice,
1557
- user_comment: response.user_comment,
1558
- response_files: response.response_files ?? existing.response_files ?? [],
1559
- });
1560
- }
1561
- }
1562
- // Build resolved rules map from snapshot (for backoffice validation to skip already-resolved rules)
1563
- let effective_clarifications_for_rules = snapshot_sent;
1564
- if (snapshot_pending.size > 0) {
1565
- const sent_responded_ids = new Set(snapshot_sent.filter(c => c.status === 'responded' || c.status === 'resolved').map(c => c.id));
1566
- const pending_items = [];
1567
- for (const [clar_id, response] of snapshot_pending) {
1568
- if (!response.response_choice)
1569
- continue;
1570
- if (sent_responded_ids.has(clar_id))
1571
- continue;
1572
- const original = all_clarification_items.get(clar_id);
1573
- if (original) {
1574
- pending_items.push({ ...original, status: 'responded', response_choice: response.response_choice, user_comment: response.user_comment });
1575
- }
1576
- }
1577
- if (pending_items.length > 0) {
1578
- effective_clarifications_for_rules = [...snapshot_sent, ...pending_items];
1579
- }
1580
- }
1581
- // ── Step 1: Backoffice validation ──
1582
- let effective_file_validation = { ...file_validation_results };
1583
- const has_backoffice_validation = !!(validation_api_endpoint && validation_rules?.length);
1584
- if (has_backoffice_validation) {
1585
- update_progress({ status: 'validating', total_steps: 1, completed_steps: 0, current_step: 'Running back-office validation...', error: undefined });
1586
- const resolved_rules = build_resolved_rules_map(effective_clarifications_for_rules);
1587
- // Also mark accepted rules as resolved so they skip re-validation
1588
- for (const [, result] of Object.entries(effective_file_validation)) {
1589
- if (result.rule_results) {
1590
- for (const rr of result.rule_results) {
1591
- if (rr.accepted && rr.rule_id) {
1592
- resolved_rules.set(rr.rule_id, { response_choice: rr.user_resolution?.response_choice ?? 'accepted', user_comment: rr.user_resolution?.user_comment });
1593
- }
1594
- }
1595
- }
1596
- }
1597
- const vr = await validate_classified_files({
1598
- classifications: classification_results,
1599
- validation_api_endpoint: validation_api_endpoint,
1600
- validation_rules: validation_rules,
1601
- file_manager,
1602
- front_form_data,
1603
- update_progress,
1604
- set_file_validation_results,
1605
- run_log,
1606
- manual_review_file_ids,
1607
- check_type_filter: 'backoffice',
1608
- resolved_rules,
1609
- });
1610
- errors.push(...vr.errors);
1611
- // Merge backoffice validation results into local copy for eligibility check
1612
- for (const result of vr.all_validation_results) {
1613
- effective_file_validation[result.file_id] = result;
1614
- }
1615
- }
1616
- // ── Step 1b: Text classification ──
1617
- // Extract text from front-office fields (non-file content) and classify it
1618
- const { field_textbox_configs, llm_api_endpoint } = props;
1619
- if (field_textbox_configs && llm_api_endpoint) {
1620
- const text_entries = [];
1621
- for (const field_id of Object.keys(field_textbox_configs)) {
1622
- const text = get_text_from_value(front_form_data[field_id]);
1623
- if (text.trim().length > 0) {
1624
- text_entries.push({ field_id, text });
1625
- }
1626
- }
1627
- if (text_entries.length > 0) {
1628
- update_progress({ current_step: `Classifying ${text_entries.length} text entry(ies)...` });
1629
- for (const entry of text_entries) {
1630
- try {
1631
- const classify_text_start = Date.now();
1632
- const response = await fetch(llm_api_endpoint, {
1633
- method: 'POST',
1634
- headers: { 'Content-Type': 'application/json' },
1635
- body: JSON.stringify({
1636
- action: 'classify_text',
1637
- field_id: entry.field_id,
1638
- text: entry.text,
1639
- available_tags: field_textbox_configs[entry.field_id]?.classification?.available_tags ?? props.available_tags ?? [],
1640
- ...(props.available_document_types?.length ? { available_document_types: props.available_document_types } : {}),
1641
- }),
1642
- });
1643
- const result = await response.json();
1644
- run_log.push({
1645
- step: 'classify_text',
1646
- label: `Text Classification: ${entry.field_id}`,
1647
- request: { action: 'classify_text', field_id: entry.field_id, text_length: entry.text.length },
1648
- response: result,
1649
- timestamp: classify_text_start,
1650
- duration_ms: Date.now() - classify_text_start,
1651
- });
1652
- if (result.success && result.tags?.length > 0) {
1653
- // Add text classification to classification_results so review queue can use categories
1654
- set_classification_results((prev) => {
1655
- const text_cls_id = `__text_${entry.field_id}`;
1656
- const text_file_cls = {
1657
- file_id: text_cls_id,
1658
- file_name: `Text from ${entry.field_id}`,
1659
- tags: result.tags,
1660
- document_nature: result.document_nature,
1661
- };
1662
- const existing = prev.find((c) => c.field_id === entry.field_id);
1663
- if (existing) {
1664
- // Merge with existing file classifications
1665
- const filtered = existing.file_classifications.filter((fc) => fc.file_id !== text_cls_id);
1666
- return [
1667
- ...prev.filter((c) => c.field_id !== entry.field_id),
1668
- {
1669
- ...existing,
1670
- tags: [...new Set([...existing.tags, ...result.tags])],
1671
- file_classifications: [...filtered, text_file_cls],
1672
- },
1673
- ];
1674
- }
1675
- return [...prev, { field_id: entry.field_id, tags: result.tags, file_classifications: [text_file_cls] }];
1676
- });
1677
- }
1678
- // Create draft clarifications from text issues
1679
- if (result.success && result.issues?.length > 0) {
1680
- const text_clarifications = result.issues.map((issue, idx) => ({
1681
- id: `text_issue_${entry.field_id}_${idx}_${Date.now()}`,
1682
- type: 'validation_issue',
1683
- status: 'pending',
1684
- target_field_id: entry.field_id,
1685
- target_label: `Text from ${entry.field_id}`,
1686
- issue_description: issue.description,
1687
- severity: issue.severity === 'error' ? 'error' : 'warning',
1688
- doc_references: [{
1689
- file_id: `__text_${entry.field_id}`,
1690
- file_name: `Text from ${entry.field_id}`,
1691
- mime_type: 'text/plain',
1692
- }],
1693
- response_options: [
1694
- { value: 'acknowledge', label: 'Acknowledge' },
1695
- { value: 'will_fix', label: 'Will fix' },
1696
- { value: 'ignore', label: 'Ignore' },
1697
- ],
1698
- created_at: new Date().toISOString(),
1699
- }));
1700
- set_file_validation_results((prev) => ({
1701
- ...prev,
1702
- [`__text_${entry.field_id}`]: {
1703
- file_id: `__text_${entry.field_id}`,
1704
- file_name: `Text from ${entry.field_id}`,
1705
- status: 'failed',
1706
- errors: text_clarifications,
1707
- document_types: [],
1708
- },
1709
- }));
1710
- }
1711
- }
1712
- catch (err) {
1713
- errors.push({ step: `classify_text:${entry.field_id}`, error: err instanceof Error ? err.message : 'Text classification failed' });
1714
- logger.error('[use_llm_run] classify_text_error', { field_id: entry.field_id, error: err instanceof Error ? err.message : String(err) });
1715
- }
1716
- }
1717
- logger.info('[use_llm_run] text_classification_phase_complete', { text_entry_count: text_entries.length });
1718
- }
1719
- }
1720
- // Clear queued state — autofill will re-queue eligible files via on_queue_files
1721
- if (all_classified_file_ids.length > 0) {
1722
- remove_queued_files(all_classified_file_ids);
1723
- }
1724
- // ── Step 2: Determine eligible files and route + autofill ──
1725
- update_progress({ status: 'routing', total_steps: 1, completed_steps: 0, current_step: 'Routing files to back-office...', error: undefined });
1726
- // Determine eligible files — use effective_file_validation which merges backoffice results
1727
- const eligible_classifications = [];
1728
- for (const cls of classification_results) {
1729
- const eligible_file_cls = [];
1730
- for (const fc of cls.file_classifications) {
1731
- // Skip files already routed via review accept / skip_process (prevent double-processing)
1732
- if (skipped_file_ids_ref.current.has(fc.file_id)) {
1733
- logger.info('[use_llm_run] backoffice: file already routed via review accept — skipping', { file_id: fc.file_id });
1734
- continue;
1735
- }
1736
- const vr = effective_file_validation[fc.file_id];
1737
- // Exclude files with any rule sent back to client (pending client fix)
1738
- const has_sent_back = vr?.rule_results?.some(rr => rr.sent_back);
1739
- if (has_sent_back) {
1740
- logger.warn('[use_llm_run] backoffice: file ineligible — has sent-back rule pending client response', { file_id: fc.file_id });
1741
- continue;
1742
- }
1743
- // Exclude files with unreviewed resolved rules (pending agent acceptance)
1744
- const resolved_rules_for_file = vr?.rule_results?.filter(rr => rr.issues.length > 0 && rr.user_resolved) ?? [];
1745
- if (resolved_rules_for_file.length > 0 && !resolved_rules_for_file.every(rr => rr.accepted)) {
1746
- logger.warn('[use_llm_run] backoffice: file ineligible — has unaccepted resolved rules', { file_id: fc.file_id });
1747
- continue;
1748
- }
1749
- if (!vr || vr.status === 'passed' || vr.status === 'skipped' || vr.status === 'pending' || vr.status === 'validating' || vr.status === 'manual_review') {
1750
- eligible_file_cls.push(fc);
1751
- }
1752
- else if (vr.status === 'failed') {
1753
- const all_resolved = vr.errors.length === 0 || vr.errors.every(err => {
1754
- const item = all_clarification_items.get(err.id);
1755
- if (!item || (item.status !== 'responded' && item.status !== 'resolved'))
1756
- return false;
1757
- return item.response_choice !== 'ignore';
1758
- });
1759
- if (all_resolved)
1760
- eligible_file_cls.push(fc);
1761
- else
1762
- logger.warn('[use_llm_run] backoffice: file ineligible — failed with unresolved errors', { file_id: fc.file_id, error_count: vr.errors.length, errors: vr.errors.map(e => ({ id: e.id, status: e.status, rule_id: e.rule_id })) });
1763
- }
1764
- else {
1765
- logger.warn('[use_llm_run] backoffice: file ineligible — unexpected status', { file_id: fc.file_id, status: vr.status });
1766
- }
1767
- }
1768
- if (eligible_file_cls.length > 0) {
1769
- eligible_classifications.push({
1770
- field_id: cls.field_id,
1771
- tags: [...new Set(eligible_file_cls.flatMap(fc => fc.tags))],
1772
- file_classifications: eligible_file_cls,
1773
- });
1774
- }
1775
- }
1776
- // Include response files from resolved clarifications
1777
- const routed_file_ids = new Set(eligible_classifications.flatMap(c => c.file_classifications.map(f => f.file_id)));
1778
- for (const item of all_clarification_items.values()) {
1779
- if ((item.status !== 'responded' && item.status !== 'resolved') || !item.response_files?.length)
1780
- continue;
1781
- const source_file_id = item.doc_references?.[0]?.file_id;
1782
- if (!source_file_id)
1783
- continue;
1784
- let source_tags = [];
1785
- let source_field_id = '';
1786
- for (const cls of classification_results) {
1787
- const fc = cls.file_classifications.find(f => f.file_id === source_file_id);
1788
- if (fc && fc.tags.length > 0) {
1789
- source_tags = fc.tags;
1790
- source_field_id = cls.field_id;
1791
- break;
1792
- }
1793
- }
1794
- if (source_tags.length === 0)
1795
- continue;
1796
- const new_file_cls = [];
1797
- for (const att of item.response_files) {
1798
- if (!routed_file_ids.has(att.file_id)) {
1799
- new_file_cls.push({ file_id: att.file_id, file_name: att.file_name, tags: source_tags });
1800
- routed_file_ids.add(att.file_id);
1801
- }
1802
- }
1803
- if (new_file_cls.length > 0) {
1804
- eligible_classifications.push({ field_id: source_field_id, tags: source_tags, file_classifications: new_file_cls });
1805
- }
1806
- }
1807
- // Route + autofill (force_autofill: true so "Run Again" always re-runs autofill)
1808
- const handle_autofill_file = (file_id, active) => {
1809
- set_autofilling_file_ids(prev => {
1810
- const next = new Set(prev);
1811
- if (active)
1812
- next.add(file_id);
1813
- else
1814
- next.delete(file_id);
1815
- return next;
1816
- });
1817
- };
1818
- const routing_result = await route_files_to_back_office({
1819
- classifications: eligible_classifications,
1820
- back_sections,
1821
- front_form_data,
1822
- on_back_change,
1823
- back_form_data,
1824
- autofill_api_endpoint,
1825
- file_manager,
1826
- update_progress,
1827
- set_group_autofill_log,
1828
- run_log,
1829
- on_autofill_file: handle_autofill_file,
1830
- on_queue_files: (file_ids, queued) => {
1831
- if (queued)
1832
- add_queued_files(file_ids);
1833
- else
1834
- remove_queued_files(file_ids);
1835
- },
1836
- force_autofill: true,
1837
- on_autofill_activity: emit_autofill_activity,
1838
- autofilled_file_groups: get_autofilled_file_groups(form_data_entries_ref.current ?? []),
1839
- on_autofill_run: record_autofill_run,
1840
- });
1841
- set_unassigned_files(routing_result.unassigned);
1842
- errors.push(...routing_result.errors);
1843
- logger.info('[use_llm_run] trigger_backoffice_run_complete', {
1844
- duration_ms: Date.now() - bo_start,
1845
- eligible_file_count: eligible_classifications.reduce((sum, c) => sum + c.file_classifications.length, 0),
1846
- unassigned_count: routing_result.unassigned.length,
1847
- error_count: errors.length,
1848
- });
1849
- update_progress({
1850
- status: errors.length > 0 ? 'error' : 'done',
1851
- completed_steps: 1,
1852
- total_steps: 1,
1853
- current_step: undefined,
1854
- overall_percent: 100,
1855
- error: errors.length > 0 ? `${errors.length} error(s) during processing` : undefined,
1856
- error_details: errors.length > 0 ? errors : undefined,
1857
- run_log,
1858
- });
1859
- set_active_tab('back');
1860
- }, [props, classification_results, file_validation_results, update_progress, set_active_tab, set_unassigned_files, set_group_autofill_log, set_file_validation_results, manual_review_file_ids]);
1861
- return { trigger_run, trigger_complete, trigger_classify_file, trigger_assign_file, route_skipped_files, trigger_backoffice_run };
1862
- }
1863
- //# sourceMappingURL=use_llm_run.js.map