@startsimpli/funnels 0.1.4 → 0.1.6

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