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 +2 -2
- package/templates/src/client/belief-events.js +29 -10
- package/templates/src/client/sse.js +34 -16
- package/templates/src/components/Menu.tsx +22 -26
- package/templates/src/components/edit/pane/PanePanel_path.tsx +0 -2
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +253 -109
- package/templates/src/components/form/ActionBuilderField.tsx +20 -3
- package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +37 -33
- package/templates/src/utils/actions/preParse_Action.ts +3 -0
- package/templates/src/utils/api/beliefHelpers.ts +12 -36
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-tractstack",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
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.
|
|
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)
|
|
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;
|
|
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();
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const tokens =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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:
|
|
106
|
-
beliefType: 'Belief',
|
|
107
|
-
beliefValue:
|
|
105
|
+
beliefId: beliefId,
|
|
106
|
+
beliefType: 'Belief',
|
|
107
|
+
beliefValue: value,
|
|
108
108
|
};
|
|
109
109
|
} else if (command === 'identifyAs') {
|
|
110
110
|
hxValsMap = {
|
|
111
|
-
beliefId:
|
|
112
|
-
beliefType: 'Belief',
|
|
113
|
-
beliefVerb: 'IDENTIFY_AS',
|
|
114
|
-
beliefObject:
|
|
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<
|
|
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-
|
|
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 ${
|
|
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
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 ${
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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 =
|
|
273
|
+
const loadedDisclosures =
|
|
274
|
+
(parsed.disclosures as StoredDisclosureItem[]) || [];
|
|
208
275
|
|
|
209
|
-
const
|
|
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
|
|
220
|
-
|
|
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
|
-
...
|
|
294
|
+
...loadedItem,
|
|
223
295
|
id: generateId(),
|
|
224
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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(
|
|
318
|
+
setDisclosures(finalDisclosures);
|
|
239
319
|
} catch (e) {
|
|
240
|
-
|
|
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)
|
|
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:
|
|
270
|
-
string,
|
|
271
|
-
Omit<StoredDisclosureItem, 'beliefValue'>
|
|
272
|
-
> = {};
|
|
273
|
-
disclosures
|
|
353
|
+
const disclosuresToStore: StoredDisclosureItem[] = disclosures
|
|
274
354
|
.filter((d) => !d.isDisabled)
|
|
275
|
-
.
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
106
|
-
|
|
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
|
-
<
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
{
|
|
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"
|
|
@@ -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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 }>;
|