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.
- package/CHANGE_LOG.md +115 -0
- package/README.md +203 -111
- package/dist/audit/built_in_actions.d.ts +6 -0
- package/dist/audit/built_in_actions.d.ts.map +1 -0
- package/dist/audit/built_in_actions.js +23 -0
- package/dist/audit/built_in_actions.js.map +1 -0
- package/dist/audit/intent_emitter.d.ts +4 -0
- package/dist/audit/intent_emitter.d.ts.map +1 -0
- package/dist/audit/intent_emitter.js +25 -0
- package/dist/audit/intent_emitter.js.map +1 -0
- package/dist/audit/lazy_audit_icon.d.ts +8 -0
- package/dist/audit/lazy_audit_icon.d.ts.map +1 -0
- package/dist/audit/lazy_audit_icon.js +20 -0
- package/dist/audit/lazy_audit_icon.js.map +1 -0
- package/dist/components/_internal_form_set.d.ts +6 -0
- package/dist/components/_internal_form_set.d.ts.map +1 -1
- package/dist/components/_internal_form_set.js +48 -51
- package/dist/components/_internal_form_set.js.map +1 -1
- package/dist/components/clarification/clarification_item_body.d.ts +1 -1
- package/dist/components/clarification/clarification_item_body.d.ts.map +1 -1
- package/dist/components/clarification/resolution_status_strip.d.ts +1 -1
- package/dist/components/clarification/resolution_status_strip.d.ts.map +1 -1
- package/dist/components/field_audit/auditor.d.ts +30 -0
- package/dist/components/field_audit/auditor.d.ts.map +1 -0
- package/dist/components/field_audit/auditor.js +91 -0
- package/dist/components/field_audit/auditor.js.map +1 -0
- package/dist/components/field_audit/context.d.ts +29 -0
- package/dist/components/field_audit/context.d.ts.map +1 -0
- package/dist/components/field_audit/context.js +123 -0
- package/dist/components/field_audit/context.js.map +1 -0
- package/dist/components/field_audit/field_audit_icon.d.ts +12 -0
- package/dist/components/field_audit/field_audit_icon.d.ts.map +1 -0
- package/dist/components/field_audit/field_audit_icon.js +23 -0
- package/dist/components/field_audit/field_audit_icon.js.map +1 -0
- package/dist/components/field_audit/field_audit_panel.d.ts +9 -0
- package/dist/components/field_audit/field_audit_panel.d.ts.map +1 -0
- package/dist/components/field_audit/field_audit_panel.js +54 -0
- package/dist/components/field_audit/field_audit_panel.js.map +1 -0
- package/dist/components/field_audit/index.d.ts +33 -0
- package/dist/components/field_audit/index.d.ts.map +1 -0
- package/dist/components/field_audit/index.js +29 -0
- package/dist/components/field_audit/index.js.map +1 -0
- package/dist/components/field_audit/types.d.ts +75 -0
- package/dist/components/field_audit/types.d.ts.map +1 -0
- package/dist/components/field_audit/types.js +10 -0
- package/dist/components/field_audit/types.js.map +1 -0
- package/dist/components/field_audit/with_field_audit.d.ts +32 -0
- package/dist/components/field_audit/with_field_audit.d.ts.map +1 -0
- package/dist/components/field_audit/with_field_audit.js +42 -0
- package/dist/components/field_audit/with_field_audit.js.map +1 -0
- package/dist/components/hazo_collab_form_checkbox.d.ts.map +1 -1
- package/dist/components/hazo_collab_form_checkbox.js +3 -1
- package/dist/components/hazo_collab_form_checkbox.js.map +1 -1
- package/dist/components/hazo_collab_form_doc.d.ts.map +1 -1
- package/dist/components/hazo_collab_form_doc.js +4 -1
- package/dist/components/hazo_collab_form_doc.js.map +1 -1
- package/dist/components/hazo_collab_form_radio.d.ts.map +1 -1
- package/dist/components/hazo_collab_form_radio.js +4 -2
- package/dist/components/hazo_collab_form_radio.js.map +1 -1
- package/dist/components/hazo_collab_form_view/context.d.ts +7 -0
- package/dist/components/hazo_collab_form_view/context.d.ts.map +1 -1
- package/dist/components/hazo_collab_form_view/context.js +46 -0
- package/dist/components/hazo_collab_form_view/context.js.map +1 -1
- package/dist/components/hazo_collab_form_view/hooks/use_view_callbacks.d.ts +8 -1
- package/dist/components/hazo_collab_form_view/hooks/use_view_callbacks.d.ts.map +1 -1
- package/dist/components/hazo_collab_form_view/hooks/use_view_callbacks.js +4 -2
- package/dist/components/hazo_collab_form_view/hooks/use_view_callbacks.js.map +1 -1
- package/dist/components/hazo_collab_form_view/index.d.ts +1 -1
- package/dist/components/hazo_collab_form_view/index.d.ts.map +1 -1
- package/dist/components/hazo_collab_form_view/index.js +59 -3
- package/dist/components/hazo_collab_form_view/index.js.map +1 -1
- package/dist/components/hazo_collab_form_view/types.d.ts +134 -0
- package/dist/components/hazo_collab_form_view/types.d.ts.map +1 -1
- package/dist/components/hazo_collab_form_view/views/approval_view.d.ts.map +1 -1
- package/dist/components/hazo_collab_form_view/views/approval_view.js +3 -1
- package/dist/components/hazo_collab_form_view/views/approval_view.js.map +1 -1
- package/dist/components/hazo_collab_form_view/views/edit_view.d.ts.map +1 -1
- package/dist/components/hazo_collab_form_view/views/edit_view.js +8 -3
- package/dist/components/hazo_collab_form_view/views/edit_view.js.map +1 -1
- package/dist/components/hazo_collab_form_view/views/print_view.d.ts.map +1 -1
- package/dist/components/hazo_collab_form_view/views/print_view.js +3 -1
- package/dist/components/hazo_collab_form_view/views/print_view.js.map +1 -1
- package/dist/components/hazo_collab_form_view/views/summary_view.d.ts.map +1 -1
- package/dist/components/hazo_collab_form_view/views/summary_view.js +4 -2
- package/dist/components/hazo_collab_form_view/views/summary_view.js.map +1 -1
- package/dist/components/hazo_data_form/group_renderer.d.ts +8 -2
- package/dist/components/hazo_data_form/group_renderer.d.ts.map +1 -1
- package/dist/components/hazo_data_form/group_renderer.js +3 -3
- package/dist/components/hazo_data_form/group_renderer.js.map +1 -1
- package/dist/components/hazo_data_form/hazo_data_form.d.ts +2 -1
- package/dist/components/hazo_data_form/hazo_data_form.d.ts.map +1 -1
- package/dist/components/hazo_data_form/hazo_data_form.js +47 -6
- package/dist/components/hazo_data_form/hazo_data_form.js.map +1 -1
- package/dist/components/hazo_data_form/section_renderer.d.ts +4 -2
- package/dist/components/hazo_data_form/section_renderer.d.ts.map +1 -1
- package/dist/components/hazo_data_form/section_renderer.js +2 -2
- package/dist/components/hazo_data_form/section_renderer.js.map +1 -1
- package/dist/components/hazo_data_form/shared/data_form_field_layout.d.ts +4 -1
- package/dist/components/hazo_data_form/shared/data_form_field_layout.d.ts.map +1 -1
- package/dist/components/hazo_data_form/shared/data_form_field_layout.js +34 -8
- package/dist/components/hazo_data_form/shared/data_form_field_layout.js.map +1 -1
- package/dist/components/hazo_data_form/types.d.ts +56 -1
- package/dist/components/hazo_data_form/types.d.ts.map +1 -1
- package/dist/components/index.d.ts +5 -5
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +4 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/shared/base_field_layout.d.ts.map +1 -1
- package/dist/components/shared/base_field_layout.js +5 -1
- package/dist/components/shared/base_field_layout.js.map +1 -1
- package/dist/components/shared/field_action_array_slot.d.ts +10 -0
- package/dist/components/shared/field_action_array_slot.d.ts.map +1 -0
- package/dist/components/shared/field_action_array_slot.js +33 -0
- package/dist/components/shared/field_action_array_slot.js.map +1 -0
- package/dist/components/shared/field_action_slot.d.ts +22 -0
- package/dist/components/shared/field_action_slot.d.ts.map +1 -0
- package/dist/components/shared/field_action_slot.js +20 -0
- package/dist/components/shared/field_action_slot.js.map +1 -0
- package/dist/components/shared/ihelp_icon.d.ts +1 -1
- package/dist/components/shared/ihelp_icon.js +1 -1
- package/dist/components/shared/rule_result_card.js +1 -1
- package/dist/components/shared/rule_result_card.js.map +1 -1
- package/dist/components/shared/use_field_action_slot.d.ts +37 -0
- package/dist/components/shared/use_field_action_slot.d.ts.map +1 -0
- package/dist/components/shared/use_field_action_slot.js +77 -0
- package/dist/components/shared/use_field_action_slot.js.map +1 -0
- package/dist/components/thread_form/components/key_info_drawer.d.ts +7 -1
- package/dist/components/thread_form/components/key_info_drawer.d.ts.map +1 -1
- package/dist/components/thread_form/components/key_info_drawer.js +2 -2
- package/dist/components/thread_form/components/key_info_drawer.js.map +1 -1
- package/dist/components/thread_form/components/send_back_message.d.ts +1 -1
- package/dist/components/thread_form/components/send_back_message.d.ts.map +1 -1
- package/dist/components/thread_form/hooks/use_file_pipeline.d.ts +1 -1
- package/dist/components/thread_form/hooks/use_file_pipeline.d.ts.map +1 -1
- package/dist/components/thread_form/hooks/use_file_pipeline.js +1 -1
- package/dist/components/thread_form/hooks/use_file_pipeline.js.map +1 -1
- package/dist/components/thread_form/index.d.ts +1 -1
- package/dist/components/thread_form/index.d.ts.map +1 -1
- package/dist/components/thread_form/index.js.map +1 -1
- package/dist/components/thread_form/thread_form.d.ts.map +1 -1
- package/dist/components/thread_form/thread_form.js +3 -3
- package/dist/components/thread_form/thread_form.js.map +1 -1
- package/dist/components/thread_form/types.d.ts +32 -4
- package/dist/components/thread_form/types.d.ts.map +1 -1
- package/dist/components/thread_form/types.js.map +1 -1
- package/dist/lib/index.d.ts +0 -2
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +0 -2
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/resolution_handler.d.ts +1 -1
- package/dist/lib/resolution_handler.d.ts.map +1 -1
- package/dist/lib/resolve_variable.d.ts +1 -1
- package/dist/lib/resolve_variable.d.ts.map +1 -1
- package/dist/types/clarification.d.ts +1 -1
- package/dist/types/clarification.d.ts.map +1 -1
- package/dist/types/field_action.d.ts +25 -0
- package/dist/types/field_action.d.ts.map +1 -0
- package/dist/types/field_action.js +8 -0
- package/dist/types/field_action.js.map +1 -0
- package/dist/types/index.d.ts +3 -6
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -3
- package/dist/types/index.js.map +1 -1
- package/dist/types/{fb_form_data.d.ts → shared_data.d.ts} +1 -3
- package/dist/types/shared_data.d.ts.map +1 -0
- package/dist/types/{fb_form_data.js → shared_data.js} +1 -2
- package/dist/types/shared_data.js.map +1 -0
- package/dist/utils/dev_file_manager.d.ts +1 -1
- package/dist/utils/dev_file_manager.js +1 -1
- package/dist/{components/hazo_fb_form/shared → utils}/format.d.ts +2 -2
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/{components/hazo_fb_form/shared → utils}/format.js +1 -1
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/index.d.ts +1 -9
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -15
- package/dist/utils/index.js.map +1 -1
- package/package.json +6 -1
- package/dist/components/hazo_fb_form/components/backoffice_run_button.d.ts +0 -18
- package/dist/components/hazo_fb_form/components/backoffice_run_button.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/components/backoffice_run_button.js +0 -23
- package/dist/components/hazo_fb_form/components/backoffice_run_button.js.map +0 -1
- package/dist/components/hazo_fb_form/components/draft_clarification_card.d.ts +0 -39
- package/dist/components/hazo_fb_form/components/draft_clarification_card.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/components/draft_clarification_card.js +0 -94
- package/dist/components/hazo_fb_form/components/draft_clarification_card.js.map +0 -1
- package/dist/components/hazo_fb_form/components/fb_document_type_editor.d.ts +0 -11
- package/dist/components/hazo_fb_form/components/fb_document_type_editor.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/components/fb_document_type_editor.js +0 -82
- package/dist/components/hazo_fb_form/components/fb_document_type_editor.js.map +0 -1
- package/dist/components/hazo_fb_form/components/fb_tag_editor.d.ts +0 -11
- package/dist/components/hazo_fb_form/components/fb_tag_editor.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/components/fb_tag_editor.js +0 -107
- package/dist/components/hazo_fb_form/components/fb_tag_editor.js.map +0 -1
- package/dist/components/hazo_fb_form/components/front_office_stepper.d.ts +0 -15
- package/dist/components/hazo_fb_form/components/front_office_stepper.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/components/front_office_stepper.js +0 -21
- package/dist/components/hazo_fb_form/components/front_office_stepper.js.map +0 -1
- package/dist/components/hazo_fb_form/components/instance_sidebar.d.ts +0 -21
- package/dist/components/hazo_fb_form/components/instance_sidebar.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/components/instance_sidebar.js +0 -58
- package/dist/components/hazo_fb_form/components/instance_sidebar.js.map +0 -1
- package/dist/components/hazo_fb_form/components/reject_clarification_dialog.d.ts +0 -15
- package/dist/components/hazo_fb_form/components/reject_clarification_dialog.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/components/reject_clarification_dialog.js +0 -26
- package/dist/components/hazo_fb_form/components/reject_clarification_dialog.js.map +0 -1
- package/dist/components/hazo_fb_form/components/run_button.d.ts +0 -19
- package/dist/components/hazo_fb_form/components/run_button.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/components/run_button.js +0 -38
- package/dist/components/hazo_fb_form/components/run_button.js.map +0 -1
- package/dist/components/hazo_fb_form/components/run_details_dialog.d.ts +0 -17
- package/dist/components/hazo_fb_form/components/run_details_dialog.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/components/run_details_dialog.js +0 -35
- package/dist/components/hazo_fb_form/components/run_details_dialog.js.map +0 -1
- package/dist/components/hazo_fb_form/components/sent_clarification_group.d.ts +0 -30
- package/dist/components/hazo_fb_form/components/sent_clarification_group.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/components/sent_clarification_group.js +0 -76
- package/dist/components/hazo_fb_form/components/sent_clarification_group.js.map +0 -1
- package/dist/components/hazo_fb_form/components/tag_pill.d.ts +0 -15
- package/dist/components/hazo_fb_form/components/tag_pill.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/components/tag_pill.js +0 -15
- package/dist/components/hazo_fb_form/components/tag_pill.js.map +0 -1
- package/dist/components/hazo_fb_form/context.d.ts +0 -135
- package/dist/components/hazo_fb_form/context.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/context.js +0 -13
- package/dist/components/hazo_fb_form/context.js.map +0 -1
- package/dist/components/hazo_fb_form/hazo_fb_form.d.ts +0 -13
- package/dist/components/hazo_fb_form/hazo_fb_form.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/hazo_fb_form.js +0 -1188
- package/dist/components/hazo_fb_form/hazo_fb_form.js.map +0 -1
- package/dist/components/hazo_fb_form/hooks/use_fb_form_state.d.ts +0 -58
- package/dist/components/hazo_fb_form/hooks/use_fb_form_state.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/hooks/use_fb_form_state.js +0 -919
- package/dist/components/hazo_fb_form/hooks/use_fb_form_state.js.map +0 -1
- package/dist/components/hazo_fb_form/hooks/use_llm_run.d.ts +0 -52
- package/dist/components/hazo_fb_form/hooks/use_llm_run.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/hooks/use_llm_run.js +0 -1863
- package/dist/components/hazo_fb_form/hooks/use_llm_run.js.map +0 -1
- package/dist/components/hazo_fb_form/index.d.ts +0 -29
- package/dist/components/hazo_fb_form/index.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/index.js +0 -19
- package/dist/components/hazo_fb_form/index.js.map +0 -1
- package/dist/components/hazo_fb_form/shared/agent_stepper.d.ts +0 -9
- package/dist/components/hazo_fb_form/shared/agent_stepper.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/shared/agent_stepper.js +0 -17
- package/dist/components/hazo_fb_form/shared/agent_stepper.js.map +0 -1
- package/dist/components/hazo_fb_form/shared/clarification_helpers.d.ts +0 -15
- package/dist/components/hazo_fb_form/shared/clarification_helpers.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/shared/clarification_helpers.js +0 -23
- package/dist/components/hazo_fb_form/shared/clarification_helpers.js.map +0 -1
- package/dist/components/hazo_fb_form/shared/file_status_accordion.d.ts +0 -9
- package/dist/components/hazo_fb_form/shared/file_status_accordion.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/shared/file_status_accordion.js +0 -39
- package/dist/components/hazo_fb_form/shared/file_status_accordion.js.map +0 -1
- package/dist/components/hazo_fb_form/shared/file_utils.d.ts +0 -9
- package/dist/components/hazo_fb_form/shared/file_utils.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/shared/file_utils.js +0 -31
- package/dist/components/hazo_fb_form/shared/file_utils.js.map +0 -1
- package/dist/components/hazo_fb_form/shared/format.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/shared/format.js.map +0 -1
- package/dist/components/hazo_fb_form/shared/group_debug_icon.d.ts +0 -15
- package/dist/components/hazo_fb_form/shared/group_debug_icon.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/shared/group_debug_icon.js +0 -48
- package/dist/components/hazo_fb_form/shared/group_debug_icon.js.map +0 -1
- package/dist/components/hazo_fb_form/shared/index.d.ts +0 -10
- package/dist/components/hazo_fb_form/shared/index.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/shared/index.js +0 -9
- package/dist/components/hazo_fb_form/shared/index.js.map +0 -1
- package/dist/components/hazo_fb_form/shared/pdf_side_panel.d.ts +0 -22
- package/dist/components/hazo_fb_form/shared/pdf_side_panel.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/shared/pdf_side_panel.js +0 -10
- package/dist/components/hazo_fb_form/shared/pdf_side_panel.js.map +0 -1
- package/dist/components/hazo_fb_form/shared/send_back_item_card.d.ts +0 -44
- package/dist/components/hazo_fb_form/shared/send_back_item_card.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/shared/send_back_item_card.js +0 -80
- package/dist/components/hazo_fb_form/shared/send_back_item_card.js.map +0 -1
- package/dist/components/hazo_fb_form/shared/use_pdf_viewer.d.ts +0 -28
- package/dist/components/hazo_fb_form/shared/use_pdf_viewer.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/shared/use_pdf_viewer.js +0 -46
- package/dist/components/hazo_fb_form/shared/use_pdf_viewer.js.map +0 -1
- package/dist/components/hazo_fb_form/types.d.ts +0 -372
- package/dist/components/hazo_fb_form/types.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/types.js +0 -5
- package/dist/components/hazo_fb_form/types.js.map +0 -1
- package/dist/components/hazo_fb_form/views/back_office_view.d.ts +0 -7
- package/dist/components/hazo_fb_form/views/back_office_view.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/views/back_office_view.js +0 -425
- package/dist/components/hazo_fb_form/views/back_office_view.js.map +0 -1
- package/dist/components/hazo_fb_form/views/clarifications_view.d.ts +0 -16
- package/dist/components/hazo_fb_form/views/clarifications_view.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/views/clarifications_view.js +0 -291
- package/dist/components/hazo_fb_form/views/clarifications_view.js.map +0 -1
- package/dist/components/hazo_fb_form/views/client_data_view.d.ts +0 -6
- package/dist/components/hazo_fb_form/views/client_data_view.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/views/client_data_view.js +0 -39
- package/dist/components/hazo_fb_form/views/client_data_view.js.map +0 -1
- package/dist/components/hazo_fb_form/views/front_office_view.d.ts +0 -6
- package/dist/components/hazo_fb_form/views/front_office_view.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/views/front_office_view.js +0 -1351
- package/dist/components/hazo_fb_form/views/front_office_view.js.map +0 -1
- package/dist/components/hazo_fb_form/views/interim_view.d.ts +0 -8
- package/dist/components/hazo_fb_form/views/interim_view.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/views/interim_view.js +0 -535
- package/dist/components/hazo_fb_form/views/interim_view.js.map +0 -1
- package/dist/components/hazo_fb_form/views/review_queue_view.d.ts +0 -14
- package/dist/components/hazo_fb_form/views/review_queue_view.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/views/review_queue_view.js +0 -230
- package/dist/components/hazo_fb_form/views/review_queue_view.js.map +0 -1
- package/dist/components/hazo_fb_form/views/send_back_view.d.ts +0 -13
- package/dist/components/hazo_fb_form/views/send_back_view.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/views/send_back_view.js +0 -258
- package/dist/components/hazo_fb_form/views/send_back_view.js.map +0 -1
- package/dist/components/hazo_fb_form/views/summary_review_view.d.ts +0 -17
- package/dist/components/hazo_fb_form/views/summary_review_view.d.ts.map +0 -1
- package/dist/components/hazo_fb_form/views/summary_review_view.js +0 -258
- package/dist/components/hazo_fb_form/views/summary_review_view.js.map +0 -1
- package/dist/components/shared/json_data_panel/index.d.ts +0 -3
- package/dist/components/shared/json_data_panel/index.d.ts.map +0 -1
- package/dist/components/shared/json_data_panel/index.js +0 -2
- package/dist/components/shared/json_data_panel/index.js.map +0 -1
- package/dist/components/shared/json_data_panel/json_data_panel.d.ts +0 -28
- package/dist/components/shared/json_data_panel/json_data_panel.d.ts.map +0 -1
- package/dist/components/shared/json_data_panel/json_data_panel.js +0 -156
- package/dist/components/shared/json_data_panel/json_data_panel.js.map +0 -1
- package/dist/lib/fb_form_handler.d.ts +0 -63
- package/dist/lib/fb_form_handler.d.ts.map +0 -1
- package/dist/lib/fb_form_handler.js +0 -425
- package/dist/lib/fb_form_handler.js.map +0 -1
- package/dist/types/fb_form_data.d.ts.map +0 -1
- package/dist/types/fb_form_data.js.map +0 -1
- package/dist/types/fb_form_data_v1.d.ts +0 -250
- package/dist/types/fb_form_data_v1.d.ts.map +0 -1
- package/dist/types/fb_form_data_v1.js +0 -117
- package/dist/types/fb_form_data_v1.js.map +0 -1
- package/dist/types/fb_form_instance.d.ts +0 -49
- package/dist/types/fb_form_instance.d.ts.map +0 -1
- package/dist/types/fb_form_instance.js +0 -10
- package/dist/types/fb_form_instance.js.map +0 -1
- package/dist/utils/fb_data_adapter.d.ts +0 -33
- package/dist/utils/fb_data_adapter.d.ts.map +0 -1
- package/dist/utils/fb_data_adapter.js +0 -436
- package/dist/utils/fb_data_adapter.js.map +0 -1
- package/dist/utils/fb_data_adapter_v2.d.ts +0 -17
- package/dist/utils/fb_data_adapter_v2.d.ts.map +0 -1
- package/dist/utils/fb_data_adapter_v2.js +0 -483
- package/dist/utils/fb_data_adapter_v2.js.map +0 -1
- package/dist/utils/fb_data_helpers.d.ts +0 -86
- package/dist/utils/fb_data_helpers.d.ts.map +0 -1
- package/dist/utils/fb_data_helpers.js +0 -269
- package/dist/utils/fb_data_helpers.js.map +0 -1
- package/dist/utils/fb_data_mutations.d.ts +0 -43
- package/dist/utils/fb_data_mutations.d.ts.map +0 -1
- package/dist/utils/fb_data_mutations.js +0 -379
- package/dist/utils/fb_data_mutations.js.map +0 -1
- package/dist/utils/fb_data_mutations_v2.d.ts +0 -46
- package/dist/utils/fb_data_mutations_v2.d.ts.map +0 -1
- package/dist/utils/fb_data_mutations_v2.js +0 -341
- package/dist/utils/fb_data_mutations_v2.js.map +0 -1
- package/dist/utils/fb_data_queries.d.ts +0 -81
- package/dist/utils/fb_data_queries.d.ts.map +0 -1
- package/dist/utils/fb_data_queries.js +0 -354
- 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
|