astro-tractstack 2.0.0-rc.62 → 2.0.0-rc.64

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.0.0-rc.62",
3
+ "version": "2.0.0-rc.64",
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",
@@ -1,8 +1,10 @@
1
1
  const VERBOSE = false;
2
2
 
3
- // This function now contains all essential one-time and recurring HTMX setup.
4
3
  function configureHtmx() {
5
- if (!window.htmx) return;
4
+ if (!window.htmx || window.HTMX_LISTENER_ATTACHED) {
5
+ return;
6
+ }
7
+ window.HTMX_LISTENER_ATTACHED = true;
6
8
 
7
9
  if (!window.HTMX_CONFIGURED) {
8
10
  window.htmx.config.selfRequestsOnly = false;
@@ -11,13 +13,12 @@ function configureHtmx() {
11
13
 
12
14
  window.htmx.on(document.body, 'htmx:configRequest', function (evt) {
13
15
  const config = window.TRACTSTACK_CONFIG;
14
- if (!config || !config.sessionId) return; // Check for config and session ID
16
+ if (!config || !config.sessionId) return;
15
17
 
16
18
  if (evt.detail.path && evt.detail.path.startsWith('/api/v1/')) {
17
19
  evt.detail.path = config.backendUrl + evt.detail.path;
18
20
  }
19
21
 
20
- // MODIFIED: Use session ID from the global config object
21
22
  const sessionId = config.sessionId;
22
23
  evt.detail.headers['X-Tenant-ID'] = config.tenantId;
23
24
  evt.detail.headers['X-StoryFragment-ID'] = config.storyfragmentId;
@@ -25,6 +26,27 @@ function configureHtmx() {
25
26
  evt.detail.headers['X-TractStack-Session-ID'] = sessionId;
26
27
  }
27
28
  });
29
+
30
+ window.htmx.on(document.body, 'htmx:beforeRequest', async function (evt) {
31
+ const params = evt.detail.requestConfig.parameters;
32
+ if (params && params.beliefVerb === 'IDENTIFY_AS') {
33
+ evt.preventDefault();
34
+
35
+ const originalPayload = params;
36
+ const unsetPayload = {
37
+ unsetBeliefIds: originalPayload.beliefId,
38
+ paneId: originalPayload.paneId || '',
39
+ };
40
+
41
+ try {
42
+ await sendBeliefUpdate(unsetPayload);
43
+ await sendBeliefUpdate(originalPayload);
44
+ } catch (error) {
45
+ if (VERBOSE)
46
+ console.error('🔴 BELIEF: Two-step identifyAs update failed', error);
47
+ }
48
+ }
49
+ });
28
50
  }
29
51
 
30
52
  const pageBeliefs = {};
@@ -32,7 +54,6 @@ let activeStoryfragmentId = null;
32
54
 
33
55
  function waitForSessionReady() {
34
56
  return new Promise((resolve) => {
35
- // This event is fired by sse.ts after the handshake is complete.
36
57
  if (window.TRACTSTACK_CONFIG?.session?.isReady) {
37
58
  resolve();
38
59
  } else {
@@ -54,7 +75,7 @@ function initializeBeliefs() {
54
75
  '🔧 BELIEF: First-time initialization of belief handlers and HTMX config.'
55
76
  );
56
77
 
57
- configureHtmx(); // Run config on initial load.
78
+ configureHtmx();
58
79
 
59
80
  document.addEventListener('change', function (event) {
60
81
  const target = event.target;
@@ -96,7 +117,6 @@ async function handleBeliefChange(element) {
96
117
 
97
118
  trackBeliefState(beliefId, beliefValue);
98
119
 
99
- // Pass the current page's beliefs to the backend
100
120
  await sendBeliefUpdate({
101
121
  beliefId,
102
122
  beliefType,
@@ -110,7 +130,7 @@ async function sendBeliefUpdate(data) {
110
130
 
111
131
  try {
112
132
  const config = window.TRACTSTACK_CONFIG;
113
- if (!config || !config.sessionId) return; // Check for config and session ID
133
+ if (!config || !config.sessionId) return;
114
134
 
115
135
  if (VERBOSE)
116
136
  console.log('🚨 FRONTEND DEBUG: Sending belief update with headers:', {
@@ -170,7 +190,7 @@ function setActiveStoryFragment() {
170
190
  console.log(
171
191
  `📖 BELIEF: Active story fragment set to ${activeStoryfragmentId}`
172
192
  );
173
- // Ensure a state object exists for the newly active page
193
+
174
194
  if (!pageBeliefs[activeStoryfragmentId]) {
175
195
  pageBeliefs[activeStoryfragmentId] = {};
176
196
  }
@@ -184,7 +204,6 @@ document.addEventListener('astro:page-load', () => {
184
204
  setActiveStoryFragment();
185
205
  });
186
206
 
187
- // Also set the active story on the very first page load.
188
207
  document.addEventListener('DOMContentLoaded', setActiveStoryFragment);
189
208
 
190
209
  if (VERBOSE)
@@ -481,23 +481,41 @@ function processStoryfragmentUpdate(update) {
481
481
  log(`📊 Refresh summary: ${refreshedCount} successful, ${errorCount} failed`);
482
482
 
483
483
  if (update.gotoPaneId) {
484
- const targetElement = document.getElementById(`pane-${update.gotoPaneId}`);
485
- if (targetElement) {
486
- log(`🔍 Scrolling to target pane: ${update.gotoPaneId}`);
487
- try {
488
- targetElement.scrollIntoView({ behavior: 'smooth' });
489
- log('✅ Scroll completed successfully');
490
- } catch (error) {
491
- log('❌ Scroll failed:', error);
484
+ // Wait a brief moment for the DOM to update and the element to become visible.
485
+ setTimeout(() => {
486
+ const targetElement = document.getElementById(
487
+ `pane-${update.gotoPaneId}`
488
+ );
489
+ if (targetElement) {
490
+ log(`🔍 Smart scrolling to target pane: ${update.gotoPaneId}`);
491
+ try {
492
+ const elementRect = targetElement.getBoundingClientRect();
493
+ const viewportHeight = window.innerHeight;
494
+
495
+ // If the element is taller than the viewport, just scroll to the top of it.
496
+ if (elementRect.height > viewportHeight) {
497
+ targetElement.scrollIntoView({
498
+ behavior: 'smooth',
499
+ block: 'start',
500
+ });
501
+ log('✅ Scroll completed (long element - align to top).');
502
+ } else {
503
+ // Otherwise, center it in the viewport.
504
+ targetElement.scrollIntoView({
505
+ behavior: 'smooth',
506
+ block: 'center',
507
+ });
508
+ log('✅ Scroll completed (short element - align to center).');
509
+ }
510
+ } catch (error) {
511
+ log('❌ Smart scroll failed:', error);
512
+ }
513
+ } else {
514
+ log(
515
+ `⚠️ Target pane element not found after delay: pane-${update.gotoPaneId}`
516
+ );
492
517
  }
493
- } else {
494
- log(`⚠️ Target pane element not found: pane-${update.gotoPaneId}`, {
495
- expectedId: `pane-${update.gotoPaneId}`,
496
- availablePaneElements: Array.from(
497
- document.querySelectorAll('[id^="pane-"]')
498
- ).map((el) => el.id),
499
- });
500
- }
518
+ }, 100);
501
519
  }
502
520
 
503
521
  log('🔄 === UPDATE PROCESSING COMPLETE ===');
@@ -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
 
@@ -18,22 +18,30 @@ import ArrowUturnLeftIcon from '@heroicons/react/24/outline/ArrowUturnLeftIcon';
18
18
  import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
19
19
  import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
20
20
  import ChevronUpDownIcon from '@heroicons/react/24/outline/ChevronUpDownIcon';
21
+ import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
22
+ import ArrowUpIcon from '@heroicons/react/24/outline/ArrowUpIcon';
23
+ import ArrowDownIcon from '@heroicons/react/24/outline/ArrowDownIcon';
21
24
 
22
25
  interface DisclosureItem {
23
26
  id: string;
24
27
  beliefValue: string;
28
+ isCustom: boolean;
25
29
  title: string;
26
30
  description?: string;
27
31
  icon: string;
28
32
  actionLisp: string;
29
33
  isDisabled?: boolean;
30
34
  }
35
+
31
36
  interface WidgetStyles {
32
37
  textColor: string;
33
38
  bgColor: string;
34
39
  bgOpacity: number;
35
40
  }
36
- type StoredDisclosureItem = Omit<DisclosureItem, 'id' | 'isDisabled'>;
41
+ type StoredDisclosureItem = Omit<
42
+ DisclosureItem,
43
+ 'id' | 'isDisabled' | 'isCustom'
44
+ >;
37
45
  interface InteractiveDisclosureWidgetProps {
38
46
  node: FlatNode;
39
47
  onUpdate: (params: string[]) => void;
@@ -42,6 +50,13 @@ interface InteractiveDisclosureWidgetProps {
42
50
 
43
51
  const generateId = (): string => Math.random().toString(36).substring(2, 9);
44
52
 
53
+ const quoteIfNecessary = (command: string, value: string): string => {
54
+ if (command === 'identifyAs' && value.includes(' ')) {
55
+ return `"${value}"`;
56
+ }
57
+ return value;
58
+ };
59
+
45
60
  const IconSelector = ({
46
61
  value,
47
62
  onChange,
@@ -61,12 +76,7 @@ const IconSelector = ({
61
76
  () => createListCollection({ items: filteredIcons }),
62
77
  [filteredIcons]
63
78
  );
64
-
65
- const iconSelectorStyles = `
66
- .icon-item .icon-indicator { display: none; }
67
- .icon-item[data-state="checked"] .icon-indicator { display: flex; }
68
- `;
69
-
79
+ const iconSelectorStyles = `.icon-item .icon-indicator { display: none; } .icon-item[data-state="checked"] .icon-indicator { display: flex; }`;
70
80
  return (
71
81
  <div>
72
82
  <style>{iconSelectorStyles}</style>
@@ -88,8 +98,8 @@ const IconSelector = ({
88
98
  </Combobox.Trigger>
89
99
  </Combobox.Control>
90
100
  <Portal>
91
- <Combobox.Positioner style={{ zIndex: 9010 }}>
92
- <Combobox.Content className="max-h-60 w-[--reference-width] overflow-y-auto rounded-md bg-white shadow-lg">
101
+ <Combobox.Positioner style={{ zIndex: 9010, minWidth: '250px' }}>
102
+ <Combobox.Content className="max-h-60 w-full overflow-y-auto rounded-md bg-white shadow-lg">
93
103
  {filteredIcons.map((icon) => (
94
104
  <Combobox.Item
95
105
  key={icon}
@@ -117,27 +127,59 @@ const DisclosureItemEditor = ({
117
127
  onUpdate,
118
128
  onToggle,
119
129
  config,
130
+ onMoveUp,
131
+ onMoveDown,
132
+ isFirst,
133
+ isLast,
120
134
  }: {
121
135
  item: DisclosureItem;
122
136
  onUpdate: (updates: Partial<DisclosureItem>) => void;
123
137
  onToggle: () => void;
124
138
  config: BrandConfig;
139
+ onMoveUp: () => void;
140
+ onMoveDown: () => void;
141
+ isFirst: boolean;
142
+ isLast: boolean;
125
143
  }) => {
126
144
  return (
127
145
  <div
128
- className={`space-y-4 rounded-lg border bg-white p-4 shadow-sm transition-opacity ${item.isDisabled ? 'border-gray-100 opacity-40' : 'border-gray-200'}`}
146
+ className={`space-y-4 rounded-lg border bg-white p-4 shadow-sm transition-opacity ${
147
+ item.isDisabled ? 'border-gray-100 opacity-40' : 'border-gray-200'
148
+ }`}
129
149
  >
130
150
  <div className="flex items-center justify-between">
131
- <h4 className="font-bold text-gray-800">
132
- {item.title}{' '}
133
- <span className="text-xs font-normal text-gray-500">
134
- (for value: {item.beliefValue})
135
- </span>
136
- </h4>
151
+ <div className="flex items-center gap-2">
152
+ <div className="flex flex-col">
153
+ <button
154
+ type="button"
155
+ onClick={onMoveUp}
156
+ disabled={isFirst}
157
+ className="rounded p-0.5 text-gray-500 hover:bg-gray-100 disabled:opacity-25"
158
+ >
159
+ <ArrowUpIcon className="h-4 w-4" />
160
+ </button>
161
+ <button
162
+ type="button"
163
+ onClick={onMoveDown}
164
+ disabled={isLast}
165
+ className="rounded p-0.5 text-gray-500 hover:bg-gray-100 disabled:opacity-25"
166
+ >
167
+ <ArrowDownIcon className="h-4 w-4" />
168
+ </button>
169
+ </div>
170
+ <h4 className="font-bold text-gray-800">
171
+ {item.title}{' '}
172
+ <span className="text-xs font-normal text-gray-500">
173
+ (Key: {item.beliefValue})
174
+ </span>
175
+ </h4>
176
+ </div>
137
177
  <button
138
178
  type="button"
139
179
  onClick={onToggle}
140
- className={`rounded p-1 hover:bg-gray-100 ${item.isDisabled ? 'text-blue-600' : 'text-red-600'}`}
180
+ className={`rounded p-1 hover:bg-gray-100 ${
181
+ item.isDisabled ? 'text-blue-600' : 'text-red-600'
182
+ }`}
141
183
  >
142
184
  {item.isDisabled ? (
143
185
  <ArrowUturnLeftIcon className="h-4 w-4" />
@@ -147,6 +189,13 @@ const DisclosureItemEditor = ({
147
189
  </button>
148
190
  </div>
149
191
  <fieldset disabled={item.isDisabled} className="space-y-4">
192
+ {item.isCustom && (
193
+ <SingleParam
194
+ label="Key / Value"
195
+ value={item.beliefValue}
196
+ onChange={(value) => onUpdate({ beliefValue: value })}
197
+ />
198
+ )}
150
199
  <SingleParam
151
200
  label="Display Title"
152
201
  value={item.title}
@@ -161,13 +210,25 @@ const DisclosureItemEditor = ({
161
210
  value={item.icon}
162
211
  onChange={(value) => onUpdate({ icon: value })}
163
212
  />
164
- <div className="relative rounded-md border p-3">
165
- <ActionBuilderField
166
- value={item.actionLisp}
167
- onChange={(value) => onUpdate({ actionLisp: value })}
168
- contentMap={fullContentMapStore.get()}
169
- />
170
- </div>
213
+
214
+ {item.isCustom ? (
215
+ <div className="relative rounded-md border p-3">
216
+ <ActionBuilderField
217
+ value={item.actionLisp}
218
+ onChange={(value) => onUpdate({ actionLisp: value })}
219
+ contentMap={fullContentMapStore.get()}
220
+ />
221
+ </div>
222
+ ) : (
223
+ <div>
224
+ <label className="block text-xs font-bold text-gray-600">
225
+ Action (Locked)
226
+ </label>
227
+ <div className="mt-1 rounded-md border border-gray-200 bg-gray-50 p-2 font-mono text-xs text-gray-500">
228
+ {item.actionLisp}
229
+ </div>
230
+ </div>
231
+ )}
171
232
  </fieldset>
172
233
  </div>
173
234
  );
@@ -187,6 +248,7 @@ export default function InteractiveDisclosureWidget({
187
248
  bgOpacity: 100,
188
249
  });
189
250
  const [isModalOpen, setIsModalOpen] = useState(false);
251
+ const [isDataLoaded, setIsDataLoaded] = useState(false);
190
252
 
191
253
  const selectedBelief = beliefs.find((b) => b.slug === selectedBeliefTag);
192
254
  const hasRealSelection = !!selectedBelief;
@@ -194,8 +256,12 @@ export default function InteractiveDisclosureWidget({
194
256
  useEffect(() => {
195
257
  const beliefTag = String(node.codeHookParams?.[0] || '');
196
258
  const payloadJson = String(node.codeHookParams?.[1] || '');
197
- setSelectedBeliefTag(beliefTag && beliefTag !== 'BELIEF' ? beliefTag : '');
198
259
 
260
+ if (beliefs.length === 0 && beliefTag && beliefTag !== 'BELIEF') {
261
+ return;
262
+ }
263
+
264
+ setSelectedBeliefTag(beliefTag && beliefTag !== 'BELIEF' ? beliefTag : '');
199
265
  const currentBelief = beliefs.find((b) => b.slug === beliefTag);
200
266
 
201
267
  if (payloadJson && currentBelief) {
@@ -204,9 +270,10 @@ export default function InteractiveDisclosureWidget({
204
270
  setWidgetStyles(
205
271
  parsed.styles || { textColor: '', bgColor: '', bgOpacity: 100 }
206
272
  );
207
- const loadedDisclosures = parsed.disclosures || {};
273
+ const loadedDisclosures =
274
+ (parsed.disclosures as StoredDisclosureItem[]) || [];
208
275
 
209
- const possibleKeys =
276
+ const scaleKeys =
210
277
  currentBelief.scale === 'custom'
211
278
  ? (currentBelief.customValues || []).map((v) => ({
212
279
  slug: v,
@@ -216,34 +283,47 @@ export default function InteractiveDisclosureWidget({
216
283
  currentBelief.scale as keyof typeof heldBeliefsScales
217
284
  ] || [];
218
285
 
219
- const allDisclosures = possibleKeys.map(({ slug, name }) => {
220
- if (loadedDisclosures[slug]) {
286
+ const actionCommand =
287
+ currentBelief.scale === 'custom' ? 'identifyAs' : 'declare';
288
+ const finalDisclosures: DisclosureItem[] = loadedDisclosures.map(
289
+ (loadedItem) => {
290
+ const isFromScale = scaleKeys.some(
291
+ (sk) => sk.slug === loadedItem.beliefValue
292
+ );
221
293
  return {
222
- ...(loadedDisclosures[slug] as StoredDisclosureItem),
294
+ ...loadedItem,
223
295
  id: generateId(),
224
- beliefValue: slug,
296
+ isCustom: !isFromScale,
297
+ actionLisp: isFromScale
298
+ ? `(${actionCommand} ${beliefTag} ${quoteIfNecessary(actionCommand, loadedItem.beliefValue)})`
299
+ : loadedItem.actionLisp,
225
300
  isDisabled: false,
226
301
  };
227
302
  }
228
- return {
229
- id: generateId(),
230
- beliefValue: slug,
231
- title: name,
232
- description: '',
233
- icon: 'app',
234
- actionLisp: '',
235
- isDisabled: true,
236
- };
303
+ );
304
+ scaleKeys.forEach(({ slug, name }) => {
305
+ if (!finalDisclosures.some((d) => d.beliefValue === slug)) {
306
+ finalDisclosures.push({
307
+ id: generateId(),
308
+ beliefValue: slug,
309
+ title: name,
310
+ description: '',
311
+ icon: 'app',
312
+ actionLisp: `(${actionCommand} ${beliefTag} ${quoteIfNecessary(actionCommand, slug)})`,
313
+ isCustom: false,
314
+ isDisabled: true,
315
+ });
316
+ }
237
317
  });
238
- setDisclosures(allDisclosures);
318
+ setDisclosures(finalDisclosures);
239
319
  } catch (e) {
240
- setDisclosures([]);
241
- setWidgetStyles({ textColor: '', bgColor: '', bgOpacity: 100 });
320
+ console.error('Error parsing disclosure payload:', e);
242
321
  }
243
322
  } else {
244
323
  setDisclosures([]);
245
324
  setWidgetStyles({ textColor: '', bgColor: '', bgOpacity: 100 });
246
325
  }
326
+ setIsDataLoaded(true);
247
327
  }, [node, beliefs]);
248
328
 
249
329
  useEffect(() => {
@@ -253,30 +333,26 @@ export default function InteractiveDisclosureWidget({
253
333
  const {
254
334
  data: { beliefIds },
255
335
  } = await api.get('/api/v1/nodes/beliefs');
256
- if (!beliefIds?.length) return;
336
+ if (!beliefIds?.length) {
337
+ setBeliefs([]);
338
+ return;
339
+ }
257
340
  const {
258
341
  data: { beliefs },
259
342
  } = await api.post('/api/v1/nodes/beliefs', { beliefIds });
260
343
  setBeliefs(beliefs || []);
261
344
  } catch (error) {
262
345
  console.error('Error fetching beliefs:', error);
346
+ setBeliefs([]);
263
347
  }
264
348
  };
265
349
  fetchData();
266
- }, []);
350
+ }, [node]);
267
351
 
268
352
  const handleUpdate = () => {
269
- const disclosuresToStore: Record<
270
- string,
271
- Omit<StoredDisclosureItem, 'beliefValue'>
272
- > = {};
273
- disclosures
353
+ const disclosuresToStore: StoredDisclosureItem[] = disclosures
274
354
  .filter((d) => !d.isDisabled)
275
- .forEach(({ id, beliefValue, isDisabled, ...rest }) => {
276
- if (beliefValue) {
277
- disclosuresToStore[beliefValue] = rest;
278
- }
279
- });
355
+ .map(({ id, isCustom, isDisabled, ...rest }) => rest);
280
356
  const payload = { styles: widgetStyles, disclosures: disclosuresToStore };
281
357
  onUpdate([selectedBeliefTag, JSON.stringify(payload)]);
282
358
  };
@@ -286,30 +362,62 @@ export default function InteractiveDisclosureWidget({
286
362
  const belief = beliefs.find((b) => b.slug === tag);
287
363
  let newDisclosures: DisclosureItem[] = [];
288
364
  if (belief) {
365
+ const actionCommand =
366
+ belief.scale === 'custom' ? 'identifyAs' : 'declare';
289
367
  const keys =
290
368
  belief.scale === 'custom'
291
369
  ? (belief.customValues || []).map((v) => ({ slug: v, name: v }))
292
370
  : heldBeliefsScales[belief.scale as keyof typeof heldBeliefsScales] ||
293
371
  [];
372
+
294
373
  newDisclosures = keys.map(({ slug, name }) => ({
295
374
  id: generateId(),
296
375
  beliefValue: slug,
297
376
  title: name,
298
377
  description: '',
299
378
  icon: 'app',
300
- actionLisp: '',
379
+ actionLisp: `(${actionCommand} ${tag} ${quoteIfNecessary(actionCommand, slug)})`,
380
+ isCustom: false,
301
381
  isDisabled: false,
302
382
  }));
303
383
  }
304
384
  setDisclosures(newDisclosures);
305
385
  };
306
386
 
387
+ const moveDisclosure = (id: string, direction: 'up' | 'down') => {
388
+ const index = disclosures.findIndex((d) => d.id === id);
389
+ if (index === -1) return;
390
+ const newIndex = direction === 'up' ? index - 1 : index + 1;
391
+ if (newIndex < 0 || newIndex >= disclosures.length) return;
392
+
393
+ const newDisclosures = [...disclosures];
394
+ const [movedItem] = newDisclosures.splice(index, 1);
395
+ newDisclosures.splice(newIndex, 0, movedItem);
396
+ setDisclosures(newDisclosures);
397
+ };
398
+
399
+ const addCustomDisclosure = () => {
400
+ const newItem: DisclosureItem = {
401
+ id: generateId(),
402
+ beliefValue: `custom-key-${disclosures.length + 1}`,
403
+ title: 'New Custom Item',
404
+ description: '',
405
+ icon: 'plus-circle',
406
+ actionLisp: '',
407
+ isCustom: true,
408
+ isDisabled: false,
409
+ };
410
+ setDisclosures([...disclosures, newItem]);
411
+ };
412
+
307
413
  const updateDisclosure = (id: string, updates: Partial<DisclosureItem>) =>
308
414
  setDisclosures(
309
415
  disclosures.map((d) => (d.id === id ? { ...d, ...updates } : d))
310
416
  );
417
+
311
418
  const updateWidgetStyles = (updates: Partial<WidgetStyles>) =>
312
419
  setWidgetStyles((prev) => ({ ...prev, ...updates }));
420
+
313
421
  const toggleDisclosure = (id: string) =>
314
422
  setDisclosures(
315
423
  disclosures.map((d) =>
@@ -317,6 +425,13 @@ export default function InteractiveDisclosureWidget({
317
425
  )
318
426
  );
319
427
 
428
+ const handleColorChange = (
429
+ key: 'textColor' | 'bgColor',
430
+ hex: string | null
431
+ ) => {
432
+ updateWidgetStyles({ [key]: hex || '' });
433
+ };
434
+
320
435
  return (
321
436
  <div className="space-y-4">
322
437
  <div className="flex items-center gap-2">
@@ -347,7 +462,6 @@ export default function InteractiveDisclosureWidget({
347
462
  </button>
348
463
  )}
349
464
  </div>
350
-
351
465
  {hasRealSelection && (
352
466
  <div className="mt-4 border-t border-gray-200 pt-4">
353
467
  <button
@@ -393,64 +507,94 @@ export default function InteractiveDisclosureWidget({
393
507
  </Dialog.Title>
394
508
  </div>
395
509
  <div className="flex-1 space-y-6 overflow-y-auto p-4">
396
- <div className="space-y-4 rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
397
- <h3 className="font-bold text-gray-800">Widget Styles</h3>
398
- <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
399
- <div>
400
- <ColorPickerCombo
401
- title="Background Color"
402
- defaultColor={widgetStyles.bgColor}
403
- onColorChange={(hex) =>
404
- updateWidgetStyles({ bgColor: hex })
405
- }
406
- config={config}
407
- allowNull={true}
408
- />
510
+ {isDataLoaded ? (
511
+ <>
512
+ <div className="space-y-4 rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
513
+ <h3 className="font-bold text-gray-800">
514
+ Widget Styles
515
+ </h3>
516
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
517
+ <div>
518
+ <ColorPickerCombo
519
+ title="Background Color"
520
+ defaultColor={widgetStyles.bgColor}
521
+ onColorChange={(hex) =>
522
+ handleColorChange('bgColor', hex)
523
+ }
524
+ config={config}
525
+ allowNull={true}
526
+ skipTailwind={false}
527
+ />
528
+ </div>
529
+ <div>
530
+ <ColorPickerCombo
531
+ title="Text Color"
532
+ defaultColor={widgetStyles.textColor}
533
+ onColorChange={(hex) =>
534
+ handleColorChange('textColor', hex)
535
+ }
536
+ config={config}
537
+ allowNull={true}
538
+ skipTailwind={false}
539
+ />
540
+ </div>
541
+ <div>
542
+ <label className="block text-xs font-bold text-gray-600">
543
+ BG Opacity (%)
544
+ </label>
545
+ <div className="mt-1 flex items-center gap-2">
546
+ <input
547
+ type="range"
548
+ min="0"
549
+ max="100"
550
+ value={widgetStyles.bgOpacity}
551
+ onChange={(e) =>
552
+ updateWidgetStyles({
553
+ bgOpacity: parseInt(e.target.value),
554
+ })
555
+ }
556
+ className="w-full"
557
+ />
558
+ <span className="w-12 text-center font-mono text-sm">
559
+ {widgetStyles.bgOpacity}%
560
+ </span>
561
+ </div>
562
+ </div>
563
+ </div>
409
564
  </div>
410
- <div>
411
- <ColorPickerCombo
412
- title="Text Color"
413
- defaultColor={widgetStyles.textColor}
414
- onColorChange={(hex) =>
415
- updateWidgetStyles({ textColor: hex })
565
+
566
+ {disclosures.map((item, index) => (
567
+ <DisclosureItemEditor
568
+ key={item.id}
569
+ item={item}
570
+ onUpdate={(updates) =>
571
+ updateDisclosure(item.id, updates)
416
572
  }
573
+ onToggle={() => toggleDisclosure(item.id)}
417
574
  config={config}
418
- allowNull={true}
575
+ onMoveUp={() => moveDisclosure(item.id, 'up')}
576
+ onMoveDown={() => moveDisclosure(item.id, 'down')}
577
+ isFirst={index === 0}
578
+ isLast={index === disclosures.length - 1}
419
579
  />
580
+ ))}
581
+
582
+ <div className="pt-4">
583
+ <button
584
+ type="button"
585
+ onClick={addCustomDisclosure}
586
+ className="flex w-full items-center justify-center rounded-md border-2 border-dashed border-gray-300 bg-white px-3 py-2 text-sm font-bold text-gray-500 hover:border-cyan-600 hover:text-cyan-600"
587
+ >
588
+ <PlusIcon className="mr-2 h-5 w-5" />
589
+ Add Custom Disclosure
590
+ </button>
420
591
  </div>
421
- <div>
422
- <label className="block text-xs font-bold text-gray-600">
423
- BG Opacity (%)
424
- </label>
425
- <div className="mt-1 flex items-center gap-2">
426
- <input
427
- type="range"
428
- min="0"
429
- max="100"
430
- value={widgetStyles.bgOpacity}
431
- onChange={(e) =>
432
- updateWidgetStyles({
433
- bgOpacity: parseInt(e.target.value),
434
- })
435
- }
436
- className="w-full"
437
- />
438
- <span className="w-12 text-center font-mono text-sm">
439
- {widgetStyles.bgOpacity}%
440
- </span>
441
- </div>
442
- </div>
592
+ </>
593
+ ) : (
594
+ <div className="p-8 text-center">
595
+ Loading configuration...
443
596
  </div>
444
- </div>
445
- {disclosures.map((item) => (
446
- <DisclosureItemEditor
447
- key={item.id}
448
- item={item}
449
- onUpdate={(updates) => updateDisclosure(item.id, updates)}
450
- onToggle={() => toggleDisclosure(item.id)}
451
- config={config}
452
- />
453
- ))}
597
+ )}
454
598
  </div>
455
599
  <div className="flex-shrink-0 justify-end border-t border-gray-200 bg-white px-6 py-3">
456
600
  <Dialog.CloseTrigger asChild>
@@ -80,10 +80,27 @@ export default function ActionBuilderField({
80
80
 
81
81
  const handleParamChange = (newParams: string) => {
82
82
  setParams(newParams);
83
- if (newParams && newParams.trim() !== '' && newParams.trim() !== '()') {
84
- onChange(`(${command} ${newParams})`);
85
- } else {
83
+ const trimmedParams = newParams.trim();
84
+
85
+ if (!trimmedParams || trimmedParams === '()') {
86
86
  onChange('');
87
+ return;
88
+ }
89
+
90
+ if (command === 'identifyAs') {
91
+ const firstSpaceIndex = trimmedParams.indexOf(' ');
92
+ if (firstSpaceIndex === -1) {
93
+ // Handle case with only beliefId and no value
94
+ onChange(`(${command} ${trimmedParams})`);
95
+ } else {
96
+ const beliefId = trimmedParams.substring(0, firstSpaceIndex);
97
+ const value = trimmedParams.substring(firstSpaceIndex + 1);
98
+ const finalValue = value.includes(' ') ? `"${value}"` : value;
99
+ onChange(`(${command} ${beliefId} ${finalValue})`);
100
+ }
101
+ } else {
102
+ // Original behavior for all other commands
103
+ onChange(`(${command} ${trimmedParams})`);
87
104
  }
88
105
  };
89
106
 
@@ -37,16 +37,14 @@ export default function BeliefForm({
37
37
  onClose,
38
38
  }: BeliefFormProps) {
39
39
  const [customValue, setCustomValue] = useState('');
40
+ const [customValueError, setCustomValueError] = useState<string | null>(null);
40
41
 
41
- // Subscribe to orphan analysis store
42
42
  const orphanState = useStore(orphanAnalysisStore);
43
43
 
44
- // Load orphan analysis on component mount
45
44
  useEffect(() => {
46
45
  loadOrphanAnalysis();
47
46
  }, []);
48
47
 
49
- // Get usage information for this belief
50
48
  const getBeliefUsage = (): string[] => {
51
49
  if (!belief?.id || !orphanState.data || !orphanState.data.beliefs) {
52
50
  return [];
@@ -54,7 +52,6 @@ export default function BeliefForm({
54
52
  return orphanState.data.beliefs[belief.id] || [];
55
53
  };
56
54
 
57
- // Check if belief is in use
58
55
  const isBeliefInUse = (): boolean => {
59
56
  if (isCreate || !belief?.id) return false;
60
57
  return getBeliefUsage().length > 0;
@@ -63,7 +60,6 @@ export default function BeliefForm({
63
60
  const beliefInUse = isBeliefInUse();
64
61
  const usageCount = getBeliefUsage().length;
65
62
 
66
- // Initialize form state
67
63
  const initialState: BeliefNodeState = belief
68
64
  ? convertToLocalState(belief)
69
65
  : {
@@ -85,7 +81,6 @@ export default function BeliefForm({
85
81
  data
86
82
  );
87
83
 
88
- // Call success callback after save (original pattern)
89
84
  setTimeout(() => {
90
85
  onClose?.(true);
91
86
  }, 1000);
@@ -102,23 +97,30 @@ export default function BeliefForm({
102
97
  },
103
98
  });
104
99
 
105
- const handleAddCustomValue = () => {
106
- if (!customValue.trim()) return;
100
+ const handleCustomValueChange = (value: string) => {
101
+ setCustomValue(value);
102
+ const valueRegex = /^[a-zA-Z]([a-zA-Z0-9?!]| (?=[a-zA-Z0-9?!]))*$/;
103
+ if (value && !valueRegex.test(value)) {
104
+ setCustomValueError(
105
+ 'Must start with a letter. No double or trailing spaces.'
106
+ );
107
+ } else {
108
+ setCustomValueError(null);
109
+ }
110
+ };
107
111
 
112
+ const handleAddCustomValue = () => {
113
+ if (!customValue.trim() || customValueError) return;
108
114
  const newState = addCustomValue(formState.state, customValue);
109
115
  formState.updateField('customValues', newState.customValues);
110
116
  setCustomValue('');
111
117
  };
112
118
 
113
119
  const handleRemoveCustomValue = (index: number) => {
114
- // Check if this is a newly added value (not saved yet)
115
120
  const currentValue = formState.state.customValues[index];
116
121
  const originalValues = formState.originalState.customValues || [];
117
122
  const isNewValue = !originalValues.includes(currentValue);
118
123
 
119
- // Allow removal if:
120
- // 1. Belief is not in use, OR
121
- // 2. This is a new value that hasn't been saved yet
122
124
  if (!beliefInUse || isNewValue) {
123
125
  const newState = removeCustomValue(formState.state, index);
124
126
  formState.updateField('customValues', newState.customValues);
@@ -188,7 +190,6 @@ export default function BeliefForm({
188
190
 
189
191
  return (
190
192
  <div className="space-y-8">
191
- {/* Header */}
192
193
  <div className="border-b border-gray-200 pb-4">
193
194
  <h2 className="text-2xl font-bold text-gray-900">
194
195
  {isCreate ? 'Create Belief' : 'Edit Belief'}
@@ -200,10 +201,8 @@ export default function BeliefForm({
200
201
  </p>
201
202
  </div>
202
203
 
203
- {/* Usage Warning */}
204
204
  {renderUsageWarning()}
205
205
 
206
- {/* Info Box */}
207
206
  <div className="rounded-md bg-blue-50 p-4">
208
207
  <div className="text-sm text-blue-700">
209
208
  <p className="font-bold">What are Beliefs?</p>
@@ -225,7 +224,6 @@ export default function BeliefForm({
225
224
  </div>
226
225
  </div>
227
226
 
228
- {/* Basic Fields */}
229
227
  <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
230
228
  <StringInput
231
229
  value={formState.state.title}
@@ -254,7 +252,6 @@ export default function BeliefForm({
254
252
  </div>
255
253
  </div>
256
254
 
257
- {/* Scale Selection */}
258
255
  <div className="space-y-4">
259
256
  <div className="relative">
260
257
  <EnumSelect
@@ -276,7 +273,6 @@ export default function BeliefForm({
276
273
  {renderScalePreview()}
277
274
  </div>
278
275
 
279
- {/* Custom Values Section */}
280
276
  {formState.state.scale === 'custom' && (
281
277
  <div className="space-y-4">
282
278
  <div>
@@ -286,31 +282,41 @@ export default function BeliefForm({
286
282
  </p>
287
283
  </div>
288
284
 
289
- {/* Add Custom Value */}
290
285
  <div className="flex gap-2">
291
286
  <div className="flex-1">
292
- <div className="flex-1">
293
- <input
294
- type="text"
295
- value={customValue}
296
- onChange={(e) => setCustomValue(e.target.value)}
297
- onKeyDown={handleKeyDown}
298
- placeholder="Enter custom value"
299
- className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-cyan-600 sm:text-sm sm:leading-6"
300
- />
301
- </div>
287
+ <input
288
+ type="text"
289
+ value={customValue}
290
+ onChange={(e) => handleCustomValueChange(e.target.value)}
291
+ onKeyDown={handleKeyDown}
292
+ placeholder="Enter custom value"
293
+ className={`block w-full rounded-md border-0 px-3 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset placeholder:text-gray-400 focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 ${
294
+ customValueError
295
+ ? 'ring-red-500 focus:ring-red-600'
296
+ : 'ring-gray-300 focus:ring-cyan-600'
297
+ }`}
298
+ />
302
299
  </div>
303
300
  <button
304
301
  type="button"
305
302
  onClick={handleAddCustomValue}
306
- disabled={!customValue.trim()}
303
+ disabled={!customValue.trim() || !!customValueError}
307
304
  className="inline-flex items-center rounded-md bg-cyan-600 px-3 py-2 text-sm font-bold text-white shadow-sm hover:bg-cyan-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-600 disabled:cursor-not-allowed disabled:opacity-50"
308
305
  >
309
306
  <PlusIcon className="h-4 w-4" />
310
307
  </button>
311
308
  </div>
312
309
 
313
- {/* Custom Values List */}
310
+ {customValueError && (
311
+ <p className="mt-1 text-sm text-red-600">{customValueError}</p>
312
+ )}
313
+
314
+ {formState.errors.customValues && (
315
+ <p className="text-sm text-red-600">
316
+ {formState.errors.customValues}
317
+ </p>
318
+ )}
319
+
314
320
  {formState.state.customValues.length > 0 && (
315
321
  <div className="space-y-2">
316
322
  {formState.state.customValues.map((value, index) => {
@@ -355,7 +361,6 @@ export default function BeliefForm({
355
361
  </div>
356
362
  )}
357
363
 
358
- {/* Save/Cancel Bar */}
359
364
  <UnsavedChangesBar
360
365
  formState={formState}
361
366
  message="You have unsaved belief changes"
@@ -363,7 +368,6 @@ export default function BeliefForm({
363
368
  cancelLabel="Discard Changes"
364
369
  />
365
370
 
366
- {/* Cancel Navigation Button */}
367
371
  <div className="flex justify-start">
368
372
  <button
369
373
  type="button"
@@ -17,6 +17,9 @@ export const preParseAction = (
17
17
  //const parameterFour = (parameters && parameters[3]) || null;
18
18
 
19
19
  switch (command) {
20
+ case `declare`:
21
+ case `identifyAs`:
22
+ return ``;
20
23
  case `goto`:
21
24
  switch (parameterOne) {
22
25
  case `storykeep`:
@@ -4,9 +4,6 @@ import type {
4
4
  FieldErrors,
5
5
  } from '@/types/tractstack';
6
6
 
7
- /**
8
- * Convert backend BeliefNode to frontend BeliefNodeState
9
- */
10
7
  export function convertToLocalState(beliefNode: BeliefNode): BeliefNodeState {
11
8
  return {
12
9
  id: beliefNode.id,
@@ -17,9 +14,6 @@ export function convertToLocalState(beliefNode: BeliefNode): BeliefNodeState {
17
14
  };
18
15
  }
19
16
 
20
- /**
21
- * Convert frontend BeliefNodeState to backend BeliefNode format
22
- */
23
17
  export function convertToBackendFormat(state: BeliefNodeState): BeliefNode {
24
18
  return {
25
19
  id: state.id,
@@ -31,47 +25,45 @@ export function convertToBackendFormat(state: BeliefNodeState): BeliefNode {
31
25
  };
32
26
  }
33
27
 
34
- /**
35
- * Validate belief node state
36
- */
37
28
  export function validateBeliefNode(state: BeliefNodeState): FieldErrors {
38
29
  const errors: FieldErrors = {};
39
30
 
40
- // Validate title
41
31
  if (!state.title?.trim()) {
42
32
  errors.title = 'Title is required';
43
33
  }
44
34
 
45
- // Validate slug
46
35
  if (!state.slug?.trim()) {
47
36
  errors.slug = 'Slug is required';
37
+ } else {
38
+ const slugRegex = /^[a-zA-Z]+$/;
39
+ if (!slugRegex.test(state.slug)) {
40
+ errors.slug = 'Slug must contain only letters (a-z, A-Z)';
41
+ }
48
42
  }
49
43
 
50
- // Validate scale
51
44
  if (!state.scale?.trim()) {
52
45
  errors.scale = 'Scale is required';
53
46
  }
54
47
 
55
- // Validate custom values if scale is custom
56
48
  if (state.scale === 'custom') {
57
49
  if (!state.customValues || state.customValues.length === 0) {
58
50
  errors.customValues =
59
51
  'At least one custom value is required for custom scale';
60
52
  } else {
61
- state.customValues.forEach((value, index) => {
62
- if (!value?.trim()) {
63
- errors[`customValues.${index}`] = 'Custom value cannot be empty';
53
+ const valueRegex = /^[a-zA-Z]([a-zA-Z0-9?!]| (?=[a-zA-Z0-9?!]))*$/;
54
+ for (const value of state.customValues) {
55
+ if (value.trim() && !valueRegex.test(value)) {
56
+ errors.customValues =
57
+ 'Values must start with a letter, have no double or trailing spaces, and use valid characters.';
58
+ break;
64
59
  }
65
- });
60
+ }
66
61
  }
67
62
  }
68
63
 
69
64
  return errors;
70
65
  }
71
66
 
72
- /**
73
- * State interceptor for form state management
74
- */
75
67
  export function beliefStateIntercept(
76
68
  state: BeliefNodeState,
77
69
  field: keyof BeliefNodeState,
@@ -88,7 +80,6 @@ export function beliefStateIntercept(
88
80
  break;
89
81
  case 'scale':
90
82
  newState.scale = value || '';
91
- // Clear custom values when scale changes away from custom
92
83
  if (value !== 'custom') {
93
84
  newState.customValues = [];
94
85
  }
@@ -103,9 +94,6 @@ export function beliefStateIntercept(
103
94
  return newState;
104
95
  }
105
96
 
106
- /**
107
- * Add a new custom value to the state
108
- */
109
97
  export function addCustomValue(
110
98
  state: BeliefNodeState,
111
99
  value: string
@@ -118,9 +106,6 @@ export function addCustomValue(
118
106
  };
119
107
  }
120
108
 
121
- /**
122
- * Remove a custom value from the state
123
- */
124
109
  export function removeCustomValue(
125
110
  state: BeliefNodeState,
126
111
  index: number
@@ -131,9 +116,6 @@ export function removeCustomValue(
131
116
  };
132
117
  }
133
118
 
134
- /**
135
- * Update a specific custom value in the state
136
- */
137
119
  export function updateCustomValue(
138
120
  state: BeliefNodeState,
139
121
  index: number,
@@ -148,9 +130,6 @@ export function updateCustomValue(
148
130
  };
149
131
  }
150
132
 
151
- /**
152
- * Scale options for the belief form
153
- */
154
133
  export const SCALE_OPTIONS = [
155
134
  { value: 'likert', label: 'Likert Scale (1-5)' },
156
135
  { value: 'agreement', label: 'Agreement (Agree/Disagree)' },
@@ -160,9 +139,6 @@ export const SCALE_OPTIONS = [
160
139
  { value: 'custom', label: 'Custom Values' },
161
140
  ];
162
141
 
163
- /**
164
- * Get scale preview data for displaying scale options
165
- */
166
142
  export function getScalePreview(scale: string) {
167
143
  const scalePreviewData: {
168
144
  [key: string]: Array<{ id: number; name: string; color: string }>;