@startsimpli/funnels 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/package.json +9 -31
  2. package/src/api/README.md +507 -0
  3. package/src/api/adapter.ts +106 -0
  4. package/src/api/client.test.ts +640 -0
  5. package/src/api/client.ts +385 -0
  6. package/src/api/default-adapter.ts +243 -0
  7. package/src/api/index.ts +24 -0
  8. package/src/components/FilterRuleEditor/ARCHITECTURE.md +354 -0
  9. package/src/components/FilterRuleEditor/FieldSelector.tsx +91 -0
  10. package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +462 -0
  11. package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +520 -0
  12. package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +225 -0
  13. package/src/components/FilterRuleEditor/LogicToggle.tsx +64 -0
  14. package/src/components/FilterRuleEditor/OperatorSelector.tsx +75 -0
  15. package/src/components/FilterRuleEditor/README.md +291 -0
  16. package/src/components/FilterRuleEditor/RuleRow.tsx +246 -0
  17. package/src/components/FilterRuleEditor/ValueInputs/BooleanValueInput.tsx +54 -0
  18. package/src/components/FilterRuleEditor/ValueInputs/ChoiceValueInput.tsx +83 -0
  19. package/src/components/FilterRuleEditor/ValueInputs/DateValueInput.tsx +70 -0
  20. package/src/components/FilterRuleEditor/ValueInputs/MultiChoiceValueInput.tsx +132 -0
  21. package/src/components/FilterRuleEditor/ValueInputs/NumberValueInput.tsx +73 -0
  22. package/src/components/FilterRuleEditor/ValueInputs/TextValueInput.tsx +50 -0
  23. package/src/components/FilterRuleEditor/ValueInputs/index.ts +12 -0
  24. package/src/components/FilterRuleEditor/constants.ts +64 -0
  25. package/src/components/FilterRuleEditor/index.ts +14 -0
  26. package/src/components/FunnelCard/DESIGN.md +447 -0
  27. package/src/components/FunnelCard/FunnelCard.stories.tsx +484 -0
  28. package/src/components/FunnelCard/FunnelCard.test.ts +257 -0
  29. package/src/components/FunnelCard/FunnelCard.test.tsx +336 -0
  30. package/src/components/FunnelCard/FunnelCard.tsx +204 -0
  31. package/src/components/FunnelCard/FunnelStats.tsx +68 -0
  32. package/src/components/FunnelCard/IMPLEMENTATION_SUMMARY.md +505 -0
  33. package/src/components/FunnelCard/INSTALLATION.md +304 -0
  34. package/src/components/FunnelCard/MatchBar.tsx +49 -0
  35. package/src/components/FunnelCard/README.md +294 -0
  36. package/src/components/FunnelCard/StageIndicator.tsx +62 -0
  37. package/src/components/FunnelCard/StatusBadge.tsx +52 -0
  38. package/src/components/FunnelCard/index.ts +14 -0
  39. package/src/components/FunnelPreview/EntityCard.tsx +72 -0
  40. package/src/components/FunnelPreview/FunnelPreview.stories.tsx +227 -0
  41. package/src/components/FunnelPreview/FunnelPreview.test.tsx +316 -0
  42. package/src/components/FunnelPreview/FunnelPreview.tsx +249 -0
  43. package/src/components/FunnelPreview/LoadingPreview.tsx +60 -0
  44. package/src/components/FunnelPreview/PreviewStats.tsx +78 -0
  45. package/src/components/FunnelPreview/README.md +337 -0
  46. package/src/components/FunnelPreview/StageBreakdown.tsx +94 -0
  47. package/src/components/FunnelPreview/example.tsx +286 -0
  48. package/src/components/FunnelPreview/index.ts +14 -0
  49. package/src/components/FunnelRunHistory/COMPONENT_SUMMARY.md +246 -0
  50. package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +272 -0
  51. package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +323 -0
  52. package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +329 -0
  53. package/src/components/FunnelRunHistory/README.md +325 -0
  54. package/src/components/FunnelRunHistory/RunActions.tsx +168 -0
  55. package/src/components/FunnelRunHistory/RunDetailsModal.tsx +221 -0
  56. package/src/components/FunnelRunHistory/RunFilters.tsx +128 -0
  57. package/src/components/FunnelRunHistory/RunRow.tsx +122 -0
  58. package/src/components/FunnelRunHistory/RunStatusBadge.tsx +75 -0
  59. package/src/components/FunnelRunHistory/StageBreakdownList.tsx +110 -0
  60. package/src/components/FunnelRunHistory/index.ts +51 -0
  61. package/src/components/FunnelRunHistory/types.ts +40 -0
  62. package/src/components/FunnelRunHistory/utils.test.ts +126 -0
  63. package/src/components/FunnelRunHistory/utils.ts +100 -0
  64. package/src/components/FunnelStageBuilder/AddStageButton.tsx +52 -0
  65. package/src/components/FunnelStageBuilder/FunnelStageBuilder.css +413 -0
  66. package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +312 -0
  67. package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +304 -0
  68. package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +321 -0
  69. package/src/components/FunnelStageBuilder/README.md +341 -0
  70. package/src/components/FunnelStageBuilder/StageActions.test.tsx +205 -0
  71. package/src/components/FunnelStageBuilder/StageActions.tsx +126 -0
  72. package/src/components/FunnelStageBuilder/StageCard.tsx +202 -0
  73. package/src/components/FunnelStageBuilder/StageForm.tsx +262 -0
  74. package/src/components/FunnelStageBuilder/TagInput.test.tsx +178 -0
  75. package/src/components/FunnelStageBuilder/TagInput.tsx +129 -0
  76. package/src/components/FunnelStageBuilder/index.ts +21 -0
  77. package/src/components/FunnelVisualFlow/FlowLegend.tsx +77 -0
  78. package/{dist/components/index.css → src/components/FunnelVisualFlow/FunnelVisualFlow.css} +89 -13
  79. package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +254 -0
  80. package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +208 -0
  81. package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +229 -0
  82. package/src/components/FunnelVisualFlow/README.md +323 -0
  83. package/src/components/FunnelVisualFlow/StageNode.tsx +188 -0
  84. package/src/components/FunnelVisualFlow/example.tsx +227 -0
  85. package/src/components/FunnelVisualFlow/index.ts +10 -0
  86. package/src/components/index.ts +102 -0
  87. package/src/core/README.md +307 -0
  88. package/src/core/engine.test.ts +1087 -0
  89. package/src/core/engine.ts +329 -0
  90. package/src/core/evaluator.example.ts +353 -0
  91. package/src/core/evaluator.test.ts +639 -0
  92. package/src/core/evaluator.ts +261 -0
  93. package/src/core/field-resolver.example.ts +175 -0
  94. package/src/core/field-resolver.test.ts +541 -0
  95. package/src/core/field-resolver.ts +247 -0
  96. package/src/core/index.ts +34 -0
  97. package/src/core/operators.test.ts +539 -0
  98. package/src/core/operators.ts +241 -0
  99. package/src/hooks/index.ts +5 -0
  100. package/src/hooks/useDebouncedValue.ts +28 -0
  101. package/src/index.ts +155 -0
  102. package/src/store/README.md +342 -0
  103. package/src/store/create-funnel-store.test.ts +686 -0
  104. package/src/store/create-funnel-store.ts +538 -0
  105. package/src/store/index.ts +9 -0
  106. package/src/store/types.ts +294 -0
  107. package/src/stories/CrossDomain.stories.tsx +149 -0
  108. package/src/stories/Welcome.stories.tsx +81 -0
  109. package/src/stories/demo-data/index.ts +3 -0
  110. package/src/stories/demo-data/investors.ts +216 -0
  111. package/src/stories/demo-data/leads.ts +223 -0
  112. package/src/stories/demo-data/recipes.ts +217 -0
  113. package/src/test/setup.ts +5 -0
  114. package/src/types/index.ts +843 -0
  115. package/dist/client-3ESO2NHy.d.ts +0 -310
  116. package/dist/client-CZu03ACp.d.cts +0 -310
  117. package/dist/components/index.cjs +0 -3243
  118. package/dist/components/index.cjs.map +0 -1
  119. package/dist/components/index.css.map +0 -1
  120. package/dist/components/index.d.cts +0 -726
  121. package/dist/components/index.d.ts +0 -726
  122. package/dist/components/index.js +0 -3196
  123. package/dist/components/index.js.map +0 -1
  124. package/dist/core/index.cjs +0 -500
  125. package/dist/core/index.cjs.map +0 -1
  126. package/dist/core/index.d.cts +0 -359
  127. package/dist/core/index.d.ts +0 -359
  128. package/dist/core/index.js +0 -486
  129. package/dist/core/index.js.map +0 -1
  130. package/dist/hooks/index.cjs +0 -21
  131. package/dist/hooks/index.cjs.map +0 -1
  132. package/dist/hooks/index.d.cts +0 -11
  133. package/dist/hooks/index.d.ts +0 -11
  134. package/dist/hooks/index.js +0 -19
  135. package/dist/hooks/index.js.map +0 -1
  136. package/dist/index-BGDEXbuz.d.cts +0 -434
  137. package/dist/index-BGDEXbuz.d.ts +0 -434
  138. package/dist/index.cjs +0 -4499
  139. package/dist/index.cjs.map +0 -1
  140. package/dist/index.css +0 -198
  141. package/dist/index.css.map +0 -1
  142. package/dist/index.d.cts +0 -99
  143. package/dist/index.d.ts +0 -99
  144. package/dist/index.js +0 -4421
  145. package/dist/index.js.map +0 -1
  146. package/dist/store/index.cjs +0 -391
  147. package/dist/store/index.cjs.map +0 -1
  148. package/dist/store/index.d.cts +0 -225
  149. package/dist/store/index.d.ts +0 -225
  150. package/dist/store/index.js +0 -388
  151. package/dist/store/index.js.map +0 -1
