hazo_collab_forms 3.1.6 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/CHANGE_LOG.md +207 -0
  2. package/README.md +3 -0
  3. package/dist/components/clarification/clarification_item_body.d.ts +19 -1
  4. package/dist/components/clarification/clarification_item_body.d.ts.map +1 -1
  5. package/dist/components/clarification/clarification_item_body.js +114 -6
  6. package/dist/components/clarification/clarification_item_body.js.map +1 -1
  7. package/dist/components/clarification/clarification_thread.js +1 -1
  8. package/dist/components/clarification/clarification_thread.js.map +1 -1
  9. package/dist/components/clarification/index.d.ts +2 -0
  10. package/dist/components/clarification/index.d.ts.map +1 -1
  11. package/dist/components/clarification/index.js +1 -0
  12. package/dist/components/clarification/index.js.map +1 -1
  13. package/dist/components/clarification/resolution_status_strip.d.ts +18 -0
  14. package/dist/components/clarification/resolution_status_strip.d.ts.map +1 -0
  15. package/dist/components/clarification/resolution_status_strip.js +20 -0
  16. package/dist/components/clarification/resolution_status_strip.js.map +1 -0
  17. package/dist/components/hazo_fb_form/context.d.ts +1 -1
  18. package/dist/components/hazo_fb_form/context.d.ts.map +1 -1
  19. package/dist/components/hazo_fb_form/hazo_fb_form.d.ts.map +1 -1
  20. package/dist/components/hazo_fb_form/hazo_fb_form.js +330 -113
  21. package/dist/components/hazo_fb_form/hazo_fb_form.js.map +1 -1
  22. package/dist/components/hazo_fb_form/hooks/use_fb_form_state.d.ts +3 -3
  23. package/dist/components/hazo_fb_form/hooks/use_fb_form_state.d.ts.map +1 -1
  24. package/dist/components/hazo_fb_form/hooks/use_fb_form_state.js +340 -61
  25. package/dist/components/hazo_fb_form/hooks/use_fb_form_state.js.map +1 -1
  26. package/dist/components/hazo_fb_form/hooks/use_llm_run.d.ts +3 -1
  27. package/dist/components/hazo_fb_form/hooks/use_llm_run.d.ts.map +1 -1
  28. package/dist/components/hazo_fb_form/hooks/use_llm_run.js +89 -11
  29. package/dist/components/hazo_fb_form/hooks/use_llm_run.js.map +1 -1
  30. package/dist/components/hazo_fb_form/shared/agent_stepper.js +1 -1
  31. package/dist/components/hazo_fb_form/shared/agent_stepper.js.map +1 -1
  32. package/dist/components/hazo_fb_form/shared/file_status_accordion.d.ts +9 -0
  33. package/dist/components/hazo_fb_form/shared/file_status_accordion.d.ts.map +1 -0
  34. package/dist/components/hazo_fb_form/shared/file_status_accordion.js +39 -0
  35. package/dist/components/hazo_fb_form/shared/file_status_accordion.js.map +1 -0
  36. package/dist/components/hazo_fb_form/shared/format.d.ts.map +1 -1
  37. package/dist/components/hazo_fb_form/shared/format.js +8 -3
  38. package/dist/components/hazo_fb_form/shared/format.js.map +1 -1
  39. package/dist/components/hazo_fb_form/shared/send_back_item_card.d.ts +7 -1
  40. package/dist/components/hazo_fb_form/shared/send_back_item_card.d.ts.map +1 -1
  41. package/dist/components/hazo_fb_form/shared/send_back_item_card.js +6 -3
  42. package/dist/components/hazo_fb_form/shared/send_back_item_card.js.map +1 -1
  43. package/dist/components/hazo_fb_form/types.d.ts +3 -1
  44. package/dist/components/hazo_fb_form/types.d.ts.map +1 -1
  45. package/dist/components/hazo_fb_form/views/back_office_view.js +1 -1
  46. package/dist/components/hazo_fb_form/views/back_office_view.js.map +1 -1
  47. package/dist/components/hazo_fb_form/views/clarifications_view.js +2 -2
  48. package/dist/components/hazo_fb_form/views/clarifications_view.js.map +1 -1
  49. package/dist/components/hazo_fb_form/views/front_office_view.d.ts.map +1 -1
  50. package/dist/components/hazo_fb_form/views/front_office_view.js +62 -41
  51. package/dist/components/hazo_fb_form/views/front_office_view.js.map +1 -1
  52. package/dist/components/hazo_fb_form/views/interim_view.js +3 -3
  53. package/dist/components/hazo_fb_form/views/interim_view.js.map +1 -1
  54. package/dist/components/hazo_fb_form/views/review_queue_view.d.ts.map +1 -1
  55. package/dist/components/hazo_fb_form/views/review_queue_view.js +22 -9
  56. package/dist/components/hazo_fb_form/views/review_queue_view.js.map +1 -1
  57. package/dist/components/hazo_validation_rule_editor/components/rule_editor.d.ts.map +1 -1
  58. package/dist/components/hazo_validation_rule_editor/components/rule_editor.js +32 -3
  59. package/dist/components/hazo_validation_rule_editor/components/rule_editor.js.map +1 -1
  60. package/dist/components/hazo_validation_rule_editor/components/variable_chain_input.d.ts +20 -0
  61. package/dist/components/hazo_validation_rule_editor/components/variable_chain_input.d.ts.map +1 -0
  62. package/dist/components/hazo_validation_rule_editor/components/variable_chain_input.js +34 -0
  63. package/dist/components/hazo_validation_rule_editor/components/variable_chain_input.js.map +1 -0
  64. package/dist/components/hazo_validation_rule_editor/context.d.ts +3 -2
  65. package/dist/components/hazo_validation_rule_editor/context.d.ts.map +1 -1
  66. package/dist/components/hazo_validation_rule_editor/context.js +15 -3
  67. package/dist/components/hazo_validation_rule_editor/context.js.map +1 -1
  68. package/dist/components/hazo_validation_rule_editor/types.d.ts +7 -1
  69. package/dist/components/hazo_validation_rule_editor/types.d.ts.map +1 -1
  70. package/dist/components/hazo_validation_rule_editor/validation_rule_editor.d.ts +1 -1
  71. package/dist/components/hazo_validation_rule_editor/validation_rule_editor.d.ts.map +1 -1
  72. package/dist/components/hazo_validation_rule_editor/validation_rule_editor.js +2 -2
  73. package/dist/components/hazo_validation_rule_editor/validation_rule_editor.js.map +1 -1
  74. package/dist/components/index.d.ts +2 -0
  75. package/dist/components/index.d.ts.map +1 -1
  76. package/dist/components/index.js +2 -0
  77. package/dist/components/index.js.map +1 -1
  78. package/dist/components/shared/document_type_editor.d.ts +31 -0
  79. package/dist/components/shared/document_type_editor.d.ts.map +1 -0
  80. package/dist/components/shared/document_type_editor.js +60 -0
  81. package/dist/components/shared/document_type_editor.js.map +1 -0
  82. package/dist/components/shared/file_bar/file_bar.d.ts +7 -1
  83. package/dist/components/shared/file_bar/file_bar.d.ts.map +1 -1
  84. package/dist/components/shared/file_bar/file_bar.js +5 -3
  85. package/dist/components/shared/file_bar/file_bar.js.map +1 -1
  86. package/dist/components/shared/file_bar/file_bar_validation_dialog.js +4 -4
  87. package/dist/components/shared/file_bar/file_bar_validation_dialog.js.map +1 -1
  88. package/dist/components/shared/file_status_icon.d.ts +23 -0
  89. package/dist/components/shared/file_status_icon.d.ts.map +1 -0
  90. package/dist/components/shared/file_status_icon.js +38 -0
  91. package/dist/components/shared/file_status_icon.js.map +1 -0
  92. package/dist/components/shared/json_data_panel/json_data_panel.d.ts +1 -1
  93. package/dist/components/shared/json_data_panel/json_data_panel.d.ts.map +1 -1
  94. package/dist/components/shared/json_data_panel/json_data_panel.js +27 -2
  95. package/dist/components/shared/json_data_panel/json_data_panel.js.map +1 -1
  96. package/dist/components/shared/rule_result_card.d.ts.map +1 -1
  97. package/dist/components/shared/rule_result_card.js +5 -4
  98. package/dist/components/shared/rule_result_card.js.map +1 -1
  99. package/dist/components/thread_form/components/add_question_dialog.d.ts +12 -0
  100. package/dist/components/thread_form/components/add_question_dialog.d.ts.map +1 -0
  101. package/dist/components/thread_form/components/add_question_dialog.js +36 -0
  102. package/dist/components/thread_form/components/add_question_dialog.js.map +1 -0
  103. package/dist/components/thread_form/components/agent_compose_dialog.d.ts +30 -0
  104. package/dist/components/thread_form/components/agent_compose_dialog.d.ts.map +1 -0
  105. package/dist/components/thread_form/components/agent_compose_dialog.js +45 -0
  106. package/dist/components/thread_form/components/agent_compose_dialog.js.map +1 -0
  107. package/dist/components/thread_form/components/clarification.d.ts +14 -0
  108. package/dist/components/thread_form/components/clarification.d.ts.map +1 -0
  109. package/dist/components/thread_form/components/clarification.js +12 -0
  110. package/dist/components/thread_form/components/clarification.js.map +1 -0
  111. package/dist/components/thread_form/components/collected_data_view.d.ts +15 -0
  112. package/dist/components/thread_form/components/collected_data_view.d.ts.map +1 -0
  113. package/dist/components/thread_form/components/collected_data_view.js +121 -0
  114. package/dist/components/thread_form/components/collected_data_view.js.map +1 -0
  115. package/dist/components/thread_form/components/coverage_card.d.ts +11 -0
  116. package/dist/components/thread_form/components/coverage_card.d.ts.map +1 -0
  117. package/dist/components/thread_form/components/coverage_card.js +60 -0
  118. package/dist/components/thread_form/components/coverage_card.js.map +1 -0
  119. package/dist/components/thread_form/components/file_bar.d.ts +93 -0
  120. package/dist/components/thread_form/components/file_bar.d.ts.map +1 -0
  121. package/dist/components/thread_form/components/file_bar.js +251 -0
  122. package/dist/components/thread_form/components/file_bar.js.map +1 -0
  123. package/dist/components/thread_form/components/file_info_dialog.d.ts +15 -0
  124. package/dist/components/thread_form/components/file_info_dialog.d.ts.map +1 -0
  125. package/dist/components/thread_form/components/file_info_dialog.js +64 -0
  126. package/dist/components/thread_form/components/file_info_dialog.js.map +1 -0
  127. package/dist/components/thread_form/components/issue_group_tree.d.ts +20 -0
  128. package/dist/components/thread_form/components/issue_group_tree.d.ts.map +1 -0
  129. package/dist/components/thread_form/components/issue_group_tree.js +164 -0
  130. package/dist/components/thread_form/components/issue_group_tree.js.map +1 -0
  131. package/dist/components/thread_form/components/pdf_side_panel.d.ts +20 -0
  132. package/dist/components/thread_form/components/pdf_side_panel.d.ts.map +1 -0
  133. package/dist/components/thread_form/components/pdf_side_panel.js +63 -0
  134. package/dist/components/thread_form/components/pdf_side_panel.js.map +1 -0
  135. package/dist/components/thread_form/components/rule_decision_row.d.ts +31 -0
  136. package/dist/components/thread_form/components/rule_decision_row.d.ts.map +1 -0
  137. package/dist/components/thread_form/components/rule_decision_row.js +20 -0
  138. package/dist/components/thread_form/components/rule_decision_row.js.map +1 -0
  139. package/dist/components/thread_form/components/send_back_message.d.ts +32 -0
  140. package/dist/components/thread_form/components/send_back_message.d.ts.map +1 -0
  141. package/dist/components/thread_form/components/send_back_message.js +82 -0
  142. package/dist/components/thread_form/components/send_back_message.js.map +1 -0
  143. package/dist/components/thread_form/components/shared.d.ts +54 -0
  144. package/dist/components/thread_form/components/shared.d.ts.map +1 -0
  145. package/dist/components/thread_form/components/shared.js +136 -0
  146. package/dist/components/thread_form/components/shared.js.map +1 -0
  147. package/dist/components/thread_form/components/task_card.d.ts +90 -0
  148. package/dist/components/thread_form/components/task_card.d.ts.map +1 -0
  149. package/dist/components/thread_form/components/task_card.js +63 -0
  150. package/dist/components/thread_form/components/task_card.js.map +1 -0
  151. package/dist/components/thread_form/components/text_doc_check.d.ts +15 -0
  152. package/dist/components/thread_form/components/text_doc_check.d.ts.map +1 -0
  153. package/dist/components/thread_form/components/text_doc_check.js +16 -0
  154. package/dist/components/thread_form/components/text_doc_check.js.map +1 -0
  155. package/dist/components/thread_form/components/text_extraction.d.ts +14 -0
  156. package/dist/components/thread_form/components/text_extraction.d.ts.map +1 -0
  157. package/dist/components/thread_form/components/text_extraction.js +16 -0
  158. package/dist/components/thread_form/components/text_extraction.js.map +1 -0
  159. package/dist/components/thread_form/components/thread_composer.d.ts +15 -0
  160. package/dist/components/thread_form/components/thread_composer.d.ts.map +1 -0
  161. package/dist/components/thread_form/components/thread_composer.js +93 -0
  162. package/dist/components/thread_form/components/thread_composer.js.map +1 -0
  163. package/dist/components/thread_form/components/thread_timeline.d.ts +65 -0
  164. package/dist/components/thread_form/components/thread_timeline.d.ts.map +1 -0
  165. package/dist/components/thread_form/components/thread_timeline.js +225 -0
  166. package/dist/components/thread_form/components/thread_timeline.js.map +1 -0
  167. package/dist/components/thread_form/hooks/use_file_pipeline.d.ts +126 -0
  168. package/dist/components/thread_form/hooks/use_file_pipeline.d.ts.map +1 -0
  169. package/dist/components/thread_form/hooks/use_file_pipeline.js +760 -0
  170. package/dist/components/thread_form/hooks/use_file_pipeline.js.map +1 -0
  171. package/dist/components/thread_form/hooks/use_thread_form.d.ts +36 -0
  172. package/dist/components/thread_form/hooks/use_thread_form.d.ts.map +1 -0
  173. package/dist/components/thread_form/hooks/use_thread_form.js +126 -0
  174. package/dist/components/thread_form/hooks/use_thread_form.js.map +1 -0
  175. package/dist/components/thread_form/index.d.ts +33 -0
  176. package/dist/components/thread_form/index.d.ts.map +1 -0
  177. package/dist/components/thread_form/index.js +30 -0
  178. package/dist/components/thread_form/index.js.map +1 -0
  179. package/dist/components/thread_form/sample_data.d.ts +8 -0
  180. package/dist/components/thread_form/sample_data.d.ts.map +1 -0
  181. package/dist/components/thread_form/sample_data.js +658 -0
  182. package/dist/components/thread_form/sample_data.js.map +1 -0
  183. package/dist/components/thread_form/thread_form.d.ts +7 -0
  184. package/dist/components/thread_form/thread_form.d.ts.map +1 -0
  185. package/dist/components/thread_form/thread_form.js +1385 -0
  186. package/dist/components/thread_form/thread_form.js.map +1 -0
  187. package/dist/components/thread_form/types.d.ts +402 -0
  188. package/dist/components/thread_form/types.d.ts.map +1 -0
  189. package/dist/components/thread_form/types.js +23 -0
  190. package/dist/components/thread_form/types.js.map +1 -0
  191. package/dist/components/thread_form/utils/file_decision_state.d.ts +22 -0
  192. package/dist/components/thread_form/utils/file_decision_state.d.ts.map +1 -0
  193. package/dist/components/thread_form/utils/file_decision_state.js +37 -0
  194. package/dist/components/thread_form/utils/file_decision_state.js.map +1 -0
  195. package/dist/components/thread_form/utils/merge_send_back.d.ts +13 -0
  196. package/dist/components/thread_form/utils/merge_send_back.d.ts.map +1 -0
  197. package/dist/components/thread_form/utils/merge_send_back.js +23 -0
  198. package/dist/components/thread_form/utils/merge_send_back.js.map +1 -0
  199. package/dist/lib/autofill_handler.d.ts.map +1 -1
  200. package/dist/lib/autofill_handler.js +5 -44
  201. package/dist/lib/autofill_handler.js.map +1 -1
  202. package/dist/lib/classification_handler.d.ts +105 -0
  203. package/dist/lib/classification_handler.d.ts.map +1 -0
  204. package/dist/lib/classification_handler.js +342 -0
  205. package/dist/lib/classification_handler.js.map +1 -0
  206. package/dist/lib/content_gate_handler.d.ts +37 -0
  207. package/dist/lib/content_gate_handler.d.ts.map +1 -0
  208. package/dist/lib/content_gate_handler.js +126 -0
  209. package/dist/lib/content_gate_handler.js.map +1 -0
  210. package/dist/lib/index.d.ts +10 -0
  211. package/dist/lib/index.d.ts.map +1 -1
  212. package/dist/lib/index.js +5 -0
  213. package/dist/lib/index.js.map +1 -1
  214. package/dist/lib/periodic_coverage_runner.d.ts +24 -0
  215. package/dist/lib/periodic_coverage_runner.d.ts.map +1 -0
  216. package/dist/lib/periodic_coverage_runner.js +121 -0
  217. package/dist/lib/periodic_coverage_runner.js.map +1 -0
  218. package/dist/lib/resolution_handler.d.ts +150 -0
  219. package/dist/lib/resolution_handler.d.ts.map +1 -0
  220. package/dist/lib/resolution_handler.js +597 -0
  221. package/dist/lib/resolution_handler.js.map +1 -0
  222. package/dist/lib/resolve_variable.d.ts +25 -0
  223. package/dist/lib/resolve_variable.d.ts.map +1 -0
  224. package/dist/lib/resolve_variable.js +77 -0
  225. package/dist/lib/resolve_variable.js.map +1 -0
  226. package/dist/lib/validation_handler.d.ts +27 -3
  227. package/dist/lib/validation_handler.d.ts.map +1 -1
  228. package/dist/lib/validation_handler.js +338 -288
  229. package/dist/lib/validation_handler.js.map +1 -1
  230. package/dist/types/clarification.d.ts +54 -0
  231. package/dist/types/clarification.d.ts.map +1 -1
  232. package/dist/types/fb_form_data.d.ts +273 -123
  233. package/dist/types/fb_form_data.d.ts.map +1 -1
  234. package/dist/types/fb_form_data.js +44 -58
  235. package/dist/types/fb_form_data.js.map +1 -1
  236. package/dist/types/fb_form_data_v1.d.ts +250 -0
  237. package/dist/types/fb_form_data_v1.d.ts.map +1 -0
  238. package/dist/types/fb_form_data_v1.js +117 -0
  239. package/dist/types/fb_form_data_v1.js.map +1 -0
  240. package/dist/types/fb_form_instance.d.ts +1 -1
  241. package/dist/types/fb_form_instance.d.ts.map +1 -1
  242. package/dist/types/index.d.ts +5 -3
  243. package/dist/types/index.d.ts.map +1 -1
  244. package/dist/types/index.js +2 -1
  245. package/dist/types/index.js.map +1 -1
  246. package/dist/types/validation.d.ts +134 -12
  247. package/dist/types/validation.d.ts.map +1 -1
  248. package/dist/utils/expectation_extractor.d.ts +31 -0
  249. package/dist/utils/expectation_extractor.d.ts.map +1 -0
  250. package/dist/utils/expectation_extractor.js +142 -0
  251. package/dist/utils/expectation_extractor.js.map +1 -0
  252. package/dist/utils/fb_data_adapter.d.ts +7 -2
  253. package/dist/utils/fb_data_adapter.d.ts.map +1 -1
  254. package/dist/utils/fb_data_adapter.js +58 -7
  255. package/dist/utils/fb_data_adapter.js.map +1 -1
  256. package/dist/utils/fb_data_adapter_v2.d.ts +17 -0
  257. package/dist/utils/fb_data_adapter_v2.d.ts.map +1 -0
  258. package/dist/utils/fb_data_adapter_v2.js +483 -0
  259. package/dist/utils/fb_data_adapter_v2.js.map +1 -0
  260. package/dist/utils/fb_data_helpers.d.ts +1 -1
  261. package/dist/utils/fb_data_helpers.d.ts.map +1 -1
  262. package/dist/utils/fb_data_mutations.d.ts +1 -1
  263. package/dist/utils/fb_data_mutations.d.ts.map +1 -1
  264. package/dist/utils/fb_data_mutations_v2.d.ts +46 -0
  265. package/dist/utils/fb_data_mutations_v2.d.ts.map +1 -0
  266. package/dist/utils/fb_data_mutations_v2.js +341 -0
  267. package/dist/utils/fb_data_mutations_v2.js.map +1 -0
  268. package/dist/utils/fb_data_queries.d.ts +81 -0
  269. package/dist/utils/fb_data_queries.d.ts.map +1 -0
  270. package/dist/utils/fb_data_queries.js +354 -0
  271. package/dist/utils/fb_data_queries.js.map +1 -0
  272. package/dist/utils/index.d.ts +4 -0
  273. package/dist/utils/index.d.ts.map +1 -1
  274. package/dist/utils/index.js +6 -0
  275. package/dist/utils/index.js.map +1 -1
  276. package/dist/utils/issue_bucketing.d.ts +36 -0
  277. package/dist/utils/issue_bucketing.d.ts.map +1 -0
  278. package/dist/utils/issue_bucketing.js +107 -0
  279. package/dist/utils/issue_bucketing.js.map +1 -0
  280. package/dist/utils/validation_result.d.ts +32 -0
  281. package/dist/utils/validation_result.d.ts.map +1 -0
  282. package/dist/utils/validation_result.js +55 -0
  283. package/dist/utils/validation_result.js.map +1 -0
  284. package/package.json +16 -4
