astro-tractstack 2.0.0-rc.63 → 2.0.0-rc.65

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.
package/dist/index.js CHANGED
@@ -1292,16 +1292,12 @@ async function w(t, e, c) {
1292
1292
  dest: "public/client/htmx.min.js"
1293
1293
  },
1294
1294
  {
1295
- src: t("../templates/src/client/sse.js"),
1296
- dest: "public/client/sse.js"
1295
+ src: t("../templates/src/client/view.js"),
1296
+ dest: "public/client/view.js"
1297
1297
  },
1298
1298
  {
1299
- src: t("../templates/src/client/belief-events.js"),
1300
- dest: "public/client/belief-events.js"
1301
- },
1302
- {
1303
- src: t("../templates/src/client/analytics-events.js"),
1304
- dest: "public/client/analytics-events.js"
1299
+ src: t("../templates/src/client/app.js"),
1300
+ dest: "public/client/app.js"
1305
1301
  },
1306
1302
  // StoryKeep Editor (add new section)
1307
1303
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.0.0-rc.63",
3
+ "version": "2.0.0-rc.65",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -55,7 +55,7 @@
55
55
  "@ark-ui/react": "^5.21.0",
56
56
  "@astrojs/react": "^3.6.3",
57
57
  "@heroicons/react": "^2.1.1",
58
- "@internationalized/date": "3.8.2",
58
+ "@internationalized/date": "^3.9.0",
59
59
  "@mhsdesign/jit-browser-tailwindcss": "^0.4.2",
60
60
  "@nanostores/persistent": "^1.1.0",
61
61
  "@nanostores/react": "^1.0.0",
