@paro.io/expert-shared-components 1.14.55 → 1.14.57
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/lib/tax-axis/components/clientReport/Methodology.js +2 -2
- package/lib/tax-axis/components/dashboard/StrategyDetailPanel.js +33 -36
- package/lib/tax-axis/components/dashboard/TaxAxisDashboard.js +127 -8
- package/lib/tax-axis/components/processing/TaxAxisProcessing.js +30 -5
- package/package.json +1 -1
|
@@ -10,13 +10,13 @@ const SectionOpener_1 = require("./SectionOpener");
|
|
|
10
10
|
function Methodology({ profile, eligible, palette }) {
|
|
11
11
|
return (react_1.default.createElement(react_1.default.Fragment, null,
|
|
12
12
|
react_1.default.createElement(SectionOpener_1.SectionOpener, { number: "04", eyebrow: "METHODOLOGY", headline: "How TaxAxis arrives at these numbers.", bullets: [
|
|
13
|
-
"25 federal strategies evaluated against your profile
|
|
13
|
+
"25 federal strategies evaluated against your profile through a two-stage AI and deterministic pipeline \u2014 every recommendation traces to a specific IRC section, revenue ruling, or OBBBA provision.",
|
|
14
14
|
"Confidence bands reflect data quality: \u00B130% on single-year data, narrowing to \u00B115% with multi-year history.",
|
|
15
15
|
"Aggregate savings on the cover chart are adjusted by an interaction-discount factor reflecting strategy stacking, marginal-rate exhaustion, and \u00A76694 reasonableness.",
|
|
16
16
|
], palette: palette }),
|
|
17
17
|
react_1.default.createElement("div", { style: { marginBottom: 28 } },
|
|
18
18
|
react_1.default.createElement("div", { style: { fontSize: 10, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.12em", color: palette.gray400, fontFamily: palette.mono, marginBottom: 8 } }, "EVAL PIPELINE"),
|
|
19
|
-
react_1.default.createElement("div", { style: { fontSize: 13, color: palette.gray700, lineHeight: 1.8, fontFamily: palette.body } }, "TaxAxis evaluates 25 federal tax strategies against each client profile.
|
|
19
|
+
react_1.default.createElement("div", { style: { fontSize: 13, color: palette.gray700, lineHeight: 1.8, fontFamily: palette.body } }, "TaxAxis evaluates 25 federal tax strategies against each client profile through a two-stage pipeline. An AI model (Claude Sonnet 4.5 via AWS Bedrock) reads your financial documents, identifies applicable strategies, and drafts narrative content. A deterministic post-processing engine then recomputes savings ranges against IRC-cited rules, corrects confidence bands based on data quality, and validates every output through an 8-gate compliance check \u2014 covering schema integrity, strategy coverage, profile consistency, mathematical accuracy, business rules, \u00A76694 compliance, narrative quality, and entity eligibility. Every recommendation is reviewed by a qualified tax professional before the report is issued.")),
|
|
20
20
|
react_1.default.createElement("div", { style: { marginBottom: 28 } },
|
|
21
21
|
react_1.default.createElement("div", { style: { fontSize: 10, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.12em", color: palette.gray400, fontFamily: palette.mono, marginBottom: 8 } }, "CONFIDENCE BANDS"),
|
|
22
22
|
react_1.default.createElement("div", { style: { fontSize: 13, color: palette.gray700, lineHeight: 1.8, fontFamily: palette.body, marginBottom: 12 } }, "Savings ranges reflect data quality. Clients who provide multiple years of financial history receive tighter bands of \u00B115%, because multi-year data reveals trends and reduces estimation variance. Single-year data widens the bands to \u00B130%. Within each band, the low estimate uses conservative inputs at the minimum applicable rate, while the high estimate uses full applicable inputs at the client's marginal rate."),
|
|
@@ -25,49 +25,46 @@ function positionStrengthLabel(positionStrength) {
|
|
|
25
25
|
return { label: positionStrength, color: "#9498B8", dots: 2 };
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
* Split engagement_recommendation
|
|
29
|
-
* - savingsSection: sentences/paragraphs that contain dollar figures or formula references
|
|
30
|
-
* → shown as "HOW SAVINGS BREAK DOWN"
|
|
31
|
-
* - cpaSection: the remaining implementation/CPA-action paragraphs
|
|
32
|
-
* → shown as "CPA ENGAGEMENT NOTES"
|
|
28
|
+
* Split engagement_recommendation at the implementation/action boundary.
|
|
33
29
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
30
|
+
* The LLM writes engagement_recommendation as a single paragraph covering both
|
|
31
|
+
* the savings math AND the CPA implementation steps. We split at the first
|
|
32
|
+
* implementation keyword so the savings math shows under "HOW SAVINGS BREAK DOWN"
|
|
33
|
+
* and the CPA action steps show under "CPA ENGAGEMENT NOTES".
|
|
34
|
+
*
|
|
35
|
+
* Heuristic: split at "Implementation:", "CPA should", "CPA must", or numbered
|
|
36
|
+
* action lists "(1)". If no split point found, entire text goes to savings section.
|
|
36
37
|
*/
|
|
37
38
|
function splitEngagementText(text) {
|
|
38
39
|
if (!text)
|
|
39
40
|
return { savingsSection: "", cpaSection: "" };
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const hasPercent = /\d+\.?\d*%/.test(para);
|
|
48
|
-
const hasCpaKeyword = /\bCPA\b|actuar|implement|deadline|establish|file|form\s+\d|verify|engage|model|recommend/i.test(para);
|
|
49
|
-
if ((hasDollar || hasFormula || hasPercent) && !hasCpaKeyword) {
|
|
50
|
-
savingsParas.push(para);
|
|
51
|
-
}
|
|
52
|
-
else if (savingsParas.length === 0) {
|
|
53
|
-
// First para with dollar amounts seeds savings section even if it has CPA keywords
|
|
54
|
-
if (hasDollar || hasFormula)
|
|
55
|
-
savingsParas.push(para);
|
|
56
|
-
else
|
|
57
|
-
cpaParas.push(para);
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
cpaParas.push(para);
|
|
61
|
-
}
|
|
41
|
+
// Try splitting on double-newline first (well-structured LLM output)
|
|
42
|
+
const paraBreak = text.indexOf("\n\n");
|
|
43
|
+
if (paraBreak !== -1) {
|
|
44
|
+
return {
|
|
45
|
+
savingsSection: text.slice(0, paraBreak).trim(),
|
|
46
|
+
cpaSection: text.slice(paraBreak).trim(),
|
|
47
|
+
};
|
|
62
48
|
}
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
49
|
+
// Single-paragraph: find the first implementation boundary
|
|
50
|
+
const splitPatterns = [
|
|
51
|
+
/\s+Implementation:\s+/i,
|
|
52
|
+
/\s+CPA should\s+/i,
|
|
53
|
+
/\s+CPA must\s+/i,
|
|
54
|
+
/\s+To implement:\s+/i,
|
|
55
|
+
/\s+\(1\)\s+/, // numbered list start
|
|
56
|
+
];
|
|
57
|
+
for (const pattern of splitPatterns) {
|
|
58
|
+
const match = text.search(pattern);
|
|
59
|
+
if (match > 80) { // only split if there's meaningful content before it
|
|
60
|
+
return {
|
|
61
|
+
savingsSection: text.slice(0, match).trim(),
|
|
62
|
+
cpaSection: text.slice(match).trim(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
66
65
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
cpaSection: cpaParas.join("\n\n"),
|
|
70
|
-
};
|
|
66
|
+
// No clean split point — show everything as savings breakdown, nothing as CPA notes
|
|
67
|
+
return { savingsSection: text, cpaSection: "" };
|
|
71
68
|
}
|
|
72
69
|
function SourceDocRows({ docs, trace }) {
|
|
73
70
|
if (docs && docs.length > 0) {
|
|
@@ -26,7 +26,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
26
26
|
exports.TaxAxisDashboard = TaxAxisDashboard;
|
|
27
27
|
const react_1 = __importStar(require("react"));
|
|
28
28
|
const data_1 = require("../../lib/data");
|
|
29
|
+
const strategyNarrative_1 = require("../../lib/data/strategyNarrative");
|
|
29
30
|
const compute_1 = require("../../lib/compute");
|
|
31
|
+
const useEngineOutput_1 = require("../../lib/adapters/useEngineOutput");
|
|
30
32
|
const TaxAxisButton_1 = require("../shared/TaxAxisButton");
|
|
31
33
|
const TaxAxisBadge_1 = require("../shared/TaxAxisBadge");
|
|
32
34
|
const DashboardSummary_1 = require("./DashboardSummary");
|
|
@@ -96,18 +98,109 @@ function buildRoadmapBucketLookup(llm) {
|
|
|
96
98
|
}
|
|
97
99
|
return map;
|
|
98
100
|
}
|
|
99
|
-
|
|
101
|
+
// LLM sometimes emits these literal placeholder strings as why_it_applies / next_step
|
|
102
|
+
// content. When detected, we fall back to STRATEGY_NARRATIVE static content.
|
|
103
|
+
const PLACEHOLDER_WHY = "This strategy may apply based on your financial profile.";
|
|
104
|
+
const PLACEHOLDER_NEXT = "Discuss with your CPA to evaluate and implement this strategy.";
|
|
105
|
+
function isMissingOrPlaceholder(value, knownPlaceholder) {
|
|
106
|
+
if (!value)
|
|
107
|
+
return true;
|
|
108
|
+
const trimmed = value.trim();
|
|
109
|
+
if (trimmed === "")
|
|
110
|
+
return true;
|
|
111
|
+
if (trimmed === knownPlaceholder)
|
|
112
|
+
return true;
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
// Parse "S10" -> 10. Returns null if strategyId doesn't match the SN pattern.
|
|
116
|
+
function parseStrategyNumber(strategyId) {
|
|
117
|
+
if (!strategyId)
|
|
118
|
+
return null;
|
|
119
|
+
const match = strategyId.match(/^S(\d+)$/);
|
|
120
|
+
if (!match)
|
|
121
|
+
return null;
|
|
122
|
+
return parseInt(match[1], 10);
|
|
123
|
+
}
|
|
124
|
+
// Build a strategy_id -> client_summary entry lookup using strategy_analysis as the join table.
|
|
125
|
+
function buildClientSummaryLookup(llm) {
|
|
126
|
+
var _a, _b, _c, _d;
|
|
127
|
+
const lookup = new Map();
|
|
128
|
+
const engineOutput = (_a = llm.engineOutput) !== null && _a !== void 0 ? _a : llm.rawOutput;
|
|
129
|
+
if (!engineOutput)
|
|
130
|
+
return lookup;
|
|
131
|
+
const strategyAnalysis = (_b = engineOutput.strategy_analysis) !== null && _b !== void 0 ? _b : [];
|
|
132
|
+
const clientSummaryStrategies = (_d = (_c = engineOutput.client_summary) === null || _c === void 0 ? void 0 : _c.strategies) !== null && _d !== void 0 ? _d : [];
|
|
133
|
+
// strategy_name -> client_summary entry
|
|
134
|
+
const byName = new Map();
|
|
135
|
+
for (const cs of clientSummaryStrategies) {
|
|
136
|
+
if (cs.strategy_name) {
|
|
137
|
+
byName.set(cs.strategy_name, cs);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// strategy_id -> client_summary entry (via strategy_analysis name lookup)
|
|
141
|
+
for (const sa of strategyAnalysis) {
|
|
142
|
+
if (sa.strategy_id && sa.strategy_name) {
|
|
143
|
+
const cs = byName.get(sa.strategy_name);
|
|
144
|
+
if (cs) {
|
|
145
|
+
lookup.set(sa.strategy_id, cs);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return lookup;
|
|
150
|
+
}
|
|
151
|
+
function mapLlmToStrategies(llm, profile) {
|
|
100
152
|
const nameLookup = buildEngineNameLookup(llm);
|
|
101
153
|
const bucketLookup = buildRoadmapBucketLookup(llm);
|
|
154
|
+
const clientSummaryLookup = buildClientSummaryLookup(llm);
|
|
102
155
|
return llm.strategies
|
|
103
156
|
.filter((s) => s.applicable)
|
|
104
157
|
.map((s, idx) => {
|
|
105
|
-
var _a, _b, _c, _d, _e;
|
|
158
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
106
159
|
const id = (_a = s.strategyId) !== null && _a !== void 0 ? _a : s.strategyType;
|
|
107
|
-
// Prefer the real strategy_name from engineOutput, fall back to humanized strategyType
|
|
108
160
|
const name = (_b = nameLookup.get(id)) !== null && _b !== void 0 ? _b : humanizeStrategyType(s.strategyType);
|
|
109
161
|
const timelineBucket = (_c = bucketLookup.get(id)) !== null && _c !== void 0 ? _c : (s.priority === "HIGH" ? "now" : "90d");
|
|
110
162
|
const weightedScore = (_d = s.weightedScore) !== null && _d !== void 0 ? _d : (s.priority === "HIGH" ? 85 : s.priority === "MEDIUM" ? 70 : 55);
|
|
163
|
+
// Lookup the client_summary entry for this strategy (top-3 strategies only)
|
|
164
|
+
const csEntry = clientSummaryLookup.get(id);
|
|
165
|
+
const llmWhy = csEntry === null || csEntry === void 0 ? void 0 : csEntry.why_it_applies;
|
|
166
|
+
const llmNext = csEntry === null || csEntry === void 0 ? void 0 : csEntry.next_step;
|
|
167
|
+
// Strategy number for STRATEGY_NARRATIVE static lookup (e.g. "S10" -> 10)
|
|
168
|
+
const strategyNumber = parseStrategyNumber(id);
|
|
169
|
+
const staticNarrative = strategyNumber !== null ? strategyNarrative_1.STRATEGY_NARRATIVE[strategyNumber] : undefined;
|
|
170
|
+
// clientBrief: prefer LLM why_it_applies, fall back to static, then neutral default
|
|
171
|
+
let clientBrief;
|
|
172
|
+
if (!isMissingOrPlaceholder(llmWhy, PLACEHOLDER_WHY)) {
|
|
173
|
+
clientBrief = llmWhy;
|
|
174
|
+
}
|
|
175
|
+
else if (staticNarrative === null || staticNarrative === void 0 ? void 0 : staticNarrative.whyMatters) {
|
|
176
|
+
clientBrief = staticNarrative.whyMatters(profile);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
clientBrief = "Your preparer will review this strategy and include the analysis in your engagement report.";
|
|
180
|
+
}
|
|
181
|
+
// action: prefer LLM next_step, fall back to static nextSteps, then implementationSteps[0]
|
|
182
|
+
let action;
|
|
183
|
+
if (!isMissingOrPlaceholder(llmNext, PLACEHOLDER_NEXT)) {
|
|
184
|
+
action = llmNext;
|
|
185
|
+
}
|
|
186
|
+
else if (staticNarrative === null || staticNarrative === void 0 ? void 0 : staticNarrative.nextSteps) {
|
|
187
|
+
action = staticNarrative.nextSteps;
|
|
188
|
+
}
|
|
189
|
+
else if ((_e = s.implementationSteps) === null || _e === void 0 ? void 0 : _e[0]) {
|
|
190
|
+
action = s.implementationSteps[0];
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
action = "Work with your CPA to implement this strategy.";
|
|
194
|
+
}
|
|
195
|
+
// Pull calculation_trace fields. Defensive about shape.
|
|
196
|
+
const ct = ((_f = s.calculationTrace) !== null && _f !== void 0 ? _f : {});
|
|
197
|
+
const sourceDocuments = Array.isArray(ct.source_documents) ? ct.source_documents : undefined;
|
|
198
|
+
const specialistNote = typeof ct.specialist_note === "string" && ct.specialist_note.trim() !== ""
|
|
199
|
+
? ct.specialist_note
|
|
200
|
+
: ((_g = staticNarrative === null || staticNarrative === void 0 ? void 0 : staticNarrative.whySpecialist) !== null && _g !== void 0 ? _g : undefined);
|
|
201
|
+
const positionStrength = typeof ct.position_strength === "string" ? ct.position_strength : undefined;
|
|
202
|
+
const authority = typeof ct.irs_cite === "string" ? ct.irs_cite : undefined;
|
|
203
|
+
const formsFromTrace = Array.isArray(ct.forms_required) ? ct.forms_required.join(", ") : undefined;
|
|
111
204
|
return {
|
|
112
205
|
rank: idx + 1,
|
|
113
206
|
code: id,
|
|
@@ -120,13 +213,18 @@ function mapLlmToStrategies(llm) {
|
|
|
120
213
|
hi: s.estimatedSavings.max,
|
|
121
214
|
timeline: timelineBucket === "now" ? "Act now" : timelineBucket === "30d" ? "Within 30 days" : "Within 90 days",
|
|
122
215
|
timelineBucket,
|
|
123
|
-
clientBrief
|
|
124
|
-
action
|
|
125
|
-
forms: s.requiredForms.join(", "),
|
|
216
|
+
clientBrief,
|
|
217
|
+
action,
|
|
218
|
+
forms: formsFromTrace !== null && formsFromTrace !== void 0 ? formsFromTrace : s.requiredForms.join(", "),
|
|
126
219
|
abstract: s.summary,
|
|
127
220
|
sources: [],
|
|
128
221
|
trace: [],
|
|
129
222
|
cost: undefined,
|
|
223
|
+
sourceDocuments,
|
|
224
|
+
specialistNote,
|
|
225
|
+
positionStrength,
|
|
226
|
+
authority,
|
|
227
|
+
quickWin: s.quickWin,
|
|
130
228
|
};
|
|
131
229
|
});
|
|
132
230
|
}
|
|
@@ -163,8 +261,29 @@ function TaxAxisDashboard({ profile, llmResult, parsedDocuments, onDownloadClien
|
|
|
163
261
|
// Extracted documents: live from session if provided, otherwise empty (no mock fallback)
|
|
164
262
|
const extractionDocs = parsedDocuments !== null && parsedDocuments !== void 0 ? parsedDocuments : [];
|
|
165
263
|
// ─── Derived data ──────────────────────────────────────────────
|
|
166
|
-
|
|
167
|
-
|
|
264
|
+
// Prefer useEngineOutput (full rich data: sourceDocuments, positionStrength,
|
|
265
|
+
// clientBrief from why_it_applies, etc.) over the thin mapLlmToStrategies fallback.
|
|
266
|
+
// engineOutput is at llmResult.engineOutput or llmResult.rawOutput (both present).
|
|
267
|
+
const engineOutputForAdapter = (0, react_1.useMemo)(() => { var _a, _b; return (_b = (_a = llmResult === null || llmResult === void 0 ? void 0 : llmResult.engineOutput) !== null && _a !== void 0 ? _a : llmResult === null || llmResult === void 0 ? void 0 : llmResult.rawOutput) !== null && _b !== void 0 ? _b : null; }, [llmResult]);
|
|
268
|
+
const adapted = (0, useEngineOutput_1.useEngineOutput)(engineOutputForAdapter);
|
|
269
|
+
const llmStrategies = (0, react_1.useMemo)(() => {
|
|
270
|
+
var _a;
|
|
271
|
+
if (!hasLlm)
|
|
272
|
+
return [];
|
|
273
|
+
// Use adapted strategies when available — they carry the full engine data.
|
|
274
|
+
if ((_a = adapted === null || adapted === void 0 ? void 0 : adapted.strategies) === null || _a === void 0 ? void 0 : _a.length)
|
|
275
|
+
return adapted.strategies;
|
|
276
|
+
// Fallback to thin mapping for old payload shapes without engineOutput.
|
|
277
|
+
return mapLlmToStrategies(llmResult, profile);
|
|
278
|
+
}, [hasLlm, adapted, llmResult, profile]);
|
|
279
|
+
const llmComputed = (0, react_1.useMemo)(() => {
|
|
280
|
+
var _a;
|
|
281
|
+
if (!hasLlm)
|
|
282
|
+
return new Map();
|
|
283
|
+
if ((_a = adapted === null || adapted === void 0 ? void 0 : adapted.computedMap) === null || _a === void 0 ? void 0 : _a.size)
|
|
284
|
+
return adapted.computedMap;
|
|
285
|
+
return buildLlmComputed(llmStrategies);
|
|
286
|
+
}, [hasLlm, adapted, llmStrategies]);
|
|
168
287
|
const computed = (0, react_1.useMemo)(() => (hasLlm ? llmComputed : (0, compute_1.computeAllStrategies)(profile)), [hasLlm, llmComputed, profile]);
|
|
169
288
|
const dashEligible = (0, react_1.useMemo)(() => (hasLlm ? llmStrategies : (0, compute_1.filterEligibleStrategies)(profile)), [hasLlm, llmStrategies, profile]);
|
|
170
289
|
const maxSavings = Math.max(...dashEligible.map((s) => { var _a, _b; return (_b = (_a = computed.get(s.rank)) === null || _a === void 0 ? void 0 : _a.hi) !== null && _b !== void 0 ? _b : s.hi; }), 1);
|
|
@@ -26,10 +26,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
26
26
|
exports.TaxAxisProcessing = TaxAxisProcessing;
|
|
27
27
|
const react_1 = __importStar(require("react"));
|
|
28
28
|
const ProcessingStages_1 = require("./ProcessingStages");
|
|
29
|
-
const CRAWL_CEILING =
|
|
30
|
-
const CRAWL_TICK_MS =
|
|
31
|
-
const CRAWL_STEP = 0.
|
|
32
|
-
const FINISH_MS =
|
|
29
|
+
const CRAWL_CEILING = 99; // pause here until reportReady
|
|
30
|
+
const CRAWL_TICK_MS = 300; // interval between crawl ticks
|
|
31
|
+
const CRAWL_STEP = 0.27; // % added each tick → 0.27 / 0.3s = 0.9%/s
|
|
32
|
+
const FINISH_MS = 400; // ms to animate from 99% → 100% once ready
|
|
33
33
|
const STAGE_ADVANCE_MS = 3200; // how often the displayed stage advances
|
|
34
34
|
function buildStages(profile) {
|
|
35
35
|
var _a;
|
|
@@ -69,6 +69,30 @@ function TaxAxisProcessing({ onComplete, profile, reportReady = false, userConte
|
|
|
69
69
|
// Track current progress in a ref so the finish animation can read it synchronously
|
|
70
70
|
// without relying on the async functional-updater pattern.
|
|
71
71
|
const progressRef = (0, react_1.useRef)(0);
|
|
72
|
+
// ── Stall detection: show patience message after 35s of no progress change ──
|
|
73
|
+
const [showPatience, setShowPatience] = (0, react_1.useState)(false);
|
|
74
|
+
const lastProgressRef = (0, react_1.useRef)(0);
|
|
75
|
+
const stallTimerRef = (0, react_1.useRef)(null);
|
|
76
|
+
(0, react_1.useEffect)(() => {
|
|
77
|
+
if (finishingRef.current) {
|
|
78
|
+
setShowPatience(false);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (progress !== lastProgressRef.current) {
|
|
82
|
+
lastProgressRef.current = progress;
|
|
83
|
+
setShowPatience(false);
|
|
84
|
+
if (stallTimerRef.current)
|
|
85
|
+
clearTimeout(stallTimerRef.current);
|
|
86
|
+
stallTimerRef.current = setTimeout(() => {
|
|
87
|
+
if (!finishingRef.current)
|
|
88
|
+
setShowPatience(true);
|
|
89
|
+
}, 8000);
|
|
90
|
+
}
|
|
91
|
+
return () => {
|
|
92
|
+
if (stallTimerRef.current)
|
|
93
|
+
clearTimeout(stallTimerRef.current);
|
|
94
|
+
};
|
|
95
|
+
}, [progress]);
|
|
72
96
|
const setProgressSync = (v) => {
|
|
73
97
|
progressRef.current = v;
|
|
74
98
|
setProgress(v);
|
|
@@ -147,5 +171,6 @@ function TaxAxisProcessing({ onComplete, profile, reportReady = false, userConte
|
|
|
147
171
|
transition: isFinishing ? "none" : "width 0.18s linear",
|
|
148
172
|
} })),
|
|
149
173
|
react_1.default.createElement(ProcessingStages_1.ProcessingStages, { stages: stages, progress: stageIdx + 1 }),
|
|
150
|
-
pct >= CRAWL_CEILING && !isFinishing && (react_1.default.createElement("p", { className: "text-[11px] text-tax-axis-text-4 font-tax-axis-mono mt-4 animate-pulse" }, "Finalizing analysis\u2026"))
|
|
174
|
+
pct >= CRAWL_CEILING && !isFinishing && (react_1.default.createElement("p", { className: "text-[11px] text-tax-axis-text-4 font-tax-axis-mono mt-4 animate-pulse" }, "Finalizing analysis\u2026")),
|
|
175
|
+
showPatience && !isFinishing && (react_1.default.createElement("p", { className: "text-[13px] text-tax-axis-text-3 font-tax-axis-body mt-4 text-center" }, "Still running \u2014 document analysis typically takes 2\u20133 minutes. Do not navigate away."))));
|
|
151
176
|
}
|