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.
- package/CHANGE_LOG.md +207 -0
- package/README.md +3 -0
- package/dist/components/clarification/clarification_item_body.d.ts +19 -1
- package/dist/components/clarification/clarification_item_body.d.ts.map +1 -1
- package/dist/components/clarification/clarification_item_body.js +114 -6
- package/dist/components/clarification/clarification_item_body.js.map +1 -1
- package/dist/components/clarification/clarification_thread.js +1 -1
- package/dist/components/clarification/clarification_thread.js.map +1 -1
- package/dist/components/clarification/index.d.ts +2 -0
- package/dist/components/clarification/index.d.ts.map +1 -1
- package/dist/components/clarification/index.js +1 -0
- package/dist/components/clarification/index.js.map +1 -1
- package/dist/components/clarification/resolution_status_strip.d.ts +18 -0
- package/dist/components/clarification/resolution_status_strip.d.ts.map +1 -0
- package/dist/components/clarification/resolution_status_strip.js +20 -0
- package/dist/components/clarification/resolution_status_strip.js.map +1 -0
- package/dist/components/hazo_fb_form/context.d.ts +1 -1
- package/dist/components/hazo_fb_form/context.d.ts.map +1 -1
- package/dist/components/hazo_fb_form/hazo_fb_form.d.ts.map +1 -1
- package/dist/components/hazo_fb_form/hazo_fb_form.js +330 -113
- package/dist/components/hazo_fb_form/hazo_fb_form.js.map +1 -1
- package/dist/components/hazo_fb_form/hooks/use_fb_form_state.d.ts +3 -3
- package/dist/components/hazo_fb_form/hooks/use_fb_form_state.d.ts.map +1 -1
- package/dist/components/hazo_fb_form/hooks/use_fb_form_state.js +340 -61
- package/dist/components/hazo_fb_form/hooks/use_fb_form_state.js.map +1 -1
- package/dist/components/hazo_fb_form/hooks/use_llm_run.d.ts +3 -1
- package/dist/components/hazo_fb_form/hooks/use_llm_run.d.ts.map +1 -1
- package/dist/components/hazo_fb_form/hooks/use_llm_run.js +89 -11
- package/dist/components/hazo_fb_form/hooks/use_llm_run.js.map +1 -1
- package/dist/components/hazo_fb_form/shared/agent_stepper.js +1 -1
- package/dist/components/hazo_fb_form/shared/agent_stepper.js.map +1 -1
- package/dist/components/hazo_fb_form/shared/file_status_accordion.d.ts +9 -0
- package/dist/components/hazo_fb_form/shared/file_status_accordion.d.ts.map +1 -0
- package/dist/components/hazo_fb_form/shared/file_status_accordion.js +39 -0
- package/dist/components/hazo_fb_form/shared/file_status_accordion.js.map +1 -0
- package/dist/components/hazo_fb_form/shared/format.d.ts.map +1 -1
- package/dist/components/hazo_fb_form/shared/format.js +8 -3
- package/dist/components/hazo_fb_form/shared/format.js.map +1 -1
- package/dist/components/hazo_fb_form/shared/send_back_item_card.d.ts +7 -1
- package/dist/components/hazo_fb_form/shared/send_back_item_card.d.ts.map +1 -1
- package/dist/components/hazo_fb_form/shared/send_back_item_card.js +6 -3
- package/dist/components/hazo_fb_form/shared/send_back_item_card.js.map +1 -1
- package/dist/components/hazo_fb_form/types.d.ts +3 -1
- package/dist/components/hazo_fb_form/types.d.ts.map +1 -1
- package/dist/components/hazo_fb_form/views/back_office_view.js +1 -1
- package/dist/components/hazo_fb_form/views/back_office_view.js.map +1 -1
- package/dist/components/hazo_fb_form/views/clarifications_view.js +2 -2
- package/dist/components/hazo_fb_form/views/clarifications_view.js.map +1 -1
- package/dist/components/hazo_fb_form/views/front_office_view.d.ts.map +1 -1
- package/dist/components/hazo_fb_form/views/front_office_view.js +62 -41
- package/dist/components/hazo_fb_form/views/front_office_view.js.map +1 -1
- package/dist/components/hazo_fb_form/views/interim_view.js +3 -3
- package/dist/components/hazo_fb_form/views/interim_view.js.map +1 -1
- package/dist/components/hazo_fb_form/views/review_queue_view.d.ts.map +1 -1
- package/dist/components/hazo_fb_form/views/review_queue_view.js +22 -9
- package/dist/components/hazo_fb_form/views/review_queue_view.js.map +1 -1
- package/dist/components/hazo_validation_rule_editor/components/rule_editor.d.ts.map +1 -1
- package/dist/components/hazo_validation_rule_editor/components/rule_editor.js +32 -3
- package/dist/components/hazo_validation_rule_editor/components/rule_editor.js.map +1 -1
- package/dist/components/hazo_validation_rule_editor/components/variable_chain_input.d.ts +20 -0
- package/dist/components/hazo_validation_rule_editor/components/variable_chain_input.d.ts.map +1 -0
- package/dist/components/hazo_validation_rule_editor/components/variable_chain_input.js +34 -0
- package/dist/components/hazo_validation_rule_editor/components/variable_chain_input.js.map +1 -0
- package/dist/components/hazo_validation_rule_editor/context.d.ts +3 -2
- package/dist/components/hazo_validation_rule_editor/context.d.ts.map +1 -1
- package/dist/components/hazo_validation_rule_editor/context.js +15 -3
- package/dist/components/hazo_validation_rule_editor/context.js.map +1 -1
- package/dist/components/hazo_validation_rule_editor/types.d.ts +7 -1
- package/dist/components/hazo_validation_rule_editor/types.d.ts.map +1 -1
- package/dist/components/hazo_validation_rule_editor/validation_rule_editor.d.ts +1 -1
- package/dist/components/hazo_validation_rule_editor/validation_rule_editor.d.ts.map +1 -1
- package/dist/components/hazo_validation_rule_editor/validation_rule_editor.js +2 -2
- package/dist/components/hazo_validation_rule_editor/validation_rule_editor.js.map +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/shared/document_type_editor.d.ts +31 -0
- package/dist/components/shared/document_type_editor.d.ts.map +1 -0
- package/dist/components/shared/document_type_editor.js +60 -0
- package/dist/components/shared/document_type_editor.js.map +1 -0
- package/dist/components/shared/file_bar/file_bar.d.ts +7 -1
- package/dist/components/shared/file_bar/file_bar.d.ts.map +1 -1
- package/dist/components/shared/file_bar/file_bar.js +5 -3
- package/dist/components/shared/file_bar/file_bar.js.map +1 -1
- package/dist/components/shared/file_bar/file_bar_validation_dialog.js +4 -4
- package/dist/components/shared/file_bar/file_bar_validation_dialog.js.map +1 -1
- package/dist/components/shared/file_status_icon.d.ts +23 -0
- package/dist/components/shared/file_status_icon.d.ts.map +1 -0
- package/dist/components/shared/file_status_icon.js +38 -0
- package/dist/components/shared/file_status_icon.js.map +1 -0
- package/dist/components/shared/json_data_panel/json_data_panel.d.ts +1 -1
- package/dist/components/shared/json_data_panel/json_data_panel.d.ts.map +1 -1
- package/dist/components/shared/json_data_panel/json_data_panel.js +27 -2
- package/dist/components/shared/json_data_panel/json_data_panel.js.map +1 -1
- package/dist/components/shared/rule_result_card.d.ts.map +1 -1
- package/dist/components/shared/rule_result_card.js +5 -4
- package/dist/components/shared/rule_result_card.js.map +1 -1
- package/dist/components/thread_form/components/add_question_dialog.d.ts +12 -0
- package/dist/components/thread_form/components/add_question_dialog.d.ts.map +1 -0
- package/dist/components/thread_form/components/add_question_dialog.js +36 -0
- package/dist/components/thread_form/components/add_question_dialog.js.map +1 -0
- package/dist/components/thread_form/components/agent_compose_dialog.d.ts +30 -0
- package/dist/components/thread_form/components/agent_compose_dialog.d.ts.map +1 -0
- package/dist/components/thread_form/components/agent_compose_dialog.js +45 -0
- package/dist/components/thread_form/components/agent_compose_dialog.js.map +1 -0
- package/dist/components/thread_form/components/clarification.d.ts +14 -0
- package/dist/components/thread_form/components/clarification.d.ts.map +1 -0
- package/dist/components/thread_form/components/clarification.js +12 -0
- package/dist/components/thread_form/components/clarification.js.map +1 -0
- package/dist/components/thread_form/components/collected_data_view.d.ts +15 -0
- package/dist/components/thread_form/components/collected_data_view.d.ts.map +1 -0
- package/dist/components/thread_form/components/collected_data_view.js +121 -0
- package/dist/components/thread_form/components/collected_data_view.js.map +1 -0
- package/dist/components/thread_form/components/coverage_card.d.ts +11 -0
- package/dist/components/thread_form/components/coverage_card.d.ts.map +1 -0
- package/dist/components/thread_form/components/coverage_card.js +60 -0
- package/dist/components/thread_form/components/coverage_card.js.map +1 -0
- package/dist/components/thread_form/components/file_bar.d.ts +93 -0
- package/dist/components/thread_form/components/file_bar.d.ts.map +1 -0
- package/dist/components/thread_form/components/file_bar.js +251 -0
- package/dist/components/thread_form/components/file_bar.js.map +1 -0
- package/dist/components/thread_form/components/file_info_dialog.d.ts +15 -0
- package/dist/components/thread_form/components/file_info_dialog.d.ts.map +1 -0
- package/dist/components/thread_form/components/file_info_dialog.js +64 -0
- package/dist/components/thread_form/components/file_info_dialog.js.map +1 -0
- package/dist/components/thread_form/components/issue_group_tree.d.ts +20 -0
- package/dist/components/thread_form/components/issue_group_tree.d.ts.map +1 -0
- package/dist/components/thread_form/components/issue_group_tree.js +164 -0
- package/dist/components/thread_form/components/issue_group_tree.js.map +1 -0
- package/dist/components/thread_form/components/pdf_side_panel.d.ts +20 -0
- package/dist/components/thread_form/components/pdf_side_panel.d.ts.map +1 -0
- package/dist/components/thread_form/components/pdf_side_panel.js +63 -0
- package/dist/components/thread_form/components/pdf_side_panel.js.map +1 -0
- package/dist/components/thread_form/components/rule_decision_row.d.ts +31 -0
- package/dist/components/thread_form/components/rule_decision_row.d.ts.map +1 -0
- package/dist/components/thread_form/components/rule_decision_row.js +20 -0
- package/dist/components/thread_form/components/rule_decision_row.js.map +1 -0
- package/dist/components/thread_form/components/send_back_message.d.ts +32 -0
- package/dist/components/thread_form/components/send_back_message.d.ts.map +1 -0
- package/dist/components/thread_form/components/send_back_message.js +82 -0
- package/dist/components/thread_form/components/send_back_message.js.map +1 -0
- package/dist/components/thread_form/components/shared.d.ts +54 -0
- package/dist/components/thread_form/components/shared.d.ts.map +1 -0
- package/dist/components/thread_form/components/shared.js +136 -0
- package/dist/components/thread_form/components/shared.js.map +1 -0
- package/dist/components/thread_form/components/task_card.d.ts +90 -0
- package/dist/components/thread_form/components/task_card.d.ts.map +1 -0
- package/dist/components/thread_form/components/task_card.js +63 -0
- package/dist/components/thread_form/components/task_card.js.map +1 -0
- package/dist/components/thread_form/components/text_doc_check.d.ts +15 -0
- package/dist/components/thread_form/components/text_doc_check.d.ts.map +1 -0
- package/dist/components/thread_form/components/text_doc_check.js +16 -0
- package/dist/components/thread_form/components/text_doc_check.js.map +1 -0
- package/dist/components/thread_form/components/text_extraction.d.ts +14 -0
- package/dist/components/thread_form/components/text_extraction.d.ts.map +1 -0
- package/dist/components/thread_form/components/text_extraction.js +16 -0
- package/dist/components/thread_form/components/text_extraction.js.map +1 -0
- package/dist/components/thread_form/components/thread_composer.d.ts +15 -0
- package/dist/components/thread_form/components/thread_composer.d.ts.map +1 -0
- package/dist/components/thread_form/components/thread_composer.js +93 -0
- package/dist/components/thread_form/components/thread_composer.js.map +1 -0
- package/dist/components/thread_form/components/thread_timeline.d.ts +65 -0
- package/dist/components/thread_form/components/thread_timeline.d.ts.map +1 -0
- package/dist/components/thread_form/components/thread_timeline.js +225 -0
- package/dist/components/thread_form/components/thread_timeline.js.map +1 -0
- package/dist/components/thread_form/hooks/use_file_pipeline.d.ts +126 -0
- package/dist/components/thread_form/hooks/use_file_pipeline.d.ts.map +1 -0
- package/dist/components/thread_form/hooks/use_file_pipeline.js +760 -0
- package/dist/components/thread_form/hooks/use_file_pipeline.js.map +1 -0
- package/dist/components/thread_form/hooks/use_thread_form.d.ts +36 -0
- package/dist/components/thread_form/hooks/use_thread_form.d.ts.map +1 -0
- package/dist/components/thread_form/hooks/use_thread_form.js +126 -0
- package/dist/components/thread_form/hooks/use_thread_form.js.map +1 -0
- package/dist/components/thread_form/index.d.ts +33 -0
- package/dist/components/thread_form/index.d.ts.map +1 -0
- package/dist/components/thread_form/index.js +30 -0
- package/dist/components/thread_form/index.js.map +1 -0
- package/dist/components/thread_form/sample_data.d.ts +8 -0
- package/dist/components/thread_form/sample_data.d.ts.map +1 -0
- package/dist/components/thread_form/sample_data.js +658 -0
- package/dist/components/thread_form/sample_data.js.map +1 -0
- package/dist/components/thread_form/thread_form.d.ts +7 -0
- package/dist/components/thread_form/thread_form.d.ts.map +1 -0
- package/dist/components/thread_form/thread_form.js +1385 -0
- package/dist/components/thread_form/thread_form.js.map +1 -0
- package/dist/components/thread_form/types.d.ts +402 -0
- package/dist/components/thread_form/types.d.ts.map +1 -0
- package/dist/components/thread_form/types.js +23 -0
- package/dist/components/thread_form/types.js.map +1 -0
- package/dist/components/thread_form/utils/file_decision_state.d.ts +22 -0
- package/dist/components/thread_form/utils/file_decision_state.d.ts.map +1 -0
- package/dist/components/thread_form/utils/file_decision_state.js +37 -0
- package/dist/components/thread_form/utils/file_decision_state.js.map +1 -0
- package/dist/components/thread_form/utils/merge_send_back.d.ts +13 -0
- package/dist/components/thread_form/utils/merge_send_back.d.ts.map +1 -0
- package/dist/components/thread_form/utils/merge_send_back.js +23 -0
- package/dist/components/thread_form/utils/merge_send_back.js.map +1 -0
- package/dist/lib/autofill_handler.d.ts.map +1 -1
- package/dist/lib/autofill_handler.js +5 -44
- package/dist/lib/autofill_handler.js.map +1 -1
- package/dist/lib/classification_handler.d.ts +105 -0
- package/dist/lib/classification_handler.d.ts.map +1 -0
- package/dist/lib/classification_handler.js +342 -0
- package/dist/lib/classification_handler.js.map +1 -0
- package/dist/lib/content_gate_handler.d.ts +37 -0
- package/dist/lib/content_gate_handler.d.ts.map +1 -0
- package/dist/lib/content_gate_handler.js +126 -0
- package/dist/lib/content_gate_handler.js.map +1 -0
- package/dist/lib/index.d.ts +10 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +5 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/periodic_coverage_runner.d.ts +24 -0
- package/dist/lib/periodic_coverage_runner.d.ts.map +1 -0
- package/dist/lib/periodic_coverage_runner.js +121 -0
- package/dist/lib/periodic_coverage_runner.js.map +1 -0
- package/dist/lib/resolution_handler.d.ts +150 -0
- package/dist/lib/resolution_handler.d.ts.map +1 -0
- package/dist/lib/resolution_handler.js +597 -0
- package/dist/lib/resolution_handler.js.map +1 -0
- package/dist/lib/resolve_variable.d.ts +25 -0
- package/dist/lib/resolve_variable.d.ts.map +1 -0
- package/dist/lib/resolve_variable.js +77 -0
- package/dist/lib/resolve_variable.js.map +1 -0
- package/dist/lib/validation_handler.d.ts +27 -3
- package/dist/lib/validation_handler.d.ts.map +1 -1
- package/dist/lib/validation_handler.js +338 -288
- package/dist/lib/validation_handler.js.map +1 -1
- package/dist/types/clarification.d.ts +54 -0
- package/dist/types/clarification.d.ts.map +1 -1
- package/dist/types/fb_form_data.d.ts +273 -123
- package/dist/types/fb_form_data.d.ts.map +1 -1
- package/dist/types/fb_form_data.js +44 -58
- package/dist/types/fb_form_data.js.map +1 -1
- package/dist/types/fb_form_data_v1.d.ts +250 -0
- package/dist/types/fb_form_data_v1.d.ts.map +1 -0
- package/dist/types/fb_form_data_v1.js +117 -0
- package/dist/types/fb_form_data_v1.js.map +1 -0
- package/dist/types/fb_form_instance.d.ts +1 -1
- package/dist/types/fb_form_instance.d.ts.map +1 -1
- package/dist/types/index.d.ts +5 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/validation.d.ts +134 -12
- package/dist/types/validation.d.ts.map +1 -1
- package/dist/utils/expectation_extractor.d.ts +31 -0
- package/dist/utils/expectation_extractor.d.ts.map +1 -0
- package/dist/utils/expectation_extractor.js +142 -0
- package/dist/utils/expectation_extractor.js.map +1 -0
- package/dist/utils/fb_data_adapter.d.ts +7 -2
- package/dist/utils/fb_data_adapter.d.ts.map +1 -1
- package/dist/utils/fb_data_adapter.js +58 -7
- package/dist/utils/fb_data_adapter.js.map +1 -1
- package/dist/utils/fb_data_adapter_v2.d.ts +17 -0
- package/dist/utils/fb_data_adapter_v2.d.ts.map +1 -0
- package/dist/utils/fb_data_adapter_v2.js +483 -0
- package/dist/utils/fb_data_adapter_v2.js.map +1 -0
- package/dist/utils/fb_data_helpers.d.ts +1 -1
- package/dist/utils/fb_data_helpers.d.ts.map +1 -1
- package/dist/utils/fb_data_mutations.d.ts +1 -1
- package/dist/utils/fb_data_mutations.d.ts.map +1 -1
- package/dist/utils/fb_data_mutations_v2.d.ts +46 -0
- package/dist/utils/fb_data_mutations_v2.d.ts.map +1 -0
- package/dist/utils/fb_data_mutations_v2.js +341 -0
- package/dist/utils/fb_data_mutations_v2.js.map +1 -0
- package/dist/utils/fb_data_queries.d.ts +81 -0
- package/dist/utils/fb_data_queries.d.ts.map +1 -0
- package/dist/utils/fb_data_queries.js +354 -0
- package/dist/utils/fb_data_queries.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/issue_bucketing.d.ts +36 -0
- package/dist/utils/issue_bucketing.d.ts.map +1 -0
- package/dist/utils/issue_bucketing.js +107 -0
- package/dist/utils/issue_bucketing.js.map +1 -0
- package/dist/utils/validation_result.d.ts +32 -0
- package/dist/utils/validation_result.d.ts.map +1 -0
- package/dist/utils/validation_result.js +55 -0
- package/dist/utils/validation_result.js.map +1 -0
- 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
|