@cognior/iap-sdk 0.1.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.

Potentially problematic release.


This version of @cognior/iap-sdk might be problematic. Click here for more details.

Files changed (60) hide show
  1. package/.github/copilot-instructions.md +95 -0
  2. package/README.md +79 -0
  3. package/TRACKING.md +105 -0
  4. package/USER_CONTEXT_README.md +284 -0
  5. package/package.json +154 -0
  6. package/src/config.ts +25 -0
  7. package/src/core/flowEngine.ts +1833 -0
  8. package/src/core/triggerManager.ts +1011 -0
  9. package/src/experiences/banner.ts +366 -0
  10. package/src/experiences/beacon.ts +668 -0
  11. package/src/experiences/hotspotTour.ts +654 -0
  12. package/src/experiences/hotspots.ts +566 -0
  13. package/src/experiences/modal.ts +1337 -0
  14. package/src/experiences/modalSequence.ts +1247 -0
  15. package/src/experiences/popover.ts +652 -0
  16. package/src/experiences/registry.ts +21 -0
  17. package/src/experiences/survey.ts +1639 -0
  18. package/src/experiences/taskList.ts +625 -0
  19. package/src/experiences/tooltip.ts +740 -0
  20. package/src/experiences/types.ts +395 -0
  21. package/src/experiences/walkthrough.ts +670 -0
  22. package/src/flow-sequence.ts +177 -0
  23. package/src/flows.ts +512 -0
  24. package/src/http.ts +61 -0
  25. package/src/index.ts +355 -0
  26. package/src/services/flowManager.ts +905 -0
  27. package/src/services/flowNormalizer.ts +74 -0
  28. package/src/services/locationContextService.ts +189 -0
  29. package/src/services/pageContextService.ts +221 -0
  30. package/src/services/userContextService.ts +286 -0
  31. package/src/state/appState.ts +0 -0
  32. package/src/state/hooks.ts +0 -0
  33. package/src/state/index.ts +0 -0
  34. package/src/state/migration.ts +0 -0
  35. package/src/state/store.ts +0 -0
  36. package/src/styles/banner.css.ts +0 -0
  37. package/src/styles/hotspot.css.ts +0 -0
  38. package/src/styles/hotspotTour.css.ts +0 -0
  39. package/src/styles/modal.css.ts +564 -0
  40. package/src/styles/survey.css.ts +1013 -0
  41. package/src/styles/taskList.css.ts +0 -0
  42. package/src/styles/tooltip.css.ts +149 -0
  43. package/src/styles/walkthrough.css.ts +0 -0
  44. package/src/tourUtils.ts +0 -0
  45. package/src/tracking.ts +223 -0
  46. package/src/utils/debounce.ts +66 -0
  47. package/src/utils/eventSequenceValidator.ts +124 -0
  48. package/src/utils/flowTrackingSystem.ts +524 -0
  49. package/src/utils/idGenerator.ts +155 -0
  50. package/src/utils/immediateValidationPrevention.ts +184 -0
  51. package/src/utils/normalize.ts +50 -0
  52. package/src/utils/privacyManager.ts +166 -0
  53. package/src/utils/ruleEvaluator.ts +199 -0
  54. package/src/utils/sanitize.ts +79 -0
  55. package/src/utils/selectors.ts +107 -0
  56. package/src/utils/stepExecutor.ts +345 -0
  57. package/src/utils/triggerNormalizer.ts +149 -0
  58. package/src/utils/validationInterceptor.ts +650 -0
  59. package/tsconfig.json +13 -0
  60. package/tsup.config.ts +13 -0
