astro-tractstack 2.0.1 → 2.0.3

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.
@@ -72,6 +72,24 @@ const EpinetTableView = ({
72
72
  const getContentInfo = (
73
73
  contentId: string
74
74
  ): { title: string; type: string } => {
75
+ if (contentId === 'commitmentAction-Previously-Entered') {
76
+ return { title: 'Previously Entered', type: 'Virtual' };
77
+ }
78
+ if (contentId === 'identifyAs-Anonymous-Traffic') {
79
+ return { title: 'Anonymous Traffic', type: 'Virtual' };
80
+ }
81
+
82
+ const mainParts = contentId.split('_');
83
+ if (mainParts[0] === 'identifyAs' && mainParts.length >= 3) {
84
+ const beliefSlug = mainParts[1];
85
+ const value = mainParts[2].replace(/-/g, ' ');
86
+ const titleCaseSlug = beliefSlug.replace(/([A-Z])/g, ' $1').trim();
87
+ return {
88
+ title: `${titleCaseSlug} - ${value}`,
89
+ type: 'Persona',
90
+ };
91
+ }
92
+
75
93
  const content = fullContentMap.find((item) => item.id === contentId);
76
94
  if (content) {
77
95
  return {
@@ -79,8 +97,9 @@ const EpinetTableView = ({
79
97
  type: content.type,
80
98
  };
81
99
  }
100
+
82
101
  return {
83
- title: contentId.substring(0, 8) + '...',
102
+ title: '[deleted content]',
84
103
  type: 'Unknown',
85
104
  };
86
105
  };
@@ -100,11 +119,9 @@ const EpinetTableView = ({
100
119
  return getHumanReadableTime(startHour);
101
120
  }
102
121
  const startTime = getHumanReadableTime(startHour);
103
- // If end hour is 23 (11pm), show "end of day"
104
122
  if (endHour === 23) {
105
123
  return `${startTime} - end of day`;
106
124
  }
107
- // Show endHour:59 format
108
125
  if (endHour === 0) {
109
126
  return `${startTime} - 12:59am`;
110
127
  } else if (endHour < 12) {
@@ -116,7 +133,6 @@ const EpinetTableView = ({
116
133
  }
117
134
  };
118
135
 
119
- // Parse UTC hourKey and convert to local timezone for display
120
136
  const getLocalDisplayTime = (
121
137
  hourKey: string
122
138
  ): {
@@ -128,24 +144,20 @@ const EpinetTableView = ({
128
144
  try {
129
145
  const [year, month, day, hour] = hourKey.split('-').map(Number);
130
146
  const utcDate = new Date(Date.UTC(year, month - 1, day, hour));
131
-
132
- // Convert UTC to local timezone for display
133
147
  const localDate = new Date(utcDate.toLocaleString());
134
-
135
148
  const localDay = `${localDate.getFullYear()}-${String(
136
149
  localDate.getMonth() + 1
137
150
  ).padStart(2, '0')}-${String(localDate.getDate()).padStart(2, '0')}`;
138
-
139
151
  const localHour = localDate.getHours();
140
152
  const localHourDisplay = `${localHour.toString().padStart(2, '0')}:00`;
141
153
  const humanReadableTime = getHumanReadableTime(localHour);
142
-
143
154
  return { localDay, localHour, localHourDisplay, humanReadableTime };
144
155
  } catch (e) {
145
156
  console.warn(`Failed to parse hourKey: ${hourKey}`, e);
146
- // Fallback to treating as already local
147
157
  const [year, month, day] = hourKey.split('-').slice(0, 3).map(Number);
148
- const localDay = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
158
+ const localDay = `${year}-${String(month).padStart(2, '0')}-${String(
159
+ day
160
+ ).padStart(2, '0')}`;
149
161
  const localHour = Number(hourKey.split('-')[3]) || 0;
150
162
  const localHourDisplay = `${localHour.toString().padStart(2, '0')}:00`;
151
163
  const humanReadableTime = getHumanReadableTime(localHour);
@@ -153,19 +165,15 @@ const EpinetTableView = ({
153
165
  }
154
166
  };
155
167
 
156
- // Convert hourKey (UTC) to exact UTC time range for focusing
157
168
  const focusOnThisHour = (hourKey: string) => {
158
169
  try {
159
170
  const [year, month, day, hour] = hourKey.split('-').map(Number);
160
-
161
- // Create exact UTC hour boundaries
162
171
  const startTimeUTC = new Date(
163
172
  Date.UTC(year, month - 1, day, hour, 0, 0, 0)
164
173
  );
165
174
  const endTimeUTC = new Date(
166
175
  Date.UTC(year, month - 1, day, hour, 59, 59, 999)
167
176
  );
168
-
169
177
  epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
170
178
  ...$epinetCustomFilters,
171
179
  startTimeUTC: startTimeUTC.toISOString(),
@@ -173,7 +181,6 @@ const EpinetTableView = ({
173
181
  });
174
182
  } catch (e) {
175
183
  console.warn(`Failed to focus on hour: ${hourKey}`, e);
176
- // Fallback - do nothing rather than use legacy system
177
184
  }
178
185
  };
179
186
 
@@ -224,12 +231,11 @@ const EpinetTableView = ({
224
231
  let maxHourlyTotal = 0;
225
232
 
226
233
  const dailyUniqueVisitors = new Set<string>();
227
-
228
- // Get current local time for "future" detection
229
234
  const now = new Date();
230
- const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(
231
- now.getDate()
232
- ).padStart(2, '0')}`;
235
+ const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(
236
+ 2,
237
+ '0'
238
+ )}-${String(now.getDate()).padStart(2, '0')}`;
233
239
  const isToday = currentDay === today;
234
240
  const currentLocalHour = now.getHours();
235
241
 
@@ -306,7 +312,11 @@ const EpinetTableView = ({
306
312
  display:
307
313
  emptyRangeStart === localEmptyEnd
308
314
  ? `${emptyRangeStart.toString().padStart(2, '0')}:00`
309
- : `${emptyRangeStart.toString().padStart(2, '0')}:00 - ${localEmptyEnd.toString().padStart(2, '0')}:59`,
315
+ : `${emptyRangeStart
316
+ .toString()
317
+ .padStart(2, '0')}:00 - ${localEmptyEnd
318
+ .toString()
319
+ .padStart(2, '0')}:59`,
310
320
  humanReadableDisplay: getHumanReadableTimeRange(
311
321
  emptyRangeStart,
312
322
  localEmptyEnd
@@ -351,7 +361,9 @@ const EpinetTableView = ({
351
361
  display:
352
362
  emptyRangeStart === localEmptyEnd
353
363
  ? `${emptyRangeStart.toString().padStart(2, '0')}:00`
354
- : `${emptyRangeStart.toString().padStart(2, '0')}:00 - ${localEmptyEnd.toString().padStart(2, '0')}:59`,
364
+ : `${emptyRangeStart.toString().padStart(2, '0')}:00 - ${localEmptyEnd
365
+ .toString()
366
+ .padStart(2, '0')}:59`,
355
367
  humanReadableDisplay: getHumanReadableTimeRange(
356
368
  emptyRangeStart,
357
369
  localEmptyEnd
@@ -480,69 +492,82 @@ const EpinetTableView = ({
480
492
  value={item.type === 'active' ? item.hourKey : `empty-${index}`}
481
493
  className="border-b border-gray-100 last:border-b-0"
482
494
  >
483
- <Accordion.ItemTrigger className="flex w-full items-center justify-between p-3 text-left transition-colors duration-200 hover:bg-gray-50">
495
+ <Accordion.ItemTrigger className="flex w-full cursor-pointer items-center justify-between p-3 text-left transition-colors duration-200 hover:bg-gray-100">
484
496
  {item.type === 'active' ? (
485
- <div className="flex flex-grow items-center space-x-3">
486
- <span className="text-sm font-bold text-gray-700">
487
- {item.humanReadableTime}
488
- </span>
489
- <span className="text-xs text-gray-600">
490
- {item.hourlyTotal} event
491
- {item.hourlyTotal !== 1 ? 's' : ''} /{' '}
492
- {item.hourlyVisitors} visitor
493
- {item.hourlyVisitors !== 1 ? 's' : ''}
494
- </span>
495
- <div className="relative h-2 w-full max-w-48 rounded bg-gray-200">
496
- <div
497
- className="absolute left-0 top-0 h-2 rounded bg-cyan-600"
498
- style={{
499
- width: `${Math.max(item.relativeToMax * 100, 5)}%`,
500
- }}
501
- title={`${item.hourlyTotal} events (${(item.relativeToMax * 100).toFixed(1)}% of busiest hour)`}
502
- />
497
+ <div className="flex flex-grow items-center justify-between space-x-3">
498
+ <div className="flex flex-grow items-center space-x-3">
499
+ <span className="text-sm font-bold text-gray-700">
500
+ {item.humanReadableTime}
501
+ </span>
502
+ <span className="text-xs text-gray-600">
503
+ {item.hourlyTotal} event
504
+ {item.hourlyTotal !== 1 ? 's' : ''} /{' '}
505
+ {item.hourlyVisitors} visitor
506
+ {item.hourlyVisitors !== 1 ? 's' : ''}
507
+ </span>
508
+ <div className="relative h-2 w-full max-w-48 rounded bg-gray-200">
509
+ <div
510
+ className="absolute left-0 top-0 h-2 rounded bg-cyan-600"
511
+ style={{
512
+ width: `${Math.max(item.relativeToMax * 100, 5)}%`,
513
+ }}
514
+ title={`${item.hourlyTotal} events (${(
515
+ item.relativeToMax * 100
516
+ ).toFixed(1)}% of busiest hour)`}
517
+ />
518
+ </div>
503
519
  </div>
504
- <div
505
- onClick={(e) => {
506
- e.stopPropagation(); // Prevent accordion toggle
507
- focusOnThisHour(item.hourKey);
508
- }}
509
- className="flex cursor-pointer items-center rounded-md bg-orange-100 px-2 py-1 text-xs font-bold text-orange-800 transition-colors duration-200 hover:bg-orange-200"
510
- title="Focus analytics dashboard on this hour's user journeys"
511
- role="button"
512
- tabIndex={0}
513
- onKeyDown={(e) => {
514
- if (e.key === 'Enter' || e.key === ' ') {
515
- e.preventDefault();
520
+ <div className="flex items-center space-x-2">
521
+ <div
522
+ onClick={(e) => {
516
523
  e.stopPropagation();
517
524
  focusOnThisHour(item.hourKey);
518
- }
519
- }}
520
- >
521
- <MagnifyingGlassIcon className="mr-1 h-3 w-3" />
522
- Journeys this Hour
525
+ }}
526
+ className="flex cursor-pointer items-center rounded-md bg-orange-100 px-2 py-1 text-xs font-bold text-orange-800 transition-colors duration-200 hover:bg-orange-200"
527
+ role="button"
528
+ tabIndex={0}
529
+ onKeyDown={(e) => {
530
+ if (e.key === 'Enter' || e.key === ' ') {
531
+ e.preventDefault();
532
+ e.stopPropagation();
533
+ focusOnThisHour(item.hourKey);
534
+ }
535
+ }}
536
+ >
537
+ <MagnifyingGlassIcon className="mr-1 h-3 w-3" />
538
+ Journeys this Hour
539
+ </div>
540
+ <div className="flex items-center rounded-md bg-orange-100 px-2 py-1 text-xs font-bold text-orange-800">
541
+ <Accordion.ItemIndicator>
542
+ <ChevronDownIcon className="h-3 w-3 transition-transform duration-200 data-[state=open]:rotate-180" />
543
+ </Accordion.ItemIndicator>
544
+ <span className="ml-1 data-[state=closed]:block data-[state=open]:hidden">
545
+ Expand Details
546
+ </span>
547
+ <span className="ml-1 data-[state=open]:block data-[state=closed]:hidden">
548
+ Hide Details
549
+ </span>
550
+ </div>
523
551
  </div>
524
552
  </div>
525
553
  ) : (
526
- <div className="flex flex-grow items-center">
527
- <span className="text-sm text-gray-700">
528
- {item.humanReadableDisplay}
529
- </span>
530
- <span className="ml-2 text-xs italic text-gray-500">
531
- {item.isFuture ? 'The future awaits!' : 'No activity'}
532
- </span>
554
+ <div className="flex flex-grow items-center justify-between">
555
+ <div className="flex items-center">
556
+ <span className="text-sm text-gray-700">
557
+ {item.humanReadableDisplay}
558
+ </span>
559
+ <span className="ml-2 text-xs italic text-gray-500">
560
+ {item.isFuture ? 'The future awaits!' : 'No activity'}
561
+ </span>
562
+ </div>
563
+ <Accordion.ItemIndicator>
564
+ <ChevronDownIcon className="h-5 w-5 text-gray-500 transition-transform duration-200 data-[state=open]:rotate-180" />
565
+ </Accordion.ItemIndicator>
533
566
  </div>
534
567
  )}
535
- <Accordion.ItemIndicator>
536
- <ChevronDownIcon
537
- className={classNames(
538
- 'h-5 w-5 text-gray-500 transition-transform duration-200',
539
- 'data-[state=open]:rotate-180'
540
- )}
541
- />
542
- </Accordion.ItemIndicator>
543
568
  </Accordion.ItemTrigger>
544
569
 
545
- <Accordion.ItemContent className="pt-2">
570
+ <Accordion.ItemContent className="p-4">
546
571
  {item.type === 'active' && (
547
572
  <div className="space-y-4">
548
573
  {item.contentItems.map((content) => (
@@ -6,7 +6,7 @@ import {
6
6
  type ReactNode,
7
7
  } from 'react';
8
8
  import { useStore } from '@nanostores/react';
9
- import { epinetCustomFilters } from '@/stores/analytics';
9
+ import { epinetCustomFilters, type AppliedFilter } from '@/stores/analytics';
10
10
  import { TractStackAPI } from '@/utils/api';
11
11
  import SankeyDiagram from './SankeyDiagram';
12
12
  import EpinetDurationSelector from './EpinetDurationSelector';
@@ -34,7 +34,6 @@ const EpinetWrapper = ({
34
34
  }: {
35
35
  fullContentMap: FullContentMapItem[];
36
36
  }) => {
37
- // Use the global store instead of local state
38
37
  const $epinetCustomFilters = useStore(epinetCustomFilters);
39
38
 
40
39
  const [analytics, setAnalytics] = useState<{
@@ -54,15 +53,13 @@ const EpinetWrapper = ({
54
53
  const [epinetId, setEpinetId] = useState<string | null>(null);
55
54
 
56
55
  const MAX_POLLING_ATTEMPTS = 3;
57
- const POLLING_DELAYS = [2000, 5000, 10000]; // 2s, 5s, 10s
56
+ const POLLING_DELAYS = [2000, 5000, 10000];
58
57
 
59
- // Initialize TractStackAPI
60
58
  const api = useMemo(
61
59
  () => new TractStackAPI(window.TRACTSTACK_CONFIG?.tenantId || 'default'),
62
60
  []
63
61
  );
64
62
 
65
- // Clear polling timer on unmount
66
63
  useEffect(() => {
67
64
  return () => {
68
65
  if (pollingTimer) {
@@ -74,43 +71,32 @@ const EpinetWrapper = ({
74
71
  useEffect(() => {
75
72
  const discoverEpinetId = async () => {
76
73
  try {
77
- // First, try to find a promoted epinet from content map
78
74
  const promotedEpinet = fullContentMap.find(
79
75
  (item) => item.type === 'Epinet' && item.promoted
80
76
  );
81
-
82
77
  if (promotedEpinet) {
83
78
  setEpinetId(promotedEpinet.id);
84
79
  return;
85
80
  }
86
-
87
- // If no promoted epinet, get first epinet from content map
88
81
  const firstEpinet = fullContentMap.find(
89
82
  (item) => item.type === 'Epinet'
90
83
  );
91
-
92
84
  if (firstEpinet) {
93
85
  setEpinetId(firstEpinet.id);
94
86
  return;
95
87
  }
96
-
97
- // Fallback: no epinet found
98
- console.warn('No epinet found in content map');
99
88
  setEpinetId(null);
100
89
  } catch (error) {
101
90
  console.error('Error discovering epinet ID:', error);
102
91
  setEpinetId(null);
103
92
  }
104
93
  };
105
-
106
94
  discoverEpinetId();
107
95
  }, [fullContentMap]);
108
96
 
109
- // Initialize epinet custom filters with default values on mount
110
97
  useEffect(() => {
111
98
  const nowUTC = new Date();
112
99
  const oneWeekAgoUTC = new Date(nowUTC.getTime() - 7 * 24 * 60 * 60 * 1000);
113
-
114
100
  epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
115
101
  enabled: true,
116
102
  visitorType: 'all',
@@ -119,40 +105,11 @@ const EpinetWrapper = ({
119
105
  endTimeUTC: nowUTC.toISOString(),
120
106
  userCounts: [],
121
107
  hourlyNodeActivity: {},
108
+ availableFilters: [],
109
+ appliedFilters: [],
122
110
  });
123
111
  }, []);
124
112
 
125
- // Detect current duration type from epinetCustomFilters (for UI helpers only)
126
- //const currentDurationHelper = useMemo(():
127
- // | 'daily'
128
- // | 'weekly'
129
- // | 'monthly'
130
- // | 'custom' => {
131
- // const { startTimeUTC, endTimeUTC } = $epinetCustomFilters;
132
-
133
- // if (startTimeUTC && endTimeUTC) {
134
- // const startTime = new Date(startTimeUTC);
135
- // const endTime = new Date(endTimeUTC);
136
- // const diffMs = endTime.getTime() - startTime.getTime();
137
- // const diffHours = diffMs / (1000 * 60 * 60);
138
-
139
- // if (Math.abs(diffHours - 24) <= 1) return 'daily';
140
- // if (Math.abs(diffHours - 168) <= 1) return 'weekly';
141
- // if (Math.abs(diffHours - 672) <= 1) return 'monthly';
142
- // return 'custom';
143
- // }
144
-
145
- // return 'weekly'; // default
146
- //}, [$epinetCustomFilters.startTimeUTC, $epinetCustomFilters.endTimeUTC]);
147
-
148
- // Fetch data when epinet ID is available
149
- useEffect(() => {
150
- if (epinetId) {
151
- fetchEpinetData();
152
- }
153
- }, [epinetId]);
154
-
155
- // Watch for changes in the global filters and refetch data
156
113
  useEffect(() => {
157
114
  if (
158
115
  epinetId &&
@@ -171,43 +128,44 @@ const EpinetWrapper = ({
171
128
  $epinetCustomFilters.selectedUserId,
172
129
  $epinetCustomFilters.startTimeUTC,
173
130
  $epinetCustomFilters.endTimeUTC,
131
+ $epinetCustomFilters.appliedFilters,
174
132
  ]);
175
133
 
176
- // Handle filter preset changes
177
- //const handleFilterChange = useCallback(
178
- // (newValue: string) => {
179
- // const nowUTC = new Date();
180
- // const hoursBack: number =
181
- // newValue === 'daily' ? 24 : newValue === 'weekly' ? 168 : 672;
182
- // const startTimeUTC = new Date(
183
- // nowUTC.getTime() - hoursBack * 60 * 60 * 1000
184
- // );
134
+ const handleBeliefFilterChange = (beliefSlug: string, value: string) => {
135
+ const tenantId = window.TRACTSTACK_CONFIG?.tenantId || 'default';
136
+ const currentFilters = epinetCustomFilters.get();
137
+ let newFilters: AppliedFilter[] = [
138
+ ...(currentFilters.appliedFilters || []),
139
+ ];
140
+
141
+ if (value === 'All') {
142
+ newFilters = newFilters.filter((f) => f.beliefSlug !== beliefSlug);
143
+ } else {
144
+ const existingIndex = newFilters.findIndex(
145
+ (f) => f.beliefSlug === beliefSlug
146
+ );
147
+ if (existingIndex > -1) {
148
+ newFilters[existingIndex] = { beliefSlug, value };
149
+ } else {
150
+ newFilters.push({ beliefSlug, value });
151
+ }
152
+ }
185
153
 
186
- // epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
187
- // ...$epinetCustomFilters,
188
- // enabled: true,
189
- // startTimeUTC: startTimeUTC.toISOString(),
190
- // endTimeUTC: nowUTC.toISOString(),
191
- // });
192
- // },
193
- // [$epinetCustomFilters]
194
- //);
154
+ epinetCustomFilters.set(tenantId, {
155
+ ...currentFilters,
156
+ appliedFilters: newFilters,
157
+ });
158
+ };
195
159
 
196
160
  const fetchEpinetData = useCallback(async () => {
197
161
  if (!epinetId) return;
198
-
199
162
  try {
200
- setAnalytics((prev) => ({ ...prev, isLoading: true }));
201
-
163
+ setAnalytics((prev) => ({ ...prev, isLoading: true, status: 'loading' }));
202
164
  if (pollingTimer) {
203
165
  clearTimeout(pollingTimer);
204
166
  setPollingTimer(null);
205
167
  }
206
-
207
- // Build query parameters
208
168
  const params = new URLSearchParams();
209
-
210
- // Convert UTC timestamps to hours-back integers (what backend expects)
211
169
  if (
212
170
  $epinetCustomFilters.startTimeUTC &&
213
171
  $epinetCustomFilters.endTimeUTC
@@ -215,76 +173,71 @@ const EpinetWrapper = ({
215
173
  const now = new Date();
216
174
  const startTime = new Date($epinetCustomFilters.startTimeUTC);
217
175
  const endTime = new Date($epinetCustomFilters.endTimeUTC);
218
-
219
176
  const startHour = Math.ceil(
220
177
  (now.getTime() - startTime.getTime()) / (1000 * 60 * 60)
221
178
  );
222
179
  const endHour = Math.floor(
223
180
  (now.getTime() - endTime.getTime()) / (1000 * 60 * 60)
224
181
  );
225
-
226
182
  params.append('startHour', startHour.toString());
227
183
  params.append('endHour', endHour.toString());
228
184
  }
229
-
230
185
  params.append('visitorType', $epinetCustomFilters.visitorType || 'all');
231
186
  if ($epinetCustomFilters.selectedUserId) {
232
187
  params.append('userId', $epinetCustomFilters.selectedUserId);
233
188
  }
234
189
 
235
- // Use TractStackAPI instead of raw fetch
190
+ // MODIFICATION: Properly format appliedFilters for the backend
191
+ if (
192
+ $epinetCustomFilters.appliedFilters &&
193
+ $epinetCustomFilters.appliedFilters.length > 0
194
+ ) {
195
+ params.append(
196
+ 'appliedFilters',
197
+ JSON.stringify($epinetCustomFilters.appliedFilters)
198
+ );
199
+ }
200
+
236
201
  const response = await api.get(
237
202
  `/api/v1/analytics/epinet/${epinetId}?${params.toString()}`
238
203
  );
239
-
240
- if (!response.success) {
204
+ if (!response.success)
241
205
  throw new Error(`API request failed: ${response.error}`);
242
- }
243
-
244
206
  const result = response.data;
245
-
246
207
  if (result.success !== false) {
247
- // Check if data is still loading
248
208
  const epinetData = result.epinet;
249
-
250
209
  if (
251
210
  epinetData &&
252
211
  (epinetData.status === 'loading' ||
253
212
  epinetData.status === 'refreshing')
254
213
  ) {
255
- // If data is still loading, poll again after delay
256
214
  if (pollingAttempts < MAX_POLLING_ATTEMPTS) {
257
215
  const delayMs =
258
216
  POLLING_DELAYS[pollingAttempts] ||
259
217
  POLLING_DELAYS[POLLING_DELAYS.length - 1];
260
-
261
218
  const newTimer = setTimeout(() => {
262
219
  setPollingAttempts(pollingAttempts + 1);
263
220
  fetchEpinetData();
264
221
  }, delayMs);
265
-
266
222
  setPollingTimer(newTimer);
267
223
  return;
268
224
  }
269
225
  }
270
-
271
- setAnalytics((prev) => ({
272
- ...prev,
226
+ setAnalytics({
273
227
  epinet: result.epinet,
274
228
  status: 'complete',
275
229
  error: null,
276
- }));
277
-
278
- // Update the global store with additional data from API response
230
+ isLoading: false,
231
+ });
279
232
  epinetCustomFilters.set(
280
233
  window.TRACTSTACK_CONFIG?.tenantId || 'default',
281
234
  {
282
235
  ...$epinetCustomFilters,
283
236
  userCounts: result.userCounts || [],
284
237
  hourlyNodeActivity: result.hourlyNodeActivity || {},
238
+ availableFilters: result?.availableFilters || [],
285
239
  }
286
240
  );
287
-
288
241
  setPollingAttempts(0);
289
242
  } else {
290
243
  throw new Error(result.error || 'Unknown API error');
@@ -295,18 +248,14 @@ const EpinetWrapper = ({
295
248
  error: error instanceof Error ? error.message : 'Unknown error',
296
249
  status: 'error',
297
250
  }));
298
-
299
- // Schedule a retry if we haven't reached max attempts
300
251
  if (pollingAttempts < MAX_POLLING_ATTEMPTS) {
301
252
  const delayMs =
302
253
  POLLING_DELAYS[pollingAttempts] ||
303
254
  POLLING_DELAYS[POLLING_DELAYS.length - 1];
304
-
305
255
  const newTimer = setTimeout(() => {
306
256
  setPollingAttempts(pollingAttempts + 1);
307
257
  fetchEpinetData();
308
258
  }, delayMs);
309
-
310
259
  setPollingTimer(newTimer);
311
260
  }
312
261
  } finally {
@@ -316,7 +265,6 @@ const EpinetWrapper = ({
316
265
 
317
266
  const { epinet, isLoading, status, error } = analytics;
318
267
 
319
- // Show loading while discovering epinet ID
320
268
  if (!epinetId) {
321
269
  return (
322
270
  <div className="flex h-96 w-full items-center justify-center rounded bg-gray-100">
@@ -330,18 +278,13 @@ const EpinetWrapper = ({
330
278
  );
331
279
  }
332
280
 
333
- if (
334
- !epinet ||
335
- !epinet.nodes ||
336
- !epinet.links ||
337
- epinet.nodes.length === 0 ||
338
- epinet.links.length === 0
339
- )
281
+ if ((isLoading || status === 'loading') && !epinet) {
340
282
  return (
341
283
  <div className="flex h-64 items-center justify-center">
342
284
  <div className="h-8 w-8 animate-spin rounded-full border-b-2 border-cyan-600"></div>
343
285
  </div>
344
286
  );
287
+ }
345
288
 
346
289
  if (error && !epinet) {
347
290
  return (
@@ -368,13 +311,27 @@ const EpinetWrapper = ({
368
311
  epinet.nodes.length === 0 ||
369
312
  epinet.links.length === 0
370
313
  ) {
314
+ if (isLoading || status === 'loading') {
315
+ return (
316
+ <div className="flex h-64 items-center justify-center">
317
+ <div className="h-8 w-8 animate-spin rounded-full border-b-2 border-cyan-600"></div>
318
+ </div>
319
+ );
320
+ }
371
321
  return (
372
- <div className="rounded-lg bg-gray-50 p-8 text-center text-gray-800">
373
- <p>
374
- No user journey data is available yet. This visualization will appear
375
- when users start interacting with your content.
376
- </p>
377
- </div>
322
+ <>
323
+ <div className="rounded-lg bg-gray-50 p-8 text-center text-gray-800">
324
+ <p>No user journey data is available for the selected filters.</p>
325
+ </div>
326
+ <EpinetDurationSelector
327
+ fullContentMap={fullContentMap}
328
+ isLoading={isLoading || status === 'loading'}
329
+ hourlyNodeActivity={$epinetCustomFilters.hourlyNodeActivity}
330
+ availableFilters={$epinetCustomFilters.availableFilters}
331
+ appliedFilters={$epinetCustomFilters.appliedFilters}
332
+ onBeliefFilterChange={handleBeliefFilterChange}
333
+ />
334
+ </>
378
335
  );
379
336
  }
380
337
 
@@ -396,7 +353,7 @@ const EpinetWrapper = ({
396
353
  }
397
354
  >
398
355
  <div className="space-y-6">
399
- <div className="rounded-lg bg-white p-6 shadow">
356
+ <div className="rounded-lg bg-white p-2 shadow md:p-6">
400
357
  <div className="mb-4 flex items-center justify-between">
401
358
  {(isLoading || status === 'loading') && (
402
359
  <div className="flex items-center space-x-2 text-sm text-gray-500">
@@ -410,11 +367,13 @@ const EpinetWrapper = ({
410
367
  isLoading={isLoading || status === 'loading'}
411
368
  />
412
369
  </div>
413
-
414
370
  <EpinetDurationSelector
415
371
  fullContentMap={fullContentMap}
416
372
  isLoading={isLoading || status === 'loading'}
417
373
  hourlyNodeActivity={$epinetCustomFilters.hourlyNodeActivity}
374
+ availableFilters={$epinetCustomFilters.availableFilters}
375
+ appliedFilters={$epinetCustomFilters.appliedFilters}
376
+ onBeliefFilterChange={handleBeliefFilterChange}
418
377
  />
419
378
  </div>
420
379
  </ErrorBoundary>