@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.
- package/.github/copilot-instructions.md +95 -0
- package/README.md +79 -0
- package/TRACKING.md +105 -0
- package/USER_CONTEXT_README.md +284 -0
- package/package.json +154 -0
- package/src/config.ts +25 -0
- package/src/core/flowEngine.ts +1833 -0
- package/src/core/triggerManager.ts +1011 -0
- package/src/experiences/banner.ts +366 -0
- package/src/experiences/beacon.ts +668 -0
- package/src/experiences/hotspotTour.ts +654 -0
- package/src/experiences/hotspots.ts +566 -0
- package/src/experiences/modal.ts +1337 -0
- package/src/experiences/modalSequence.ts +1247 -0
- package/src/experiences/popover.ts +652 -0
- package/src/experiences/registry.ts +21 -0
- package/src/experiences/survey.ts +1639 -0
- package/src/experiences/taskList.ts +625 -0
- package/src/experiences/tooltip.ts +740 -0
- package/src/experiences/types.ts +395 -0
- package/src/experiences/walkthrough.ts +670 -0
- package/src/flow-sequence.ts +177 -0
- package/src/flows.ts +512 -0
- package/src/http.ts +61 -0
- package/src/index.ts +355 -0
- package/src/services/flowManager.ts +905 -0
- package/src/services/flowNormalizer.ts +74 -0
- package/src/services/locationContextService.ts +189 -0
- package/src/services/pageContextService.ts +221 -0
- package/src/services/userContextService.ts +286 -0
- package/src/state/appState.ts +0 -0
- package/src/state/hooks.ts +0 -0
- package/src/state/index.ts +0 -0
- package/src/state/migration.ts +0 -0
- package/src/state/store.ts +0 -0
- package/src/styles/banner.css.ts +0 -0
- package/src/styles/hotspot.css.ts +0 -0
- package/src/styles/hotspotTour.css.ts +0 -0
- package/src/styles/modal.css.ts +564 -0
- package/src/styles/survey.css.ts +1013 -0
- package/src/styles/taskList.css.ts +0 -0
- package/src/styles/tooltip.css.ts +149 -0
- package/src/styles/walkthrough.css.ts +0 -0
- package/src/tourUtils.ts +0 -0
- package/src/tracking.ts +223 -0
- package/src/utils/debounce.ts +66 -0
- package/src/utils/eventSequenceValidator.ts +124 -0
- package/src/utils/flowTrackingSystem.ts +524 -0
- package/src/utils/idGenerator.ts +155 -0
- package/src/utils/immediateValidationPrevention.ts +184 -0
- package/src/utils/normalize.ts +50 -0
- package/src/utils/privacyManager.ts +166 -0
- package/src/utils/ruleEvaluator.ts +199 -0
- package/src/utils/sanitize.ts +79 -0
- package/src/utils/selectors.ts +107 -0
- package/src/utils/stepExecutor.ts +345 -0
- package/src/utils/triggerNormalizer.ts +149 -0
- package/src/utils/validationInterceptor.ts +650 -0
- package/tsconfig.json +13 -0
- 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();
|