@@ -0,0 +1,760 @@
1
+ /**
2
+ * use_file_pipeline — orchestrates classification → validation for uploaded files.
3
+ *
4
+ * Phase 1 (client upload): classify → fetch immediate rules → validate
5
+ * Phase 2 (agent review): fetch backoffice rules → validate
6
+ */
7
+ 'use client';
8
+ import { useState, useCallback, useRef } from 'react';
9
+ import { infer_mime_type } from '../../hazo_fb_form/shared/format.js';
10
+ /** Defensive normaliser: some upload paths still hand us octet-stream (the
11
+ * classification API rejects it). Sniff the extension when it does. */
12
+ function normalize_mime(mime_type, file_name) {
13
+ if (mime_type && mime_type !== 'application/octet-stream')
14
+ return mime_type;
15
+ return infer_mime_type(file_name);
16
+ }
17
+ function make_id(prefix = 'id') {
18
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
19
+ }
20
+ export function use_file_pipeline({ classification_api_url, validation_api_url, validation_rules_api_url, content_gate_api_url, response_extraction_api_url, document_types, available_tags, file_manager, on_log, }) {
21
+ // Keep a ref so classify_file reads the latest list without needing a fresh callback
22
+ const document_types_ref = useRef(document_types);
23
+ document_types_ref.current = document_types;
24
+ const available_tags_ref = useRef(available_tags);
25
+ available_tags_ref.current = available_tags;
26
+ const file_manager_ref = useRef(file_manager);
27
+ file_manager_ref.current = file_manager;
28
+ const on_log_ref = useRef(on_log);
29
+ on_log_ref.current = on_log;
30
+ const log_event = useCallback((msg) => on_log_ref.current?.(msg), []);
31
+ const [file_statuses, set_file_statuses] = useState(new Map());
32
+ const [classification_results, set_classification_results] = useState(new Map());
33
+ const [validation_results_map, set_validation_results] = useState(new Map());
34
+ // Use refs to avoid stale closures
35
+ const statuses_ref = useRef(file_statuses);
36
+ statuses_ref.current = file_statuses;
37
+ const update_status = useCallback((content_id, status) => {
38
+ set_file_statuses(prev => {
39
+ const next = new Map(prev);
40
+ next.set(content_id, status);
41
+ return next;
42
+ });
43
+ }, []);
44
+ /** Classify a file via the classification API */
45
+ const classify_file = useCallback(async (file_b64, file_name, mime_type) => {
46
+ if (!classification_api_url)
47
+ return null;
48
+ // Pre-flight diagnostic: empty allow-list is the #1 cause of "unknown"
49
+ // classifications (LLM has nothing to choose from). Warn loudly so the
50
+ // root cause is obvious in the activity log instead of silent.
51
+ const doc_types_list = document_types_ref.current;
52
+ if (!doc_types_list || doc_types_list.length === 0) {
53
+ const msg = `Classify: document_types allow-list is EMPTY — LLM will return "unknown" by design. ` +
54
+ `Populate available_document_types config (e.g. via the doc-type-editor page) before classification will work.`;
55
+ console.warn('[file-pipeline]', msg);
56
+ on_log_ref.current?.(msg);
57
+ }
58
+ const safe_mime = normalize_mime(mime_type, file_name);
59
+ if (safe_mime === 'application/octet-stream') {
60
+ const msg = `Skipping classification for "${file_name}": cannot determine file type from extension. Add the extension to infer_mime_type or pass an explicit mime_type when uploading.`;
61
+ console.warn('[file-pipeline]', msg);
62
+ on_log_ref.current?.(msg);
63
+ return null;
64
+ }
65
+ const res = await fetch(classification_api_url, {
66
+ method: 'POST',
67
+ headers: { 'Content-Type': 'application/json' },
68
+ body: JSON.stringify({
69
+ file_b64,
70
+ file_name,
71
+ mime_type: safe_mime,
72
+ document_types: doc_types_list,
73
+ available_tags: available_tags_ref.current,
74
+ }),
75
+ });
76
+ const data = await res.json();
77
+ if (!data.success) {
78
+ console.error('[file-pipeline] Classification failed:', data.error);
79
+ on_log_ref.current?.(`Classification failed: ${data.error ?? 'unknown error'}`);
80
+ return null;
81
+ }
82
+ // Surface what the classify route returned so we can see if extracted_fields
83
+ // is missing/wrong-shape. Goes to the Activity Log via on_log_ref.
84
+ on_log_ref.current?.(`Classify response keys: ${Object.keys(data).join(', ')}` +
85
+ (data.extracted_fields
86
+ ? ` · extracted_fields: ${JSON.stringify(data.extracted_fields)}`
87
+ : ' · extracted_fields: <missing>'));
88
+ // Post-flight diagnostic: explain the most common failure mode if the
89
+ // LLM returned the default "unknown" verdict despite a non-empty list.
90
+ if (data.document_type === 'unknown' && (data.confidence ?? 0) === 0) {
91
+ const had_list = doc_types_list && doc_types_list.length > 0;
92
+ on_log_ref.current?.(had_list
93
+ ? `Classification returned 'unknown' even though ${doc_types_list.length} doc type(s) were offered — the LLM didn't match any. Check the prompt or expand the allow-list.`
94
+ : `Classification returned 'unknown' because the doc-type allow-list was empty (see warning above).`);
95
+ }
96
+ // Dump raw LLM text when extracted_fields is missing — helps diagnose
97
+ // whether it's a prompt-cache issue or a parser shape mismatch.
98
+ if (!data.extracted_fields && data._debug_llm_text) {
99
+ on_log_ref.current?.(`Raw LLM text: ${String(data._debug_llm_text).slice(0, 1500)}`);
100
+ }
101
+ return {
102
+ document_type: data.document_type,
103
+ tags: data.tags || [],
104
+ tag_reasons: data.tag_reasons,
105
+ confidence: data.confidence || 0,
106
+ document_date: data.document_date,
107
+ document_nature: data.document_nature,
108
+ ...(data.extracted_fields ? { extracted_fields: data.extracted_fields } : {}),
109
+ };
110
+ }, [classification_api_url]);
111
+ /** Fetch validation rules for a document type */
112
+ const fetch_rules = useCallback(async (document_type, check_type) => {
113
+ if (!validation_rules_api_url)
114
+ return [];
115
+ const params = new URLSearchParams({ document_type, check_type });
116
+ const res = await fetch(`${validation_rules_api_url}?${params}`);
117
+ const data = await res.json();
118
+ if (!res.ok || data.success === false) {
119
+ console.warn('[file-pipeline] Rules API returned non-success', {
120
+ status: res.status,
121
+ document_type,
122
+ check_type,
123
+ response: data,
124
+ });
125
+ return [];
126
+ }
127
+ const rules = data.rules || [];
128
+ console.debug('[file-pipeline] Rules fetched', {
129
+ document_type,
130
+ check_type,
131
+ rule_count: rules.length,
132
+ rule_names: rules.map((r) => `${r.name} (doc=${r.document_type ?? '∅'} chk=${r.check_type ?? '∅'})`),
133
+ });
134
+ return rules;
135
+ }, [validation_rules_api_url]);
136
+ /** Run validation rules against a file */
137
+ const validate_file = useCallback(async (file_b64, file_name, mime_type, rules) => {
138
+ if (!validation_api_url || rules.length === 0)
139
+ return [];
140
+ // Convert rules to ValidationRuleExecution format
141
+ const rule_executions = rules.map((r) => ({
142
+ rule_id: r.rule_id || r.id,
143
+ name: r.name,
144
+ prompt: r.prompt,
145
+ target_field_id: r.target_field_id || '__document',
146
+ target_label: r.target_label || r.name,
147
+ clarification_type: r.clarification_type || 'none',
148
+ check_type: r.check_type,
149
+ }));
150
+ const res = await fetch(validation_api_url, {
151
+ method: 'POST',
152
+ headers: { 'Content-Type': 'application/json' },
153
+ body: JSON.stringify({
154
+ file_b64,
155
+ file_name,
156
+ mime_type,
157
+ rules: rule_executions,
158
+ mode: 'single',
159
+ }),
160
+ });
161
+ const data = await res.json();
162
+ if (!res.ok || data.success === false) {
163
+ console.warn('[file-pipeline] Validation API returned non-success', {
164
+ status: res.status,
165
+ errors: data?.errors,
166
+ file_name,
167
+ response: data,
168
+ });
169
+ }
170
+ const results = data.rule_results || [];
171
+ // Enrich results with rule_name from the original rules (server only returns rule_id)
172
+ // Build maps using both rule_id and id (UUID) as keys since either could be in the result
173
+ const rule_name_map = new Map();
174
+ for (const r of rules) {
175
+ const name = r.name || 'Validation Rule';
176
+ if (r.rule_id)
177
+ rule_name_map.set(r.rule_id, name);
178
+ if (r.id)
179
+ rule_name_map.set(r.id, name);
180
+ }
181
+ for (const r of results) {
182
+ if (!r.rule_name || /^[0-9a-f]{8}-/.test(r.rule_name)) {
183
+ r.rule_name = rule_name_map.get(r.rule_id) || r.rule_id;
184
+ }
185
+ }
186
+ return results;
187
+ }, [validation_api_url]);
188
+ /** Full pipeline: classify → fetch rules → validate */
189
+ const process_file = useCallback(async (item, file_b64) => {
190
+ const content_id = item.content_id;
191
+ const file_name = item.file?.file_name || 'unknown';
192
+ const mime_type = normalize_mime(item.file?.mime_type, file_name);
193
+ let classification = null;
194
+ let rule_results = [];
195
+ let doc_check = null;
196
+ let ai_result = null;
197
+ try {
198
+ // Phase 1: Classify
199
+ update_status(content_id, 'classifying');
200
+ log_event(`Classifying: ${file_name}`);
201
+ classification = await classify_file(file_b64, file_name, mime_type);
202
+ if (classification) {
203
+ log_event(`Classified ${file_name} as ${classification.document_type} (${Math.round((classification.confidence ?? 0) * 100)}%)`);
204
+ set_classification_results(prev => {
205
+ const next = new Map(prev);
206
+ next.set(content_id, classification);
207
+ return next;
208
+ });
209
+ // Phase 2: Fetch immediate rules and validate
210
+ update_status(content_id, 'validating');
211
+ const all_immediate = await fetch_rules(classification.document_type, 'immediate');
212
+ // Exclude non-LLM rule types (e.g. periodic_coverage) — those have
213
+ // their own deterministic runner, not the per-file LLM call.
214
+ const rules = all_immediate.filter((r) => (r.validation_type ?? 'llm_prompt') === 'llm_prompt');
215
+ log_event(`Running ${rules.length} immediate rule(s) on ${file_name}`);
216
+ if (rules.length > 0) {
217
+ rule_results = await validate_file(file_b64, file_name, mime_type, rules);
218
+ set_validation_results(prev => {
219
+ const next = new Map(prev);
220
+ next.set(content_id, rule_results);
221
+ return next;
222
+ });
223
+ }
224
+ // Build doc_check if there are issues (client-visible)
225
+ const failed_rules = rule_results.filter(r => r.issues.length > 0);
226
+ if (failed_rules.length > 0) {
227
+ doc_check = {
228
+ content_ref: content_id,
229
+ source_type: 'file',
230
+ status: 'issues',
231
+ summary: `${failed_rules.length} issue${failed_rules.length !== 1 ? 's' : ''} found with ${file_name}`,
232
+ issues: failed_rules.map(r => ({
233
+ description: r.issues[0]?.issue_description || r.summary || r.rule_name || 'Validation issue',
234
+ })),
235
+ };
236
+ }
237
+ else {
238
+ doc_check = {
239
+ content_ref: content_id,
240
+ source_type: 'file',
241
+ status: 'passed',
242
+ summary: `All checks passed for ${file_name}`,
243
+ };
244
+ }
245
+ // Build AI result (agent-visible)
246
+ ai_result = {
247
+ content_ref: content_id,
248
+ classification: {
249
+ document_type: classification.document_type,
250
+ tags: classification.tags,
251
+ confidence: classification.confidence,
252
+ },
253
+ validation: {
254
+ status: failed_rules.length > 0 ? 'issues' : 'passed',
255
+ checks: rule_results.map(r => {
256
+ const has_issue = r.issues.length > 0;
257
+ return {
258
+ check_id: make_id('chk'),
259
+ name: (r.rule_name && !/^[0-9a-f]{8}-/.test(r.rule_name)) ? r.rule_name : 'Check',
260
+ status: has_issue ? 'failed' : 'passed',
261
+ description: has_issue ? (r.issues[0]?.issue_description || 'Issue found') : (r.summary || 'Passed'),
262
+ severity: has_issue ? 'error' : 'info',
263
+ };
264
+ }),
265
+ },
266
+ review_status: 'pending',
267
+ };
268
+ }
269
+ if (!classification)
270
+ log_event(`Classification returned nothing for ${file_name} — skipping validation`);
271
+ update_status(content_id, 'done');
272
+ }
273
+ catch (err) {
274
+ console.error('[file-pipeline] Error processing file:', err);
275
+ log_event(`Pipeline error for ${file_name}: ${err instanceof Error ? err.message : 'unknown'}`);
276
+ update_status(content_id, 'error');
277
+ }
278
+ return { classification, validation_results: rule_results, doc_check, ai_result };
279
+ }, [classify_file, fetch_rules, validate_file, update_status]);
280
+ /**
281
+ * Run back-office validation for the given files. Assumes each file has
282
+ * already been classified (reads item.classification_result). Fetches
283
+ * rules with check_type='backoffice' for the file's document_type, then
284
+ * posts to the validation API using the file_manager's download URL so
285
+ * the server can fetch the bytes directly.
286
+ *
287
+ * Returns new backoffice-phase ValidationRuleResults per content_id —
288
+ * caller is responsible for APPENDING them to item.validation_rule_results
289
+ * (not replacing immediate-phase results).
290
+ */
291
+ const run_backoffice_validation = useCallback(async (items) => {
292
+ const results = new Map();
293
+ const fm = file_manager_ref.current;
294
+ // Mark every pending item as 'queued' upfront so the agent sees at a glance
295
+ // which files are waiting vs being processed vs already done. The loop below
296
+ // transitions each one to 'validating' when its turn comes, then 'done' (or
297
+ // 'error') on completion.
298
+ for (const item of items) {
299
+ if (item.type !== 'file' || !item.file)
300
+ continue;
301
+ update_status(item.content_id, 'queued');
302
+ }
303
+ for (const item of items) {
304
+ if (item.type !== 'file' || !item.file)
305
+ continue;
306
+ const content_id = item.content_id;
307
+ const file_name = item.file.file_name;
308
+ const mime_type = normalize_mime(item.file.mime_type, file_name);
309
+ // Need a classification to know which rules apply. When missing, treat
310
+ // it as "nothing to validate" so the file stops showing as pending —
311
+ // otherwise the agent's "Run Backoffice (N)" pill would never decrement.
312
+ const classification = item.classification_result
313
+ || classification_results.get(content_id);
314
+ if (!classification) {
315
+ console.warn('[file-pipeline] No classification for', content_id, '— recording empty backoffice result');
316
+ results.set(content_id, {
317
+ validation_results: [],
318
+ ai_result: {
319
+ content_ref: content_id,
320
+ validation: { status: 'passed', checks: [] },
321
+ review_status: 'pending',
322
+ },
323
+ });
324
+ continue;
325
+ }
326
+ update_status(content_id, 'validating');
327
+ try {
328
+ const all_rules = await fetch_rules(classification.document_type, 'backoffice');
329
+ // Only run rules EXPLICITLY marked check_type='backoffice'. Rules with
330
+ // unset check_type are "both phases" and have already been executed
331
+ // during immediate phase — re-running them here would duplicate results.
332
+ // Also exclude non-LLM rule types (periodic_coverage) — those run
333
+ // deterministically via run_periodic_coverage_pass, not via the
334
+ // per-file LLM call.
335
+ const rules = all_rules.filter((r) => r.check_type === 'backoffice' &&
336
+ (r.validation_type ?? 'llm_prompt') === 'llm_prompt');
337
+ if (rules.length === 0) {
338
+ // No backoffice rules apply for this doc type. Still record an empty
339
+ // result so handle_run_backoffice stamps backoffice_validated_at and
340
+ // the "Run Backoffice (N)" counter decrements.
341
+ results.set(content_id, {
342
+ validation_results: [],
343
+ ai_result: {
344
+ content_ref: content_id,
345
+ classification: {
346
+ document_type: classification.document_type,
347
+ tags: classification.tags,
348
+ confidence: classification.confidence,
349
+ },
350
+ validation: { status: 'passed', checks: [] },
351
+ review_status: 'pending',
352
+ },
353
+ });
354
+ update_status(content_id, 'done');
355
+ continue;
356
+ }
357
+ // Resolve a URL then fetch the bytes in the BROWSER so blob: URLs
358
+ // (which the server can't see) still work. Send base64 inline.
359
+ const raw_url = fm?.get_download_url
360
+ ? await fm.get_download_url(item.file.file_id)
361
+ : null;
362
+ if (!raw_url) {
363
+ console.warn('[file-pipeline] No download_url for', content_id, '— recording empty backoffice result');
364
+ results.set(content_id, {
365
+ validation_results: [],
366
+ ai_result: {
367
+ content_ref: content_id,
368
+ classification: {
369
+ document_type: classification.document_type,
370
+ tags: classification.tags,
371
+ confidence: classification.confidence,
372
+ },
373
+ validation: { status: 'passed', checks: [] },
374
+ review_status: 'pending',
375
+ },
376
+ });
377
+ update_status(content_id, 'done');
378
+ continue;
379
+ }
380
+ let file_b64_resolved;
381
+ try {
382
+ const blob_res = await fetch(raw_url);
383
+ if (!blob_res.ok)
384
+ throw new Error(`status ${blob_res.status}`);
385
+ const buf = await blob_res.arrayBuffer();
386
+ // btoa needs a binary string; build it in chunks to avoid stack issues on large files
387
+ const bytes = new Uint8Array(buf);
388
+ let binary = '';
389
+ const chunk = 0x8000;
390
+ for (let i = 0; i < bytes.length; i += chunk) {
391
+ binary += String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + chunk)));
392
+ }
393
+ file_b64_resolved = btoa(binary);
394
+ }
395
+ catch (err) {
396
+ console.warn('[file-pipeline] Backoffice fetch failed for', content_id, err);
397
+ update_status(content_id, 'error');
398
+ continue;
399
+ }
400
+ const rule_executions = rules.map((r) => ({
401
+ rule_id: r.rule_id || r.id,
402
+ name: r.name,
403
+ prompt: r.prompt,
404
+ target_field_id: r.target_field_id || '__document',
405
+ target_label: r.target_label || r.name,
406
+ clarification_type: r.clarification_type || 'none',
407
+ check_type: r.check_type ?? 'backoffice',
408
+ }));
409
+ const res = await fetch(validation_api_url, {
410
+ method: 'POST',
411
+ headers: { 'Content-Type': 'application/json' },
412
+ body: JSON.stringify({
413
+ file_b64: file_b64_resolved,
414
+ file_name,
415
+ mime_type,
416
+ rules: rule_executions,
417
+ mode: 'single',
418
+ }),
419
+ });
420
+ const data = await res.json();
421
+ if (!res.ok || data.success === false) {
422
+ console.warn('[file-pipeline] Backoffice validation API returned non-success', {
423
+ status: res.status,
424
+ errors: data?.errors,
425
+ file_name,
426
+ response: data,
427
+ });
428
+ }
429
+ const rule_results = data.rule_results || [];
430
+ // Enrich with rule_name and ensure check_type is stamped (fallback for old servers)
431
+ const rule_name_map = new Map();
432
+ for (const r of rules) {
433
+ const name = r.name || 'Validation Rule';
434
+ if (r.rule_id)
435
+ rule_name_map.set(r.rule_id, name);
436
+ if (r.id)
437
+ rule_name_map.set(r.id, name);
438
+ }
439
+ for (const r of rule_results) {
440
+ if (!r.rule_name || /^[0-9a-f]{8}-/.test(r.rule_name)) {
441
+ r.rule_name = rule_name_map.get(r.rule_id) || r.rule_id;
442
+ }
443
+ if (!r.check_type)
444
+ r.check_type = 'backoffice';
445
+ }
446
+ set_validation_results(prev => {
447
+ const next = new Map(prev);
448
+ const existing = next.get(content_id) || [];
449
+ // Replace any prior backoffice results for the same rule_id, keep immediate ones
450
+ const kept = existing.filter(e => e.check_type !== 'backoffice');
451
+ next.set(content_id, [...kept, ...rule_results]);
452
+ return next;
453
+ });
454
+ const ai_result = {
455
+ content_ref: content_id,
456
+ classification: {
457
+ document_type: classification.document_type,
458
+ tags: classification.tags,
459
+ confidence: classification.confidence,
460
+ },
461
+ validation: {
462
+ status: rule_results.some(r => r.issues.length > 0) ? 'issues' : 'passed',
463
+ checks: rule_results.map(r => {
464
+ const has_issue = r.issues.length > 0;
465
+ return {
466
+ check_id: make_id('chk'),
467
+ name: (r.rule_name && !/^[0-9a-f]{8}-/.test(r.rule_name)) ? r.rule_name : 'Check',
468
+ status: has_issue ? 'failed' : 'passed',
469
+ description: has_issue ? (r.issues[0]?.issue_description || 'Issue found') : (r.summary || 'Passed'),
470
+ severity: has_issue ? 'error' : 'info',
471
+ };
472
+ }),
473
+ },
474
+ review_status: 'pending',
475
+ };
476
+ results.set(content_id, { validation_results: rule_results, ai_result });
477
+ update_status(content_id, 'done');
478
+ }
479
+ catch (err) {
480
+ console.error('[file-pipeline] Backoffice validation failed for', content_id, err);
481
+ update_status(content_id, 'error');
482
+ }
483
+ }
484
+ return results;
485
+ }, [classification_results, fetch_rules, validation_api_url, update_status]);
486
+ /**
487
+ * Per-group periodic_coverage pass. Run AFTER per-file backoffice
488
+ * validation has populated extracted_data on validation_results — this
489
+ * pass groups files by their classification primary tag (or document_type
490
+ * fallback), fetches periodic_coverage rules for each group's doc_type,
491
+ * resolves period bounds via resolve_variable_chain, and computes gaps
492
+ * using compute_coverage_gaps.
493
+ *
494
+ * Returns one GroupCoverageResult per (group_key, rule) pair. The consumer
495
+ * is responsible for translating these into user-visible validations or
496
+ * clarifications.
497
+ */
498
+ const run_periodic_coverage_pass = useCallback(async (items, ctx) => {
499
+ // Lazy-import the pure utilities directly (NOT via lib/index.js) so
500
+ // Turbopack doesn't pull the server-only barrel into the client bundle.
501
+ const [{ compute_coverage_gaps }, { resolve_variable_chain }] = await Promise.all([
502
+ import('../../../lib/periodic_coverage_runner.js'),
503
+ import('../../../lib/resolve_variable.js'),
504
+ ]);
505
+ // 1. Group files by document_type. Earlier versions grouped by primary tag,
506
+ // but a file with two tags (e.g. ['rental_property', 'rental_income'])
507
+ // would get assigned to its first tag — and a sibling with the tags in
508
+ // the other order would land in a different "group" with the same
509
+ // document_type, causing the coverage rule to fire twice on the same
510
+ // logical set. Document type is the rule's actual scope, so use that.
511
+ const groups = new Map();
512
+ for (const item of items) {
513
+ if (item.type !== 'file')
514
+ continue;
515
+ const classification = item.classification_result || classification_results.get(item.content_id);
516
+ if (!classification?.document_type)
517
+ continue;
518
+ const group_key = classification.document_type;
519
+ if (!groups.has(group_key))
520
+ groups.set(group_key, []);
521
+ groups.get(group_key).push(item);
522
+ }
523
+ const out = [];
524
+ // 2. For each group, run matching periodic_coverage rules.
525
+ for (const [group_key, group_items] of groups.entries()) {
526
+ // group_key === document_type after the simplification above.
527
+ const document_type = group_key;
528
+ const all_rules = await fetch_rules(document_type, 'backoffice');
529
+ const coverage_rules = all_rules.filter((r) => r.validation_type === 'periodic_coverage' && r.coverage);
530
+ if (coverage_rules.length === 0)
531
+ continue;
532
+ // 3. Build FilePeriod[] from each file's per-file extracted_data.
533
+ const file_periods = group_items.map(item => {
534
+ const file_results = validation_results_map.get(item.content_id) ?? [];
535
+ const extracted = file_results
536
+ .map(r => r.extracted_data)
537
+ .find(d => d && d.period_start && d.period_end);
538
+ return {
539
+ id: item.content_id,
540
+ period_start: typeof extracted?.period_start === 'string' ? extracted.period_start : null,
541
+ period_end: typeof extracted?.period_end === 'string' ? extracted.period_end : null,
542
+ };
543
+ });
544
+ // 4. For each rule, resolve bounds and compute gaps.
545
+ for (const rule of coverage_rules) {
546
+ const period_start_chain = rule.coverage.period_start ?? [];
547
+ const period_end_chain = rule.coverage.period_end ?? [];
548
+ const expected_start = resolve_variable_chain(period_start_chain, ctx);
549
+ const expected_end = resolve_variable_chain(period_end_chain, ctx);
550
+ if (!expected_start || !expected_end) {
551
+ out.push({
552
+ group_key,
553
+ document_type,
554
+ rule_id: rule.rule_id,
555
+ rule_name: rule.name ?? rule.rule_id,
556
+ custom_issue_description: rule.custom_issue_description,
557
+ gaps: [],
558
+ skipped: true,
559
+ });
560
+ continue;
561
+ }
562
+ const gaps = compute_coverage_gaps({
563
+ files: file_periods,
564
+ expected_start,
565
+ expected_end,
566
+ cadence: rule.coverage.cadence,
567
+ alignment: rule.coverage.alignment,
568
+ });
569
+ out.push({
570
+ group_key,
571
+ document_type,
572
+ rule_id: rule.rule_id,
573
+ rule_name: rule.name ?? rule.rule_id,
574
+ custom_issue_description: rule.custom_issue_description,
575
+ gaps,
576
+ });
577
+ }
578
+ }
579
+ return out;
580
+ }, [classification_results, fetch_rules, validation_results_map]);
581
+ /** Run validation rules against text content (no classification — uses 'general' rules) */
582
+ const process_text = useCallback(async (message_id, text) => {
583
+ let rule_results = [];
584
+ let doc_check = null;
585
+ try {
586
+ update_status(message_id, 'validating');
587
+ // Fetch 'general' validation rules (text doesn't have a document_type)
588
+ const rules = await fetch_rules('general', 'immediate');
589
+ if (rules.length > 0 && validation_api_url) {
590
+ // Build rule executions
591
+ const rule_executions = rules.map((r) => ({
592
+ rule_id: r.rule_id || r.id,
593
+ name: r.name,
594
+ prompt: r.prompt,
595
+ target_field_id: r.target_field_id || '__text',
596
+ target_label: r.target_label || r.name,
597
+ clarification_type: r.clarification_type || 'none',
598
+ check_type: r.check_type,
599
+ }));
600
+ const res = await fetch(validation_api_url, {
601
+ method: 'POST',
602
+ headers: { 'Content-Type': 'application/json' },
603
+ body: JSON.stringify({
604
+ text_content: text,
605
+ file_name: 'text input',
606
+ mime_type: 'text/plain',
607
+ rules: rule_executions,
608
+ mode: 'single',
609
+ }),
610
+ });
611
+ const data = await res.json();
612
+ rule_results = data.rule_results || [];
613
+ // Enrich with rule names
614
+ const rule_name_map = new Map();
615
+ for (const r of rules) {
616
+ const name = r.name || 'Validation Rule';
617
+ if (r.rule_id)
618
+ rule_name_map.set(r.rule_id, name);
619
+ if (r.id)
620
+ rule_name_map.set(r.id, name);
621
+ }
622
+ for (const r of rule_results) {
623
+ if (!r.rule_name || /^[0-9a-f]{8}-/.test(r.rule_name)) {
624
+ r.rule_name = rule_name_map.get(r.rule_id) || r.rule_id;
625
+ }
626
+ }
627
+ set_validation_results(prev => {
628
+ const next = new Map(prev);
629
+ next.set(message_id, rule_results);
630
+ return next;
631
+ });
632
+ }
633
+ // Build doc_check for client-visible issues
634
+ const failed_rules = rule_results.filter(r => r.issues.length > 0);
635
+ if (failed_rules.length > 0) {
636
+ doc_check = {
637
+ content_ref: message_id,
638
+ source_type: 'text',
639
+ status: 'issues',
640
+ summary: `${failed_rules.length} issue${failed_rules.length !== 1 ? 's' : ''} found in your response`,
641
+ issues: failed_rules.map(r => ({
642
+ description: r.issues[0]?.issue_description || r.summary || r.rule_name || 'Validation issue',
643
+ text_snippet: undefined,
644
+ })),
645
+ };
646
+ }
647
+ update_status(message_id, 'done');
648
+ }
649
+ catch (err) {
650
+ console.error('[file-pipeline] Error processing text:', err);
651
+ update_status(message_id, 'error');
652
+ }
653
+ return { validation_results: rule_results, doc_check };
654
+ }, [fetch_rules, validation_api_url, update_status]);
655
+ /**
656
+ * Gate that decides whether a piece of client text is worth running
657
+ * validation on. Two stages:
658
+ * 1. Cheap heuristic — short strings or strings with no digits/dates/
659
+ * currency tokens fail immediately (no LLM call).
660
+ * 2. LLM gate — only invoked when heuristic passes AND a gate URL is
661
+ * configured. If no URL, we trust the heuristic alone.
662
+ * Fails closed: any network error returns has_content=false so nothing
663
+ * AI-flavoured leaks to the client.
664
+ */
665
+ const gate_text_content = useCallback(async (text) => {
666
+ const trimmed = text.trim();
667
+ // Heuristic: too short to be a document-style statement
668
+ if (trimmed.length < 80) {
669
+ return { has_content: false, reason: 'below length threshold' };
670
+ }
671
+ // Heuristic: no numeric signal (amounts, dates, IDs) usually means prose
672
+ const has_signal = /\d/.test(trimmed);
673
+ if (!has_signal) {
674
+ return { has_content: false, reason: 'no numeric signal' };
675
+ }
676
+ // No gate endpoint configured — trust the heuristic
677
+ if (!content_gate_api_url) {
678
+ return { has_content: true, reason: 'heuristic passed; no LLM gate configured' };
679
+ }
680
+ try {
681
+ const res = await fetch(content_gate_api_url, {
682
+ method: 'POST',
683
+ headers: { 'Content-Type': 'application/json' },
684
+ body: JSON.stringify({ text: trimmed }),
685
+ });
686
+ const data = await res.json();
687
+ if (!data?.success) {
688
+ return { has_content: false, reason: data?.error || 'gate failed; defaulting to false' };
689
+ }
690
+ return {
691
+ has_content: data.has_content === true,
692
+ reason: typeof data.reason === 'string' ? data.reason : '',
693
+ };
694
+ }
695
+ catch (err) {
696
+ console.error('[file-pipeline] Content gate error:', err);
697
+ return { has_content: false, reason: 'gate network error; defaulting to false' };
698
+ }
699
+ }, [content_gate_api_url]);
700
+ /**
701
+ * Extract structured fields from a response file via response_extraction_api_url.
702
+ * The route is expected to return { success, data: { total?, date?, vendor? } }.
703
+ */
704
+ const extract_response_fields = useCallback(async (file_id, file_b64, file_name, mime_type) => {
705
+ if (!response_extraction_api_url)
706
+ return null;
707
+ try {
708
+ const res = await fetch(response_extraction_api_url, {
709
+ method: 'POST',
710
+ headers: { 'Content-Type': 'application/json' },
711
+ body: JSON.stringify({
712
+ file_id,
713
+ file_name,
714
+ mime_type,
715
+ ...(file_b64 ? { file_b64 } : {}),
716
+ }),
717
+ });
718
+ const data = await res.json();
719
+ if (!res.ok || data?.success === false) {
720
+ console.warn('[file-pipeline] Response extraction returned non-success', data);
721
+ return null;
722
+ }
723
+ const raw = data.data ?? {};
724
+ const total_value = raw.total ?? raw.amount ?? raw.invoice_total;
725
+ const date_value = raw.date ?? raw.invoice_date ?? raw.document_date;
726
+ const vendor_value = raw.vendor ?? raw.supplier ?? raw.payee;
727
+ const fields = {
728
+ ...(total_value !== undefined && total_value !== null && total_value !== ''
729
+ ? { total: typeof total_value === 'number' ? total_value : parseFloat(String(total_value).replace(/[^\d.\-]/g, '')) }
730
+ : {}),
731
+ ...(date_value ? { date: String(date_value) } : {}),
732
+ ...(vendor_value ? { vendor: String(vendor_value) } : {}),
733
+ raw,
734
+ };
735
+ // Drop NaN totals
736
+ if (fields.total !== undefined && Number.isNaN(fields.total))
737
+ delete fields.total;
738
+ // No useful fields → null so caller treats as "no extraction available"
739
+ if (fields.total === undefined && !fields.date && !fields.vendor)
740
+ return null;
741
+ return fields;
742
+ }
743
+ catch (err) {
744
+ console.warn('[file-pipeline] Response extraction failed', err);
745
+ return null;
746
+ }
747
+ }, [response_extraction_api_url]);
748
+ return {
749
+ process_file,
750
+ process_text,
751
+ gate_text_content,
752
+ run_backoffice_validation,
753
+ run_periodic_coverage_pass,
754
+ extract_response_fields,
755
+ file_statuses,
756
+ classification_results,
757
+ validation_results: validation_results_map,
758
+ };
759
+ }
760
+ //# sourceMappingURL=use_file_pipeline.js.map