@@ -1,3243 +0,0 @@
1
- 'use strict';
2
-
3
- var react = require('react');
4
- var jsxRuntime = require('react/jsx-runtime');
5
- var react$1 = require('@xyflow/react');
6
- require('@xyflow/react/dist/style.css');
7
- var core = require('@dnd-kit/core');
8
- var sortable = require('@dnd-kit/sortable');
9
- var utilities = require('@dnd-kit/utilities');
10
-
11
- // src/components/FunnelPreview/FunnelPreview.tsx
12
-
13
- // src/core/engine.ts
14
- function evaluateRule(_entity, _rule) {
15
- throw new Error("Not implemented - BEAD: fund-your-startup-a0b8. evaluateRule must import from rule evaluator.");
16
- }
17
- var FunnelEngine = class {
18
- /**
19
- * Execute a funnel on a set of entities
20
- *
21
- * @param funnel - The funnel definition to execute
22
- * @param entities - Input entities to process
23
- * @returns ExecutionResult with matched/excluded entities and stats
24
- */
25
- execute(funnel, entities) {
26
- const startTime = Date.now();
27
- const results = entities.map((entity) => ({
28
- entity,
29
- matched: true,
30
- // Start as matched, exclude as needed
31
- accumulated_tags: [],
32
- context: {},
33
- stage_results: []
34
- }));
35
- const stageStats = {};
36
- const errors = [];
37
- const sortedStages = [...funnel.stages].sort((a, b) => a.order - b.order);
38
- for (const stage of sortedStages) {
39
- const stageStartTime = Date.now();
40
- const inputEntities = results.filter((r) => r.matched && !r.excluded_at_stage);
41
- const stats = {
42
- stage_id: stage.id,
43
- stage_name: stage.name,
44
- input_count: inputEntities.length,
45
- matched_count: 0,
46
- not_matched_count: 0,
47
- excluded_count: 0,
48
- tagged_count: 0,
49
- continued_count: 0,
50
- error_count: 0
51
- };
52
- for (const result of inputEntities) {
53
- try {
54
- const stageResult = this.processStage(stage, result.entity);
55
- result.stage_results.push(stageResult);
56
- if (stageResult.matched) {
57
- stats.matched_count++;
58
- } else {
59
- stats.not_matched_count++;
60
- }
61
- if (stageResult.tags_added && stageResult.tags_added.length > 0) {
62
- result.accumulated_tags.push(...stageResult.tags_added);
63
- stats.tagged_count++;
64
- }
65
- if (stageResult.context_added) {
66
- result.context = { ...result.context, ...stageResult.context_added };
67
- }
68
- if (stageResult.excluded) {
69
- result.matched = false;
70
- result.excluded_at_stage = stage.id;
71
- stats.excluded_count++;
72
- } else if (stageResult.continued) {
73
- stats.continued_count++;
74
- }
75
- } catch (error) {
76
- stats.error_count++;
77
- errors.push(`Stage ${stage.name}: ${error instanceof Error ? error.message : String(error)}`);
78
- }
79
- }
80
- stats.duration_ms = Date.now() - stageStartTime;
81
- stageStats[stage.id] = stats;
82
- }
83
- if (funnel.completion_tags && funnel.completion_tags.length > 0) {
84
- for (const result of results) {
85
- if (result.matched) {
86
- result.accumulated_tags.push(...funnel.completion_tags);
87
- }
88
- }
89
- }
90
- const matched = results.filter((r) => r.matched);
91
- const excluded = results.filter((r) => !r.matched);
92
- const totalTagged = results.filter((r) => r.accumulated_tags.length > 0).length;
93
- return {
94
- matched,
95
- excluded,
96
- total_input: entities.length,
97
- total_matched: matched.length,
98
- total_excluded: excluded.length,
99
- total_tagged: totalTagged,
100
- stage_stats: stageStats,
101
- duration_ms: Date.now() - startTime,
102
- errors: errors.length > 0 ? errors : void 0
103
- };
104
- }
105
- /**
106
- * Process a single entity through a stage
107
- *
108
- * @param stage - The stage to process
109
- * @param entity - The entity to evaluate
110
- * @returns StageResult with match status and actions taken
111
- */
112
- processStage(stage, entity) {
113
- const ruleResults = [];
114
- let matched = false;
115
- if (stage.custom_evaluator) {
116
- try {
117
- matched = stage.custom_evaluator(entity);
118
- } catch (error) {
119
- matched = false;
120
- }
121
- } else if (stage.rules.length === 0) {
122
- matched = true;
123
- } else {
124
- for (const rule of stage.rules) {
125
- const ruleResult = evaluateRule();
126
- ruleResults.push(ruleResult);
127
- }
128
- if (stage.filter_logic === "AND") {
129
- matched = ruleResults.every((r) => r.matched);
130
- } else {
131
- matched = ruleResults.some((r) => r.matched);
132
- }
133
- }
134
- let action;
135
- let tagsAdded = [];
136
- let contextAdded;
137
- let excluded = false;
138
- let continued = false;
139
- if (matched) {
140
- action = stage.match_action;
141
- if (stage.match_tags && stage.match_tags.length > 0) {
142
- tagsAdded = [...stage.match_tags];
143
- }
144
- if (stage.match_context) {
145
- contextAdded = stage.match_context;
146
- }
147
- switch (stage.match_action) {
148
- case "continue":
149
- continued = true;
150
- break;
151
- case "tag":
152
- excluded = true;
153
- break;
154
- case "tag_continue":
155
- continued = true;
156
- break;
157
- case "output":
158
- continued = false;
159
- break;
160
- }
161
- } else {
162
- action = stage.no_match_action;
163
- if (stage.no_match_tags && stage.no_match_tags.length > 0) {
164
- tagsAdded = [...stage.no_match_tags];
165
- }
166
- switch (stage.no_match_action) {
167
- case "continue":
168
- continued = true;
169
- break;
170
- case "exclude":
171
- excluded = true;
172
- break;
173
- case "tag_exclude":
174
- excluded = true;
175
- break;
176
- }
177
- }
178
- return {
179
- stage_id: stage.id,
180
- stage_name: stage.name,
181
- matched,
182
- rule_results: ruleResults.length > 0 ? ruleResults : void 0,
183
- action,
184
- tags_added: tagsAdded.length > 0 ? tagsAdded : void 0,
185
- context_added: contextAdded,
186
- excluded,
187
- continued
188
- };
189
- }
190
- };
191
- function useDebouncedValue(value, delay = 300) {
192
- const [debouncedValue, setDebouncedValue] = react.useState(value);
193
- react.useEffect(() => {
194
- const handler = setTimeout(() => {
195
- setDebouncedValue(value);
196
- }, delay);
197
- return () => {
198
- clearTimeout(handler);
199
- };
200
- }, [value, delay]);
201
- return debouncedValue;
202
- }
203
- function PreviewStats({
204
- totalMatched,
205
- totalExcluded,
206
- matchPercentage,
207
- className = ""
208
- }) {
209
- const total = totalMatched + totalExcluded;
210
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `space-y-2 ${className}`, children: [
211
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative h-8 bg-gray-200 rounded-lg overflow-hidden", children: [
212
- /* @__PURE__ */ jsxRuntime.jsx(
213
- "div",
214
- {
215
- className: "absolute inset-y-0 left-0 bg-gradient-to-r from-green-500 to-green-600 transition-all duration-300 flex items-center justify-center",
216
- style: { width: `${matchPercentage}%` },
217
- role: "progressbar",
218
- "aria-valuenow": matchPercentage,
219
- "aria-valuemin": 0,
220
- "aria-valuemax": 100,
221
- "aria-label": `${totalMatched} of ${total} matched (${matchPercentage}%)`,
222
- children: matchPercentage > 15 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs font-semibold text-white", children: [
223
- totalMatched.toLocaleString(),
224
- "/",
225
- total.toLocaleString(),
226
- " (",
227
- matchPercentage,
228
- "%)"
229
- ] })
230
- }
231
- ),
232
- matchPercentage <= 15 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs font-semibold text-gray-600", children: [
233
- totalMatched.toLocaleString(),
234
- "/",
235
- total.toLocaleString(),
236
- " (",
237
- matchPercentage,
238
- "%)"
239
- ] }) })
240
- ] }),
241
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-sm", children: [
242
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
243
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-sm bg-green-500" }),
244
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-medium text-gray-700", children: [
245
- totalMatched.toLocaleString(),
246
- " Matched"
247
- ] })
248
- ] }),
249
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
250
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-sm bg-gray-300" }),
251
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-medium text-gray-700", children: [
252
- totalExcluded.toLocaleString(),
253
- " Excluded"
254
- ] })
255
- ] })
256
- ] })
257
- ] });
258
- }
259
- function StageBreakdown({
260
- stageStats,
261
- stages,
262
- className = ""
263
- }) {
264
- const sortedStages = [...stages].sort((a, b) => a.order - b.order);
265
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, children: [
266
- /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold text-gray-700 mb-3", children: "Stage Breakdown" }),
267
- /* @__PURE__ */ jsxRuntime.jsx("ol", { className: "space-y-2", children: sortedStages.map((stage, index) => {
268
- const stats = stageStats[stage.id];
269
- if (!stats) return null;
270
- const isLast = index === sortedStages.length - 1;
271
- const excludedCount = stats.excluded_count;
272
- const remainingCount = stats.remaining_count;
273
- return /* @__PURE__ */ jsxRuntime.jsxs(
274
- "li",
275
- {
276
- className: "flex items-center justify-between py-2 px-3 bg-gray-50 rounded-lg",
277
- children: [
278
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 flex-1 min-w-0", children: [
279
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0 w-6 h-6 flex items-center justify-center bg-blue-100 text-blue-700 rounded-full text-xs font-bold", children: index + 1 }),
280
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-900 truncate", children: stage.name })
281
- ] }),
282
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 text-sm", children: [
283
- excludedCount > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-red-600 font-medium", children: [
284
- "-",
285
- excludedCount.toLocaleString()
286
- ] }),
287
- /* @__PURE__ */ jsxRuntime.jsxs(
288
- "span",
289
- {
290
- className: `font-semibold ${isLast ? "text-green-600" : "text-gray-700"}`,
291
- children: [
292
- remainingCount.toLocaleString(),
293
- " ",
294
- isLast ? "final" : "left"
295
- ]
296
- }
297
- )
298
- ] })
299
- ]
300
- },
301
- stage.id
302
- );
303
- }) })
304
- ] });
305
- }
306
- function defaultEntityRenderer(entity) {
307
- if (entity.name) {
308
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
309
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-medium text-gray-900", children: entity.name }),
310
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-gray-600 mt-1", children: Object.keys(entity).filter((key) => key !== "name").slice(0, 3).map((key) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "mr-2", children: [
311
- key,
312
- ": ",
313
- String(entity[key]).slice(0, 20)
314
- ] }, key)) })
315
- ] });
316
- }
317
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-gray-700 font-mono", children: /* @__PURE__ */ jsxRuntime.jsxs("pre", { className: "whitespace-pre-wrap break-all", children: [
318
- JSON.stringify(entity, null, 2).slice(0, 150),
319
- JSON.stringify(entity, null, 2).length > 150 ? "..." : ""
320
- ] }) });
321
- }
322
- function EntityCard({
323
- entity,
324
- renderEntity = defaultEntityRenderer,
325
- className = ""
326
- }) {
327
- return /* @__PURE__ */ jsxRuntime.jsx(
328
- "article",
329
- {
330
- className: `p-3 bg-white border border-gray-200 rounded-lg shadow-sm hover:border-gray-300 transition-colors ${className}`,
331
- children: renderEntity(entity)
332
- }
333
- );
334
- }
335
- function LoadingPreview() {
336
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "animate-pulse", role: "status", "aria-live": "polite", children: [
337
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: "Loading preview..." }),
338
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2 mb-6", children: [
339
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-8 bg-gray-200 rounded-lg" }),
340
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between", children: [
341
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 w-32 bg-gray-200 rounded" }),
342
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 w-32 bg-gray-200 rounded" })
343
- ] })
344
- ] }),
345
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-6", children: [
346
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-5 w-40 bg-gray-200 rounded mb-3" }),
347
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: [1, 2, 3].map((i) => /* @__PURE__ */ jsxRuntime.jsxs(
348
- "div",
349
- {
350
- className: "h-12 bg-gray-100 rounded-lg flex items-center px-3 gap-3",
351
- children: [
352
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-6 h-6 bg-gray-200 rounded-full" }),
353
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 bg-gray-200 rounded flex-1" }),
354
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 w-16 bg-gray-200 rounded" })
355
- ]
356
- },
357
- i
358
- )) })
359
- ] }),
360
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
361
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-5 w-48 bg-gray-200 rounded mb-3" }),
362
- [1, 2, 3].map((i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-20 bg-gray-100 border border-gray-200 rounded-lg p-3", children: [
363
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 bg-gray-200 rounded w-3/4 mb-2" }),
364
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3 bg-gray-200 rounded w-1/2" })
365
- ] }, i))
366
- ] })
367
- ] });
368
- }
369
- function convertToPreviewResult(execResult, maxEntities = 10) {
370
- const { matched, total_matched, total_excluded, stage_stats } = execResult;
371
- const total = total_matched + total_excluded;
372
- const matchPercentage = total > 0 ? Math.round(total_matched / total * 100) : 0;
373
- const previewEntities = matched.slice(0, maxEntities).map((r) => r.entity);
374
- const previewStageStats = {};
375
- Object.entries(stage_stats).forEach(([stageId, stats]) => {
376
- previewStageStats[stageId] = {
377
- stage_id: stats.stage_id,
378
- stage_name: stats.stage_name,
379
- input_count: stats.input_count,
380
- excluded_count: stats.excluded_count,
381
- remaining_count: stats.input_count - stats.excluded_count
382
- };
383
- });
384
- return {
385
- totalMatched: total_matched,
386
- totalExcluded: total_excluded,
387
- matchPercentage,
388
- previewEntities,
389
- stageStats: previewStageStats
390
- };
391
- }
392
- function FunnelPreview({
393
- funnel,
394
- sampleEntities,
395
- onPreview,
396
- renderEntity,
397
- maxPreviewEntities = 10,
398
- className = ""
399
- }) {
400
- const [result, setResult] = react.useState(null);
401
- const [isComputing, setIsComputing] = react.useState(false);
402
- const debouncedFunnel = useDebouncedValue(funnel, 300);
403
- react.useEffect(() => {
404
- async function compute() {
405
- setIsComputing(true);
406
- try {
407
- const engine = new FunnelEngine();
408
- const execResult = engine.execute(debouncedFunnel, sampleEntities);
409
- const previewResult = convertToPreviewResult(
410
- execResult,
411
- maxPreviewEntities
412
- );
413
- setResult(previewResult);
414
- if (onPreview) {
415
- onPreview(previewResult);
416
- }
417
- } catch (error) {
418
- console.error("Preview computation failed:", error);
419
- setResult({
420
- totalMatched: 0,
421
- totalExcluded: sampleEntities.length,
422
- matchPercentage: 0,
423
- previewEntities: [],
424
- stageStats: {}
425
- });
426
- } finally {
427
- setIsComputing(false);
428
- }
429
- }
430
- compute();
431
- }, [debouncedFunnel, sampleEntities, maxPreviewEntities, onPreview]);
432
- if (isComputing && !result) {
433
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: /* @__PURE__ */ jsxRuntime.jsx(LoadingPreview, {}) });
434
- }
435
- if (!result) {
436
- return null;
437
- }
438
- const { totalMatched, totalExcluded, matchPercentage, previewEntities, stageStats } = result;
439
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, role: "region", "aria-label": "Funnel preview", children: [
440
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-semibold text-gray-900 mb-4", children: "Preview Results" }),
441
- /* @__PURE__ */ jsxRuntime.jsx(
442
- PreviewStats,
443
- {
444
- totalMatched,
445
- totalExcluded,
446
- matchPercentage,
447
- className: "mb-6"
448
- }
449
- ),
450
- funnel.stages.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
451
- StageBreakdown,
452
- {
453
- stageStats,
454
- stages: funnel.stages,
455
- className: "mb-6"
456
- }
457
- ),
458
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
459
- /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "text-sm font-semibold text-gray-700 mb-3", children: [
460
- "Sample Matches (",
461
- Math.min(previewEntities.length, maxPreviewEntities),
462
- " of",
463
- " ",
464
- totalMatched.toLocaleString(),
465
- ")"
466
- ] }),
467
- previewEntities.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-8 text-center bg-gray-50 rounded-lg border-2 border-dashed border-gray-300", children: [
468
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600", children: "No entities matched this funnel" }),
469
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 mt-1", children: "Try adjusting your filter rules" })
470
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
471
- previewEntities.map((entity, index) => /* @__PURE__ */ jsxRuntime.jsx(
472
- EntityCard,
473
- {
474
- entity,
475
- renderEntity
476
- },
477
- index
478
- )),
479
- totalMatched > maxPreviewEntities && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center py-2 text-sm text-gray-500", children: [
480
- "+ ",
481
- (totalMatched - maxPreviewEntities).toLocaleString(),
482
- " more..."
483
- ] })
484
- ] })
485
- ] }),
486
- isComputing && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center rounded-lg", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-gray-600", children: [
487
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-5 h-5 border-2 border-gray-300 border-t-blue-600 rounded-full animate-spin" }),
488
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: "Updating preview..." })
489
- ] }) })
490
- ] });
491
- }
492
- var statusConfig = {
493
- active: {
494
- color: "text-green-800",
495
- bgColor: "bg-green-100",
496
- label: "ACTIVE"
497
- },
498
- draft: {
499
- color: "text-yellow-800",
500
- bgColor: "bg-yellow-100",
501
- label: "DRAFT"
502
- },
503
- paused: {
504
- color: "text-gray-800",
505
- bgColor: "bg-gray-100",
506
- label: "PAUSED"
507
- },
508
- archived: {
509
- color: "text-red-800",
510
- bgColor: "bg-red-100",
511
- label: "ARCHIVED"
512
- }
513
- };
514
- function StatusBadge({ status, className = "" }) {
515
- const config = statusConfig[status];
516
- return /* @__PURE__ */ jsxRuntime.jsx(
517
- "span",
518
- {
519
- className: `inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${config.bgColor} ${config.color} ${className}`,
520
- children: config.label
521
- }
522
- );
523
- }
524
- function StageIndicator({
525
- order,
526
- name,
527
- ruleCount,
528
- isLast = false,
529
- className = ""
530
- }) {
531
- const circledNumber = order < 20 ? String.fromCharCode(9312 + order) : `(${order + 1})`;
532
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex items-start gap-2 ${className}`, children: [
533
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center", children: [
534
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0 w-6 h-6 rounded-full bg-blue-100 text-blue-800 flex items-center justify-center text-sm font-medium", children: circledNumber }),
535
- !isLast && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-0.5 h-6 bg-gray-200 mt-1" })
536
- ] }),
537
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 pt-0.5 min-w-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [
538
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-900 truncate", children: name }),
539
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-gray-500 whitespace-nowrap", children: [
540
- ruleCount,
541
- " ",
542
- ruleCount === 1 ? "rule" : "rules"
543
- ] })
544
- ] }) })
545
- ] });
546
- }
547
- function MatchBar({ matched, total, className = "" }) {
548
- const percentage = total > 0 ? Math.round(matched / total * 100) : 0;
549
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `space-y-1 ${className}`, children: [
550
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative h-6 bg-gray-200 rounded-md overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
551
- "div",
552
- {
553
- className: "absolute inset-y-0 left-0 bg-gradient-to-r from-green-500 to-green-600 transition-all duration-300",
554
- style: { width: `${percentage}%` },
555
- role: "progressbar",
556
- "aria-valuenow": percentage,
557
- "aria-valuemin": 0,
558
- "aria-valuemax": 100,
559
- "aria-label": `${matched} of ${total} matched`
560
- }
561
- ) }),
562
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-right", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm font-medium text-gray-700", children: [
563
- matched.toLocaleString(),
564
- " matched"
565
- ] }) })
566
- ] });
567
- }
568
- function FunnelStats({
569
- input,
570
- matched,
571
- excluded,
572
- className = ""
573
- }) {
574
- const stats = [
575
- {
576
- label: "INPUT",
577
- value: input,
578
- color: "text-blue-600",
579
- bgColor: "bg-blue-50"
580
- },
581
- {
582
- label: "MATCHED",
583
- value: matched,
584
- color: "text-green-600",
585
- bgColor: "bg-green-50"
586
- },
587
- {
588
- label: "EXCLUDED",
589
- value: excluded,
590
- color: "text-red-600",
591
- bgColor: "bg-red-50"
592
- }
593
- ];
594
- return /* @__PURE__ */ jsxRuntime.jsx("dl", { className: `grid grid-cols-3 gap-2 ${className}`, children: stats.map(({ label, value, color, bgColor }) => /* @__PURE__ */ jsxRuntime.jsxs(
595
- "div",
596
- {
597
- className: `${bgColor} rounded-lg px-3 py-2.5 text-center`,
598
- children: [
599
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "text-xs font-medium text-gray-600 mb-1", children: label }),
600
- /* @__PURE__ */ jsxRuntime.jsx("dd", { className: `text-lg font-bold ${color}`, children: value.toLocaleString() })
601
- ]
602
- },
603
- label
604
- )) });
605
- }
606
- function FunnelCard({
607
- funnel,
608
- latestRun,
609
- onViewFlow,
610
- onEdit,
611
- className = ""
612
- }) {
613
- const stats = latestRun ? {
614
- input: latestRun.total_input,
615
- matched: latestRun.total_matched,
616
- excluded: latestRun.total_excluded
617
- } : {
618
- input: 0,
619
- matched: 0,
620
- excluded: 0
621
- };
622
- const handleViewFlow = () => {
623
- if (onViewFlow) {
624
- onViewFlow(funnel);
625
- }
626
- };
627
- const hasRun = latestRun && latestRun.status === "completed";
628
- return /* @__PURE__ */ jsxRuntime.jsxs(
629
- "article",
630
- {
631
- className: `bg-white rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-shadow duration-200 ${className}`,
632
- "aria-label": `Funnel: ${funnel.name}`,
633
- children: [
634
- /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "px-6 pt-5 pb-3 border-b border-gray-100", children: [
635
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-3", children: [
636
- /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-gray-900 flex-1 min-w-0", children: funnel.name }),
637
- /* @__PURE__ */ jsxRuntime.jsx(StatusBadge, { status: funnel.status })
638
- ] }),
639
- funnel.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-gray-600 line-clamp-2", children: funnel.description })
640
- ] }),
641
- /* @__PURE__ */ jsxRuntime.jsx(
642
- "section",
643
- {
644
- className: "px-6 py-4 space-y-0",
645
- "aria-label": "Funnel stages",
646
- children: funnel.stages.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-gray-500 italic py-4 text-center", children: "No stages defined" }) : funnel.stages.map((stage, index) => /* @__PURE__ */ jsxRuntime.jsx(
647
- StageIndicator,
648
- {
649
- order: index,
650
- name: stage.name,
651
- ruleCount: stage.rules.length,
652
- isLast: index === funnel.stages.length - 1
653
- },
654
- stage.id
655
- ))
656
- }
657
- ),
658
- hasRun && /* @__PURE__ */ jsxRuntime.jsx(
659
- "section",
660
- {
661
- className: "px-6 py-4 border-t border-gray-100",
662
- "aria-label": "Match results",
663
- children: /* @__PURE__ */ jsxRuntime.jsx(
664
- MatchBar,
665
- {
666
- matched: stats.matched,
667
- total: stats.input
668
- }
669
- )
670
- }
671
- ),
672
- hasRun && /* @__PURE__ */ jsxRuntime.jsx(
673
- "section",
674
- {
675
- className: "px-6 py-4 border-t border-gray-100",
676
- "aria-label": "Funnel statistics",
677
- children: /* @__PURE__ */ jsxRuntime.jsx(
678
- FunnelStats,
679
- {
680
- input: stats.input,
681
- matched: stats.matched,
682
- excluded: stats.excluded
683
- }
684
- )
685
- }
686
- ),
687
- !hasRun && /* @__PURE__ */ jsxRuntime.jsx(
688
- "section",
689
- {
690
- className: "px-6 py-4 border-t border-gray-100 text-center",
691
- "aria-label": "Funnel status",
692
- children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500", children: latestRun?.status === "failed" ? "Last run failed" : latestRun?.status === "running" ? "Running..." : "No runs yet" })
693
- }
694
- ),
695
- /* @__PURE__ */ jsxRuntime.jsx("footer", { className: "px-6 py-4 border-t border-gray-100", children: /* @__PURE__ */ jsxRuntime.jsxs(
696
- "button",
697
- {
698
- onClick: handleViewFlow,
699
- className: "w-full inline-flex items-center justify-center gap-2 px-4 py-2.5 bg-gray-50 hover:bg-gray-100 text-gray-900 text-sm font-medium rounded-md transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
700
- "aria-label": `View flow details for ${funnel.name}`,
701
- children: [
702
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "View Flow" }),
703
- /* @__PURE__ */ jsxRuntime.jsx(
704
- "svg",
705
- {
706
- className: "w-4 h-4 transition-transform group-hover:translate-x-0.5",
707
- fill: "none",
708
- viewBox: "0 0 24 24",
709
- stroke: "currentColor",
710
- "aria-hidden": "true",
711
- children: /* @__PURE__ */ jsxRuntime.jsx(
712
- "path",
713
- {
714
- strokeLinecap: "round",
715
- strokeLinejoin: "round",
716
- strokeWidth: 2,
717
- d: "M13 7l5 5m0 0l-5 5m5-5H6"
718
- }
719
- )
720
- }
721
- )
722
- ]
723
- }
724
- ) })
725
- ]
726
- }
727
- );
728
- }
729
- function getStageColor(stage) {
730
- const matchAction = stage.match_action;
731
- const noMatchAction = stage.no_match_action;
732
- if (matchAction === "output") {
733
- return "#22c55e";
734
- }
735
- if (noMatchAction === "exclude" || noMatchAction === "tag_exclude") {
736
- return "#ef4444";
737
- }
738
- if (matchAction === "tag" || matchAction === "tag_continue") {
739
- return "#eab308";
740
- }
741
- return "#3b82f6";
742
- }
743
- function getActionLabel(stage) {
744
- const matchAction = stage.match_action;
745
- const noMatchAction = stage.no_match_action;
746
- if (matchAction === "output") return "Output";
747
- if (noMatchAction === "exclude") return "Exclude Non-Matches";
748
- if (noMatchAction === "tag_exclude") return "Tag & Exclude";
749
- if (matchAction === "tag") return "Tag Matches";
750
- if (matchAction === "tag_continue") return "Tag & Continue";
751
- return "Continue";
752
- }
753
- function StageNode({ data }) {
754
- const { stage, stats, onStageClick } = data;
755
- const color = getStageColor(stage);
756
- const actionLabel = getActionLabel(stage);
757
- const handleClick = () => {
758
- if (onStageClick) {
759
- onStageClick(stage);
760
- }
761
- };
762
- const handleKeyDown = (event) => {
763
- if (event.key === "Enter" || event.key === " ") {
764
- event.preventDefault();
765
- handleClick();
766
- }
767
- };
768
- return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
769
- /* @__PURE__ */ jsxRuntime.jsx(
770
- react$1.Handle,
771
- {
772
- type: "target",
773
- position: react$1.Position.Top,
774
- style: { background: color, opacity: 0 },
775
- isConnectable: false
776
- }
777
- ),
778
- /* @__PURE__ */ jsxRuntime.jsxs(
779
- "div",
780
- {
781
- className: "stage-node",
782
- onClick: handleClick,
783
- onKeyDown: handleKeyDown,
784
- role: "button",
785
- tabIndex: 0,
786
- "aria-label": `Stage ${stage.order + 1}: ${stage.name}`,
787
- style: {
788
- borderColor: color,
789
- borderWidth: "2px",
790
- borderStyle: "solid"
791
- },
792
- children: [
793
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "stage-number", style: { color }, children: getCircledNumber(stage.order + 1) }),
794
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "stage-name", title: stage.name, children: stage.name }),
795
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-rules", children: [
796
- stage.rules.length,
797
- " ",
798
- stage.rules.length === 1 ? "rule" : "rules"
799
- ] }),
800
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "stage-action", style: { color }, children: actionLabel }),
801
- stats && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-stats", children: [
802
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stat-row", children: [
803
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "stat-label", children: "Input:" }),
804
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "stat-value", children: stats.input_count })
805
- ] }),
806
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stat-row", children: [
807
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "stat-label", children: "Matched:" }),
808
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "stat-value text-green-600", children: stats.matched_count })
809
- ] }),
810
- stats.excluded_count > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stat-row", children: [
811
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "stat-label", children: "Excluded:" }),
812
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "stat-value text-red-600", children: stats.excluded_count })
813
- ] })
814
- ] }),
815
- stage.description && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "stage-description", title: stage.description, children: stage.description.length > 50 ? `${stage.description.substring(0, 50)}...` : stage.description })
816
- ]
817
- }
818
- ),
819
- /* @__PURE__ */ jsxRuntime.jsx(
820
- react$1.Handle,
821
- {
822
- type: "source",
823
- position: react$1.Position.Bottom,
824
- style: { background: color, opacity: 0 },
825
- isConnectable: false
826
- }
827
- )
828
- ] });
829
- }
830
- function FlowLegend() {
831
- const [isExpanded, setIsExpanded] = react.useState(true);
832
- const legendItems = [
833
- { color: "#3b82f6", label: "Continue" },
834
- { color: "#ef4444", label: "Exclude" },
835
- { color: "#eab308", label: "Tag" },
836
- { color: "#22c55e", label: "Output" }
837
- ];
838
- return /* @__PURE__ */ jsxRuntime.jsx(react$1.Panel, { position: "bottom-right", className: "flow-legend-panel", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flow-legend", children: [
839
- /* @__PURE__ */ jsxRuntime.jsxs(
840
- "button",
841
- {
842
- className: "legend-toggle",
843
- onClick: () => setIsExpanded(!isExpanded),
844
- "aria-label": isExpanded ? "Collapse legend" : "Expand legend",
845
- children: [
846
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "legend-title", children: "Legend" }),
847
- /* @__PURE__ */ jsxRuntime.jsx(
848
- "svg",
849
- {
850
- className: `legend-chevron ${isExpanded ? "expanded" : ""}`,
851
- width: "12",
852
- height: "12",
853
- viewBox: "0 0 12 12",
854
- fill: "none",
855
- xmlns: "http://www.w3.org/2000/svg",
856
- children: /* @__PURE__ */ jsxRuntime.jsx(
857
- "path",
858
- {
859
- d: "M3 4.5L6 7.5L9 4.5",
860
- stroke: "currentColor",
861
- strokeWidth: "1.5",
862
- strokeLinecap: "round",
863
- strokeLinejoin: "round"
864
- }
865
- )
866
- }
867
- )
868
- ]
869
- }
870
- ),
871
- isExpanded && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "legend-items", children: legendItems.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "legend-item", children: [
872
- /* @__PURE__ */ jsxRuntime.jsx(
873
- "div",
874
- {
875
- className: "legend-color",
876
- style: {
877
- backgroundColor: item.color,
878
- border: `2px solid ${item.color}`
879
- }
880
- }
881
- ),
882
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "legend-label", children: item.label })
883
- ] }, item.label)) })
884
- ] }) });
885
- }
886
- function getExcludedCount(runData, fromStageId, toStageId) {
887
- const fromStats = runData.stage_stats[fromStageId];
888
- const toStats = runData.stage_stats[toStageId];
889
- if (!fromStats || !toStats) return 0;
890
- return fromStats.continued_count - toStats.input_count;
891
- }
892
- function getCircledNumber(num) {
893
- const circledNumbers = ["\u2460", "\u2461", "\u2462", "\u2463", "\u2464", "\u2465", "\u2466", "\u2467", "\u2468", "\u2469"];
894
- return num <= 10 ? circledNumbers[num - 1] : `${num}`;
895
- }
896
- var VERTICAL_SPACING = 180;
897
- var HORIZONTAL_CENTER = 250;
898
- function FunnelVisualFlow({
899
- funnel,
900
- runData,
901
- onStageClick,
902
- onEdgeClick,
903
- className = "",
904
- height = 600
905
- }) {
906
- const nodeTypes = react.useMemo(
907
- () => ({
908
- stageNode: StageNode
909
- }),
910
- []
911
- );
912
- const initialNodes = react.useMemo(() => {
913
- return funnel.stages.map((stage, index) => {
914
- const stats = runData?.stage_stats?.[stage.id];
915
- return {
916
- id: stage.id,
917
- type: "stageNode",
918
- position: { x: HORIZONTAL_CENTER, y: index * VERTICAL_SPACING },
919
- data: {
920
- stage,
921
- stats,
922
- onStageClick
923
- }
924
- };
925
- });
926
- }, [funnel.stages, runData, onStageClick]);
927
- const initialEdges = react.useMemo(() => {
928
- if (funnel.stages.length < 2) return [];
929
- return funnel.stages.slice(0, -1).map((stage, index) => {
930
- const nextStage = funnel.stages[index + 1];
931
- const excludedCount = runData ? getExcludedCount(runData, stage.id, nextStage.id) : void 0;
932
- return {
933
- id: `${stage.id}-${nextStage.id}`,
934
- source: stage.id,
935
- target: nextStage.id,
936
- label: excludedCount !== void 0 ? `-${excludedCount}` : "",
937
- animated: true,
938
- style: { stroke: "#94a3b8", strokeWidth: 2 },
939
- labelStyle: { fill: "#ef4444", fontWeight: 600 },
940
- labelBgStyle: { fill: "#fef2f2", fillOpacity: 0.9 }
941
- };
942
- });
943
- }, [funnel.stages, runData]);
944
- const [nodes, , onNodesChange] = react$1.useNodesState(initialNodes);
945
- const [edges, , onEdgesChange] = react$1.useEdgesState(initialEdges);
946
- const handleEdgeClick = react.useCallback(
947
- (event, edge) => {
948
- if (onEdgeClick) {
949
- onEdgeClick(edge.source, edge.target);
950
- }
951
- },
952
- [onEdgeClick]
953
- );
954
- if (funnel.stages.length === 0) {
955
- return /* @__PURE__ */ jsxRuntime.jsx(
956
- "div",
957
- {
958
- className: `funnel-visual-flow-empty ${className}`,
959
- style: { height },
960
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "empty-state", children: [
961
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-500 text-sm", children: "No stages to visualize" }),
962
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-400 text-xs mt-1", children: "Add stages to see the funnel flow" })
963
- ] })
964
- }
965
- );
966
- }
967
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `funnel-visual-flow ${className}`, style: { height }, children: /* @__PURE__ */ jsxRuntime.jsxs(
968
- react$1.ReactFlow,
969
- {
970
- nodes,
971
- edges,
972
- onNodesChange,
973
- onEdgesChange,
974
- onEdgeClick: handleEdgeClick,
975
- nodeTypes,
976
- fitView: true,
977
- fitViewOptions: {
978
- padding: 0.2,
979
- includeHiddenNodes: false
980
- },
981
- minZoom: 0.5,
982
- maxZoom: 1.5,
983
- defaultViewport: { x: 0, y: 0, zoom: 1 },
984
- nodesDraggable: false,
985
- nodesConnectable: false,
986
- elementsSelectable: true,
987
- children: [
988
- /* @__PURE__ */ jsxRuntime.jsx(react$1.Background, { variant: react$1.BackgroundVariant.Dots, gap: 16, size: 1 }),
989
- /* @__PURE__ */ jsxRuntime.jsx(react$1.Controls, { showInteractive: false }),
990
- /* @__PURE__ */ jsxRuntime.jsx(FlowLegend, {})
991
- ]
992
- }
993
- ) });
994
- }
995
- function LogicToggle({ logic, onChange, className = "" }) {
996
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex items-center gap-4 ${className}`, children: [
997
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-700", children: "Logic:" }),
998
- /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [
999
- /* @__PURE__ */ jsxRuntime.jsx(
1000
- "input",
1001
- {
1002
- type: "radio",
1003
- name: "filter-logic",
1004
- value: "AND",
1005
- checked: logic === "AND",
1006
- onChange: (e) => onChange(e.target.value),
1007
- className: "w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500"
1008
- }
1009
- ),
1010
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm text-gray-700", children: [
1011
- "AND ",
1012
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-500", children: "(all must match)" })
1013
- ] })
1014
- ] }),
1015
- /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [
1016
- /* @__PURE__ */ jsxRuntime.jsx(
1017
- "input",
1018
- {
1019
- type: "radio",
1020
- name: "filter-logic",
1021
- value: "OR",
1022
- checked: logic === "OR",
1023
- onChange: (e) => onChange(e.target.value),
1024
- className: "w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500"
1025
- }
1026
- ),
1027
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm text-gray-700", children: [
1028
- "OR ",
1029
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-500", children: "(any can match)" })
1030
- ] })
1031
- ] })
1032
- ] });
1033
- }
1034
- function FieldSelector({
1035
- fields,
1036
- value,
1037
- onChange,
1038
- error,
1039
- className = ""
1040
- }) {
1041
- const grouped = fields.reduce((acc, field) => {
1042
- const category = field.category || "Other";
1043
- if (!acc[category]) {
1044
- acc[category] = [];
1045
- }
1046
- acc[category].push(field);
1047
- return acc;
1048
- }, {});
1049
- const categories = Object.keys(grouped).sort();
1050
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
1051
- /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "field-selector", className: "text-xs font-medium text-gray-700", children: "Field" }),
1052
- /* @__PURE__ */ jsxRuntime.jsxs(
1053
- "select",
1054
- {
1055
- id: "field-selector",
1056
- value,
1057
- onChange: (e) => onChange(e.target.value),
1058
- className: `
1059
- w-full px-3 py-2 text-sm border rounded-md
1060
- bg-white
1061
- focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
1062
- ${error ? "border-red-500" : "border-gray-300"}
1063
- `,
1064
- children: [
1065
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Select a field..." }),
1066
- categories.map((category) => /* @__PURE__ */ jsxRuntime.jsx("optgroup", { label: category, children: grouped[category].map((field) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: field.name, children: field.label }, field.name)) }, category))
1067
- ]
1068
- }
1069
- ),
1070
- error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
1071
- ] });
1072
- }
1073
-
1074
- // src/components/FilterRuleEditor/constants.ts
1075
- var OPERATOR_LABELS = {
1076
- // Equality
1077
- eq: "equals",
1078
- ne: "not equals",
1079
- // Comparison
1080
- gt: "greater than",
1081
- lt: "less than",
1082
- gte: "greater or equal",
1083
- lte: "less or equal",
1084
- // String operations
1085
- contains: "contains",
1086
- not_contains: "does not contain",
1087
- startswith: "starts with",
1088
- endswith: "ends with",
1089
- matches: "matches regex",
1090
- // Array/Set operations
1091
- in: "is one of",
1092
- not_in: "is not one of",
1093
- has_any: "has any of",
1094
- has_all: "has all of",
1095
- // Null checks
1096
- isnull: "is empty",
1097
- isnotnull: "is not empty",
1098
- // Tag operations
1099
- has_tag: "has tag",
1100
- not_has_tag: "does not have tag",
1101
- // Boolean
1102
- is_true: "is true",
1103
- is_false: "is false"
1104
- };
1105
- var NULL_VALUE_OPERATORS = [
1106
- "isnull",
1107
- "isnotnull",
1108
- "is_true",
1109
- "is_false"
1110
- ];
1111
- var MULTI_VALUE_OPERATORS = [
1112
- "in",
1113
- "not_in",
1114
- "has_any",
1115
- "has_all"
1116
- ];
1117
- function OperatorSelector({
1118
- operators,
1119
- value,
1120
- onChange,
1121
- disabled = false,
1122
- error,
1123
- className = ""
1124
- }) {
1125
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
1126
- /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "operator-selector", className: "text-xs font-medium text-gray-700", children: "Operator" }),
1127
- /* @__PURE__ */ jsxRuntime.jsxs(
1128
- "select",
1129
- {
1130
- id: "operator-selector",
1131
- value,
1132
- onChange: (e) => onChange(e.target.value),
1133
- disabled,
1134
- className: `
1135
- w-full px-3 py-2 text-sm border rounded-md
1136
- bg-white
1137
- focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
1138
- disabled:bg-gray-100 disabled:cursor-not-allowed disabled:text-gray-500
1139
- ${error ? "border-red-500" : "border-gray-300"}
1140
- `,
1141
- children: [
1142
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Select operator..." }),
1143
- operators.map((op) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: op, children: OPERATOR_LABELS[op] }, op))
1144
- ]
1145
- }
1146
- ),
1147
- error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
1148
- ] });
1149
- }
1150
- function TextValueInput({
1151
- value,
1152
- onChange,
1153
- placeholder = "Enter text...",
1154
- error,
1155
- className = ""
1156
- }) {
1157
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
1158
- /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "text-value", className: "text-xs font-medium text-gray-700", children: "Value" }),
1159
- /* @__PURE__ */ jsxRuntime.jsx(
1160
- "input",
1161
- {
1162
- id: "text-value",
1163
- type: "text",
1164
- value: value || "",
1165
- onChange: (e) => onChange(e.target.value),
1166
- placeholder,
1167
- className: `
1168
- w-full px-3 py-2 text-sm border rounded-md
1169
- focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
1170
- ${error ? "border-red-500" : "border-gray-300"}
1171
- `
1172
- }
1173
- ),
1174
- error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
1175
- ] });
1176
- }
1177
- function NumberValueInput({
1178
- value,
1179
- onChange,
1180
- min,
1181
- max,
1182
- placeholder = "Enter number...",
1183
- error,
1184
- className = ""
1185
- }) {
1186
- const handleChange = (e) => {
1187
- const val = e.target.value;
1188
- if (val === "") {
1189
- onChange(null);
1190
- } else {
1191
- const num = parseFloat(val);
1192
- if (!isNaN(num)) {
1193
- onChange(num);
1194
- }
1195
- }
1196
- };
1197
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
1198
- /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "number-value", className: "text-xs font-medium text-gray-700", children: "Value" }),
1199
- /* @__PURE__ */ jsxRuntime.jsx(
1200
- "input",
1201
- {
1202
- id: "number-value",
1203
- type: "number",
1204
- value: value ?? "",
1205
- onChange: handleChange,
1206
- min,
1207
- max,
1208
- placeholder,
1209
- className: `
1210
- w-full px-3 py-2 text-sm border rounded-md
1211
- focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
1212
- ${error ? "border-red-500" : "border-gray-300"}
1213
- `
1214
- }
1215
- ),
1216
- error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
1217
- ] });
1218
- }
1219
- function DateValueInput({
1220
- value,
1221
- onChange,
1222
- min,
1223
- max,
1224
- placeholder = "Select date...",
1225
- error,
1226
- className = ""
1227
- }) {
1228
- const handleChange = (e) => {
1229
- const val = e.target.value;
1230
- onChange(val || null);
1231
- };
1232
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
1233
- /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "date-value", className: "text-xs font-medium text-gray-700", children: "Value" }),
1234
- /* @__PURE__ */ jsxRuntime.jsx(
1235
- "input",
1236
- {
1237
- id: "date-value",
1238
- type: "date",
1239
- value: value || "",
1240
- onChange: handleChange,
1241
- min,
1242
- max,
1243
- placeholder,
1244
- className: `
1245
- w-full px-3 py-2 text-sm border rounded-md
1246
- focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
1247
- ${error ? "border-red-500" : "border-gray-300"}
1248
- `
1249
- }
1250
- ),
1251
- error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
1252
- ] });
1253
- }
1254
- function BooleanValueInput({
1255
- value,
1256
- onChange,
1257
- label = "True",
1258
- error,
1259
- className = ""
1260
- }) {
1261
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
1262
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-gray-700", children: "Value" }),
1263
- /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [
1264
- /* @__PURE__ */ jsxRuntime.jsx(
1265
- "input",
1266
- {
1267
- type: "checkbox",
1268
- checked: value,
1269
- onChange: (e) => onChange(e.target.checked),
1270
- className: "w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
1271
- }
1272
- ),
1273
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-700", children: label })
1274
- ] }),
1275
- error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
1276
- ] });
1277
- }
1278
- function ChoiceValueInput({
1279
- value,
1280
- onChange,
1281
- choices,
1282
- placeholder = "Select option...",
1283
- error,
1284
- className = ""
1285
- }) {
1286
- const getChoiceValue = (choice) => {
1287
- if (typeof choice === "string") return choice;
1288
- return choice.value || choice;
1289
- };
1290
- const getChoiceLabel = (choice) => {
1291
- if (typeof choice === "string") return choice;
1292
- return choice.label || choice.value || String(choice);
1293
- };
1294
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1 ${className}`, children: [
1295
- /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "choice-value", className: "text-xs font-medium text-gray-700", children: "Value" }),
1296
- /* @__PURE__ */ jsxRuntime.jsxs(
1297
- "select",
1298
- {
1299
- id: "choice-value",
1300
- value: value || "",
1301
- onChange: (e) => onChange(e.target.value),
1302
- className: `
1303
- w-full px-3 py-2 text-sm border rounded-md
1304
- bg-white
1305
- focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
1306
- ${error ? "border-red-500" : "border-gray-300"}
1307
- `,
1308
- children: [
1309
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: placeholder }),
1310
- choices.map((choice, index) => {
1311
- const val = getChoiceValue(choice);
1312
- const label = getChoiceLabel(choice);
1313
- return /* @__PURE__ */ jsxRuntime.jsx("option", { value: val, children: label }, `${val}-${index}`);
1314
- })
1315
- ]
1316
- }
1317
- ),
1318
- error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
1319
- ] });
1320
- }
1321
- function MultiChoiceValueInput({
1322
- value = [],
1323
- onChange,
1324
- choices,
1325
- placeholder = "Select options...",
1326
- error,
1327
- className = ""
1328
- }) {
1329
- const getChoiceValue = (choice) => {
1330
- if (typeof choice === "string") return choice;
1331
- return choice.value || choice;
1332
- };
1333
- const getChoiceLabel = (choice) => {
1334
- if (typeof choice === "string") return choice;
1335
- return choice.label || choice.value || String(choice);
1336
- };
1337
- const handleAdd = (newValue) => {
1338
- if (newValue && !value.includes(newValue)) {
1339
- onChange([...value, newValue]);
1340
- }
1341
- };
1342
- const handleRemove = (removeValue) => {
1343
- onChange(value.filter((v) => v !== removeValue));
1344
- };
1345
- const getValueLabel = (val) => {
1346
- const choice = choices.find((c) => getChoiceValue(c) === val);
1347
- return choice ? getChoiceLabel(choice) : val;
1348
- };
1349
- const availableChoices = choices.filter(
1350
- (choice) => !value.includes(getChoiceValue(choice))
1351
- );
1352
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-2 ${className}`, children: [
1353
- /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "multi-choice-value", className: "text-xs font-medium text-gray-700", children: "Values" }),
1354
- value.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1.5", children: value.map((val) => /* @__PURE__ */ jsxRuntime.jsxs(
1355
- "span",
1356
- {
1357
- className: "inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-blue-800 bg-blue-100 rounded",
1358
- children: [
1359
- getValueLabel(val),
1360
- /* @__PURE__ */ jsxRuntime.jsx(
1361
- "button",
1362
- {
1363
- type: "button",
1364
- onClick: () => handleRemove(val),
1365
- className: "hover:text-blue-900 focus:outline-none",
1366
- "aria-label": `Remove ${getValueLabel(val)}`,
1367
- children: "\xD7"
1368
- }
1369
- )
1370
- ]
1371
- },
1372
- val
1373
- )) }),
1374
- /* @__PURE__ */ jsxRuntime.jsxs(
1375
- "select",
1376
- {
1377
- id: "multi-choice-value",
1378
- value: "",
1379
- onChange: (e) => handleAdd(e.target.value),
1380
- className: `
1381
- w-full px-3 py-2 text-sm border rounded-md
1382
- bg-white
1383
- focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
1384
- ${error ? "border-red-500" : "border-gray-300"}
1385
- `,
1386
- children: [
1387
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: placeholder }),
1388
- availableChoices.map((choice, index) => {
1389
- const val = getChoiceValue(choice);
1390
- const label = getChoiceLabel(choice);
1391
- return /* @__PURE__ */ jsxRuntime.jsx("option", { value: val, children: label }, `${val}-${index}`);
1392
- })
1393
- ]
1394
- }
1395
- ),
1396
- error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-red-600", children: error })
1397
- ] });
1398
- }
1399
- function RuleRow({
1400
- rule,
1401
- onChange,
1402
- onRemove,
1403
- fieldRegistry,
1404
- className = ""
1405
- }) {
1406
- const selectedField = fieldRegistry.find((f) => f.name === rule.field_path);
1407
- const availableOperators = selectedField?.operators || [];
1408
- const needsValue = rule.operator && !NULL_VALUE_OPERATORS.includes(rule.operator);
1409
- const needsMultiValue = rule.operator && MULTI_VALUE_OPERATORS.includes(rule.operator);
1410
- const handleFieldChange = (fieldName) => {
1411
- const field = fieldRegistry.find((f) => f.name === fieldName);
1412
- onChange({
1413
- ...rule,
1414
- field_path: fieldName,
1415
- operator: field?.operators[0] || "",
1416
- value: null
1417
- });
1418
- };
1419
- const handleOperatorChange = (operator) => {
1420
- onChange({
1421
- ...rule,
1422
- operator,
1423
- value: MULTI_VALUE_OPERATORS.includes(operator) ? [] : null
1424
- });
1425
- };
1426
- const handleValueChange = (value) => {
1427
- onChange({
1428
- ...rule,
1429
- value
1430
- });
1431
- };
1432
- const renderValueInput = () => {
1433
- if (!needsValue) return null;
1434
- if (!selectedField) return null;
1435
- const { type, constraints } = selectedField;
1436
- if (needsMultiValue) {
1437
- if (constraints?.choices) {
1438
- return /* @__PURE__ */ jsxRuntime.jsx(
1439
- MultiChoiceValueInput,
1440
- {
1441
- value: Array.isArray(rule.value) ? rule.value : [],
1442
- onChange: handleValueChange,
1443
- choices: constraints.choices
1444
- }
1445
- );
1446
- }
1447
- return /* @__PURE__ */ jsxRuntime.jsx(
1448
- TextValueInput,
1449
- {
1450
- value: Array.isArray(rule.value) ? rule.value.join(", ") : "",
1451
- onChange: (val) => handleValueChange(val.split(",").map((v) => v.trim())),
1452
- placeholder: "Enter values, comma-separated..."
1453
- }
1454
- );
1455
- }
1456
- switch (type) {
1457
- case "string":
1458
- if (constraints?.choices && rule.operator === "eq") {
1459
- return /* @__PURE__ */ jsxRuntime.jsx(
1460
- ChoiceValueInput,
1461
- {
1462
- value: rule.value || "",
1463
- onChange: handleValueChange,
1464
- choices: constraints.choices
1465
- }
1466
- );
1467
- }
1468
- return /* @__PURE__ */ jsxRuntime.jsx(
1469
- TextValueInput,
1470
- {
1471
- value: rule.value || "",
1472
- onChange: handleValueChange
1473
- }
1474
- );
1475
- case "number":
1476
- return /* @__PURE__ */ jsxRuntime.jsx(
1477
- NumberValueInput,
1478
- {
1479
- value: rule.value,
1480
- onChange: handleValueChange,
1481
- min: constraints?.min_value,
1482
- max: constraints?.max_value
1483
- }
1484
- );
1485
- case "date":
1486
- return /* @__PURE__ */ jsxRuntime.jsx(
1487
- DateValueInput,
1488
- {
1489
- value: rule.value || null,
1490
- onChange: handleValueChange,
1491
- min: constraints?.min_value,
1492
- max: constraints?.max_value
1493
- }
1494
- );
1495
- case "boolean":
1496
- return /* @__PURE__ */ jsxRuntime.jsx(
1497
- BooleanValueInput,
1498
- {
1499
- value: rule.value || false,
1500
- onChange: handleValueChange
1501
- }
1502
- );
1503
- case "tag":
1504
- return /* @__PURE__ */ jsxRuntime.jsx(
1505
- TextValueInput,
1506
- {
1507
- value: rule.value || "",
1508
- onChange: handleValueChange,
1509
- placeholder: "Enter tag name..."
1510
- }
1511
- );
1512
- default:
1513
- return /* @__PURE__ */ jsxRuntime.jsx(
1514
- TextValueInput,
1515
- {
1516
- value: rule.value || "",
1517
- onChange: handleValueChange
1518
- }
1519
- );
1520
- }
1521
- };
1522
- return /* @__PURE__ */ jsxRuntime.jsxs(
1523
- "div",
1524
- {
1525
- className: `
1526
- relative group
1527
- border border-gray-200 rounded-lg p-4
1528
- bg-white hover:shadow-sm transition-shadow
1529
- ${className}
1530
- `,
1531
- children: [
1532
- /* @__PURE__ */ jsxRuntime.jsx(
1533
- "button",
1534
- {
1535
- type: "button",
1536
- onClick: onRemove,
1537
- className: "\n absolute top-2 right-2\n w-6 h-6 flex items-center justify-center\n text-gray-400 hover:text-red-600 hover:bg-red-50\n rounded transition-colors\n focus:outline-none focus:ring-2 focus:ring-red-500\n ",
1538
- "aria-label": "Remove rule",
1539
- children: "\xD7"
1540
- }
1541
- ),
1542
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 pr-8", children: [
1543
- /* @__PURE__ */ jsxRuntime.jsx(
1544
- FieldSelector,
1545
- {
1546
- fields: fieldRegistry,
1547
- value: rule.field_path,
1548
- onChange: handleFieldChange
1549
- }
1550
- ),
1551
- /* @__PURE__ */ jsxRuntime.jsx(
1552
- OperatorSelector,
1553
- {
1554
- operators: availableOperators,
1555
- value: rule.operator || "",
1556
- onChange: handleOperatorChange,
1557
- disabled: !rule.field_path
1558
- }
1559
- ),
1560
- needsValue && renderValueInput()
1561
- ] })
1562
- ]
1563
- }
1564
- );
1565
- }
1566
-
1567
- // src/types/index.ts
1568
- function validateFilterRule(rule) {
1569
- const errors = [];
1570
- if (!rule.field_path) {
1571
- errors.push("field_path is required");
1572
- }
1573
- if (!rule.operator) {
1574
- errors.push("operator is required");
1575
- }
1576
- const nullOps = ["isnull", "isnotnull", "is_true", "is_false"];
1577
- if (!nullOps.includes(rule.operator) && rule.value === void 0) {
1578
- errors.push(`value is required for operator '${rule.operator}'`);
1579
- }
1580
- return errors;
1581
- }
1582
- function FilterRuleEditor({
1583
- rules,
1584
- onChange,
1585
- fieldRegistry,
1586
- logic = "AND",
1587
- onLogicChange,
1588
- className = ""
1589
- }) {
1590
- const handleAddRule = () => {
1591
- const newRule = {
1592
- field_path: "",
1593
- operator: "eq",
1594
- value: null
1595
- };
1596
- onChange([...rules, newRule]);
1597
- };
1598
- const handleUpdateRule = (index, updatedRule) => {
1599
- const newRules = [...rules];
1600
- newRules[index] = updatedRule;
1601
- onChange(newRules);
1602
- };
1603
- const handleRemoveRule = (index) => {
1604
- const newRules = rules.filter((_, i) => i !== index);
1605
- onChange(newRules);
1606
- };
1607
- const ruleErrors = rules.map((rule) => validateFilterRule(rule));
1608
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-4 ${className}`, children: [
1609
- onLogicChange && /* @__PURE__ */ jsxRuntime.jsx(LogicToggle, { logic, onChange: onLogicChange }),
1610
- rules.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-12 border-2 border-dashed border-gray-300 rounded-lg bg-gray-50", children: [
1611
- /* @__PURE__ */ jsxRuntime.jsx(
1612
- "svg",
1613
- {
1614
- className: "w-12 h-12 text-gray-400 mb-3",
1615
- fill: "none",
1616
- viewBox: "0 0 24 24",
1617
- stroke: "currentColor",
1618
- children: /* @__PURE__ */ jsxRuntime.jsx(
1619
- "path",
1620
- {
1621
- strokeLinecap: "round",
1622
- strokeLinejoin: "round",
1623
- strokeWidth: 2,
1624
- d: "M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
1625
- }
1626
- )
1627
- }
1628
- ),
1629
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-600 mb-4", children: "No filter rules yet" }),
1630
- /* @__PURE__ */ jsxRuntime.jsxs(
1631
- "button",
1632
- {
1633
- type: "button",
1634
- onClick: handleAddRule,
1635
- className: "\n inline-flex items-center gap-2\n px-4 py-2\n text-sm font-medium text-white\n bg-blue-600 hover:bg-blue-700\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2\n transition-colors\n ",
1636
- children: [
1637
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-lg", children: "+" }),
1638
- "Add First Rule"
1639
- ]
1640
- }
1641
- )
1642
- ] }),
1643
- rules.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-3", children: rules.map((rule, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1644
- /* @__PURE__ */ jsxRuntime.jsx(
1645
- RuleRow,
1646
- {
1647
- rule,
1648
- onChange: (updatedRule) => handleUpdateRule(index, updatedRule),
1649
- onRemove: () => handleRemoveRule(index),
1650
- fieldRegistry
1651
- }
1652
- ),
1653
- index < rules.length - 1 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center py-2", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-3 py-1 text-xs font-semibold text-gray-700 bg-gray-100 border border-gray-300 rounded-full", children: logic }) }),
1654
- ruleErrors[index].length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 px-4 py-2 bg-red-50 border border-red-200 rounded text-sm text-red-700", children: ruleErrors[index].map((error, i) => /* @__PURE__ */ jsxRuntime.jsx("div", { children: error }, i)) })
1655
- ] }, index)) }),
1656
- rules.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
1657
- "button",
1658
- {
1659
- type: "button",
1660
- onClick: handleAddRule,
1661
- className: "\n w-full\n flex items-center justify-center gap-2\n px-4 py-3\n text-sm font-medium text-blue-600\n bg-white hover:bg-blue-50\n border-2 border-dashed border-blue-300 hover:border-blue-400\n rounded-lg\n focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2\n transition-colors\n ",
1662
- "aria-label": `Add rule ${rules.length + 1}`,
1663
- children: [
1664
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xl", children: "+" }),
1665
- "Add Rule"
1666
- ]
1667
- }
1668
- ),
1669
- rules.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-xs text-gray-500", children: [
1670
- /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
1671
- rules.length,
1672
- " ",
1673
- rules.length === 1 ? "rule" : "rules"
1674
- ] }),
1675
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: logic === "AND" ? "All rules must match" : "Any rule can match" })
1676
- ] })
1677
- ] });
1678
- }
1679
- var MATCH_ACTIONS = [
1680
- {
1681
- value: "continue",
1682
- label: "Continue",
1683
- description: "Continue to next stage without tagging"
1684
- },
1685
- {
1686
- value: "tag",
1687
- label: "Tag & Stop",
1688
- description: "Add tags and stop processing"
1689
- },
1690
- {
1691
- value: "tag_continue",
1692
- label: "Tag & Continue",
1693
- description: "Add tags and continue to next stage"
1694
- },
1695
- {
1696
- value: "output",
1697
- label: "Output",
1698
- description: "Add to output and stop processing"
1699
- }
1700
- ];
1701
- var NO_MATCH_ACTIONS = [
1702
- {
1703
- value: "continue",
1704
- label: "Continue",
1705
- description: "Continue to next stage"
1706
- },
1707
- {
1708
- value: "exclude",
1709
- label: "Exclude",
1710
- description: "Exclude from output and stop processing"
1711
- },
1712
- {
1713
- value: "tag_exclude",
1714
- label: "Tag & Exclude",
1715
- description: "Add tags, exclude from output, and stop"
1716
- }
1717
- ];
1718
- function StageActions({
1719
- stage,
1720
- onMatchActionChange,
1721
- onNoMatchActionChange
1722
- }) {
1723
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-actions", children: [
1724
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
1725
- /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: `match-action-${stage.id}`, className: "form-label", children: "Action on Match" }),
1726
- /* @__PURE__ */ jsxRuntime.jsx(
1727
- "select",
1728
- {
1729
- id: `match-action-${stage.id}`,
1730
- value: stage.match_action,
1731
- onChange: (e) => onMatchActionChange(e.target.value),
1732
- className: "form-select",
1733
- children: MATCH_ACTIONS.map((option) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: option.value, children: option.label }, option.value))
1734
- }
1735
- ),
1736
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "form-hint", children: MATCH_ACTIONS.find((a) => a.value === stage.match_action)?.description })
1737
- ] }),
1738
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
1739
- /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: `no-match-action-${stage.id}`, className: "form-label", children: "Action on No Match" }),
1740
- /* @__PURE__ */ jsxRuntime.jsx(
1741
- "select",
1742
- {
1743
- id: `no-match-action-${stage.id}`,
1744
- value: stage.no_match_action,
1745
- onChange: (e) => onNoMatchActionChange(e.target.value),
1746
- className: "form-select",
1747
- children: NO_MATCH_ACTIONS.map((option) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: option.value, children: option.label }, option.value))
1748
- }
1749
- ),
1750
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "form-hint", children: NO_MATCH_ACTIONS.find((a) => a.value === stage.no_match_action)?.description })
1751
- ] })
1752
- ] });
1753
- }
1754
- function TagInput({
1755
- tags,
1756
- onChange,
1757
- placeholder = "Add tag...",
1758
- className = ""
1759
- }) {
1760
- const [inputValue, setInputValue] = react.useState("");
1761
- const addTag = react.useCallback((tag) => {
1762
- const trimmed = tag.trim().toLowerCase();
1763
- if (!trimmed) {
1764
- return;
1765
- }
1766
- if (tags.includes(trimmed)) {
1767
- return;
1768
- }
1769
- onChange([...tags, trimmed]);
1770
- setInputValue("");
1771
- }, [tags, onChange]);
1772
- const removeTag = react.useCallback((index) => {
1773
- onChange(tags.filter((_, i) => i !== index));
1774
- }, [tags, onChange]);
1775
- const handleKeyDown = react.useCallback((e) => {
1776
- if (e.key === "Enter" || e.key === ",") {
1777
- e.preventDefault();
1778
- addTag(inputValue);
1779
- } else if (e.key === "Backspace" && !inputValue && tags.length > 0) {
1780
- removeTag(tags.length - 1);
1781
- }
1782
- }, [inputValue, tags, addTag, removeTag]);
1783
- const handleBlur = react.useCallback(() => {
1784
- if (inputValue) {
1785
- addTag(inputValue);
1786
- }
1787
- }, [inputValue, addTag]);
1788
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `tag-input ${className}`, children: [
1789
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "tag-input-container", children: [
1790
- tags.map((tag, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "tag-chip", children: [
1791
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tag-text", children: tag }),
1792
- /* @__PURE__ */ jsxRuntime.jsx(
1793
- "button",
1794
- {
1795
- type: "button",
1796
- onClick: () => removeTag(index),
1797
- className: "tag-remove",
1798
- "aria-label": `Remove tag ${tag}`,
1799
- children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
1800
- "path",
1801
- {
1802
- d: "M4 4l6 6M10 4l-6 6",
1803
- stroke: "currentColor",
1804
- strokeWidth: "1.5",
1805
- strokeLinecap: "round"
1806
- }
1807
- ) })
1808
- }
1809
- )
1810
- ] }, index)),
1811
- /* @__PURE__ */ jsxRuntime.jsx(
1812
- "input",
1813
- {
1814
- type: "text",
1815
- value: inputValue,
1816
- onChange: (e) => setInputValue(e.target.value),
1817
- onKeyDown: handleKeyDown,
1818
- onBlur: handleBlur,
1819
- placeholder: tags.length === 0 ? placeholder : "",
1820
- className: "tag-input-field"
1821
- }
1822
- )
1823
- ] }),
1824
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "tag-input-hint", children: "Press Enter or comma to add tags" })
1825
- ] });
1826
- }
1827
- function useDebounce(callback, delay) {
1828
- const [timeoutId, setTimeoutId] = react.useState(null);
1829
- return react.useCallback(
1830
- ((...args) => {
1831
- if (timeoutId) {
1832
- clearTimeout(timeoutId);
1833
- }
1834
- const newTimeoutId = setTimeout(() => {
1835
- callback(...args);
1836
- }, delay);
1837
- setTimeoutId(newTimeoutId);
1838
- }),
1839
- [callback, delay, timeoutId]
1840
- );
1841
- }
1842
- function StageForm({
1843
- stage,
1844
- onUpdate,
1845
- fieldRegistry
1846
- }) {
1847
- const [name, setName] = react.useState(stage.name);
1848
- const [description, setDescription] = react.useState(stage.description || "");
1849
- const debouncedUpdateName = useDebounce((value) => {
1850
- onUpdate({ ...stage, name: value });
1851
- }, 300);
1852
- const debouncedUpdateDescription = useDebounce((value) => {
1853
- onUpdate({ ...stage, description: value });
1854
- }, 500);
1855
- const handleNameChange = react.useCallback((e) => {
1856
- const value = e.target.value;
1857
- setName(value);
1858
- debouncedUpdateName(value);
1859
- }, [debouncedUpdateName]);
1860
- const handleDescriptionChange = react.useCallback((e) => {
1861
- const value = e.target.value;
1862
- setDescription(value);
1863
- debouncedUpdateDescription(value);
1864
- }, [debouncedUpdateDescription]);
1865
- const handleFilterLogicChange = react.useCallback((logic) => {
1866
- onUpdate({ ...stage, filter_logic: logic });
1867
- }, [stage, onUpdate]);
1868
- const handleMatchActionChange = react.useCallback((action) => {
1869
- onUpdate({ ...stage, match_action: action });
1870
- }, [stage, onUpdate]);
1871
- const handleNoMatchActionChange = react.useCallback((action) => {
1872
- onUpdate({ ...stage, no_match_action: action });
1873
- }, [stage, onUpdate]);
1874
- const handleMatchTagsChange = react.useCallback((tags) => {
1875
- onUpdate({ ...stage, match_tags: tags });
1876
- }, [stage, onUpdate]);
1877
- const handleNoMatchTagsChange = react.useCallback((tags) => {
1878
- onUpdate({ ...stage, no_match_tags: tags });
1879
- }, [stage, onUpdate]);
1880
- const handleRulesChange = react.useCallback((rules) => {
1881
- onUpdate({ ...stage, rules });
1882
- }, [stage, onUpdate]);
1883
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-form", children: [
1884
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
1885
- /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: `stage-name-${stage.id}`, className: "form-label", children: "Stage Name" }),
1886
- /* @__PURE__ */ jsxRuntime.jsx(
1887
- "input",
1888
- {
1889
- id: `stage-name-${stage.id}`,
1890
- type: "text",
1891
- value: name,
1892
- onChange: handleNameChange,
1893
- className: "form-input",
1894
- placeholder: "e.g., High ICP Score",
1895
- required: true
1896
- }
1897
- )
1898
- ] }),
1899
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
1900
- /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: `stage-desc-${stage.id}`, className: "form-label", children: "Description" }),
1901
- /* @__PURE__ */ jsxRuntime.jsx(
1902
- "textarea",
1903
- {
1904
- id: `stage-desc-${stage.id}`,
1905
- value: description,
1906
- onChange: handleDescriptionChange,
1907
- className: "form-textarea",
1908
- placeholder: "Describe the purpose of this stage...",
1909
- rows: 3
1910
- }
1911
- )
1912
- ] }),
1913
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
1914
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "form-label", children: "Filter Logic" }),
1915
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "filter-logic-toggle", children: [
1916
- /* @__PURE__ */ jsxRuntime.jsxs(
1917
- "button",
1918
- {
1919
- type: "button",
1920
- onClick: () => handleFilterLogicChange("AND"),
1921
- className: `toggle-button ${stage.filter_logic === "AND" ? "active" : ""}`,
1922
- children: [
1923
- /* @__PURE__ */ jsxRuntime.jsx(
1924
- "input",
1925
- {
1926
- type: "radio",
1927
- name: `filter-logic-${stage.id}`,
1928
- value: "AND",
1929
- checked: stage.filter_logic === "AND",
1930
- onChange: () => handleFilterLogicChange("AND"),
1931
- className: "sr-only"
1932
- }
1933
- ),
1934
- "AND"
1935
- ]
1936
- }
1937
- ),
1938
- /* @__PURE__ */ jsxRuntime.jsxs(
1939
- "button",
1940
- {
1941
- type: "button",
1942
- onClick: () => handleFilterLogicChange("OR"),
1943
- className: `toggle-button ${stage.filter_logic === "OR" ? "active" : ""}`,
1944
- children: [
1945
- /* @__PURE__ */ jsxRuntime.jsx(
1946
- "input",
1947
- {
1948
- type: "radio",
1949
- name: `filter-logic-${stage.id}`,
1950
- value: "OR",
1951
- checked: stage.filter_logic === "OR",
1952
- onChange: () => handleFilterLogicChange("OR"),
1953
- className: "sr-only"
1954
- }
1955
- ),
1956
- "OR"
1957
- ]
1958
- }
1959
- )
1960
- ] }),
1961
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "form-hint", children: stage.filter_logic === "AND" ? "All rules must match for this stage to pass" : "At least one rule must match for this stage to pass" })
1962
- ] }),
1963
- /* @__PURE__ */ jsxRuntime.jsx(
1964
- StageActions,
1965
- {
1966
- stage,
1967
- onMatchActionChange: handleMatchActionChange,
1968
- onNoMatchActionChange: handleNoMatchActionChange
1969
- }
1970
- ),
1971
- (stage.match_action === "tag" || stage.match_action === "tag_continue") && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
1972
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "form-label", children: "Tags on Match" }),
1973
- /* @__PURE__ */ jsxRuntime.jsx(
1974
- TagInput,
1975
- {
1976
- tags: stage.match_tags || [],
1977
- onChange: handleMatchTagsChange,
1978
- placeholder: "Add tag..."
1979
- }
1980
- ),
1981
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "form-hint", children: "Tags to add when rules match" })
1982
- ] }),
1983
- stage.no_match_action === "tag_exclude" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
1984
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "form-label", children: "Tags on No Match" }),
1985
- /* @__PURE__ */ jsxRuntime.jsx(
1986
- TagInput,
1987
- {
1988
- tags: stage.no_match_tags || [],
1989
- onChange: handleNoMatchTagsChange,
1990
- placeholder: "Add tag..."
1991
- }
1992
- ),
1993
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "form-hint", children: "Tags to add when rules don't match" })
1994
- ] }),
1995
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "form-group", children: [
1996
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rules-header", children: /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "form-label", children: [
1997
- "Filter Rules (",
1998
- stage.rules.length,
1999
- ")"
2000
- ] }) }),
2001
- /* @__PURE__ */ jsxRuntime.jsx(
2002
- FilterRuleEditor,
2003
- {
2004
- rules: stage.rules,
2005
- onChange: handleRulesChange,
2006
- fieldRegistry
2007
- }
2008
- )
2009
- ] })
2010
- ] });
2011
- }
2012
- function StageCard({
2013
- stage,
2014
- expanded,
2015
- onToggleExpanded,
2016
- onUpdate,
2017
- onRemove,
2018
- fieldRegistry,
2019
- error,
2020
- showWarnings = false
2021
- }) {
2022
- const {
2023
- attributes,
2024
- listeners,
2025
- setNodeRef,
2026
- transform,
2027
- transition,
2028
- isDragging
2029
- } = sortable.useSortable({ id: stage.id });
2030
- const style = {
2031
- transform: utilities.CSS.Transform.toString(transform),
2032
- transition,
2033
- opacity: isDragging ? 0.5 : 1
2034
- };
2035
- return /* @__PURE__ */ jsxRuntime.jsxs(
2036
- "div",
2037
- {
2038
- ref: setNodeRef,
2039
- style,
2040
- className: `stage-card ${isDragging ? "dragging" : ""} ${error ? "error" : ""}`,
2041
- children: [
2042
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-header", children: [
2043
- /* @__PURE__ */ jsxRuntime.jsx(
2044
- "button",
2045
- {
2046
- ...attributes,
2047
- ...listeners,
2048
- className: "drag-handle",
2049
- "aria-label": "Drag to reorder",
2050
- children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
2051
- "path",
2052
- {
2053
- d: "M7 4h6M7 10h6M7 16h6",
2054
- stroke: "currentColor",
2055
- strokeWidth: "2",
2056
- strokeLinecap: "round"
2057
- }
2058
- ) })
2059
- }
2060
- ),
2061
- /* @__PURE__ */ jsxRuntime.jsxs(
2062
- "button",
2063
- {
2064
- onClick: onToggleExpanded,
2065
- className: "stage-title-button",
2066
- "aria-expanded": expanded,
2067
- children: [
2068
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "stage-number", children: [
2069
- "Stage ",
2070
- stage.order + 1,
2071
- ":"
2072
- ] }),
2073
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "stage-name", children: stage.name || "Untitled Stage" }),
2074
- /* @__PURE__ */ jsxRuntime.jsx(
2075
- "svg",
2076
- {
2077
- width: "20",
2078
- height: "20",
2079
- viewBox: "0 0 20 20",
2080
- fill: "none",
2081
- className: `expand-icon ${expanded ? "expanded" : ""}`,
2082
- children: /* @__PURE__ */ jsxRuntime.jsx(
2083
- "path",
2084
- {
2085
- d: "M6 8l4 4 4-4",
2086
- stroke: "currentColor",
2087
- strokeWidth: "2",
2088
- strokeLinecap: "round",
2089
- strokeLinejoin: "round"
2090
- }
2091
- )
2092
- }
2093
- )
2094
- ]
2095
- }
2096
- ),
2097
- /* @__PURE__ */ jsxRuntime.jsx(
2098
- "button",
2099
- {
2100
- onClick: onRemove,
2101
- className: "delete-button",
2102
- "aria-label": "Delete stage",
2103
- children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
2104
- "path",
2105
- {
2106
- d: "M6 6l8 8M14 6l-8 8",
2107
- stroke: "currentColor",
2108
- strokeWidth: "2",
2109
- strokeLinecap: "round"
2110
- }
2111
- ) })
2112
- }
2113
- )
2114
- ] }),
2115
- error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "error-message", children: [
2116
- /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", children: [
2117
- /* @__PURE__ */ jsxRuntime.jsx(
2118
- "path",
2119
- {
2120
- d: "M8 1l7 13H1L8 1z",
2121
- stroke: "currentColor",
2122
- strokeWidth: "2",
2123
- strokeLinejoin: "round"
2124
- }
2125
- ),
2126
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 6v3M8 11h.01", stroke: "currentColor", strokeWidth: "2" })
2127
- ] }),
2128
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: error })
2129
- ] }),
2130
- showWarnings && !error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "warning-message", children: [
2131
- /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", children: [
2132
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "8", cy: "8", r: "7", stroke: "currentColor", strokeWidth: "2" }),
2133
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 5v3M8 10h.01", stroke: "currentColor", strokeWidth: "2" })
2134
- ] }),
2135
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Stage has no filter rules" })
2136
- ] }),
2137
- !expanded && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-summary", children: [
2138
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "summary-item", children: [
2139
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-label", children: "Rules:" }),
2140
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-value", children: stage.rules.length })
2141
- ] }),
2142
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "summary-item", children: [
2143
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-label", children: "Logic:" }),
2144
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-value", children: stage.filter_logic })
2145
- ] }),
2146
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "summary-item", children: [
2147
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-label", children: "On Match:" }),
2148
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-value", children: stage.match_action })
2149
- ] }),
2150
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "summary-item", children: [
2151
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-label", children: "On No Match:" }),
2152
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "summary-value", children: stage.no_match_action })
2153
- ] })
2154
- ] }),
2155
- expanded && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "stage-form-wrapper", children: /* @__PURE__ */ jsxRuntime.jsx(
2156
- StageForm,
2157
- {
2158
- stage,
2159
- onUpdate,
2160
- fieldRegistry
2161
- }
2162
- ) })
2163
- ]
2164
- }
2165
- );
2166
- }
2167
- function AddStageButton({
2168
- onClick,
2169
- position,
2170
- className = ""
2171
- }) {
2172
- return /* @__PURE__ */ jsxRuntime.jsxs(
2173
- "button",
2174
- {
2175
- type: "button",
2176
- onClick,
2177
- className: `add-stage-button ${position} ${className}`,
2178
- "aria-label": `Add stage ${position === "top" ? "at top" : position === "bottom" ? "at bottom" : "below"}`,
2179
- children: [
2180
- /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
2181
- "path",
2182
- {
2183
- d: "M10 5v10M5 10h10",
2184
- stroke: "currentColor",
2185
- strokeWidth: "2",
2186
- strokeLinecap: "round"
2187
- }
2188
- ) }),
2189
- /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
2190
- position === "top" && "Add Stage",
2191
- position === "bottom" && "Add Stage Below",
2192
- position === "inline" && "Add Stage"
2193
- ] })
2194
- ]
2195
- }
2196
- );
2197
- }
2198
- function generateStageId() {
2199
- return `stage-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
2200
- }
2201
- function createEmptyStage(order) {
2202
- return {
2203
- id: generateStageId(),
2204
- order,
2205
- name: `Stage ${order + 1}`,
2206
- description: "",
2207
- filter_logic: "AND",
2208
- rules: [],
2209
- match_action: "continue",
2210
- no_match_action: "continue",
2211
- match_tags: [],
2212
- no_match_tags: []
2213
- };
2214
- }
2215
- function validateStageName(name, stages, currentStageId) {
2216
- const trimmedName = name.trim();
2217
- if (!trimmedName) {
2218
- return "Stage name is required";
2219
- }
2220
- const duplicate = stages.find(
2221
- (s) => s.id !== currentStageId && s.name.trim().toLowerCase() === trimmedName.toLowerCase()
2222
- );
2223
- if (duplicate) {
2224
- return "Stage name must be unique";
2225
- }
2226
- return null;
2227
- }
2228
- function FunnelStageBuilder({
2229
- funnel,
2230
- onUpdate,
2231
- fieldRegistry,
2232
- className = ""
2233
- }) {
2234
- const [expandedStages, setExpandedStages] = react.useState(
2235
- new Set(funnel.stages.map((s) => s.id))
2236
- );
2237
- const [errors, setErrors] = react.useState(/* @__PURE__ */ new Map());
2238
- const sensors = core.useSensors(
2239
- core.useSensor(core.PointerSensor),
2240
- core.useSensor(core.KeyboardSensor, {
2241
- coordinateGetter: sortable.sortableKeyboardCoordinates
2242
- })
2243
- );
2244
- const toggleExpanded = react.useCallback((stageId) => {
2245
- setExpandedStages((prev) => {
2246
- const next = new Set(prev);
2247
- if (next.has(stageId)) {
2248
- next.delete(stageId);
2249
- } else {
2250
- next.add(stageId);
2251
- }
2252
- return next;
2253
- });
2254
- }, []);
2255
- const handleAddStage = react.useCallback((insertAfterIndex) => {
2256
- const newOrder = insertAfterIndex !== void 0 ? insertAfterIndex + 1 : funnel.stages.length;
2257
- const newStage = createEmptyStage(newOrder);
2258
- const updatedStages = funnel.stages.map((stage) => {
2259
- if (stage.order >= newOrder) {
2260
- return { ...stage, order: stage.order + 1 };
2261
- }
2262
- return stage;
2263
- });
2264
- updatedStages.splice(newOrder, 0, newStage);
2265
- setExpandedStages((prev) => new Set(prev).add(newStage.id));
2266
- onUpdate({
2267
- ...funnel,
2268
- stages: updatedStages
2269
- });
2270
- }, [funnel, onUpdate]);
2271
- const handleRemoveStage = react.useCallback((stageId) => {
2272
- const stageIndex = funnel.stages.findIndex((s) => s.id === stageId);
2273
- if (stageIndex === -1) return;
2274
- const updatedStages = funnel.stages.filter((s) => s.id !== stageId);
2275
- updatedStages.forEach((stage, index) => {
2276
- stage.order = index;
2277
- });
2278
- setExpandedStages((prev) => {
2279
- const next = new Set(prev);
2280
- next.delete(stageId);
2281
- return next;
2282
- });
2283
- setErrors((prev) => {
2284
- const next = new Map(prev);
2285
- next.delete(stageId);
2286
- return next;
2287
- });
2288
- onUpdate({
2289
- ...funnel,
2290
- stages: updatedStages
2291
- });
2292
- }, [funnel, onUpdate]);
2293
- const handleUpdateStage = react.useCallback((updatedStage) => {
2294
- const nameError = validateStageName(updatedStage.name, funnel.stages, updatedStage.id);
2295
- setErrors((prev) => {
2296
- const next = new Map(prev);
2297
- if (nameError) {
2298
- next.set(updatedStage.id, nameError);
2299
- } else {
2300
- next.delete(updatedStage.id);
2301
- }
2302
- return next;
2303
- });
2304
- const updatedStages = funnel.stages.map(
2305
- (stage) => stage.id === updatedStage.id ? updatedStage : stage
2306
- );
2307
- onUpdate({
2308
- ...funnel,
2309
- stages: updatedStages
2310
- });
2311
- }, [funnel, onUpdate]);
2312
- const handleDragEnd = react.useCallback((event) => {
2313
- const { active, over } = event;
2314
- if (!over || active.id === over.id) {
2315
- return;
2316
- }
2317
- const oldIndex = funnel.stages.findIndex((s) => s.id === active.id);
2318
- const newIndex = funnel.stages.findIndex((s) => s.id === over.id);
2319
- if (oldIndex === -1 || newIndex === -1) {
2320
- return;
2321
- }
2322
- const reorderedStages = sortable.arrayMove(funnel.stages, oldIndex, newIndex);
2323
- reorderedStages.forEach((stage, index) => {
2324
- stage.order = index;
2325
- });
2326
- onUpdate({
2327
- ...funnel,
2328
- stages: reorderedStages
2329
- });
2330
- }, [funnel, onUpdate]);
2331
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `funnel-stage-builder ${className}`, children: [
2332
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-4", children: /* @__PURE__ */ jsxRuntime.jsx(AddStageButton, { onClick: () => handleAddStage(), position: "top" }) }),
2333
- /* @__PURE__ */ jsxRuntime.jsx(
2334
- core.DndContext,
2335
- {
2336
- sensors,
2337
- collisionDetection: core.closestCenter,
2338
- onDragEnd: handleDragEnd,
2339
- children: /* @__PURE__ */ jsxRuntime.jsx(
2340
- sortable.SortableContext,
2341
- {
2342
- items: funnel.stages.map((s) => s.id),
2343
- strategy: sortable.verticalListSortingStrategy,
2344
- children: funnel.stages.map((stage, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stage-wrapper", children: [
2345
- /* @__PURE__ */ jsxRuntime.jsx(
2346
- StageCard,
2347
- {
2348
- stage,
2349
- expanded: expandedStages.has(stage.id),
2350
- onToggleExpanded: () => toggleExpanded(stage.id),
2351
- onUpdate: handleUpdateStage,
2352
- onRemove: () => handleRemoveStage(stage.id),
2353
- fieldRegistry,
2354
- error: errors.get(stage.id),
2355
- showWarnings: stage.rules.length === 0
2356
- }
2357
- ),
2358
- index < funnel.stages.length - 1 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "stage-arrow", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
2359
- "path",
2360
- {
2361
- d: "M12 5v14m0 0l-4-4m4 4l4-4",
2362
- stroke: "currentColor",
2363
- strokeWidth: "2",
2364
- strokeLinecap: "round",
2365
- strokeLinejoin: "round"
2366
- }
2367
- ) }) })
2368
- ] }, stage.id))
2369
- }
2370
- )
2371
- }
2372
- ),
2373
- funnel.stages.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-4", children: /* @__PURE__ */ jsxRuntime.jsx(
2374
- AddStageButton,
2375
- {
2376
- onClick: () => handleAddStage(funnel.stages.length - 1),
2377
- position: "bottom"
2378
- }
2379
- ) }),
2380
- funnel.stages.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "empty-state", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-500 text-center py-8", children: "No stages yet. Add your first stage to get started." }) })
2381
- ] });
2382
- }
2383
- function RunFilters({
2384
- filters,
2385
- onFiltersChange,
2386
- className = ""
2387
- }) {
2388
- const updateFilter = (key, value) => {
2389
- onFiltersChange({ ...filters, [key]: value });
2390
- };
2391
- const clearFilters = () => {
2392
- onFiltersChange({
2393
- status: "all",
2394
- trigger_type: "all",
2395
- date_range: "month"
2396
- });
2397
- };
2398
- const hasActiveFilters = filters.status !== "all" || filters.trigger_type !== "all" || filters.date_range !== "month";
2399
- return /* @__PURE__ */ jsxRuntime.jsxs(
2400
- "div",
2401
- {
2402
- className: `flex items-center gap-3 p-3 bg-gray-50 border-b border-gray-200 ${className}`,
2403
- children: [
2404
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
2405
- /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "status-filter", className: "text-sm font-medium text-gray-700", children: "Status:" }),
2406
- /* @__PURE__ */ jsxRuntime.jsxs(
2407
- "select",
2408
- {
2409
- id: "status-filter",
2410
- value: filters.status || "all",
2411
- onChange: (e) => updateFilter("status", e.target.value),
2412
- className: "px-2 py-1 text-sm border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500",
2413
- children: [
2414
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All" }),
2415
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "completed", children: "Complete" }),
2416
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "running", children: "Running" }),
2417
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "failed", children: "Failed" }),
2418
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "pending", children: "Pending" }),
2419
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "cancelled", children: "Cancelled" })
2420
- ]
2421
- }
2422
- )
2423
- ] }),
2424
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
2425
- /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "trigger-filter", className: "text-sm font-medium text-gray-700", children: "Trigger:" }),
2426
- /* @__PURE__ */ jsxRuntime.jsxs(
2427
- "select",
2428
- {
2429
- id: "trigger-filter",
2430
- value: filters.trigger_type || "all",
2431
- onChange: (e) => updateFilter("trigger_type", e.target.value),
2432
- className: "px-2 py-1 text-sm border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500",
2433
- children: [
2434
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All" }),
2435
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "manual", children: "Manual" }),
2436
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "scheduled", children: "Scheduled" }),
2437
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "webhook", children: "Webhook" }),
2438
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "api", children: "API" })
2439
- ]
2440
- }
2441
- )
2442
- ] }),
2443
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
2444
- /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "date-filter", className: "text-sm font-medium text-gray-700", children: "Date:" }),
2445
- /* @__PURE__ */ jsxRuntime.jsxs(
2446
- "select",
2447
- {
2448
- id: "date-filter",
2449
- value: filters.date_range || "month",
2450
- onChange: (e) => updateFilter("date_range", e.target.value),
2451
- className: "px-2 py-1 text-sm border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500",
2452
- children: [
2453
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All time" }),
2454
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "today", children: "Today" }),
2455
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "week", children: "Last 7 days" }),
2456
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "month", children: "Last 30 days" })
2457
- ]
2458
- }
2459
- )
2460
- ] }),
2461
- hasActiveFilters && /* @__PURE__ */ jsxRuntime.jsx(
2462
- "button",
2463
- {
2464
- onClick: clearFilters,
2465
- className: "ml-auto px-3 py-1 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500",
2466
- children: "Clear filters"
2467
- }
2468
- )
2469
- ]
2470
- }
2471
- );
2472
- }
2473
- var statusConfig2 = {
2474
- completed: {
2475
- icon: "\u2713",
2476
- label: "Complete",
2477
- color: "text-green-800",
2478
- bgColor: "bg-green-100"
2479
- },
2480
- running: {
2481
- icon: "\u23F8",
2482
- label: "Running",
2483
- color: "text-blue-800",
2484
- bgColor: "bg-blue-100",
2485
- spinning: true
2486
- },
2487
- failed: {
2488
- icon: "\u2717",
2489
- label: "Failed",
2490
- color: "text-red-800",
2491
- bgColor: "bg-red-100"
2492
- },
2493
- pending: {
2494
- icon: "\u25CB",
2495
- label: "Pending",
2496
- color: "text-yellow-800",
2497
- bgColor: "bg-yellow-100"
2498
- },
2499
- cancelled: {
2500
- icon: "\xD7",
2501
- label: "Cancelled",
2502
- color: "text-gray-800",
2503
- bgColor: "bg-gray-100"
2504
- }
2505
- };
2506
- function RunStatusBadge({ status, className = "" }) {
2507
- const config = statusConfig2[status];
2508
- return /* @__PURE__ */ jsxRuntime.jsxs(
2509
- "span",
2510
- {
2511
- className: `inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium ${config.bgColor} ${config.color} ${className}`,
2512
- children: [
2513
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: config.spinning ? "animate-spin" : "", "aria-hidden": "true", children: config.icon }),
2514
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: config.label })
2515
- ]
2516
- }
2517
- );
2518
- }
2519
- function RunActions({
2520
- run,
2521
- onViewDetails,
2522
- onViewResults,
2523
- onReRun,
2524
- onCancel,
2525
- className = ""
2526
- }) {
2527
- const [isOpen, setIsOpen] = react.useState(false);
2528
- const dropdownRef = react.useRef(null);
2529
- react.useEffect(() => {
2530
- const handleClickOutside = (event) => {
2531
- if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
2532
- setIsOpen(false);
2533
- }
2534
- };
2535
- if (isOpen) {
2536
- document.addEventListener("mousedown", handleClickOutside);
2537
- return () => document.removeEventListener("mousedown", handleClickOutside);
2538
- }
2539
- }, [isOpen]);
2540
- react.useEffect(() => {
2541
- const handleEscape = (event) => {
2542
- if (event.key === "Escape" && isOpen) {
2543
- setIsOpen(false);
2544
- }
2545
- };
2546
- if (isOpen) {
2547
- document.addEventListener("keydown", handleEscape);
2548
- return () => document.removeEventListener("keydown", handleEscape);
2549
- }
2550
- }, [isOpen]);
2551
- const canCancel = run.status === "pending" || run.status === "running";
2552
- const canViewResults = run.status === "completed";
2553
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `relative ${className}`, ref: dropdownRef, children: [
2554
- /* @__PURE__ */ jsxRuntime.jsx(
2555
- "button",
2556
- {
2557
- onClick: () => setIsOpen(!isOpen),
2558
- className: "p-1 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded focus:outline-none focus:ring-2 focus:ring-blue-500",
2559
- "aria-label": "Run actions",
2560
- "aria-haspopup": "true",
2561
- "aria-expanded": isOpen,
2562
- children: /* @__PURE__ */ jsxRuntime.jsx(
2563
- "svg",
2564
- {
2565
- className: "w-5 h-5",
2566
- fill: "none",
2567
- stroke: "currentColor",
2568
- viewBox: "0 0 24 24",
2569
- children: /* @__PURE__ */ jsxRuntime.jsx(
2570
- "path",
2571
- {
2572
- strokeLinecap: "round",
2573
- strokeLinejoin: "round",
2574
- strokeWidth: 2,
2575
- d: "M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"
2576
- }
2577
- )
2578
- }
2579
- )
2580
- }
2581
- ),
2582
- isOpen && /* @__PURE__ */ jsxRuntime.jsx(
2583
- "div",
2584
- {
2585
- className: "absolute right-0 mt-1 w-48 bg-white rounded-md shadow-lg border border-gray-200 z-10",
2586
- role: "menu",
2587
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "py-1", children: [
2588
- /* @__PURE__ */ jsxRuntime.jsxs(
2589
- "button",
2590
- {
2591
- onClick: () => {
2592
- onViewDetails(run);
2593
- setIsOpen(false);
2594
- },
2595
- className: "w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2",
2596
- role: "menuitem",
2597
- children: [
2598
- /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u{1F441}" }),
2599
- "View Details"
2600
- ]
2601
- }
2602
- ),
2603
- /* @__PURE__ */ jsxRuntime.jsxs(
2604
- "button",
2605
- {
2606
- onClick: () => {
2607
- onViewResults(run);
2608
- setIsOpen(false);
2609
- },
2610
- disabled: !canViewResults,
2611
- className: "w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white",
2612
- role: "menuitem",
2613
- children: [
2614
- /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u{1F4CA}" }),
2615
- "View Results"
2616
- ]
2617
- }
2618
- ),
2619
- /* @__PURE__ */ jsxRuntime.jsxs(
2620
- "button",
2621
- {
2622
- onClick: () => {
2623
- onReRun(run);
2624
- setIsOpen(false);
2625
- },
2626
- className: "w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2",
2627
- role: "menuitem",
2628
- children: [
2629
- /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u21BB" }),
2630
- "Re-run"
2631
- ]
2632
- }
2633
- ),
2634
- canCancel && onCancel && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2635
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t border-gray-200 my-1" }),
2636
- /* @__PURE__ */ jsxRuntime.jsxs(
2637
- "button",
2638
- {
2639
- onClick: () => {
2640
- if (confirm("Are you sure you want to cancel this run?")) {
2641
- onCancel(run);
2642
- setIsOpen(false);
2643
- }
2644
- },
2645
- className: "w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-2",
2646
- role: "menuitem",
2647
- children: [
2648
- /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\xD7" }),
2649
- "Cancel Run"
2650
- ]
2651
- }
2652
- )
2653
- ] })
2654
- ] })
2655
- }
2656
- )
2657
- ] });
2658
- }
2659
-
2660
- // src/components/FunnelRunHistory/utils.ts
2661
- function formatDuration(ms) {
2662
- if (ms === void 0 || ms === null) return "-";
2663
- if (ms === 0) return "0ms";
2664
- const seconds = Math.floor(ms / 1e3);
2665
- const minutes = Math.floor(seconds / 60);
2666
- const hours = Math.floor(minutes / 60);
2667
- if (hours > 0) {
2668
- const remainingMinutes = minutes % 60;
2669
- return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
2670
- }
2671
- if (minutes > 0) {
2672
- const remainingSeconds = seconds % 60;
2673
- return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
2674
- }
2675
- if (seconds > 0) {
2676
- return `${seconds}s`;
2677
- }
2678
- return `${ms}ms`;
2679
- }
2680
- function formatRelativeTime(date) {
2681
- const now = /* @__PURE__ */ new Date();
2682
- const then = new Date(date);
2683
- const diffMs = now.getTime() - then.getTime();
2684
- const diffSeconds = Math.floor(diffMs / 1e3);
2685
- const diffMinutes = Math.floor(diffSeconds / 60);
2686
- const diffHours = Math.floor(diffMinutes / 60);
2687
- const diffDays = Math.floor(diffHours / 24);
2688
- if (diffDays > 0) {
2689
- return `${diffDays}d ago`;
2690
- }
2691
- if (diffHours > 0) {
2692
- return `${diffHours}h ago`;
2693
- }
2694
- if (diffMinutes > 0) {
2695
- return `${diffMinutes}m ago`;
2696
- }
2697
- return "Just now";
2698
- }
2699
- function calculateMatchRate(matched, total) {
2700
- if (total === 0) return 0;
2701
- return Math.round(matched / total * 100);
2702
- }
2703
- function formatNumber(num) {
2704
- return num.toLocaleString();
2705
- }
2706
- function formatFullTimestamp(date) {
2707
- const d = new Date(date);
2708
- return d.toLocaleString("en-US", {
2709
- year: "numeric",
2710
- month: "short",
2711
- day: "numeric",
2712
- hour: "2-digit",
2713
- minute: "2-digit",
2714
- second: "2-digit"
2715
- });
2716
- }
2717
- function RunRow({
2718
- run,
2719
- onViewDetails,
2720
- onViewResults,
2721
- onReRun,
2722
- onCancel
2723
- }) {
2724
- const matchRate = run.status === "completed" ? calculateMatchRate(run.total_matched, run.total_input) : null;
2725
- return /* @__PURE__ */ jsxRuntime.jsxs(
2726
- "tr",
2727
- {
2728
- onClick: () => onViewDetails(run),
2729
- onKeyDown: (e) => {
2730
- if (e.key === "Enter") {
2731
- onViewDetails(run);
2732
- }
2733
- },
2734
- tabIndex: 0,
2735
- className: "border-b border-gray-200 hover:bg-gray-50 cursor-pointer transition-colors focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500",
2736
- children: [
2737
- /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsx(
2738
- "span",
2739
- {
2740
- className: "text-sm text-gray-900",
2741
- title: formatFullTimestamp(run.started_at),
2742
- children: formatRelativeTime(run.started_at)
2743
- }
2744
- ) }),
2745
- /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsx(RunStatusBadge, { status: run.status }) }),
2746
- /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-700 capitalize", children: run.trigger_type }) }),
2747
- /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-900", children: formatDuration(run.duration_ms) }) }),
2748
- /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3 text-right", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-900", children: formatNumber(run.total_input) }) }),
2749
- /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3 text-right", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-green-600", children: run.status === "completed" ? formatNumber(run.total_matched) : "-" }) }),
2750
- /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3 text-right", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-900", children: matchRate !== null ? `${matchRate}%` : "-" }) }),
2751
- /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntime.jsx(
2752
- RunActions,
2753
- {
2754
- run,
2755
- onViewDetails,
2756
- onViewResults,
2757
- onReRun,
2758
- onCancel
2759
- }
2760
- ) })
2761
- ]
2762
- }
2763
- );
2764
- }
2765
- function StageBreakdownList({
2766
- stages,
2767
- className = ""
2768
- }) {
2769
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `space-y-3 ${className}`, children: stages.map((stage, index) => {
2770
- const delta = stage.matched_count - stage.input_count;
2771
- const matchRate = stage.input_count > 0 ? Math.round(stage.matched_count / stage.input_count * 100) : 0;
2772
- return /* @__PURE__ */ jsxRuntime.jsxs(
2773
- "div",
2774
- {
2775
- className: "p-3 bg-gray-50 rounded-lg border border-gray-200",
2776
- children: [
2777
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-2", children: [
2778
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex items-center justify-center w-6 h-6 text-xs font-bold text-white bg-blue-600 rounded-full", children: index + 1 }),
2779
- /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "text-sm font-semibold text-gray-900", children: stage.stage_name })
2780
- ] }),
2781
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-3 gap-2 text-center", children: [
2782
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2783
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Input" }),
2784
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-lg font-bold text-blue-600", children: formatNumber(stage.input_count) })
2785
- ] }),
2786
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2787
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Matched" }),
2788
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-lg font-bold text-green-600", children: formatNumber(stage.matched_count) })
2789
- ] }),
2790
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2791
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Rate" }),
2792
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-lg font-bold text-gray-700", children: [
2793
- matchRate,
2794
- "%"
2795
- ] })
2796
- ] })
2797
- ] }),
2798
- delta !== 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 pt-2 border-t border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-1 text-sm", children: [
2799
- /* @__PURE__ */ jsxRuntime.jsxs(
2800
- "span",
2801
- {
2802
- className: `font-medium ${delta > 0 ? "text-green-600" : "text-red-600"}`,
2803
- children: [
2804
- delta > 0 ? "\u25B2" : "\u25BC",
2805
- " ",
2806
- formatNumber(Math.abs(delta))
2807
- ]
2808
- }
2809
- ),
2810
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-500", children: delta > 0 ? "added" : "excluded" })
2811
- ] }) }),
2812
- stage.error_count && stage.error_count > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 pt-2 border-t border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm text-red-600 text-center", children: [
2813
- "\u26A0 ",
2814
- formatNumber(stage.error_count),
2815
- " errors"
2816
- ] }) })
2817
- ]
2818
- },
2819
- stage.stage_id
2820
- );
2821
- }) });
2822
- }
2823
- function RunDetailsModal({
2824
- run,
2825
- onClose,
2826
- onViewResults,
2827
- onReRun
2828
- }) {
2829
- const modalRef = react.useRef(null);
2830
- react.useEffect(() => {
2831
- const handleEscape = (e) => {
2832
- if (e.key === "Escape") {
2833
- onClose();
2834
- }
2835
- };
2836
- if (run) {
2837
- document.addEventListener("keydown", handleEscape);
2838
- document.body.style.overflow = "hidden";
2839
- return () => {
2840
- document.removeEventListener("keydown", handleEscape);
2841
- document.body.style.overflow = "unset";
2842
- };
2843
- }
2844
- }, [run, onClose]);
2845
- react.useEffect(() => {
2846
- if (run && modalRef.current) {
2847
- const focusableElements = modalRef.current.querySelectorAll(
2848
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
2849
- );
2850
- const firstElement = focusableElements[0];
2851
- const lastElement = focusableElements[focusableElements.length - 1];
2852
- firstElement?.focus();
2853
- const handleTab = (e) => {
2854
- if (e.key !== "Tab") return;
2855
- if (e.shiftKey && document.activeElement === firstElement) {
2856
- e.preventDefault();
2857
- lastElement?.focus();
2858
- } else if (!e.shiftKey && document.activeElement === lastElement) {
2859
- e.preventDefault();
2860
- firstElement?.focus();
2861
- }
2862
- };
2863
- document.addEventListener("keydown", handleTab);
2864
- return () => document.removeEventListener("keydown", handleTab);
2865
- }
2866
- }, [run]);
2867
- if (!run) return null;
2868
- const stageStatsArray = Object.values(run.stage_stats);
2869
- return /* @__PURE__ */ jsxRuntime.jsx(
2870
- "div",
2871
- {
2872
- className: "fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50",
2873
- onClick: onClose,
2874
- role: "dialog",
2875
- "aria-modal": "true",
2876
- "aria-labelledby": "modal-title",
2877
- children: /* @__PURE__ */ jsxRuntime.jsxs(
2878
- "div",
2879
- {
2880
- ref: modalRef,
2881
- onClick: (e) => e.stopPropagation(),
2882
- className: "bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col",
2883
- children: [
2884
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 border-b border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
2885
- /* @__PURE__ */ jsxRuntime.jsx("h2", { id: "modal-title", className: "text-lg font-semibold text-gray-900", children: "Run Details" }),
2886
- /* @__PURE__ */ jsxRuntime.jsx(
2887
- "button",
2888
- {
2889
- onClick: onClose,
2890
- className: "p-1 text-gray-400 hover:text-gray-600 rounded focus:outline-none focus:ring-2 focus:ring-blue-500",
2891
- "aria-label": "Close modal",
2892
- children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsxRuntime.jsx(
2893
- "path",
2894
- {
2895
- fillRule: "evenodd",
2896
- d: "M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z",
2897
- clipRule: "evenodd"
2898
- }
2899
- ) })
2900
- }
2901
- )
2902
- ] }) }),
2903
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 overflow-y-auto flex-1", children: [
2904
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-6", children: [
2905
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-4 mb-4", children: [
2906
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2907
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Status" }),
2908
- /* @__PURE__ */ jsxRuntime.jsx(RunStatusBadge, { status: run.status })
2909
- ] }),
2910
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2911
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Duration" }),
2912
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-medium text-gray-900", children: formatDuration(run.duration_ms) })
2913
- ] }),
2914
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2915
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Started" }),
2916
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-medium text-gray-900", children: /* @__PURE__ */ jsxRuntime.jsx("span", { title: formatFullTimestamp(run.started_at), children: formatRelativeTime(run.started_at) }) })
2917
- ] }),
2918
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2919
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "Triggered by" }),
2920
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm font-medium text-gray-900", children: [
2921
- run.triggered_by || "System",
2922
- " (",
2923
- run.trigger_type,
2924
- ")"
2925
- ] })
2926
- ] })
2927
- ] }),
2928
- run.status === "failed" && run.error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 bg-red-50 border border-red-200 rounded-lg", children: [
2929
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-medium text-red-800 mb-1", children: "Error" }),
2930
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-red-700", children: run.error })
2931
- ] })
2932
- ] }),
2933
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2934
- /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold text-gray-900 mb-3", children: "Stage Breakdown" }),
2935
- stageStatsArray.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(StageBreakdownList, { stages: stageStatsArray }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-gray-500 text-center py-4", children: "No stage data available" })
2936
- ] })
2937
- ] }),
2938
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 border-t border-gray-200 flex items-center justify-end gap-3", children: [
2939
- /* @__PURE__ */ jsxRuntime.jsx(
2940
- "button",
2941
- {
2942
- onClick: onClose,
2943
- className: "px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500",
2944
- children: "Close"
2945
- }
2946
- ),
2947
- onReRun && /* @__PURE__ */ jsxRuntime.jsx(
2948
- "button",
2949
- {
2950
- onClick: () => {
2951
- onReRun(run);
2952
- onClose();
2953
- },
2954
- className: "px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500",
2955
- children: "\u21BB Re-run"
2956
- }
2957
- ),
2958
- onViewResults && run.status === "completed" && /* @__PURE__ */ jsxRuntime.jsx(
2959
- "button",
2960
- {
2961
- onClick: () => {
2962
- onViewResults(run);
2963
- onClose();
2964
- },
2965
- className: "px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500",
2966
- children: "View Results"
2967
- }
2968
- )
2969
- ] })
2970
- ]
2971
- }
2972
- )
2973
- }
2974
- );
2975
- }
2976
- function FunnelRunHistory({
2977
- funnelId,
2978
- apiClient,
2979
- onViewResults,
2980
- className = ""
2981
- }) {
2982
- const [runs, setRuns] = react.useState([]);
2983
- const [isLoading, setIsLoading] = react.useState(true);
2984
- const [error, setError] = react.useState(null);
2985
- const [selectedRun, setSelectedRun] = react.useState(null);
2986
- const [filters, setFilters] = react.useState({
2987
- status: "all",
2988
- trigger_type: "all",
2989
- date_range: "month"
2990
- });
2991
- const [pagination, setPagination] = react.useState({
2992
- page: 1,
2993
- page_size: 10,
2994
- total: 0
2995
- });
2996
- const [isRefreshing, setIsRefreshing] = react.useState(false);
2997
- const loadRuns = react.useCallback(async () => {
2998
- try {
2999
- setError(null);
3000
- const params = {
3001
- page: pagination.page,
3002
- page_size: pagination.page_size,
3003
- ordering: "-started_at"
3004
- // Most recent first
3005
- };
3006
- if (filters.status && filters.status !== "all") {
3007
- params.status = filters.status;
3008
- }
3009
- if (filters.trigger_type && filters.trigger_type !== "all") {
3010
- params.trigger_type = filters.trigger_type;
3011
- }
3012
- const response = await apiClient.getFunnelRuns(funnelId, params);
3013
- setRuns(response.results);
3014
- setPagination((prev) => ({
3015
- ...prev,
3016
- total: response.count
3017
- }));
3018
- } catch (err) {
3019
- setError(err instanceof Error ? err.message : "Failed to load runs");
3020
- console.error("Failed to load funnel runs:", err);
3021
- } finally {
3022
- setIsLoading(false);
3023
- setIsRefreshing(false);
3024
- }
3025
- }, [funnelId, apiClient, pagination.page, pagination.page_size, filters]);
3026
- react.useEffect(() => {
3027
- loadRuns();
3028
- }, [loadRuns]);
3029
- react.useEffect(() => {
3030
- const hasActiveRuns = runs.some(
3031
- (r) => r.status === "pending" || r.status === "running"
3032
- );
3033
- if (hasActiveRuns) {
3034
- const interval = setInterval(() => {
3035
- setIsRefreshing(true);
3036
- loadRuns();
3037
- }, 5e3);
3038
- return () => clearInterval(interval);
3039
- }
3040
- }, [runs, loadRuns]);
3041
- const handleRefresh = () => {
3042
- setIsRefreshing(true);
3043
- loadRuns();
3044
- };
3045
- const handleReRun = async (run) => {
3046
- try {
3047
- await apiClient.runFunnel(funnelId, {
3048
- trigger_type: "manual",
3049
- metadata: { re_run_of: run.id }
3050
- });
3051
- loadRuns();
3052
- } catch (err) {
3053
- console.error("Failed to re-run funnel:", err);
3054
- alert("Failed to re-run funnel. Please try again.");
3055
- }
3056
- };
3057
- const handleCancel = async (run) => {
3058
- try {
3059
- await apiClient.cancelFunnelRun(run.id);
3060
- loadRuns();
3061
- } catch (err) {
3062
- console.error("Failed to cancel run:", err);
3063
- alert("Failed to cancel run. Please try again.");
3064
- }
3065
- };
3066
- const handleViewResults = (run) => {
3067
- if (onViewResults) {
3068
- onViewResults(run);
3069
- } else {
3070
- setSelectedRun(run);
3071
- }
3072
- };
3073
- const totalPages = Math.ceil(pagination.total / pagination.page_size);
3074
- const canGoBack = pagination.page > 1;
3075
- const canGoForward = pagination.page < totalPages;
3076
- const handlePreviousPage = () => {
3077
- if (canGoBack) {
3078
- setPagination((prev) => ({ ...prev, page: prev.page - 1 }));
3079
- }
3080
- };
3081
- const handleNextPage = () => {
3082
- if (canGoForward) {
3083
- setPagination((prev) => ({ ...prev, page: prev.page + 1 }));
3084
- }
3085
- };
3086
- const startIndex = (pagination.page - 1) * pagination.page_size + 1;
3087
- const endIndex = Math.min(
3088
- pagination.page * pagination.page_size,
3089
- pagination.total
3090
- );
3091
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `bg-white rounded-lg border border-gray-200 ${className}`, children: [
3092
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 border-b border-gray-200 flex items-center justify-between", children: [
3093
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-semibold text-gray-900", children: "Funnel Run History" }),
3094
- /* @__PURE__ */ jsxRuntime.jsxs(
3095
- "button",
3096
- {
3097
- onClick: handleRefresh,
3098
- disabled: isRefreshing,
3099
- className: "px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-blue-500",
3100
- "aria-label": "Refresh run history",
3101
- children: [
3102
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: isRefreshing ? "animate-spin inline-block" : "", children: "\u21BB" }),
3103
- " ",
3104
- "Refresh"
3105
- ]
3106
- }
3107
- )
3108
- ] }),
3109
- /* @__PURE__ */ jsxRuntime.jsx(RunFilters, { filters, onFiltersChange: setFilters }),
3110
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full", children: [
3111
- /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "bg-gray-50 border-b border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
3112
- /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium text-gray-600 uppercase tracking-wider", children: "Date" }),
3113
- /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium text-gray-600 uppercase tracking-wider", children: "Status" }),
3114
- /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium text-gray-600 uppercase tracking-wider", children: "Trigger" }),
3115
- /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium text-gray-600 uppercase tracking-wider", children: "Duration" }),
3116
- /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-right text-xs font-medium text-gray-600 uppercase tracking-wider", children: "Input" }),
3117
- /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-right text-xs font-medium text-gray-600 uppercase tracking-wider", children: "Matched" }),
3118
- /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-right text-xs font-medium text-gray-600 uppercase tracking-wider", children: "%" }),
3119
- /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-4 py-3 text-right text-xs font-medium text-gray-600 uppercase tracking-wider", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: "Actions" }) })
3120
- ] }) }),
3121
- /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: isLoading && runs.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: 8, className: "px-4 py-12 text-center", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-gray-500", children: "Loading runs..." }) }) }) : error ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsxs("td", { colSpan: 8, className: "px-4 py-12 text-center", children: [
3122
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-red-600", children: [
3123
- "Error: ",
3124
- error
3125
- ] }),
3126
- /* @__PURE__ */ jsxRuntime.jsx(
3127
- "button",
3128
- {
3129
- onClick: loadRuns,
3130
- className: "mt-2 text-sm text-blue-600 hover:text-blue-700 underline",
3131
- children: "Try again"
3132
- }
3133
- )
3134
- ] }) }) : runs.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: 8, className: "px-4 py-12 text-center", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-gray-500", children: "No runs found. Run this funnel to see history." }) }) }) : runs.map((run) => /* @__PURE__ */ jsxRuntime.jsx(
3135
- RunRow,
3136
- {
3137
- run,
3138
- onViewDetails: setSelectedRun,
3139
- onViewResults: handleViewResults,
3140
- onReRun: handleReRun,
3141
- onCancel: handleCancel
3142
- },
3143
- run.id
3144
- )) })
3145
- ] }) }),
3146
- runs.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-3 border-t border-gray-200 flex items-center justify-between", children: [
3147
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm text-gray-600", children: [
3148
- "Showing ",
3149
- startIndex,
3150
- "-",
3151
- endIndex,
3152
- " of ",
3153
- pagination.total
3154
- ] }),
3155
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
3156
- /* @__PURE__ */ jsxRuntime.jsx(
3157
- "button",
3158
- {
3159
- onClick: handlePreviousPage,
3160
- disabled: !canGoBack,
3161
- className: "px-3 py-1 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-blue-500",
3162
- "aria-label": "Previous page",
3163
- children: "\u2039"
3164
- }
3165
- ),
3166
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm text-gray-600", children: [
3167
- "Page ",
3168
- pagination.page,
3169
- " of ",
3170
- totalPages
3171
- ] }),
3172
- /* @__PURE__ */ jsxRuntime.jsx(
3173
- "button",
3174
- {
3175
- onClick: handleNextPage,
3176
- disabled: !canGoForward,
3177
- className: "px-3 py-1 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-blue-500",
3178
- "aria-label": "Next page",
3179
- children: "\u203A"
3180
- }
3181
- )
3182
- ] })
3183
- ] }),
3184
- /* @__PURE__ */ jsxRuntime.jsx(
3185
- RunDetailsModal,
3186
- {
3187
- run: selectedRun,
3188
- onClose: () => setSelectedRun(null),
3189
- onViewResults,
3190
- onReRun: handleReRun
3191
- }
3192
- )
3193
- ] });
3194
- }
3195
-
3196
- exports.AddStageButton = AddStageButton;
3197
- exports.BooleanValueInput = BooleanValueInput;
3198
- exports.ChoiceValueInput = ChoiceValueInput;
3199
- exports.DateValueInput = DateValueInput;
3200
- exports.EntityCard = EntityCard;
3201
- exports.FieldSelector = FieldSelector;
3202
- exports.FilterRuleEditor = FilterRuleEditor;
3203
- exports.FlowLegend = FlowLegend;
3204
- exports.FunnelCard = FunnelCard;
3205
- exports.FunnelPreview = FunnelPreview;
3206
- exports.FunnelRunHistory = FunnelRunHistory;
3207
- exports.FunnelStageBuilder = FunnelStageBuilder;
3208
- exports.FunnelStats = FunnelStats;
3209
- exports.FunnelVisualFlow = FunnelVisualFlow;
3210
- exports.LoadingPreview = LoadingPreview;
3211
- exports.LogicToggle = LogicToggle;
3212
- exports.MULTI_VALUE_OPERATORS = MULTI_VALUE_OPERATORS;
3213
- exports.MatchBar = MatchBar;
3214
- exports.MultiChoiceValueInput = MultiChoiceValueInput;
3215
- exports.NULL_VALUE_OPERATORS = NULL_VALUE_OPERATORS;
3216
- exports.NumberValueInput = NumberValueInput;
3217
- exports.OPERATOR_LABELS = OPERATOR_LABELS;
3218
- exports.OperatorSelector = OperatorSelector;
3219
- exports.PreviewStats = PreviewStats;
3220
- exports.RuleRow = RuleRow;
3221
- exports.RunActions = RunActions;
3222
- exports.RunDetailsModal = RunDetailsModal;
3223
- exports.RunFilters = RunFilters;
3224
- exports.RunRow = RunRow;
3225
- exports.RunStatusBadge = RunStatusBadge;
3226
- exports.StageActions = StageActions;
3227
- exports.StageBreakdown = StageBreakdown;
3228
- exports.StageBreakdownList = StageBreakdownList;
3229
- exports.StageCard = StageCard;
3230
- exports.StageForm = StageForm;
3231
- exports.StageIndicator = StageIndicator;
3232
- exports.StageNode = StageNode;
3233
- exports.StatusBadge = StatusBadge;
3234
- exports.TagInput = TagInput;
3235
- exports.TextValueInput = TextValueInput;
3236
- exports.calculateMatchRate = calculateMatchRate;
3237
- exports.formatDuration = formatDuration;
3238
- exports.formatFullTimestamp = formatFullTimestamp;
3239
- exports.formatNumber = formatNumber;
3240
- exports.formatRelativeTime = formatRelativeTime;
3241
- exports.getCircledNumber = getCircledNumber;
3242
- //# sourceMappingURL=index.cjs.map
3243
- //# sourceMappingURL=index.cjs.map