@@ -0,0 +1,1833 @@
1
+ // src/core/flowEngine.ts
2
+ // Enhanced flow execution engine with step-level trigger support
3
+
4
+ import { LocationContextService } from '../services/locationContextService';
5
+ import { userContextService } from '../services/userContextService';
6
+ import { pageContextService, type PageChangeEvent } from '../services/pageContextService';
7
+ import { resolveSelector } from '../utils/selectors';
8
+ import { normalizeTrigger } from '../utils/triggerNormalizer';
9
+ import { evaluateRuleBlock } from '../utils/ruleEvaluator';
10
+ import { trackStepView, resetFlowTracking } from '../tracking';
11
+ import { triggerManager, type EnhancedStepData } from './triggerManager';
12
+ import type {
13
+ RuleCondition,
14
+ ConditionRuleBlock as RuleBlock,
15
+ ConditionOperator,
16
+ ConditionValueType,
17
+ TriggerDefinition,
18
+ TriggerEvaluationContext,
19
+ FlowExecutionMode,
20
+ StepType,
21
+ BranchType
22
+ } from '../experiences/types';
23
+
24
+ /**
25
+ * Resolve selector to multiple elements (handles both CSS and XPath)
26
+ */
27
+ function resolveSelectorAll(selector: string, root: Document | Element = document): Element[] {
28
+ if (!selector || typeof selector !== 'string') return [];
29
+
30
+ // Try CSS
31
+ try {
32
+ const cssElements = (root as Document | Element).querySelectorAll(selector);
33
+ if (cssElements.length > 0) return Array.from(cssElements);
34
+ } catch {
35
+ // ignore invalid CSS: we'll try XPath next
36
+ }
37
+
38
+ // Try XPath
39
+ try {
40
+ const doc = root instanceof Document ? root : root.ownerDocument ?? document;
41
+ const result = doc.evaluate(
42
+ selector,
43
+ root as Node,
44
+ null,
45
+ XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
46
+ null
47
+ );
48
+
49
+ const elements: Element[] = [];
50
+ for (let i = 0; i < result.snapshotLength; i++) {
51
+ const element = result.snapshotItem(i) as Element;
52
+ if (element) elements.push(element);
53
+ }
54
+ return elements;
55
+ } catch {
56
+ return [];
57
+ }
58
+ }
59
+
60
+ // Enhanced flow model interfaces with trigger support
61
+ export interface FlowData {
62
+ flowId: string;
63
+ flowName: string;
64
+ steps: EnhancedStepData[];
65
+ execution?: {
66
+ mode?: FlowExecutionMode;
67
+ frequencyType?: string;
68
+ };
69
+ }
70
+
71
+ export interface UXExperience {
72
+ uxExperienceType: string;
73
+ elementSelector?: string;
74
+ elementTrigger?: string;
75
+ elementLocation?: string;
76
+ position?: { x: string; y: string };
77
+ content: any;
78
+ modalContent?: any;
79
+ }
80
+
81
+ // Flow execution states
82
+ type FlowExecutionState = 'ACTIVE' | 'WAITING_FOR_INPUT' | 'TRANSITIONING' | 'TERMINATED';
83
+
84
+ interface FlowState {
85
+ activeFlowId: string | null;
86
+ flowInProgress: boolean;
87
+ activeStep: number;
88
+ activeStepTriggered: boolean;
89
+ executionState: FlowExecutionState;
90
+ executionMode: FlowExecutionMode;
91
+ triggeredSteps: Set<number>; // Track triggered steps for AnyOrder mode
92
+ stepAdvancing?: boolean; // Prevent concurrent step advancement
93
+ }
94
+
95
+ /**
96
+ * Core flow execution engine
97
+ * Handles step-by-step flow execution with page awareness
98
+ */
99
+ export class FlowEngine {
100
+ private static _instance: FlowEngine;
101
+ private _locationService = LocationContextService.getInstance();
102
+ private _state: FlowState = {
103
+ activeFlowId: null,
104
+ flowInProgress: false,
105
+ activeStep: 0,
106
+ activeStepTriggered: false,
107
+ executionState: 'TERMINATED',
108
+ executionMode: 'Linear',
109
+ triggeredSteps: new Set()
110
+ };
111
+
112
+ private _currentFlow: FlowData | null = null;
113
+ private _stepTriggerListeners: Map<string, () => void> = new Map();
114
+ private _pageChangeUnsubscribe: (() => void) | null = null;
115
+ private _domObservers: Map<string, MutationObserver> = new Map();
116
+
117
+ private constructor() {
118
+ // Initialize page context service and trigger manager
119
+ pageContextService.initialize();
120
+ triggerManager.initialize();
121
+
122
+ // Listen for location-based page changes (legacy)
123
+ this._pageChangeUnsubscribe = this._locationService.subscribe((context) => {
124
+ this.checkFlowResumption();
125
+ });
126
+
127
+ // Listen for page context changes (new page-aware system)
128
+ pageContextService.subscribe(this.handlePageChange.bind(this));
129
+ }
130
+
131
+ /**
132
+ * Handle page changes and re-evaluate active flows
133
+ */
134
+ private handlePageChange(event: PageChangeEvent): void {
135
+ console.debug('[DAP] FlowEngine: Handling page change:', event.type, {
136
+ from: event.previous?.pathname,
137
+ to: event.current.pathname,
138
+ activeFlow: this._state.activeFlowId
139
+ });
140
+
141
+ // If we have an active flow, re-register triggers for current step
142
+ if (this._state.flowInProgress && this._currentFlow) {
143
+ this.reRegisterActiveStepTriggers();
144
+ }
145
+
146
+ // Also check legacy flow resumption
147
+ this.checkFlowResumption();
148
+ }
149
+
150
+ /**
151
+ * Re-register triggers for the currently active step(s) after page change
152
+ */
153
+ private reRegisterActiveStepTriggers(): void {
154
+ if (!this._currentFlow || !this._state.flowInProgress) {
155
+ return;
156
+ }
157
+
158
+ console.debug('[DAP] FlowEngine: Re-registering triggers after page change');
159
+
160
+ if (this._state.executionMode === 'Linear') {
161
+ // Re-register trigger for current step
162
+ if (this._state.activeStep < this._currentFlow.steps.length && !this._state.activeStepTriggered) {
163
+ const currentStep = this._currentFlow.steps[this._state.activeStep];
164
+ this.setupStepTrigger(currentStep, this._state.activeStep);
165
+ }
166
+ } else {
167
+ // AnyOrder mode: re-register triggers for all non-triggered steps
168
+ this._currentFlow.steps.forEach((step, index) => {
169
+ if (!this._state.triggeredSteps.has(step.stepOrder)) {
170
+ this.setupStepTrigger(step, index);
171
+ }
172
+ });
173
+ }
174
+ }
175
+
176
+ public static getInstance(): FlowEngine {
177
+ if (!this._instance) {
178
+ this._instance = new FlowEngine();
179
+ }
180
+ return this._instance;
181
+ }
182
+
183
+ /**
184
+ * Check if flow requires user context
185
+ * For now, allow anonymous flows unless rules specifically reference user properties
186
+ */
187
+ private flowRequiresUserContext(flowData: FlowData): boolean {
188
+ // Check if any step has rules that reference user properties
189
+ for (const step of flowData.steps) {
190
+ if (step.conditionRuleBlocks) {
191
+ for (const ruleBlock of step.conditionRuleBlocks) {
192
+ if (ruleBlock.conditions) {
193
+ for (const condition of ruleBlock.conditions) {
194
+ // Check if condition references user properties
195
+ if ((condition as any).property?.startsWith('user.')) {
196
+ console.debug(`[DAP] Flow ${flowData.flowId} requires user context due to rule: ${(condition as any).property}`);
197
+ return true;
198
+ }
199
+ }
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ // For now, allow flows without explicit user property rules
206
+ return false;
207
+ }
208
+
209
+ /**
210
+ * Start a new flow
211
+ */
212
+ public async startFlow(flowData: FlowData): Promise<void> {
213
+ console.log(`[DAP] 🚀 Starting flow: ${flowData.flowId}`);
214
+
215
+ // Analyze and log trigger usage for the entire flow
216
+ this.analyzeTriggerUsage(flowData);
217
+
218
+ // Analyze page context for multi-page flows
219
+ this.analyzeFlowPageContext(flowData);
220
+
221
+ // Check if flow requires user context
222
+ if (this.flowRequiresUserContext(flowData) && !userContextService.hasRealUser()) {
223
+ console.warn(`[DAP] Flow ${flowData.flowId} requires user context but none available - flow execution blocked`);
224
+ return;
225
+ }
226
+
227
+ // Check for rule steps and analyze their page context
228
+ const ruleSteps = flowData.steps.filter(step =>
229
+ step.conditionRuleBlocks && step.conditionRuleBlocks.length > 0
230
+ );
231
+ console.debug(`[DAP] Flow has ${ruleSteps.length} rule steps:`, ruleSteps);
232
+
233
+ if (ruleSteps.length > 0) {
234
+ this.analyzeRuleStepsPageContext(ruleSteps);
235
+ }
236
+
237
+ // Abort current flow if any
238
+ if (this._state.flowInProgress) {
239
+ this.abortFlow();
240
+ }
241
+
242
+ // Reset tracking state for new flow
243
+ resetFlowTracking(flowData.flowId);
244
+
245
+ // Initialize new flow state
246
+ this._state = {
247
+ activeFlowId: flowData.flowId,
248
+ flowInProgress: true,
249
+ activeStep: 0,
250
+ activeStepTriggered: false,
251
+ executionState: 'ACTIVE',
252
+ executionMode: flowData.execution?.mode || 'Linear',
253
+ triggeredSteps: new Set()
254
+ };
255
+
256
+ this._currentFlow = flowData;
257
+
258
+ // Start executing steps from beginning
259
+ this.executeStep();
260
+ }
261
+
262
+ /**
263
+ * Abort current flow
264
+ */
265
+ public abortFlow(): void {
266
+ if (!this._state.flowInProgress) return;
267
+
268
+ // Clean up current step
269
+ this.cleanupCurrentStep();
270
+
271
+ // Reset trigger state for the flow
272
+ if (this._state.activeFlowId) {
273
+ triggerManager.resetOnceTriggersForFlow(this._state.activeFlowId);
274
+ }
275
+
276
+ // Reset state
277
+ this._state = {
278
+ activeFlowId: null,
279
+ flowInProgress: false,
280
+ activeStep: 0,
281
+ activeStepTriggered: false,
282
+ executionState: 'TERMINATED',
283
+ executionMode: 'Linear',
284
+ triggeredSteps: new Set()
285
+ };
286
+
287
+ this._currentFlow = null;
288
+ }
289
+
290
+ /**
291
+ * Execute current step in the flow with enhanced trigger support
292
+ */
293
+ private executeStep(): void {
294
+ if (!this._currentFlow || !this._state.flowInProgress) return;
295
+
296
+ // Handle different execution modes
297
+ if (this._state.executionMode === 'Linear') {
298
+ this.executeLinearStep();
299
+ } else if (this._state.executionMode === 'AnyOrder') {
300
+ this.executeAnyOrderSteps();
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Execute steps in linear order (traditional flow)
306
+ */
307
+ private executeLinearStep(): void {
308
+ if (!this._currentFlow) return;
309
+
310
+ const step = this._currentFlow.steps[this._state.activeStep];
311
+ if (!step) {
312
+ console.debug(`[DAP] Flow completed`);
313
+ this.completeFlow();
314
+ return;
315
+ }
316
+
317
+ this.executeStepWithTrigger(step);
318
+ }
319
+
320
+ /**
321
+ * Execute steps in any order (all steps listen simultaneously)
322
+ */
323
+ private executeAnyOrderSteps(): void {
324
+ if (!this._currentFlow) return;
325
+
326
+ // Set up triggers for all untriggered steps
327
+ for (let i = 0; i < this._currentFlow.steps.length; i++) {
328
+ if (!this._state.triggeredSteps.has(i)) {
329
+ const step = this._currentFlow.steps[i];
330
+ this.setupStepTrigger(step, i);
331
+ }
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Execute a step with enhanced trigger support
337
+ */
338
+ private executeStepWithTrigger(step: EnhancedStepData, stepIndex?: number): void {
339
+ console.log(`[DAP] ========== EXECUTING STEP ${step.stepId} ==========`);
340
+
341
+ // Step 1: Resolve trigger (step-level or fallback to elementTrigger)
342
+ const trigger = triggerManager.resolveTrigger(step);
343
+
344
+ if (!trigger) {
345
+ console.log(`[DAP] Step ${step.stepId}: NO TRIGGER - executing immediately`);
346
+ this.executeStepContent(step);
347
+ this.postStepTransition(step);
348
+ return;
349
+ }
350
+
351
+ console.log(`[DAP] Step ${step.stepId}: TRIGGER RESOLVED - setting up listeners`);
352
+
353
+ // Step 2: Register trigger listeners with flow context
354
+ const actualStepIndex = stepIndex !== undefined ? stepIndex : this._state.activeStep;
355
+ const isCurrentActiveStep = actualStepIndex === this._state.activeStep;
356
+
357
+ const flowContext = {
358
+ mode: this._state.executionMode,
359
+ currentStepActive: isCurrentActiveStep
360
+ };
361
+
362
+ triggerManager.registerTriggerListeners(step.stepId, trigger, (context) => {
363
+ // Check if this step has already been triggered (prevent double-execution)
364
+ if (this._state.executionMode === 'Linear' && this._state.activeStepTriggered) {
365
+ return;
366
+ }
367
+
368
+ // Mark step as triggered in linear mode
369
+ if (this._state.executionMode === 'Linear') {
370
+ this._state.activeStepTriggered = true;
371
+ }
372
+
373
+ // Mark step as triggered in AnyOrder mode
374
+ if (this._state.executionMode === 'AnyOrder') {
375
+ this._state.triggeredSteps.add(actualStepIndex);
376
+ }
377
+
378
+ // Execute step content
379
+ this.executeStepContent(step);
380
+
381
+ // For rule-based steps (no UX experience), evaluate rules with the input value
382
+ if (!step.uxExperience && step.conditionRuleBlocks && step.conditionRuleBlocks.length > 0) {
383
+ console.log(`[DAP] Step ${step.stepId} is rule-based, waiting for conditions`);
384
+ console.log(`[DAP] Rule-based step triggered, evaluating rules with input value: ${context.userInput}`);
385
+
386
+ // Validate page context before rule evaluation
387
+ if (step.userInputSelector) {
388
+ const inputElements = resolveSelectorAll(step.userInputSelector);
389
+ if (inputElements.length === 0) {
390
+ console.error(`[DAP] 🚨 CRITICAL: Rule-based step ${step.stepId} input selector not found: ${step.userInputSelector}`);
391
+ console.error(`[DAP] This indicates a cross-page navigation issue. Skipping rule evaluation.`);
392
+ this.advanceToNextStep();
393
+ return;
394
+ }
395
+ }
396
+
397
+ this.evaluateStepRulesWithValue(step, context.userInput || '');
398
+ } else {
399
+ // Handle post-step transition for regular UX steps
400
+ this.postStepTransition(step);
401
+ }
402
+ }, flowContext);
403
+ }
404
+
405
+ /**
406
+ * Set up trigger for a specific step (used in AnyOrder mode)
407
+ */
408
+ private setupStepTrigger(step: EnhancedStepData, stepIndex: number): void {
409
+ const trigger = triggerManager.resolveTrigger(step);
410
+
411
+ if (!trigger) {
412
+
413
+ // If it's an optional step, mark as completed
414
+ if (step.stepType === 'Optional') {
415
+ this._state.triggeredSteps.add(stepIndex);
416
+ }
417
+ return;
418
+ }
419
+
420
+ const flowContext = {
421
+ mode: this._state.executionMode,
422
+ currentStepActive: true // In AnyOrder mode, all steps are considered "active"
423
+ };
424
+
425
+ triggerManager.registerTriggerListeners(step.stepId, trigger, (context) => {
426
+
427
+ this._state.triggeredSteps.add(stepIndex);
428
+ this.executeStepContent(step);
429
+ this.postStepTransition(step);
430
+
431
+ // Check if flow is complete
432
+ this.checkFlowCompletion();
433
+ }, flowContext);
434
+ }
435
+
436
+ /**
437
+ * Execute the actual step content (UX experience)
438
+ */
439
+ private executeStepContent(step: EnhancedStepData): void {
440
+ // Track step view
441
+ if (this._state.activeFlowId) {
442
+ trackStepView(this._state.activeFlowId, step.stepId);
443
+ }
444
+
445
+ if (step.uxExperience) {
446
+ this.triggerUXExperience(step);
447
+ } else {
448
+ console.log(`[DAP] Step ${step.stepId} is rule-based, waiting for conditions`);
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Handle post-step transition (rules evaluation and flow control)
454
+ */
455
+ private postStepTransition(step: EnhancedStepData): void {
456
+ // For steps with rule blocks but no UX experience (like steps 9 & 10):
457
+ // Don't evaluate rules immediately - wait for input triggers to fire
458
+ if (step.conditionRuleBlocks && step.conditionRuleBlocks.length > 0 && !step.uxExperience) {
459
+ console.log(`[DAP] Step ${step.stepId} has rules but no UX - waiting for input trigger`);
460
+ // Don't advance - let the input trigger handle rule evaluation
461
+ return;
462
+ }
463
+
464
+ // For steps with both rules AND UX experience:
465
+ // Evaluate rules after UX experience completes
466
+ if (step.conditionRuleBlocks && step.conditionRuleBlocks.length > 0 && step.uxExperience) {
467
+ this.evaluateStepRules(step);
468
+ return;
469
+ }
470
+
471
+ // For steps with UX experiences but no rules:
472
+ // Let the completion tracker handle advancement
473
+ if (step.uxExperience && (!step.conditionRuleBlocks || step.conditionRuleBlocks.length === 0)) {
474
+ // Advancement will be handled by completion tracker
475
+ return;
476
+ }
477
+
478
+ // For steps with neither UX experience nor rules:
479
+ // Advance immediately
480
+ if (!step.uxExperience && (!step.conditionRuleBlocks || step.conditionRuleBlocks.length === 0)) {
481
+ if (this._state.executionMode === 'Linear') {
482
+ this.advanceToNextStep();
483
+ }
484
+ }
485
+ }
486
+
487
+ /**
488
+ * Evaluate step rules with a specific input value
489
+ * Used for rule-based steps when triggered by input
490
+ */
491
+ private evaluateStepRulesWithValue(step: EnhancedStepData, inputValue: string) {
492
+ if (!step.conditionRuleBlocks || step.conditionRuleBlocks.length === 0) {
493
+ this.advanceToNextStep();
494
+ return;
495
+ }
496
+
497
+ console.log(`[DAP] Evaluating rules for step ${step.stepId} with input: "${inputValue}"`);
498
+
499
+ // Page context validation for rule-based steps
500
+ console.debug(`[DAP] 📄 Rule evaluation page context check for step: ${step.stepId}`);
501
+
502
+ // Check if rule selectors exist on current page
503
+ let pageContextValid = true;
504
+ for (const ruleBlock of step.conditionRuleBlocks) {
505
+ if (ruleBlock.selector) {
506
+ const elements = resolveSelectorAll(ruleBlock.selector);
507
+ if (elements.length === 0) {
508
+ console.warn(`[DAP] ⚠️ Rule block selector not found on current page: ${ruleBlock.selector}`);
509
+ console.warn(`[DAP] This may indicate a cross-page navigation issue for rule-based step ${step.stepId}`);
510
+ pageContextValid = false;
511
+ }
512
+ }
513
+ }
514
+
515
+ if (!pageContextValid) {
516
+ console.warn(`[DAP] 🚨 Page context validation failed for rule-based step ${step.stepId}`);
517
+ console.warn(`[DAP] Skipping rule evaluation due to missing selectors - possible cross-page issue`);
518
+
519
+ // For now, advance to next step rather than hanging
520
+ // TODO: Consider implementing page navigation waiting logic here
521
+ this.advanceToNextStep();
522
+ return;
523
+ }
524
+
525
+ try {
526
+ // Evaluate each rule block with the input value
527
+ for (const ruleBlock of step.conditionRuleBlocks) {
528
+ // Need to evaluate the rule with the input value
529
+ const ruleMatched = evaluateRuleBlock(ruleBlock, inputValue);
530
+ console.log(`[DAP] Rule block result for "${inputValue}": ${ruleMatched}`);
531
+
532
+ if (ruleMatched) {
533
+ console.log(`[DAP] Rule matched for step ${step.stepId}, handling branching`);
534
+ this.handleRuleBranching(ruleBlock);
535
+ return; // First matching rule wins
536
+ }
537
+ }
538
+
539
+ console.log(`[DAP] No rules matched for step ${step.stepId}, continuing execution`);
540
+ this.advanceToNextStep();
541
+ } catch (error) {
542
+ console.error(`[DAP] Error evaluating rules for step ${step.stepId}:`, error);
543
+ this.advanceToNextStep(); // Continue on error
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Evaluate a single rule block with a specific input value
549
+ */
550
+ private evaluateRuleBlockWithValue(ruleBlock: any, inputValue: string): boolean {
551
+ return evaluateRuleBlock(ruleBlock, inputValue);
552
+ }
553
+
554
+ /**
555
+ * Evaluate condition rule blocks and handle branching
556
+ */
557
+ private evaluateStepRules(step: EnhancedStepData): void {
558
+ console.debug(`[DAP] Evaluating rules for step: ${step.stepId}`);
559
+
560
+ if (!step.conditionRuleBlocks) return;
561
+
562
+ for (const ruleBlock of step.conditionRuleBlocks) {
563
+ try {
564
+ // Get input value for rule evaluation
565
+ let inputValue: string | number | boolean = '';
566
+
567
+ if (step.userInputSelector) {
568
+ const inputElement = document.querySelector(step.userInputSelector) as HTMLInputElement;
569
+ if (inputElement) {
570
+ inputValue = inputElement.value;
571
+ }
572
+ }
573
+
574
+ const ruleMatched = evaluateRuleBlock(ruleBlock, inputValue);
575
+
576
+ if (ruleMatched) {
577
+ console.debug(`[DAP] Rule matched for step ${step.stepId}, handling branching`);
578
+ this.handleRuleBranching(ruleBlock);
579
+ return; // First matching rule wins
580
+ }
581
+ } catch (error) {
582
+ console.error(`[DAP] Error evaluating rule block:`, error);
583
+ }
584
+ }
585
+
586
+ // No rules matched, continue normal flow
587
+ if (this._state.executionMode === 'Linear') {
588
+ this.advanceToNextStep();
589
+ }
590
+ }
591
+
592
+ /**
593
+ * Handle rule-based branching based on BranchType
594
+ */
595
+ private handleRuleBranching(ruleBlock: any): void {
596
+ console.log(`[DAP] Handling rule-based branching for block:`, ruleBlock);
597
+
598
+ const branchType = ruleBlock.branchType;
599
+
600
+ switch (branchType) {
601
+ case 'Flow':
602
+ // Terminate current flow and start nextFlowId
603
+ const nextFlowId = ruleBlock.nextFlowId;
604
+ if (nextFlowId) {
605
+ console.log(`[DAP] Branching to new flow: ${nextFlowId}`);
606
+ this.terminateCurrentFlow();
607
+ this.startNewFlow(nextFlowId);
608
+ } else {
609
+ console.warn(`[DAP] Flow branch type specified but no nextFlowId found`);
610
+ this.continueToNextStep();
611
+ }
612
+ break;
613
+
614
+ case 'Step':
615
+ // Jump to specific stepId
616
+ const targetStepId = ruleBlock.stepId;
617
+ if (targetStepId) {
618
+ console.log(`[DAP] Jumping to step: ${targetStepId}`);
619
+ this.jumpToStep(targetStepId);
620
+ } else {
621
+ console.warn(`[DAP] Step branch type specified but no stepId found`);
622
+ this.continueToNextStep();
623
+ }
624
+ break;
625
+
626
+ case 'Continue':
627
+ default:
628
+ // Continue to next step in order
629
+ console.log(`[DAP] Continuing to next step`);
630
+ this.continueToNextStep();
631
+ break;
632
+ }
633
+ }
634
+
635
+ /**
636
+ * Terminate current flow execution
637
+ */
638
+ private terminateCurrentFlow(): void {
639
+ console.log(`[DAP] Terminating current flow: ${this._currentFlow?.flowId}`);
640
+
641
+ // Clean up current flow state
642
+ this.resetFlowState();
643
+
644
+ // Trigger flow completion event
645
+ if (this._currentFlow) {
646
+ resetFlowTracking(this._currentFlow.flowId);
647
+ }
648
+ }
649
+
650
+ /**
651
+ * Reset flow state to initial values
652
+ */
653
+ private resetFlowState(): void {
654
+ // Clean up current step
655
+ this.cleanupCurrentStep();
656
+
657
+ // Reset trigger state for the flow
658
+ if (this._state.activeFlowId) {
659
+ triggerManager.resetOnceTriggersForFlow(this._state.activeFlowId);
660
+ }
661
+
662
+ // Reset state
663
+ this._state = {
664
+ activeFlowId: null,
665
+ flowInProgress: false,
666
+ activeStep: 0,
667
+ activeStepTriggered: false,
668
+ executionState: 'TERMINATED',
669
+ executionMode: 'Linear',
670
+ triggeredSteps: new Set()
671
+ };
672
+
673
+ this._currentFlow = null;
674
+ }
675
+
676
+ /**
677
+ * Start a new flow by ID
678
+ */
679
+ private startNewFlow(flowId: string): void {
680
+ console.log(`[DAP] Starting new flow: ${flowId}`);
681
+
682
+ // Use the global DAP startFlow method if available
683
+ if (typeof window !== 'undefined' && (window as any).DAP && (window as any).DAP.startFlow) {
684
+ try {
685
+ (window as any).DAP.startFlow(flowId);
686
+ } catch (error) {
687
+ console.error(`[DAP] Error starting flow ${flowId}:`, error);
688
+ }
689
+ } else {
690
+ // Fallback: emit an event that can be caught by external flow manager
691
+ const event = new CustomEvent('dap:startFlow', {
692
+ detail: { flowId }
693
+ });
694
+
695
+ window.dispatchEvent(event);
696
+ }
697
+ }
698
+
699
+ /**
700
+ * Jump to a specific step within current flow
701
+ */
702
+ private jumpToStep(stepId: string): void {
703
+ console.log(`[DAP] Jumping to step: ${stepId}`);
704
+
705
+ if (!this._currentFlow) {
706
+ console.error(`[DAP] Cannot jump to step: no active flow`);
707
+ return;
708
+ }
709
+
710
+ // Find the step index by stepId
711
+ const targetStepIndex = this._currentFlow.steps.findIndex(step => step.stepId === stepId);
712
+
713
+ if (targetStepIndex === -1) {
714
+ console.error(`[DAP] Step not found: ${stepId}`);
715
+ this.continueToNextStep();
716
+ return;
717
+ }
718
+
719
+ // Update state to target step
720
+ this._state.activeStep = targetStepIndex;
721
+ this._state.activeStepTriggered = false;
722
+
723
+ // Execute target step
724
+ const targetStep = this._currentFlow.steps[targetStepIndex];
725
+ this.executeStepWithTrigger(targetStep, targetStepIndex);
726
+ }
727
+
728
+ /**
729
+ * Continue to next step in sequence
730
+ */
731
+ private continueToNextStep(): void {
732
+ if (this._state.executionMode === 'Linear') {
733
+ this.advanceToNextStep();
734
+ }
735
+ }
736
+
737
+ /**
738
+ * Check if AnyOrder flow is complete
739
+ */
740
+ private checkFlowCompletion(): void {
741
+ if (!this._currentFlow || this._state.executionMode !== 'AnyOrder') return;
742
+
743
+ // Count mandatory steps
744
+ const mandatorySteps = this._currentFlow.steps.filter((step, index) =>
745
+ step.stepType === 'Mandatory'
746
+ );
747
+
748
+ const triggeredMandatory = this._currentFlow.steps.filter((step, index) =>
749
+ step.stepType === 'Mandatory' && this._state.triggeredSteps.has(index)
750
+ );
751
+
752
+ console.debug(`[DAP] Flow completion check: ${triggeredMandatory.length}/${mandatorySteps.length} mandatory steps completed`);
753
+
754
+ if (triggeredMandatory.length === mandatorySteps.length) {
755
+ console.debug(`[DAP] All mandatory steps completed, flow complete`);
756
+ this.completeFlow();
757
+ }
758
+ }
759
+
760
+ /**
761
+ * Execute UX Experience step
762
+ */
763
+ private executeUXStep(step: EnhancedStepData): void {
764
+ const ux = step.uxExperience!;
765
+
766
+ console.debug(`[DAP] Executing UX step: ${step.stepId}`, {
767
+ elementSelector: ux.elementSelector,
768
+ elementTrigger: ux.elementTrigger,
769
+ elementLocation: ux.elementLocation
770
+ });
771
+
772
+ // Check page/location requirement
773
+ if (ux.elementLocation) {
774
+ const currentContext = this._locationService.getContext();
775
+ const locationMatches = this.matchesLocation(ux.elementLocation, currentContext);
776
+
777
+ if (!locationMatches) {
778
+ console.debug(`[DAP] Step ${step.stepId} waiting for location: ${ux.elementLocation} (current: ${currentContext.currentPath})`);
779
+ return; // Wait for page change
780
+ }
781
+ }
782
+
783
+ console.debug(`[DAP] Step ${step.stepId} location matches, setting up trigger`);
784
+
785
+ // Handle different trigger types
786
+ const trigger = ux.elementTrigger?.toLowerCase() || 'on page load';
787
+
788
+ if (trigger === 'on page load' || trigger === 'page load' || trigger === 'pageload') {
789
+ // Immediate trigger
790
+ console.debug(`[DAP] Step ${step.stepId} has immediate trigger`);
791
+ this.triggerUXExperience(step);
792
+ } else {
793
+ // DOM-based trigger
794
+ this.setupDOMTrigger(step);
795
+ }
796
+ }
797
+
798
+ /**
799
+ * Execute Rule step (DAP-standard)
800
+ */
801
+ private executeRuleStep(step: EnhancedStepData): void {
802
+ console.debug(`[DAP] === FLOWENGINE: Rule step initialized: ${step.stepId} ===`);
803
+
804
+ // Testing alert for FlowEngine rule step
805
+ alert(`🔧 FlowEngine Rule Step!\n\nStep: ${step.stepId}\nUserInputSelector: ${step.userInputSelector}\nRule blocks: ${step.conditionRuleBlocks?.length || 0}`);
806
+
807
+ if (!step.userInputSelector) {
808
+ console.warn(`[DAP] Rule step ${step.stepId} has no userInputSelector`);
809
+ alert(`❌ No userInputSelector for rule step ${step.stepId}`);
810
+ return; // Stay idle, don't advance
811
+ }
812
+
813
+ if (!step.conditionRuleBlocks || step.conditionRuleBlocks.length === 0) {
814
+ console.warn(`[DAP] Rule step ${step.stepId} has no conditionRuleBlocks`);
815
+ alert(`❌ No conditionRuleBlocks for rule step ${step.stepId}`);
816
+ return; // Stay idle, don't advance
817
+ }
818
+
819
+ // Set flow state to waiting for input
820
+ this._state.executionState = 'WAITING_FOR_INPUT';
821
+
822
+ // Try to set up rule monitoring immediately
823
+ this.setupRuleMonitoring(step);
824
+ }
825
+
826
+ private setupRuleMonitoring(step: EnhancedStepData): void {
827
+ // Clean up any existing listeners for this step first
828
+ const existingCleanup = this._stepTriggerListeners.get(step.stepId);
829
+ if (existingCleanup) {
830
+ existingCleanup();
831
+ this._stepTriggerListeners.delete(step.stepId);
832
+ }
833
+
834
+ // Find input element
835
+ const inputElement = resolveSelector(step.userInputSelector!) as HTMLInputElement;
836
+
837
+ if (!inputElement) {
838
+ console.warn(`[DAP] Input element not found: ${step.userInputSelector}`);
839
+ console.debug(`[DAP] Setting up page change listener to retry when page/component changes...`);
840
+
841
+ alert(`⏳ Element Not Found - Waiting!\n\nSelector: ${step.userInputSelector}\n\nWill retry automatically when you navigate to the correct page/component.`);
842
+
843
+ // Set up page change listener to retry
844
+ this.setupPageChangeRetry(step);
845
+ return;
846
+ }
847
+
848
+ console.debug(`[DAP] ✅ Input element found: ${step.userInputSelector}`);
849
+ console.debug(`[DAP] Listening for input on ${step.userInputSelector}`);
850
+
851
+ alert(`✅ Element Found!\n\nSelector: ${step.userInputSelector}\nElement: ${inputElement.tagName}\n\nRule monitoring active!`);
852
+
853
+ // Listen for input events - define cleanup first
854
+ const cleanup = () => {
855
+ inputElement.removeEventListener('input', evaluateRules);
856
+ inputElement.removeEventListener('change', evaluateRules);
857
+ inputElement.removeEventListener('blur', evaluateRules);
858
+ console.debug(`[DAP] ✅ Rule event listeners cleaned up for step: ${step.stepId}`);
859
+ };
860
+
861
+ // Set up rule evaluation on input change
862
+ let ruleMatched = false; // Prevent multiple rule matches
863
+ const evaluateRules = () => {
864
+ // Check location validity first (page navigation awareness)
865
+ if (step.uxExperience?.elementLocation && !this.isLocationValid(step.uxExperience.elementLocation)) {
866
+ console.debug(`[DAP] Rule evaluation paused - wrong location. Expected: ${step.uxExperience.elementLocation}`);
867
+ return;
868
+ }
869
+
870
+ // Prevent multiple rule evaluations if already matched
871
+ if (ruleMatched) {
872
+ console.debug(`[DAP] Rule already matched for step ${step.stepId}, ignoring additional events`);
873
+ return;
874
+ }
875
+
876
+ const inputValue = inputElement.value;
877
+ console.debug(`[DAP] Evaluating rules for input value: "${inputValue}"`);
878
+
879
+ // Evaluate rule blocks in order - trigger FIRST satisfied rule
880
+ for (let i = 0; i < step.conditionRuleBlocks!.length; i++) {
881
+ const ruleBlock = step.conditionRuleBlocks![i];
882
+ if (evaluateRuleBlock(ruleBlock, inputValue)) {
883
+ console.debug(`[DAP] Rule matched → transitioning to flow ${ruleBlock.nextFlowId}`);
884
+ console.debug(`[DAP] Rule block ${i + 1} of ${step.conditionRuleBlocks!.length} triggered the transition`);
885
+
886
+ // Mark rule as matched to prevent duplicates
887
+ ruleMatched = true;
888
+
889
+ // Clean up event listeners IMMEDIATELY to prevent repeated alerts
890
+ cleanup();
891
+
892
+ // Testing alert for rule evaluation success
893
+ alert(`🎯 FlowEngine Rule Matched!\n\nInput: "${inputValue}"\nNext Flow: ${ruleBlock.nextFlowId}\n\nRule evaluation successful!`);
894
+
895
+ this.transitionToFlow(ruleBlock, step.stepId);
896
+ return;
897
+ }
898
+ }
899
+
900
+ // If no rules matched, advance to next step in current flow
901
+ console.debug(`[DAP] No rules matched for input "${inputValue}", advancing to next step`);
902
+ console.debug(`[DAP] Current step: ${this._state.activeStep}, moving to: ${this._state.activeStep + 1}`);
903
+
904
+ // Mark rule as processed to prevent duplicates
905
+ ruleMatched = true;
906
+
907
+ // Clean up event listeners
908
+ cleanup();
909
+
910
+ // Testing alert for rule not matching
911
+ alert(`⏭️ FlowEngine Rule Not Matched!\n\nInput: "${inputValue}"\nNo rules satisfied\n\nAdvancing to next step...`);
912
+
913
+ // Move to next step in current flow
914
+ this.advanceToNextStep();
915
+ };
916
+
917
+ // Attach comprehensive event listeners
918
+ inputElement.addEventListener('input', evaluateRules); // Real-time typing
919
+ inputElement.addEventListener('change', evaluateRules); // Value changes
920
+ inputElement.addEventListener('blur', evaluateRules); // Focus loss (fallback)
921
+
922
+ // Store cleanup function
923
+ this._stepTriggerListeners.set(step.stepId, cleanup);
924
+ }
925
+
926
+ private setupPageChangeRetry(step: EnhancedStepData): void {
927
+ let unsubscribeLocation: (() => void) | null = null;
928
+
929
+ const retryOnPageChange = () => {
930
+ console.debug(`[DAP] Page/component changed - retrying element detection for rule step: ${step.stepId}`);
931
+
932
+ // Clean up the current listener
933
+ if (unsubscribeLocation) {
934
+ unsubscribeLocation();
935
+ unsubscribeLocation = null;
936
+ }
937
+
938
+ // Try to set up monitoring again
939
+ this.setupRuleMonitoring(step);
940
+ };
941
+
942
+ // Listen for page/component changes
943
+ unsubscribeLocation = this._locationService.subscribe(retryOnPageChange);
944
+
945
+ // Also listen for DOM mutations (component changes within same page)
946
+ const observer = new MutationObserver((mutations) => {
947
+ // Check if significant DOM changes occurred
948
+ for (const mutation of mutations) {
949
+ if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
950
+ // Check if any added nodes contain our target selector
951
+ const addedNodes = Array.from(mutation.addedNodes);
952
+ for (const node of addedNodes) {
953
+ if (node.nodeType === Node.ELEMENT_NODE) {
954
+ const element = node as Element;
955
+ // Check if target element is now available using resolveSelector
956
+ try {
957
+ const targetElement = resolveSelector(step.userInputSelector!);
958
+ if (targetElement) {
959
+ console.debug(`[DAP] Target element appeared in DOM - retrying rule setup`);
960
+ observer.disconnect();
961
+ if (unsubscribeLocation) {
962
+ unsubscribeLocation();
963
+ }
964
+ this.setupRuleMonitoring(step);
965
+ return;
966
+ }
967
+ } catch (error) {
968
+ // Selector still not valid or element not found, continue observing
969
+ console.debug(`[DAP] Element still not available: ${error}`);
970
+ }
971
+ }
972
+ }
973
+ }
974
+ }
975
+ });
976
+
977
+ // Start observing DOM changes
978
+ observer.observe(document.body, {
979
+ childList: true,
980
+ subtree: true
981
+ });
982
+
983
+ // Store observer for cleanup
984
+ const existingObserver = this._domObservers.get(step.stepId);
985
+ if (existingObserver) {
986
+ existingObserver.disconnect();
987
+ }
988
+ this._domObservers.set(step.stepId, observer);
989
+ } /**
990
+ * Evaluate a rule block (DAP-standard)
991
+ */
992
+ /**
993
+ * Transition to next flow (DAP-standard)
994
+ */
995
+ private async transitionToFlow(ruleBlock: RuleBlock, fromStepId: string): Promise<void> {
996
+ console.debug(`[DAP] Current flow terminated`);
997
+ console.debug(`[DAP] Transitioning from step ${fromStepId} to flow ${ruleBlock.nextFlowId}`);
998
+
999
+ // Set state to transitioning
1000
+ this._state.executionState = 'TRANSITIONING';
1001
+
1002
+ // Clean up current flow completely
1003
+ this.cleanupCurrentStep();
1004
+ this.abortFlow();
1005
+
1006
+ try {
1007
+ // Import flow fetching functions
1008
+ const { fetchFlowById } = await import('../flows');
1009
+ const config = (window as any).__DAP_CONFIG__;
1010
+
1011
+ if (!config) {
1012
+ console.error('[DAP] No config available for flow transition');
1013
+ return;
1014
+ }
1015
+
1016
+ // Fetch and start new flow as FRESH flow
1017
+ const flowData = await fetchFlowById(config, location.origin, ruleBlock.nextFlowId);
1018
+ await this.startFlow(flowData);
1019
+
1020
+ } catch (error) {
1021
+ console.error(`[DAP] Failed to transition to flow ${ruleBlock.nextFlowId}:`, error);
1022
+ this._state.executionState = 'TERMINATED';
1023
+ }
1024
+ }
1025
+
1026
+ /**
1027
+ * Set up DOM trigger for UX step
1028
+ */
1029
+ private setupDOMTrigger(step: EnhancedStepData): void {
1030
+ const ux = step.uxExperience!;
1031
+
1032
+ if (!ux.elementSelector) {
1033
+ console.warn(`[DAP] UX step ${step.stepId} has no elementSelector`);
1034
+ this.advanceToNextStep();
1035
+ return;
1036
+ }
1037
+
1038
+ // Wait for element to exist
1039
+ this.waitForElement(ux.elementSelector, (element) => {
1040
+ if (this._state.activeStepTriggered) return; // Already triggered
1041
+
1042
+ const trigger = ux.elementTrigger?.toLowerCase() || 'click';
1043
+
1044
+ // Use comprehensive trigger normalizer
1045
+ const normalizedTrigger = normalizeTrigger(trigger);
1046
+
1047
+ console.debug(`[DAP] Setting up trigger "${normalizedTrigger.eventType}" on ${ux.elementSelector}`);
1048
+
1049
+ const triggerHandler = () => {
1050
+ if (this._state.activeStepTriggered) return;
1051
+ console.debug(`[DAP] Trigger fired for step: ${step.stepId}`);
1052
+ this.triggerUXExperience(step);
1053
+ };
1054
+
1055
+ // Handle synthetic triggers (like page load)
1056
+ if (normalizedTrigger.isSynthetic) {
1057
+ if (normalizedTrigger.eventType === 'pageload') {
1058
+ // Fire immediately for page load triggers
1059
+ setTimeout(() => triggerHandler(), 0);
1060
+ }
1061
+ } else {
1062
+ // Set up DOM event listener
1063
+ element.addEventListener(normalizedTrigger.eventType, triggerHandler);
1064
+
1065
+ // Store cleanup function
1066
+ this._stepTriggerListeners.set(step.stepId, () => {
1067
+ element.removeEventListener(normalizedTrigger.eventType, triggerHandler);
1068
+ });
1069
+ }
1070
+ });
1071
+ }
1072
+
1073
+ /**
1074
+ * Trigger UX experience rendering
1075
+ */
1076
+ private triggerUXExperience(step: EnhancedStepData): void {
1077
+ // Track step view when it's actually triggered/rendered
1078
+ if (this._currentFlow) {
1079
+ trackStepView(this._currentFlow.flowId, step.stepId).catch(error => {
1080
+ console.debug(`[DAP] Step tracking failed: ${error.message}`);
1081
+ });
1082
+ }
1083
+
1084
+ const ux = step.uxExperience!;
1085
+
1086
+ // Import and use experience renderer
1087
+ import('../experiences/registry').then(({ getRenderer }) => {
1088
+ const experienceType = ux.uxExperienceType.toLowerCase();
1089
+ // Route microsurvey to the unified survey system
1090
+ const rendererType = experienceType === 'microsurvey' ? 'survey' : experienceType;
1091
+ const renderer = getRenderer(rendererType);
1092
+
1093
+ if (!renderer) {
1094
+ console.error(`[DAP] No renderer found for: ${ux.uxExperienceType}`);
1095
+ this.advanceToNextStep();
1096
+ return;
1097
+ }
1098
+
1099
+ // Create proper payload based on experience type
1100
+ let payload: any;
1101
+
1102
+ if (experienceType === 'modal') {
1103
+ // Transform modal content from API format to renderer format
1104
+ let bodyContent: any[] = [];
1105
+
1106
+ if (ux.modalContent) {
1107
+ const modalContent = ux.modalContent;
1108
+ const contentType = modalContent.contentType?.toLowerCase();
1109
+
1110
+ switch (contentType) {
1111
+ case 'text':
1112
+ bodyContent.push({
1113
+ kind: 'text',
1114
+ html: modalContent.contentData || modalContent.presignedUrl || ''
1115
+ });
1116
+ break;
1117
+
1118
+ case 'link':
1119
+ bodyContent.push({
1120
+ kind: 'link',
1121
+ href: modalContent.contentData || modalContent.presignedUrl || '',
1122
+ label: modalContent.contentName || 'Link'
1123
+ });
1124
+ break;
1125
+
1126
+ case 'image':
1127
+ bodyContent.push({
1128
+ kind: 'image',
1129
+ url: modalContent.presignedUrl || modalContent.contentData || '',
1130
+ alt: modalContent.contentDescription || modalContent.contentName || 'Image'
1131
+ });
1132
+ break;
1133
+
1134
+ case 'video':
1135
+ bodyContent.push({
1136
+ kind: 'video',
1137
+ sources: [{ src: modalContent.presignedUrl || modalContent.contentData || '' }]
1138
+ });
1139
+ break;
1140
+
1141
+ case 'youtube':
1142
+ bodyContent.push({
1143
+ kind: 'youtube',
1144
+ href: modalContent.contentData || modalContent.presignedUrl || '',
1145
+ title: modalContent.contentName,
1146
+ thumbnail: modalContent.contentDescription
1147
+ });
1148
+ break;
1149
+
1150
+ case 'article':
1151
+ bodyContent.push({
1152
+ kind: 'article',
1153
+ url: modalContent.presignedUrl || modalContent.contentData || '',
1154
+ fileName: modalContent.contentName,
1155
+ mime: modalContent.mime || 'application/pdf'
1156
+ });
1157
+ break;
1158
+
1159
+ case 'knowledgebase':
1160
+ // Map server KB items to renderer-friendly KB items
1161
+ try {
1162
+ const raw = Array.isArray(modalContent.contentData) ? modalContent.contentData : [];
1163
+ const items = raw.map((it: any) => ({
1164
+ url: it?.presignedUrl || it?.contentData || '',
1165
+ title: it?.contentName || it?.contentTitle || '',
1166
+ description: it?.contentDescription || it?.description || '',
1167
+ fileName: it?.contentData || undefined
1168
+ })).filter((i: any) => i.url && i.title);
1169
+
1170
+ bodyContent.push({
1171
+ kind: 'kb',
1172
+ title: modalContent.contentName || ux.content?.header || 'Knowledge Base',
1173
+ items
1174
+ });
1175
+ } catch (e) {
1176
+ console.warn('[DAP] Failed to normalize KnowledgeBase modalContent:', e, modalContent);
1177
+ // fallback to default behavior below
1178
+ bodyContent.push({
1179
+ kind: 'text',
1180
+ html: modalContent.contentData || modalContent.contentDescription || ux.content?.body || ''
1181
+ });
1182
+ }
1183
+ break;
1184
+
1185
+ default:
1186
+ // Fallback to text content
1187
+ bodyContent.push({
1188
+ kind: 'text',
1189
+ html: modalContent.contentData || modalContent.contentDescription || ux.content?.body || ''
1190
+ });
1191
+ }
1192
+ } else if (ux.content?.body) {
1193
+ // If no modalContent but has body text, use that
1194
+ bodyContent.push({
1195
+ kind: 'text',
1196
+ html: ux.content.body
1197
+ });
1198
+ }
1199
+
1200
+ payload = {
1201
+ title: ux.content?.header,
1202
+ body: bodyContent,
1203
+ footerText: ux.content?.footer,
1204
+ theme: {},
1205
+ _completionTracker: {
1206
+ onComplete: () => {
1207
+ console.debug(`[DAP] UX experience completed for step: ${step.stepId}`);
1208
+
1209
+ // Only advance if this step is still the active step (prevent race conditions)
1210
+ if (this._state.activeStep < this._currentFlow!.steps.length &&
1211
+ this._currentFlow!.steps[this._state.activeStep].stepId === step.stepId) {
1212
+ this.advanceToNextStep();
1213
+ } else {
1214
+ console.debug(`[DAP] Step ${step.stepId} is no longer active, skipping advancement`);
1215
+ }
1216
+ }
1217
+ }
1218
+ };
1219
+ } else if (experienceType === 'tooltip') {
1220
+ payload = {
1221
+ targetSelector: ux.elementSelector,
1222
+ text: ux.content?.text || ux.content?.body || 'Tooltip',
1223
+ placement: ux.content?.placement || 'auto',
1224
+ trigger: 'hover', // Ensure trigger is set correctly
1225
+ stepId: step.stepId,
1226
+ _completionTracker: {
1227
+ onComplete: () => {
1228
+ console.debug(`[DAP] UX experience completed for step: ${step.stepId}`);
1229
+ this.advanceToNextStep();
1230
+ }
1231
+ }
1232
+ };
1233
+ } else if (experienceType === 'popover') {
1234
+ payload = {
1235
+ targetSelector: ux.elementSelector,
1236
+ title: ux.content?.title || ux.content?.header,
1237
+ body: ux.content?.body,
1238
+ placement: ux.content?.placement || 'auto',
1239
+ trigger: ux.elementTrigger || ux.content?.trigger || 'click',
1240
+ showArrow: ux.content?.showArrow !== false,
1241
+ stepId: step.stepId,
1242
+ _completionTracker: {
1243
+ onComplete: () => {
1244
+ console.debug(`[DAP] UX experience completed for step: ${step.stepId}`);
1245
+ this.advanceToNextStep();
1246
+ }
1247
+ }
1248
+ };
1249
+ } else if (experienceType === 'survey') {
1250
+ payload = {
1251
+ targetSelector: ux.elementSelector,
1252
+ questions: ux.content?.questions || [],
1253
+ header: ux.content?.header,
1254
+ body: ux.content?.body,
1255
+ stepId: step.stepId,
1256
+ _completionTracker: {
1257
+ onComplete: () => {
1258
+ console.debug(`[DAP] UX experience completed for step: ${step.stepId}`);
1259
+ this.advanceToNextStep();
1260
+ }
1261
+ }
1262
+ };
1263
+ } else if (experienceType === 'beacon') {
1264
+ // Map position from backend format {x: "center", y: "center"} to string format
1265
+ let positionString = 'top-right'; // default
1266
+ if (ux.position) {
1267
+ const { x, y } = ux.position;
1268
+ if (x === 'center' && y === 'center') {
1269
+ positionString = 'center';
1270
+ } else if (y === 'top' && x === 'left') {
1271
+ positionString = 'top-left';
1272
+ } else if (y === 'top' && x === 'right') {
1273
+ positionString = 'top-right';
1274
+ } else if (y === 'bottom' && x === 'left') {
1275
+ positionString = 'bottom-left';
1276
+ } else if (y === 'bottom' && x === 'right') {
1277
+ positionString = 'bottom-right';
1278
+ }
1279
+ }
1280
+
1281
+ payload = {
1282
+ title: ux.content?.title || ux.content?.header,
1283
+ body: ux.content?.body || ux.content?.tooltipText,
1284
+ icon: ux.content?.icon,
1285
+ position: positionString,
1286
+ autoDismiss: ux.content?.autoDismiss,
1287
+ action: ux.content?.action,
1288
+ targetSelector: ux.elementSelector, // Add target selector for element-relative positioning
1289
+ trigger: ux.elementTrigger || 'click',
1290
+ beaconStyles: {
1291
+ enabled: true,
1292
+ color1: ux.content?.color || '#f59e0b',
1293
+ color2: ux.content?.color2 || ux.content?.color || '#eab308',
1294
+ duration: ux.content?.blinkRateMs ? `${ux.content.blinkRateMs / 1000}s` : '2s',
1295
+ ...ux.content?.beaconStyles
1296
+ },
1297
+ stepId: step.stepId,
1298
+ _completionTracker: {
1299
+ onComplete: () => {
1300
+ console.debug(`[DAP] UX experience completed for step: ${step.stepId}`);
1301
+ this.advanceToNextStep();
1302
+ }
1303
+ }
1304
+ };
1305
+ } else if (experienceType === 'microsurvey') {
1306
+ // MicroSurvey is now handled by the unified survey system
1307
+ // Determine if this should be inline or modal based on content
1308
+ const hasMultipleQuestions = ux.content?.questions && ux.content.questions.length > 1;
1309
+ const hasComplexQuestions = ux.content?.questions?.some((q: any) =>
1310
+ ['OpinionScaleChoice', 'NpsScale', 'NpsOptions', 'StarChoice'].includes(q.type)
1311
+ );
1312
+ const shouldUseModal = hasMultipleQuestions || hasComplexQuestions;
1313
+
1314
+ console.debug(`[DAP] MicroSurvey mode detection:`, {
1315
+ questionsCount: ux.content?.questions?.length || 0,
1316
+ hasMultipleQuestions,
1317
+ hasComplexQuestions,
1318
+ shouldUseModal,
1319
+ finalMode: shouldUseModal ? 'modal' : 'inline'
1320
+ });
1321
+
1322
+ payload = {
1323
+ // Include both single question fields (for simple micro surveys)
1324
+ question: ux.content?.question || ux.content?.title || ux.content?.header,
1325
+ type: ux.content?.type || 'choice',
1326
+ options: ux.content?.options,
1327
+ placeholder: ux.content?.placeholder,
1328
+ submitText: ux.content?.submitText,
1329
+ cancelText: ux.content?.cancelText,
1330
+ rating: ux.content?.rating,
1331
+
1332
+ // Include full survey fields (for complex surveys)
1333
+ header: ux.content?.header,
1334
+ body: ux.content?.body,
1335
+ questions: ux.content?.questions,
1336
+
1337
+ // Positioning and behavior
1338
+ targetSelector: ux.elementSelector,
1339
+ position: ux.content?.position || 'center',
1340
+ mode: shouldUseModal ? 'modal' : 'inline', // Smart mode detection
1341
+
1342
+ // Survey submission fields
1343
+ flowId: this._state.activeFlowId,
1344
+ stepId: step.stepId,
1345
+ _completionTracker: {
1346
+ onComplete: () => {
1347
+ console.debug(`[DAP] UX experience completed for step: ${step.stepId}`);
1348
+ this.advanceToNextStep();
1349
+ }
1350
+ }
1351
+ };
1352
+ } else {
1353
+ // Fallback - use modalSequence format
1354
+ payload = {
1355
+ steps: [{
1356
+ stepId: step.stepId,
1357
+ kind: experienceType,
1358
+ [experienceType]: {
1359
+ ...ux.content,
1360
+ stepId: step.stepId
1361
+ },
1362
+ title: ux.content?.header || ux.content?.title || 'Info',
1363
+ elementSelector: ux.elementSelector,
1364
+ elementTrigger: ux.elementTrigger,
1365
+ elementLocation: ux.elementLocation
1366
+ }],
1367
+ _completionTracker: {
1368
+ onComplete: () => {
1369
+ console.debug(`[DAP] UX experience completed for step: ${step.stepId}`);
1370
+ this.advanceToNextStep();
1371
+ }
1372
+ }
1373
+ };
1374
+ }
1375
+
1376
+ // Create flow structure for renderer
1377
+ const flowForRenderer = {
1378
+ id: `step-${step.stepId}`,
1379
+ type: experienceType,
1380
+ payload
1381
+ };
1382
+
1383
+ console.debug(`[DAP] Rendering ${experienceType} experience:`, flowForRenderer);
1384
+ console.debug(`[DAP] Payload structure:`, {
1385
+ type: experienceType,
1386
+ targetSelector: payload.targetSelector,
1387
+ trigger: payload.trigger,
1388
+ elementSelector: ux.elementSelector,
1389
+ elementTrigger: ux.elementTrigger
1390
+ });
1391
+
1392
+ // Normalize flow format based on experience type
1393
+ if (experienceType === 'tooltip') {
1394
+ // Ensure tooltip has proper trigger format
1395
+ flowForRenderer.payload.trigger = payload.trigger || 'hover';
1396
+ flowForRenderer.payload.targetSelector = payload.targetSelector || ux.elementSelector;
1397
+ } else if (experienceType === 'popover') {
1398
+ // Ensure popover has proper trigger format
1399
+ flowForRenderer.payload.trigger = payload.trigger || 'click';
1400
+ flowForRenderer.payload.targetSelector = payload.targetSelector || ux.elementSelector;
1401
+ } else if (experienceType === 'modal') {
1402
+ // Modal doesn't need target selector normalization
1403
+ console.debug(`[DAP] Modal content transformation:`, {
1404
+ originalModalContent: ux.modalContent,
1405
+ transformedBody: payload.body || payload.bodyBlocks,
1406
+ contentType: ux.modalContent?.contentType
1407
+ });
1408
+ }
1409
+
1410
+ renderer(flowForRenderer);
1411
+
1412
+ }).catch(err => {
1413
+ console.error('[DAP] Error loading experience renderer:', err);
1414
+ this.advanceToNextStep();
1415
+ });
1416
+ }
1417
+
1418
+ /**
1419
+ * Advance to next step intelligently (respects triggers)
1420
+ */
1421
+ private advanceToNextStep(): void {
1422
+ // Prevent concurrent step advancement
1423
+ if (this._state.stepAdvancing) {
1424
+ return;
1425
+ }
1426
+
1427
+ this._state.stepAdvancing = true;
1428
+ this.cleanupCurrentStep();
1429
+
1430
+ this._state.activeStep++;
1431
+ this._state.activeStepTriggered = false;
1432
+
1433
+ console.debug(`[DAP] Advanced to step ${this._state.activeStep}`);
1434
+
1435
+ // Check if we have more steps
1436
+ if (this._currentFlow && this._state.activeStep < this._currentFlow.steps.length) {
1437
+ const nextStep = this._currentFlow.steps[this._state.activeStep];
1438
+ const nextStepTrigger = triggerManager.resolveTrigger(nextStep);
1439
+
1440
+ if (nextStepTrigger) {
1441
+ // Set up the trigger and wait
1442
+ this.executeStepWithTrigger(nextStep, this._state.activeStep);
1443
+
1444
+ this._state.stepAdvancing = false;
1445
+ return;
1446
+ } else {
1447
+ console.debug(`[DAP] Next step ${nextStep.stepId} has no trigger - executing immediately`);
1448
+ }
1449
+ } else {
1450
+ console.debug(`[DAP] No more steps, flow completed`);
1451
+ this._state.stepAdvancing = false;
1452
+ this.completeFlow();
1453
+ return;
1454
+ }
1455
+
1456
+ this._state.stepAdvancing = false;
1457
+ // Continue execution for steps without triggers
1458
+ this.executeStep();
1459
+ }
1460
+
1461
+ /**
1462
+ * Complete current flow
1463
+ */
1464
+ private completeFlow(): void {
1465
+ console.debug(`[DAP] Flow completed: ${this._state.activeFlowId}`);
1466
+ this.abortFlow();
1467
+ }
1468
+
1469
+ /**
1470
+ * Redirect to another flow
1471
+ */
1472
+ private async redirectToFlow(nextFlowId: string): Promise<void> {
1473
+ console.debug(`[DAP] Redirecting to flow: ${nextFlowId}`);
1474
+
1475
+ try {
1476
+ // Import flow fetching functions
1477
+ const { fetchFlowById } = await import('../flows');
1478
+ const config = (window as any).__DAP_CONFIG__;
1479
+
1480
+ if (!config) {
1481
+ console.error('[DAP] No config available for flow redirect');
1482
+ return;
1483
+ }
1484
+
1485
+ const flowData = await fetchFlowById(config, location.origin, nextFlowId);
1486
+ this.startFlow(flowData);
1487
+
1488
+ } catch (err) {
1489
+ console.error(`[DAP] Failed to redirect to flow ${nextFlowId}:`, err);
1490
+ }
1491
+ }
1492
+
1493
+ /**
1494
+ * Clean up current step listeners and state
1495
+ */
1496
+ private cleanupCurrentStep(): void {
1497
+ // Only clean up trigger listeners for the CURRENT step, not all listeners
1498
+ if (this._currentFlow && this._state.activeStep < this._currentFlow.steps.length) {
1499
+ const currentStep = this._currentFlow.steps[this._state.activeStep];
1500
+ if (currentStep) {
1501
+ // Remove only this step's listeners
1502
+ triggerManager.removeTriggerListeners(currentStep.stepId);
1503
+
1504
+ // Clean up legacy listeners for this step
1505
+ const cleanup = this._stepTriggerListeners.get(currentStep.stepId);
1506
+ if (cleanup) {
1507
+ cleanup();
1508
+ this._stepTriggerListeners.delete(currentStep.stepId);
1509
+ }
1510
+ }
1511
+ }
1512
+
1513
+ // Clean up DOM observers for current step only
1514
+ if (this._currentFlow && this._state.activeStep < this._currentFlow.steps.length) {
1515
+ const currentStep = this._currentFlow.steps[this._state.activeStep];
1516
+ if (currentStep) {
1517
+ const observer = this._domObservers.get(currentStep.stepId);
1518
+ if (observer) {
1519
+ observer.disconnect();
1520
+ this._domObservers.delete(currentStep.stepId);
1521
+ }
1522
+ }
1523
+ }
1524
+ }
1525
+
1526
+ /**
1527
+ * Check if flow can resume after page change
1528
+ */
1529
+ private checkFlowResumption(): void {
1530
+ if (!this._state.flowInProgress || !this._currentFlow) return;
1531
+
1532
+ console.debug('[DAP] Checking if current step can resume after page change');
1533
+
1534
+ // Re-execute current step (will check location requirements)
1535
+ this.executeStep();
1536
+ }
1537
+
1538
+ /**
1539
+ * Check if current location matches step requirement
1540
+ */
1541
+ private matchesLocation(elementLocation: string, context: any): boolean {
1542
+ if (!elementLocation) return true;
1543
+
1544
+ const normalizedRequired = elementLocation.replace(/^\/+/, '').toLowerCase();
1545
+ const normalizedCurrent = (context.currentPath || '').replace(/^\/+/, '').toLowerCase();
1546
+ const normalizedScreen = (context.screenId || '').replace(/^\/+/, '').toLowerCase();
1547
+
1548
+ return normalizedRequired === normalizedCurrent ||
1549
+ normalizedRequired === normalizedScreen ||
1550
+ normalizedRequired === '*';
1551
+ }
1552
+
1553
+ /**
1554
+ * Wait for element to exist in DOM
1555
+ */
1556
+ private waitForElement(selector: string, callback: (element: Element) => void): void {
1557
+ const check = () => {
1558
+ const element = resolveSelector(selector);
1559
+ if (element) {
1560
+ callback(element);
1561
+ } else {
1562
+ setTimeout(check, 100); // Retry every 100ms
1563
+ }
1564
+ };
1565
+ check();
1566
+ }
1567
+
1568
+ /**
1569
+ * Check if current page location allows rule evaluation
1570
+ */
1571
+ private isLocationValid(elementLocation?: string): boolean {
1572
+ if (!elementLocation) return true; // No location restriction
1573
+
1574
+ const currentContext = this._locationService.getContext();
1575
+ const currentPath = currentContext.currentPath;
1576
+
1577
+ // Simple path matching - can be enhanced for more complex location rules
1578
+ return currentPath.includes(elementLocation) || elementLocation === 'dashboard';
1579
+ }
1580
+
1581
+ /**
1582
+ * Get current flow state for debugging
1583
+ */
1584
+ public getState() {
1585
+ return {
1586
+ ...this._state,
1587
+ currentFlow: this._currentFlow?.flowId,
1588
+ currentStepId: this._currentFlow?.steps[this._state.activeStep]?.stepId
1589
+ };
1590
+ }
1591
+
1592
+ /**
1593
+ * Clean up resources and page context subscriptions
1594
+ */
1595
+ public destroy(): void {
1596
+ console.debug('[DAP] FlowEngine: Destroying...');
1597
+
1598
+ // Clean up current flow state
1599
+ this.cleanupCurrentStep();
1600
+ this._state = {
1601
+ activeFlowId: null,
1602
+ flowInProgress: false,
1603
+ activeStep: 0,
1604
+ activeStepTriggered: false,
1605
+ executionState: 'TERMINATED',
1606
+ executionMode: 'Linear',
1607
+ triggeredSteps: new Set()
1608
+ };
1609
+ this._currentFlow = null;
1610
+
1611
+ // Clean up page change subscription
1612
+ if (this._pageChangeUnsubscribe) {
1613
+ this._pageChangeUnsubscribe();
1614
+ this._pageChangeUnsubscribe = null;
1615
+ }
1616
+
1617
+ // Clean up trigger manager
1618
+ triggerManager.destroy();
1619
+
1620
+ // Clean up page context service
1621
+ pageContextService.destroy();
1622
+
1623
+ console.debug('[DAP] FlowEngine: Destroyed');
1624
+ }
1625
+
1626
+ /**
1627
+ * Analyze flow page context to detect multi-page flows
1628
+ */
1629
+ private analyzeFlowPageContext(flowData: FlowData): void {
1630
+ console.log(`[DAP] 📄 FLOW PAGE CONTEXT ANALYSIS: ${flowData.flowId}`);
1631
+ console.log(`[DAP] ================================================================`);
1632
+
1633
+ const currentPage = pageContextService.getCurrentContext();
1634
+ const selectors = new Set<string>();
1635
+ const elementLocations = new Set<string>();
1636
+ let selectorMismatches = 0;
1637
+ let possibleMultiPage = false;
1638
+
1639
+ // Collect all selectors and element locations
1640
+ for (const step of flowData.steps) {
1641
+ // Check trigger selectors
1642
+ if (step.trigger?.conditions) {
1643
+ step.trigger.conditions.forEach(condition => {
1644
+ if (condition.selector) {
1645
+ selectors.add(condition.selector);
1646
+ }
1647
+ });
1648
+ }
1649
+
1650
+ // Check UX experience selectors
1651
+ if (step.uxExperience?.elementSelector) {
1652
+ selectors.add(step.uxExperience.elementSelector);
1653
+ }
1654
+
1655
+ // Check user input selectors (important for rule-based steps)
1656
+ if (step.userInputSelector) {
1657
+ selectors.add(step.userInputSelector);
1658
+ }
1659
+
1660
+ // Check element locations
1661
+ if (step.uxExperience?.elementLocation) {
1662
+ elementLocations.add(step.uxExperience.elementLocation);
1663
+ }
1664
+ }
1665
+
1666
+ console.log(`[DAP] Total unique selectors in flow: ${selectors.size}`);
1667
+ console.log(`[DAP] Element locations: ${Array.from(elementLocations).join(', ') || 'none specified'}`);
1668
+
1669
+ // Check if selectors exist on current page
1670
+ for (const selector of selectors) {
1671
+ try {
1672
+ const elements = resolveSelectorAll(selector);
1673
+ if (elements.length === 0) {
1674
+ selectorMismatches++;
1675
+ console.warn(`[DAP] ⚠️ Selector not found on current page: ${selector}`);
1676
+ } else {
1677
+ console.debug(`[DAP] ✅ Selector found (${elements.length} elements): ${selector}`);
1678
+ }
1679
+ } catch (error) {
1680
+ selectorMismatches++;
1681
+ console.warn(`[DAP] ⚠️ Invalid selector: ${selector}`, error);
1682
+ }
1683
+ }
1684
+
1685
+ // Determine if this might be a multi-page flow
1686
+ if (selectorMismatches > 0) {
1687
+ possibleMultiPage = true;
1688
+ const missPercentage = Math.round((selectorMismatches / selectors.size) * 100);
1689
+ console.warn(`[DAP] 🚨 ${selectorMismatches}/${selectors.size} (${missPercentage}%) selectors not found on current page`);
1690
+ console.warn(`[DAP] This suggests a multi-page flow or cross-page navigation issue`);
1691
+ }
1692
+
1693
+ // Check execution mode and provide recommendations
1694
+ const executionMode = flowData.execution?.mode || 'Linear';
1695
+ console.log(`[DAP] Flow execution mode: ${executionMode}`);
1696
+
1697
+ if (possibleMultiPage && executionMode === 'Linear') {
1698
+ console.warn(`[DAP] 💡 Recommendation: Consider enabling multiPage support for this flow`);
1699
+ console.warn(`[DAP] Some steps may wait indefinitely for elements on other pages`);
1700
+ }
1701
+
1702
+ console.log(`[DAP] Current page: ${currentPage?.pathname || 'unknown'}`);
1703
+ console.log(`[DAP] ================================================================`);
1704
+ }
1705
+
1706
+ /**
1707
+ * Analyze rule-based steps for page context issues
1708
+ */
1709
+ private analyzeRuleStepsPageContext(ruleSteps: EnhancedStepData[]): void {
1710
+ console.log(`[DAP] 🤖 RULE STEPS PAGE CONTEXT ANALYSIS`);
1711
+ console.log(`[DAP] ================================================================`);
1712
+
1713
+ const currentPage = pageContextService.getCurrentContext();
1714
+
1715
+ for (const step of ruleSteps) {
1716
+ console.log(`[DAP] Rule step ${step.stepId}:`);
1717
+
1718
+ // Check trigger selector
1719
+ const triggerSelectors = step.trigger?.conditions?.map(c => c.selector).filter(Boolean) || [];
1720
+ console.log(`[DAP] Trigger selectors: ${triggerSelectors.length}`);
1721
+
1722
+ for (const selector of triggerSelectors) {
1723
+ if (selector) {
1724
+ const elements = resolveSelectorAll(selector);
1725
+ if (elements.length === 0) {
1726
+ console.error(`[DAP] ❌ CRITICAL: Rule trigger selector not found: ${selector}`);
1727
+ console.error(`[DAP] This rule-based step will wait indefinitely!`);
1728
+ } else {
1729
+ console.debug(`[DAP] ✅ Trigger selector found: ${selector}`);
1730
+ }
1731
+ }
1732
+ }
1733
+
1734
+ // Check user input selector (critical for rule evaluation)
1735
+ if (step.userInputSelector) {
1736
+ const inputElements = resolveSelectorAll(step.userInputSelector);
1737
+ if (inputElements.length === 0) {
1738
+ console.error(`[DAP] ❌ CRITICAL: Rule input selector not found: ${step.userInputSelector}`);
1739
+ console.error(`[DAP] Rule condition evaluation will fail!`);
1740
+ console.error(`[DAP] Possible cross-page navigation issue detected.`);
1741
+ } else {
1742
+ console.debug(`[DAP] ✅ Input selector found: ${step.userInputSelector}`);
1743
+ }
1744
+ }
1745
+
1746
+ // Check rule block selectors
1747
+ if (step.conditionRuleBlocks) {
1748
+ for (let i = 0; i < step.conditionRuleBlocks.length; i++) {
1749
+ const ruleBlock = step.conditionRuleBlocks[i];
1750
+ if (ruleBlock.selector) {
1751
+ const ruleElements = resolveSelectorAll(ruleBlock.selector);
1752
+ if (ruleElements.length === 0) {
1753
+ console.warn(`[DAP] ⚠️ Rule block ${i} selector not found: ${ruleBlock.selector}`);
1754
+ }
1755
+ }
1756
+ }
1757
+ }
1758
+ }
1759
+
1760
+ console.log(`[DAP] ================================================================`);
1761
+ }
1762
+
1763
+ /**
1764
+ * Analyze and log trigger usage for the entire flow
1765
+ * This helps identify which steps use step-level vs element-level triggers
1766
+ */
1767
+ private analyzeTriggerUsage(flowData: FlowData): void {
1768
+ console.log(`[DAP] 📊 TRIGGER USAGE ANALYSIS FOR FLOW: ${flowData.flowId}`);
1769
+ console.log(`[DAP] ================================================================`);
1770
+
1771
+ let stepLevelCount = 0;
1772
+ let elementLevelCount = 0;
1773
+ let noTriggerCount = 0;
1774
+ let ruleBasedCount = 0;
1775
+
1776
+ for (const step of flowData.steps) {
1777
+ const hasStepTrigger = step.trigger && step.trigger.conditions && step.trigger.conditions.length > 0;
1778
+ const hasElementTrigger = step.uxExperience?.elementTrigger;
1779
+ const hasRuleBlocks = step.conditionRuleBlocks && step.conditionRuleBlocks.length > 0;
1780
+
1781
+ if (hasStepTrigger) {
1782
+ stepLevelCount++;
1783
+ const triggerType = `${step.trigger?.conditions?.[0]?.kind}-${step.trigger?.conditions?.[0]?.event}`;
1784
+
1785
+ if (hasRuleBlocks) {
1786
+ console.log(`[DAP] 🤖 Step ${step.stepId}: STEP-LEVEL trigger (${triggerType}) + RULE-BASED decision`);
1787
+ ruleBasedCount++;
1788
+ } else {
1789
+ console.log(`[DAP] ✅ Step ${step.stepId}: STEP-LEVEL trigger (${triggerType}) + UX experience`);
1790
+ }
1791
+
1792
+ if (hasElementTrigger) {
1793
+ console.log(`[DAP] └── ⚠️ elementTrigger "${step.uxExperience?.elementTrigger}" will be IGNORED`);
1794
+ }
1795
+
1796
+ } else if (hasElementTrigger) {
1797
+ elementLevelCount++;
1798
+ console.warn(`[DAP] ⚠️ Step ${step.stepId}: ELEMENT-LEVEL trigger "${step.uxExperience?.elementTrigger}" (LEGACY)`);
1799
+ console.warn(`[DAP] └── 🚨 This will BREAK when element-level triggers are removed!`);
1800
+
1801
+ } else {
1802
+ noTriggerCount++;
1803
+ console.error(`[DAP] ❌ Step ${step.stepId}: NO TRIGGER - will execute immediately`);
1804
+ }
1805
+ }
1806
+
1807
+ console.log(`[DAP] ================================================================`);
1808
+ console.log(`[DAP] 📊 SUMMARY:`);
1809
+ console.log(`[DAP] ✅ Step-level triggers: ${stepLevelCount}`);
1810
+ console.log(`[DAP] ⚠️ Element-level triggers: ${elementLevelCount}`);
1811
+ console.log(`[DAP] ❌ No triggers: ${noTriggerCount}`);
1812
+ console.log(`[DAP] 🤖 Rule-based decision steps: ${ruleBasedCount}`);
1813
+ console.log(`[DAP] ================================================================`);
1814
+
1815
+ if (elementLevelCount > 0) {
1816
+ console.warn(`[DAP] 🚨 WARNING: ${elementLevelCount} steps use element-level triggers!`);
1817
+ console.warn(`[DAP] These will BREAK when element-level support is removed.`);
1818
+ console.warn(`[DAP] Please migrate to step-level triggers.`);
1819
+ }
1820
+
1821
+ if (noTriggerCount > 0) {
1822
+ console.error(`[DAP] 🚨 ERROR: ${noTriggerCount} steps have no triggers!`);
1823
+ console.error(`[DAP] These steps will execute immediately without user interaction.`);
1824
+ }
1825
+
1826
+ if (stepLevelCount === flowData.steps.length) {
1827
+ console.log(`[DAP] 🎉 PERFECT! All steps use step-level triggers. Ready for element-level trigger removal.`);
1828
+ }
1829
+ }
1830
+ }
1831
+
1832
+ // Export singleton
1833
+ export const flowEngine = FlowEngine.getInstance();