@@ -0,0 +1,127 @@
1
+ /**
2
+ * TractStack Singleton Application Manager
3
+ *
4
+ * This script is loaded with `is:persist` and runs only once per session.
5
+ * Its purpose is to manage long-lived application state and services,
6
+ * primarily the Server-Sent Events (SSE) connection. It provides a stable
7
+ * API on `window.TractStackApp` for transient view scripts to interact with.
8
+ * It is completely decoupled from the DOM and HTMX.
9
+ */
10
+
11
+ const VERBOSE = false;
12
+
13
+ function log(message, ...args) {
14
+ if (VERBOSE) console.log('✅ SINGLETON [app.js]:', message, ...args);
15
+ }
16
+
17
+ function logError(message, ...args) {
18
+ if (VERBOSE) console.error('❌ SINGLETON [app.js]:', message, ...args);
19
+ }
20
+
21
+ if (!window.TractStackApp) {
22
+ log('INITIALIZING SINGLETON for the first time.');
23
+
24
+ const TractStackApp = {
25
+ config: {},
26
+ eventSource: null,
27
+
28
+ initialize(config) {
29
+ log('Initializing with config from first page load.', config);
30
+ this.config = config;
31
+ if (config.sessionId && !this.eventSource) {
32
+ this.startSSE();
33
+ } else {
34
+ log(
35
+ 'SSE connection not started: missing sessionId or already connected.'
36
+ );
37
+ }
38
+ },
39
+
40
+ updateConfig(newConfig) {
41
+ const oldStoryfragmentId = this.config.storyfragmentId;
42
+ this.config = { ...this.config, ...newConfig };
43
+ log('Configuration updated due to page navigation.', {
44
+ newConfig,
45
+ storyfragmentIdChanged:
46
+ oldStoryfragmentId !== newConfig.storyfragmentId,
47
+ });
48
+
49
+ if (this.config.sessionId && !this.eventSource) {
50
+ log(
51
+ 'Session ID became available after navigation. Starting SSE connection.'
52
+ );
53
+ this.startSSE();
54
+ }
55
+ },
56
+
57
+ getConfig() {
58
+ return this.config;
59
+ },
60
+
61
+ startSSE() {
62
+ if (this.eventSource) {
63
+ log('Closing existing SSE connection before starting a new one.');
64
+ this.eventSource.close();
65
+ }
66
+
67
+ const { backendUrl, sessionId, storyfragmentId, tenantId } = this.config;
68
+ if (!sessionId || !tenantId) {
69
+ logError('Cannot start SSE connection: missing sessionId or tenantId.');
70
+ return;
71
+ }
72
+
73
+ const sseUrl = `${backendUrl}/api/v1/auth/sse?sessionId=${sessionId}&storyfragmentId=${storyfragmentId}&tenantId=${tenantId}`;
74
+ log('Attempting to establish SSE connection...', { url: sseUrl });
75
+
76
+ this.eventSource = new EventSource(sseUrl);
77
+
78
+ this.eventSource.onopen = () => {
79
+ log('SSE Connection opened successfully.');
80
+ };
81
+
82
+ this.eventSource.onerror = (error) => {
83
+ logError('SSE Connection error occurred.', error);
84
+ this.eventSource.close();
85
+ this.eventSource = null;
86
+ };
87
+
88
+ this.eventSource.addEventListener('panes_updated', (event) => {
89
+ try {
90
+ const data = JSON.parse(event.data);
91
+ log('Received `panes_updated` event from server.', data);
92
+
93
+ log(
94
+ 'Dispatching `tractstack:panes-updated` CustomEvent to the window.'
95
+ );
96
+ window.dispatchEvent(
97
+ new CustomEvent('tractstack:panes-updated', { detail: data })
98
+ );
99
+ } catch (error) {
100
+ logError('Failed to parse `panes_updated` event data.', {
101
+ error,
102
+ rawData: event.data,
103
+ });
104
+ }
105
+ });
106
+ },
107
+ };
108
+
109
+ window.TractStackApp = TractStackApp;
110
+
111
+ if (window.TRACTSTACK_CONFIG) {
112
+ window.TractStackApp.initialize(window.TRACTSTACK_CONFIG);
113
+ } else {
114
+ logError('Initial config not found at singleton creation time.');
115
+ }
116
+
117
+ document.addEventListener('astro:page-load', () => {
118
+ log('`astro:page-load` detected. Updating internal config.');
119
+ if (window.TRACTSTACK_CONFIG) {
120
+ window.TractStackApp.updateConfig(window.TRACTSTACK_CONFIG);
121
+ } else {
122
+ logError(
123
+ '`astro:page-load` fired, but `window.TRACTSTACK_CONFIG` was not found!'
124
+ );
125
+ }
126
+ });
127
+ }
@@ -0,0 +1,423 @@
1
+ /**
2
+ * TractStack View Initializer
3
+ *
4
+ * This script is transient and runs on every page load and client-side navigation.
5
+ * It is responsible for initializing all logic related to the current document,
6
+ * including HTMX configuration, analytics, and DOM event listeners. It gets its
7
+ * configuration state from the persistent `app.js` singleton.
8
+ */
9
+
10
+ const VERBOSE = false;
11
+
12
+ // ============================================================================
13
+ // LOGGING UTILITIES
14
+ // ============================================================================
15
+ function log(message, ...args) {
16
+ if (VERBOSE) console.log('📄 VIEW [view.js]:', message, ...args);
17
+ }
18
+
19
+ function logError(message, ...args) {
20
+ if (VERBOSE) console.error('❌ VIEW [view.js]:', message, ...args);
21
+ }
22
+
23
+ // ============================================================================
24
+ // STATE & CONFIGURATION
25
+ // ============================================================================
26
+
27
+ let paneViewTimes = new Map();
28
+ let globalObserver = null;
29
+ let isPageInitialized = false;
30
+
31
+ // ============================================================================
32
+ // CORE LOGIC FUNCTIONS
33
+ // ============================================================================
34
+
35
+ /**
36
+ * A generic utility to send state updates to the backend API.
37
+ * @param {object} data - The payload for the state update.
38
+ */
39
+ async function sendStateUpdate(data) {
40
+ if (!window.TractStackApp) {
41
+ logError('Singleton not found, cannot send state update.');
42
+ return;
43
+ }
44
+ const config = window.TractStackApp.getConfig();
45
+ const url = `${config.backendUrl}/api/v1/state`;
46
+ const body = { paneId: '', duration: 0, ...data };
47
+ log('Sending state update to backend.', { url, body });
48
+
49
+ try {
50
+ const response = await fetch(url, {
51
+ method: 'POST',
52
+ headers: {
53
+ 'Content-Type': 'application/x-www-form-urlencoded',
54
+ 'X-Tenant-ID': config.tenantId,
55
+ 'X-TractStack-Session-ID': config.sessionId,
56
+ 'X-StoryFragment-ID': config.storyfragmentId,
57
+ },
58
+ body: new URLSearchParams(body),
59
+ });
60
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
61
+ } catch (error) {
62
+ logError('Failed to send state update.', { error, data });
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Initializes all analytics tracking for the current page view.
68
+ * @param {object} config - The configuration object for the current page.
69
+ */
70
+ function initAnalyticsTracking(config) {
71
+ const { storyfragmentId } = config;
72
+
73
+ log(`Initializing analytics tracking for storyfragment: ${storyfragmentId}`);
74
+
75
+ if (globalObserver) {
76
+ globalObserver.disconnect();
77
+ }
78
+
79
+ globalObserver = new IntersectionObserver(
80
+ (entries) => {
81
+ entries.forEach((entry) => {
82
+ const paneId = entry.target.getAttribute('data-pane-id');
83
+ if (!paneId) return;
84
+ if (entry.isIntersecting) {
85
+ if (!paneViewTimes.has(paneId)) {
86
+ paneViewTimes.set(paneId, Date.now());
87
+ }
88
+ } else {
89
+ const startTime = paneViewTimes.get(paneId);
90
+ if (startTime) {
91
+ const duration = Date.now() - startTime;
92
+ paneViewTimes.delete(paneId);
93
+ const THRESHOLD_GLOSSED = 7000;
94
+ const THRESHOLD_READ = 42000;
95
+ let eventVerb = null;
96
+ if (duration >= THRESHOLD_READ) eventVerb = 'READ';
97
+ else if (duration >= THRESHOLD_GLOSSED) eventVerb = 'GLOSSED';
98
+ if (eventVerb) {
99
+ sendStateUpdate({
100
+ beliefId: paneId,
101
+ beliefType: 'Pane',
102
+ beliefValue: eventVerb,
103
+ duration,
104
+ });
105
+ }
106
+ }
107
+ }
108
+ });
109
+ },
110
+ { threshold: 0.1, rootMargin: '0px' }
111
+ );
112
+
113
+ const panes = document.querySelectorAll('[data-pane-id]');
114
+ log(`Observing ${panes.length} panes for visibility tracking.`);
115
+ panes.forEach((pane) => globalObserver.observe(pane));
116
+
117
+ const hasTrackedEntered =
118
+ localStorage.getItem('tractstack_entered_tracked') === 'true';
119
+ if (!hasTrackedEntered && storyfragmentId) {
120
+ log('Tracking first-ever "ENTERED" event.');
121
+ sendStateUpdate({
122
+ beliefId: storyfragmentId,
123
+ beliefType: 'StoryFragment',
124
+ beliefValue: 'ENTERED',
125
+ });
126
+ localStorage.setItem('tractstack_entered_tracked', 'true');
127
+ }
128
+
129
+ if (storyfragmentId) {
130
+ log(`Tracking "PAGEVIEWED" event for storyfragment: ${storyfragmentId}`);
131
+ sendStateUpdate({
132
+ beliefId: storyfragmentId,
133
+ beliefType: 'StoryFragment',
134
+ beliefValue: 'PAGEVIEWED',
135
+ });
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Flushes any pending analytics events, typically called before the page unloads.
141
+ */
142
+ function flushPendingPaneEvents() {
143
+ if (paneViewTimes.size === 0) return;
144
+ log('Flushing pending pane view events before page unload.');
145
+ const flushTime = Date.now();
146
+ paneViewTimes.forEach((startTime, paneId) => {
147
+ const duration = flushTime - startTime;
148
+ const THRESHOLD_GLOSSED = 7000;
149
+ const THRESHOLD_READ = 42000;
150
+ let eventVerb = null;
151
+ if (duration >= THRESHOLD_READ) eventVerb = 'READ';
152
+ else if (duration >= THRESHOLD_GLOSSED) eventVerb = 'GLOSSED';
153
+ if (eventVerb) {
154
+ sendStateUpdate({
155
+ beliefId: paneId,
156
+ beliefType: 'Pane',
157
+ beliefValue: eventVerb,
158
+ duration,
159
+ });
160
+ }
161
+ });
162
+ paneViewTimes.clear();
163
+ }
164
+
165
+ /**
166
+ * Configures the fresh HTMX instance on each page load.
167
+ */
168
+ function configureHtmxForPage() {
169
+ if (!window.htmx) {
170
+ logError('Cannot configure HTMX: window.htmx is not defined.');
171
+ return;
172
+ }
173
+
174
+ htmx.config.selfRequestsOnly = false;
175
+
176
+ log('Configuring HTMX listeners for new page view.', {
177
+ selfRequestsOnly: htmx.config.selfRequestsOnly,
178
+ });
179
+
180
+ htmx.on('htmx:configRequest', function (evt) {
181
+ if (!window.TractStackApp) return;
182
+ const config = window.TractStackApp.getConfig();
183
+ log('Intercepting HTMX request with `htmx:configRequest`.', {
184
+ originalPath: evt.detail.path,
185
+ });
186
+ evt.detail.headers['X-Tenant-ID'] = config.tenantId;
187
+ evt.detail.headers['X-StoryFragment-ID'] = config.storyfragmentId;
188
+ evt.detail.headers['X-TractStack-Session-ID'] = config.sessionId;
189
+
190
+ if (evt.detail.path && evt.detail.path.startsWith('/api/v1/')) {
191
+ evt.detail.path = config.backendUrl + evt.detail.path;
192
+ log('Request path rewritten.', { newPath: evt.detail.path });
193
+ }
194
+ });
195
+
196
+ htmx.on('htmx:beforeRequest', async function (evt) {
197
+ const params = evt.detail.requestConfig.parameters;
198
+ if (params && params.beliefVerb === 'IDENTIFY_AS') {
199
+ log('Intercepting IDENTIFY_AS action to perform pre-unset.');
200
+ evt.preventDefault();
201
+
202
+ const originalPayload = { ...params };
203
+ const unsetPayload = {
204
+ unsetBeliefIds: originalPayload.beliefId,
205
+ paneId: originalPayload.paneId || '',
206
+ gotoPaneID: originalPayload.gotoPaneID || '',
207
+ };
208
+
209
+ log('Step 1: Sending UNSET request.', unsetPayload);
210
+ await sendStateUpdate(unsetPayload);
211
+
212
+ log('Step 2: Sending original IDENTIFY_AS request.', originalPayload);
213
+ await sendStateUpdate(originalPayload);
214
+ }
215
+ });
216
+
217
+ log('Processing the document body with htmx.process().');
218
+ htmx.process(document.body);
219
+ }
220
+
221
+ /**
222
+ * Processes a `panes_updated` event received from the singleton.
223
+ * @param {object} update - The update payload from the SSE event.
224
+ * @param {object} config - The current page's configuration.
225
+ */
226
+ function processStoryfragmentUpdate(update, config) {
227
+ if (update.storyfragmentId !== config.storyfragmentId) {
228
+ log('Ignoring update for a different storyfragment.', {
229
+ eventStoryfragment: update.storyfragmentId,
230
+ currentStoryfragment: config.storyfragmentId,
231
+ });
232
+ return;
233
+ }
234
+
235
+ log('Processing storyfragment update from Singleton.', { update });
236
+
237
+ const uniquePaneIds = [...new Set(update.affectedPanes)];
238
+ const codeHookPaneIds = [];
239
+ const regularPaneIds = [];
240
+
241
+ uniquePaneIds.forEach((paneId) => {
242
+ if (
243
+ update.CodeHookVisibility &&
244
+ update.CodeHookVisibility.hasOwnProperty(paneId)
245
+ ) {
246
+ codeHookPaneIds.push(paneId);
247
+ } else {
248
+ regularPaneIds.push(paneId);
249
+ }
250
+ });
251
+
252
+ log('Split panes for processing.', { codeHookPaneIds, regularPaneIds });
253
+
254
+ codeHookPaneIds.forEach((paneId) => {
255
+ const element = document.querySelector(`#pane-${paneId}`);
256
+ if (!element) {
257
+ logError(`Code hook element not found: #pane-${paneId}`);
258
+ return;
259
+ }
260
+ const visibilityValue = update.CodeHookVisibility[paneId];
261
+ log(`Handling Code Hook pane: ${paneId}`, { visibilityValue });
262
+ element.style.display = visibilityValue === false ? 'none' : 'block';
263
+
264
+ const unsetDiv = document.querySelector(`#pane-${paneId}-unset`);
265
+ if (unsetDiv) {
266
+ if (Array.isArray(visibilityValue)) {
267
+ log(
268
+ `Generating "unset" button for pane ${paneId} with beliefs:`,
269
+ visibilityValue
270
+ );
271
+ const hxValsObject = {
272
+ unsetBeliefIds: visibilityValue.join(','),
273
+ paneId: paneId,
274
+ gotoPaneID: update.gotoPaneId || '',
275
+ };
276
+ unsetDiv.innerHTML = `
277
+ <button
278
+ type="button"
279
+ class="text-mydarkgrey absolute right-2 top-2 z-10 rounded-full bg-white p-1.5 hover:bg-black hover:text-white"
280
+ title="Go Back"
281
+ hx-post="/api/v1/state"
282
+ hx-trigger="click"
283
+ hx-swap="none"
284
+ hx-vals='${JSON.stringify(hxValsObject)}'
285
+ hx-preserve="true"
286
+ >
287
+ <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
288
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3" />
289
+ </svg>
290
+ </button>`;
291
+ htmx.process(unsetDiv);
292
+ } else {
293
+ log(`Clearing "unset" button for pane ${paneId}`);
294
+ unsetDiv.innerHTML = '';
295
+ }
296
+ }
297
+ });
298
+
299
+ regularPaneIds.forEach((paneId) => {
300
+ const element = document.querySelector(`[data-pane-id="${paneId}"]`);
301
+ if (element && window.htmx) {
302
+ log(`Triggering 'refresh' on regular pane element: ${paneId}`);
303
+ htmx.trigger(element, 'refresh');
304
+ } else {
305
+ logError(`Could not find regular pane element to refresh: ${paneId}`);
306
+ }
307
+ });
308
+
309
+ if (update.gotoPaneId) {
310
+ setTimeout(() => {
311
+ const targetElement = document.getElementById(
312
+ `pane-${update.gotoPaneId}`
313
+ );
314
+ if (targetElement) {
315
+ log(`Smart scrolling to target pane: ${update.gotoPaneId}`);
316
+ const elementRect = targetElement.getBoundingClientRect();
317
+ const viewportHeight = window.innerHeight;
318
+ const scrollBlock =
319
+ elementRect.height > viewportHeight ? 'start' : 'center';
320
+ log(`Using scroll behavior: block: '${scrollBlock}'`);
321
+ targetElement.scrollIntoView({
322
+ behavior: 'smooth',
323
+ block: scrollBlock,
324
+ });
325
+ } else {
326
+ logError(
327
+ `Target pane element for scrolling not found: #pane-${update.gotoPaneId}`
328
+ );
329
+ }
330
+ }, 150);
331
+ }
332
+ }
333
+
334
+ // ============================================================================
335
+ // MAIN EXECUTION & LIFECYCLE MANAGEMENT
336
+ // ============================================================================
337
+
338
+ function initializeCurrentView() {
339
+ if (isPageInitialized) {
340
+ log('View already initialized for this page. Skipping redundant setup.');
341
+ return;
342
+ }
343
+
344
+ log('INITIALIZING VIEW for new page.');
345
+
346
+ if (!window.TractStackApp) {
347
+ logError('Singleton `TractStackApp` not found! Cannot initialize view.');
348
+ return;
349
+ }
350
+ const config = window.TractStackApp.getConfig();
351
+ if (!config.configured) {
352
+ logError('Singleton config not ready. Aborting view initialization.');
353
+ return;
354
+ }
355
+
356
+ configureHtmxForPage();
357
+ initAnalyticsTracking(config);
358
+
359
+ isPageInitialized = true;
360
+ }
361
+
362
+ function resetViewState() {
363
+ log('Resetting view state before new page preparation.');
364
+ isPageInitialized = false;
365
+ paneViewTimes.clear();
366
+ }
367
+
368
+ if (!window.tractstackViewLifecycleListenersAttached) {
369
+ log(
370
+ 'Attaching one-time lifecycle listeners that persist across navigations.'
371
+ );
372
+
373
+ document.addEventListener('astro:before-preparation', resetViewState);
374
+
375
+ document.addEventListener('change', function (event) {
376
+ const target = event.target;
377
+ if (
378
+ target.matches &&
379
+ (target.matches('select[data-belief-id]') ||
380
+ target.matches('input[type="checkbox"][data-belief-id]'))
381
+ ) {
382
+ const beliefId = target.getAttribute('data-belief-id');
383
+ const beliefType = target.getAttribute('data-belief-type');
384
+ const paneId = target.getAttribute('data-pane-id');
385
+
386
+ let beliefValue;
387
+ if (target.type === 'checkbox') {
388
+ // TEMPORARY HARDCODING: Use YES/NO for all toggles.
389
+ beliefValue = target.checked ? 'BELIEVES_YES' : 'BELIEVES_NO';
390
+ } else {
391
+ beliefValue = target.value;
392
+ }
393
+
394
+ sendStateUpdate({
395
+ beliefId,
396
+ beliefType,
397
+ beliefValue,
398
+ paneId: paneId || '',
399
+ });
400
+ }
401
+ });
402
+
403
+ window.addEventListener('tractstack:panes-updated', (event) => {
404
+ log('Received `tractstack:panes-updated` event from Singleton.');
405
+ if (!window.TractStackApp) return;
406
+ const data = event.detail;
407
+ const currentConfig = window.TractStackApp.getConfig();
408
+ if (data.updates) {
409
+ data.updates.forEach((update) =>
410
+ processStoryfragmentUpdate(update, currentConfig)
411
+ );
412
+ } else {
413
+ processStoryfragmentUpdate(data, currentConfig);
414
+ }
415
+ });
416
+
417
+ window.addEventListener('beforeunload', flushPendingPaneEvents);
418
+
419
+ window.tractstackViewLifecycleListenersAttached = true;
420
+ }
421
+
422
+ initializeCurrentView();
423
+ document.addEventListener('astro:page-load', initializeCurrentView);
@@ -64,7 +64,6 @@ const MenuComponent = (props: MenuProps) => {
64
64
  const { payload, slug, isContext, brandConfig } = props;
65
65
  const thisPayload = payload.optionsPayload;
66
66
 
67
- // Helper function to process menu links - MODIFIED to build the correct hx-vals payload
68
67
  function processMenuLink(e: MenuLink): ProcessedMenuLinkDatum {
69
68
  const item = { ...e } as ProcessedMenuLinkDatum;
70
69
  const actionLisp = item.actionLisp?.trim();
@@ -83,35 +82,36 @@ const MenuComponent = (props: MenuProps) => {
83
82
  return item;
84
83
  }
85
84
 
86
- if (
87
- actionLisp.startsWith('(declare') ||
88
- actionLisp.startsWith('(identifyAs')
89
- ) {
90
- const tokens = lispLexer(actionLisp);
91
- const commandExpression = (
92
- tokens?.[0] as LispToken[]
93
- )?.[0] as LispToken[];
94
- const command = commandExpression?.[0] as string;
95
- const parameters = commandExpression?.[1] as (string | number)[];
96
- const beliefId = parameters?.[0];
97
- const value = parameters?.[1];
85
+ const [lispTokens] = lispLexer(actionLisp);
86
+
87
+ if (lispTokens && lispTokens.length > 0) {
88
+ // Deconstruct the nested structure: e.g., ['declare', ['HotLead', 'BELIEVES_YES']]
89
+ const tokens = lispTokens[0] as LispToken[];
90
+
91
+ if (
92
+ (tokens[0] === 'declare' || tokens[0] === 'identifyAs') &&
93
+ Array.isArray(tokens[1]) &&
94
+ tokens[1].length >= 2
95
+ ) {
96
+ const command = tokens[0] as string;
97
+ const params = tokens[1] as (string | number)[];
98
+ const beliefId = params[0] as string;
99
+ const value = params[1] as string;
98
100
 
99
- if (command && beliefId !== undefined && value !== undefined) {
100
101
  let hxValsMap: { [key: string]: string } = {};
101
102
 
102
- // CORRECTED: Build the hx-vals payload to match server expectations.
103
103
  if (command === 'declare') {
104
104
  hxValsMap = {
105
- beliefId: String(beliefId),
106
- beliefType: 'Belief', // This was the missing required field.
107
- beliefValue: String(value), // Key changed from beliefVerb to beliefValue.
105
+ beliefId: beliefId,
106
+ beliefType: 'Belief',
107
+ beliefValue: value,
108
108
  };
109
109
  } else if (command === 'identifyAs') {
110
110
  hxValsMap = {
111
- beliefId: String(beliefId),
112
- beliefType: 'Belief', // This was the missing required field.
113
- beliefVerb: 'IDENTIFY_AS', // This is specific to identifyAs.
114
- beliefObject: String(value),
111
+ beliefId: beliefId,
112
+ beliefType: 'Belief',
113
+ beliefVerb: 'IDENTIFY_AS',
114
+ beliefObject: value,
115
115
  };
116
116
  }
117
117
 
@@ -129,12 +129,10 @@ const MenuComponent = (props: MenuProps) => {
129
129
  );
130
130
  }
131
131
 
132
- // Fallback for unknown commands or parsing failures
133
132
  item.renderAs = 'span';
134
133
  return item;
135
134
  }
136
135
 
137
- // Process featured and additional links using the modified helper
138
136
  const featuredLinks = thisPayload
139
137
  .filter((e: MenuLink) => e.featured)
140
138
  .map(processMenuLink);
@@ -142,7 +140,6 @@ const MenuComponent = (props: MenuProps) => {
142
140
  .filter((e: MenuLink) => !e.featured)
143
141
  .map(processMenuLink);
144
142
 
145
- // Helper component to render either a link or a button, avoiding repetition.
146
143
  const InteractiveMenuItem = ({ item }: { item: ProcessedMenuLinkDatum }) => {
147
144
  if (item.renderAs === 'button') {
148
145
  return (
@@ -173,7 +170,6 @@ const MenuComponent = (props: MenuProps) => {
173
170
  );
174
171
  }
175
172
 
176
- // Fallback for 'span'
177
173
  return (
178
174
  <span
179
175
  className="text-mydarkgrey block text-2xl font-bold leading-6 opacity-50"
@@ -60,7 +60,6 @@ const PaneMagicPathPanel = ({ nodeId, setMode }: PaneMagicPathPanelProps) => {
60
60
  if (!idsResponse.ok) throw new Error('Failed to fetch belief IDs');
61
61
 
62
62
  const idsResult = await idsResponse.json();
63
- // CORRECTED: The key from the backend is "beliefIds", not "beliefs"
64
63
  if (!idsResult.beliefIds || idsResult.beliefIds.length === 0) {
65
64
  setAvailableBeliefs([]);
66
65
  setIsLoading(false);
@@ -74,7 +73,6 @@ const PaneMagicPathPanel = ({ nodeId, setMode }: PaneMagicPathPanelProps) => {
74
73
  'Content-Type': 'application/json',
75
74
  'X-Tenant-ID': tenantId,
76
75
  },
77
- // CORRECTED: Pass the array from the correct key "beliefIds"
78
76
  body: JSON.stringify({ beliefIds: idsResult.beliefIds }),
79
77
  });
80
78