@esheet/builder 0.0.4-0 → 0.0.4-2
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.d.ts +19 -2
- package/dist/index.js +22105 -7909
- package/dist/lib/EsheetBuilder.js +111 -0
- package/dist/lib/builder-tools.js +321 -0
- package/dist/lib/components/BuilderHeader.js +390 -0
- package/dist/lib/components/Canvas.js +288 -0
- package/dist/lib/components/CodeView.js +197 -0
- package/dist/lib/components/FeedbackModal.js +22 -0
- package/dist/lib/components/FieldWrapper.js +152 -0
- package/dist/lib/components/MobileBottomDrawer.js +6 -0
- package/dist/lib/components/ToolPanel.js +117 -0
- package/dist/lib/components/edit-panel/CommonEditor.js +16 -0
- package/dist/lib/components/edit-panel/DraftIdEditor.js +32 -0
- package/dist/lib/components/edit-panel/EditPanel.js +156 -0
- package/dist/lib/components/edit-panel/InputTypeEditor.js +32 -0
- package/dist/lib/components/edit-panel/LogicEditor.js +392 -0
- package/dist/lib/components/edit-panel/MatrixEditor.js +32 -0
- package/dist/lib/components/edit-panel/OptionListEditor.js +27 -0
- package/dist/lib/hooks/useFormApi.js +80 -0
- package/dist/lib/hooks/useUiApi.js +37 -0
- package/dist/lib/hooks/useVisibleRootIds.js +32 -0
- package/dist/lib/icons.js +47 -0
- package/dist/lib/mcp/index.js +4 -0
- package/dist/lib/mcp/system-prompt.js +9 -0
- package/dist/lib/mcp/tool-definitions.js +436 -0
- package/dist/lib/mcp/tool-executor.js +482 -0
- package/dist/lib/mcp/useBuilderToolBridge.js +56 -0
- package/dist/lib/register-defaults.js +37 -0
- package/package.json +6 -5
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useSyncExternalStore } from 'react';
|
|
4
|
+
import { createFormStore, createUIStore, } from '@esheet/core';
|
|
5
|
+
import { createBuilderTools, } from './builder-tools.js';
|
|
6
|
+
import { FormStoreContext, UIContext, useTouchMode } from '@esheet/fields';
|
|
7
|
+
import { convertSurveyJS, isSurveyJSSchema, importFromMcp, isMcpElicitationRequest, importFromFhir, isFhirQuestionnaire, } from '@esheet/adapters';
|
|
8
|
+
import { Canvas } from './components/Canvas.js';
|
|
9
|
+
import { ToolPanel } from './components/ToolPanel.js';
|
|
10
|
+
import { EditPanel } from './components/edit-panel/EditPanel.js';
|
|
11
|
+
import { BuilderHeader } from './components/BuilderHeader.js';
|
|
12
|
+
import { CodeView } from './components/CodeView.js';
|
|
13
|
+
import { PlusIcon } from './icons.js';
|
|
14
|
+
import { ensureDefaultFieldComponentsRegistered } from './register-defaults.js';
|
|
15
|
+
import { MobileBottomDrawer } from './components/MobileBottomDrawer.js';
|
|
16
|
+
import { Switch } from '@mieweb/ui';
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Contexts
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
export { FormStoreContext, UIContext, useFormStore, useUI, } from '@esheet/fields';
|
|
21
|
+
export const InstanceIdContext = React.createContext('');
|
|
22
|
+
/** Hook to access the per-instance ID for unique DOM element IDs. */
|
|
23
|
+
export function useInstanceId() {
|
|
24
|
+
return React.useContext(InstanceIdContext);
|
|
25
|
+
}
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Component
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
export const EsheetBuilder = React.forwardRef(function EsheetBuilder({ definition, onChange, onBuilderToolsReady, dragEnabled = true, className = '', children, touchMode: touchModeProp, onTouchModeChange, }, ref) {
|
|
30
|
+
ensureDefaultFieldComponentsRegistered();
|
|
31
|
+
const formRef = React.useRef(null);
|
|
32
|
+
const uiRef = React.useRef(null);
|
|
33
|
+
if (!formRef.current) {
|
|
34
|
+
let resolved;
|
|
35
|
+
if (isFhirQuestionnaire(definition)) {
|
|
36
|
+
resolved = importFromFhir(definition);
|
|
37
|
+
}
|
|
38
|
+
else if (isSurveyJSSchema(definition)) {
|
|
39
|
+
resolved = convertSurveyJS(definition);
|
|
40
|
+
}
|
|
41
|
+
else if (isMcpElicitationRequest(definition)) {
|
|
42
|
+
const mcpReq = definition;
|
|
43
|
+
if (mcpReq.params.mode !== 'url') {
|
|
44
|
+
resolved = importFromMcp(mcpReq.params.requestedSchema, { mcpId: mcpReq.id, mcpMessage: mcpReq.params.message });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
resolved = definition;
|
|
49
|
+
}
|
|
50
|
+
formRef.current = createFormStore(resolved);
|
|
51
|
+
}
|
|
52
|
+
if (!uiRef.current) {
|
|
53
|
+
uiRef.current = createUIStore();
|
|
54
|
+
}
|
|
55
|
+
const form = formRef.current;
|
|
56
|
+
const ui = uiRef.current;
|
|
57
|
+
// Stable per-instance ID for unique DOM element IDs
|
|
58
|
+
const instanceId = React.useId();
|
|
59
|
+
// Subscribe to mode for conditional rendering
|
|
60
|
+
const mode = useSyncExternalStore((cb) => ui.subscribe(cb), () => ui.getState().mode, () => ui.getState().mode);
|
|
61
|
+
const selectedFieldId = useSyncExternalStore((cb) => ui.subscribe(cb), () => ui.getState().selectedFieldId, () => ui.getState().selectedFieldId);
|
|
62
|
+
const editModalOpen = useSyncExternalStore((cb) => ui.subscribe(cb), () => ui.getState().editModalOpen, () => ui.getState().editModalOpen);
|
|
63
|
+
const [toolsModalOpen, setToolsModalOpen] = React.useState(false);
|
|
64
|
+
// Touch mode state using shared hook
|
|
65
|
+
const { isTouchEnabled, isManualOverride, setTouchMode, resetTouchMode } = useTouchMode({ mode: touchModeProp, onChange: onTouchModeChange });
|
|
66
|
+
// Expose ref API for touch mode
|
|
67
|
+
React.useImperativeHandle(ref, () => ({
|
|
68
|
+
isTouchModeEnabled: () => isTouchEnabled,
|
|
69
|
+
setTouchMode,
|
|
70
|
+
resetTouchMode,
|
|
71
|
+
}), [isTouchEnabled, setTouchMode, resetTouchMode]);
|
|
72
|
+
React.useEffect(() => {
|
|
73
|
+
if (mode !== 'build') {
|
|
74
|
+
setToolsModalOpen(false);
|
|
75
|
+
ui.getState().setEditModalOpen(false);
|
|
76
|
+
}
|
|
77
|
+
}, [mode, ui]);
|
|
78
|
+
React.useEffect(() => {
|
|
79
|
+
if (!selectedFieldId && editModalOpen) {
|
|
80
|
+
ui.getState().setEditModalOpen(false);
|
|
81
|
+
}
|
|
82
|
+
}, [selectedFieldId, editModalOpen, ui]);
|
|
83
|
+
// Subscribe to form changes and forward to onChange.
|
|
84
|
+
React.useEffect(() => {
|
|
85
|
+
if (!onChange)
|
|
86
|
+
return;
|
|
87
|
+
return form.subscribe(() => {
|
|
88
|
+
onChange(form.getState().hydrateDefinition());
|
|
89
|
+
});
|
|
90
|
+
}, [form, onChange]);
|
|
91
|
+
// Expose a narrow BuilderTools facade to MCP / AI tool callers once on mount.
|
|
92
|
+
React.useEffect(() => {
|
|
93
|
+
if (onBuilderToolsReady)
|
|
94
|
+
onBuilderToolsReady(createBuilderTools(form));
|
|
95
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
96
|
+
}, [form]);
|
|
97
|
+
// Apply disabled class when user explicitly disabled touch mode (prevents CSS media query)
|
|
98
|
+
const applyTouchDisabledClass = isManualOverride && !isTouchEnabled;
|
|
99
|
+
// Build root class string
|
|
100
|
+
const rootClasses = [
|
|
101
|
+
'ms-builder-root',
|
|
102
|
+
isTouchEnabled && 'esheet-touch-active',
|
|
103
|
+
applyTouchDisabledClass && 'touch-mode-disabled',
|
|
104
|
+
'ms:flex ms:h-full ms:flex-1 ms:min-h-0 ms:max-h-full ms:w-full ms:min-w-0 ms:max-w-[1440px] ms:mx-auto ms:flex-col ms:gap-2',
|
|
105
|
+
'ms:overflow-x-hidden ms:bg-msbackground ms:text-mstext',
|
|
106
|
+
className,
|
|
107
|
+
]
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
.join(' ');
|
|
110
|
+
return (_jsx(FormStoreContext.Provider, { value: form, children: _jsx(UIContext.Provider, { value: ui, children: _jsx(InstanceIdContext.Provider, { value: instanceId, children: _jsxs("div", { className: rootClasses, children: [_jsx("div", { className: "ms:sticky ms:top-0 ms:z-40 ms:bg-msbackground", children: _jsx(BuilderHeader, {}) }), children, mode === 'build' && (_jsxs("div", { className: "builder-layout ms:grid ms:min-w-0 ms:grid-cols-1 ms:lg:grid-cols-[18rem_minmax(0,1fr)_340px] ms:gap-3", children: [_jsx("aside", { className: "panel-tools-wrap panel-tools ms:hidden ms:lg:flex ms:self-start ms:min-h-0 ms:max-h-[calc(100dvh-12.5rem)] ms:overflow-y-auto ms:flex-col ms:rounded-lg ms:border ms:border-msborder ms:bg-mssurface", children: _jsx(ToolPanel, {}) }), _jsxs("main", { className: "panel-canvas ms:min-w-0 ms:self-start ms:max-h-[calc(100dvh-12.5rem)] ms:overflow-hidden ms:flex ms:flex-col ms:rounded-lg ms:border ms:border-msborder ms:bg-mssurface", children: [_jsx(Canvas, { form: form, ui: ui, dragEnabled: dragEnabled }), _jsx("div", { className: "ms:lg:hidden ms:sticky ms:bottom-0 ms:z-20 ms:pt-2 ms:pb-3 ms:flex ms:justify-center ms:pointer-events-none", children: _jsxs("button", { type: "button", onClick: () => setToolsModalOpen(true), className: "ms:pointer-events-auto ms:inline-flex ms:items-center ms:gap-1.5 ms:px-3.5 ms:py-2 ms:rounded-full ms:bg-mssurface/95 ms:backdrop-blur-sm ms:text-mstext ms:text-sm ms:font-semibold ms:border ms:border-msprimary/35 ms:shadow-lg ms:shadow-msprimary/10 ms:outline-none ms:focus:outline-none ms:hover:bg-mssurface ms:hover:border-msprimary/50 ms:hover:shadow-xl ms:hover:shadow-msprimary/15 ms:transition-all", "aria-label": "Open add field tools", children: [_jsx(PlusIcon, { className: "ms:w-3.5 ms:h-3.5 ms:text-msprimary" }), _jsx("span", { children: "Add field" })] }) })] }), _jsx("aside", { className: "panel-editor-wrap panel-editor ms:hidden ms:lg:flex ms:self-start ms:min-h-0 ms:max-h-[calc(100dvh-12.5rem)] ms:overflow-y-auto ms:flex-col ms:rounded-lg ms:border ms:border-msborder ms:bg-mssurface", children: _jsx(EditPanel, {}) }), _jsx(MobileBottomDrawer, { title: "Add Field", open: toolsModalOpen, onClose: () => setToolsModalOpen(false), children: _jsx(ToolPanel, {}) }), _jsx(MobileBottomDrawer, { title: "Edit Field", open: editModalOpen && !!selectedFieldId, onClose: () => ui.getState().setEditModalOpen(false), children: _jsx(EditPanel, {}) })] })), mode === 'code' && (_jsx("div", { className: "code-layout ms:flex ms:h-[calc(100dvh-12.5rem)] ms:min-h-0 ms:min-w-0 ms:overflow-hidden ms:rounded-lg ms:border ms:border-msborder ms:bg-mssurface", children: _jsx(CodeView, { form: form, ui: ui }) })), mode === 'preview' && (_jsxs("div", { className: "preview-layout ms:flex-1 ms:min-h-0 ms:min-w-0 ms:w-full ms:max-w-2xl ms:mx-auto ms:p-4 ms:max-h-[calc(100dvh-12.5rem)] ms:overflow-y-auto", children: [_jsx("div", { className: "ms:flex ms:items-center ms:justify-end ms:mb-3", children: _jsx(Switch, { size: "sm", checked: isTouchEnabled, onCheckedChange: setTouchMode, label: "Touch Mode" }) }), _jsx(Canvas, { form: form, ui: ui, dragEnabled: false })] }))] }) }) }) }));
|
|
111
|
+
});
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// BuilderTools — narrow facade for MCP / AI tool callers only.
|
|
3
|
+
// Normal developers interact with the builder entirely through its props/UI.
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
import { getFieldTypeMeta, getRegisteredFieldTypes, hydrateDefinition, } from '@esheet/core';
|
|
6
|
+
export function createBuilderTools(form) {
|
|
7
|
+
function resolveFieldId(fieldId, fieldQuestion) {
|
|
8
|
+
if (fieldId)
|
|
9
|
+
return fieldId;
|
|
10
|
+
if (fieldQuestion) {
|
|
11
|
+
const q = fieldQuestion.toLowerCase();
|
|
12
|
+
const { normalized } = form.getState();
|
|
13
|
+
for (const id of Object.keys(normalized.byId)) {
|
|
14
|
+
const question = normalized.byId[id].definition.question;
|
|
15
|
+
if (question?.toLowerCase().includes(q))
|
|
16
|
+
return id;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
resetForm: (definition) => form.getState().loadDefinition(definition),
|
|
23
|
+
generateForm: (questions) => {
|
|
24
|
+
const fields = questions.map((q, i) => ({
|
|
25
|
+
id: `f${i + 1}`,
|
|
26
|
+
fieldType: q.fieldType ?? 'text',
|
|
27
|
+
question: q.question,
|
|
28
|
+
...(q.required !== undefined && { required: q.required }),
|
|
29
|
+
...(q.inputType && { inputType: q.inputType }),
|
|
30
|
+
...(q.options && {
|
|
31
|
+
options: q.options.map((v, j) => ({ id: `o${j + 1}`, value: v })),
|
|
32
|
+
}),
|
|
33
|
+
}));
|
|
34
|
+
form.getState().loadDefinition({
|
|
35
|
+
id: form.getState().formId,
|
|
36
|
+
fields: [],
|
|
37
|
+
});
|
|
38
|
+
form.getState().loadDefinition({
|
|
39
|
+
id: form.getState().formId,
|
|
40
|
+
fields,
|
|
41
|
+
});
|
|
42
|
+
const summary = fields
|
|
43
|
+
.map((f, i) => `${i + 1}. ${f.question} (${f.fieldType})`)
|
|
44
|
+
.join(', ');
|
|
45
|
+
return `Form generated with ${fields.length} field(s): ${summary}`;
|
|
46
|
+
},
|
|
47
|
+
addField: (fieldType, opts) => form.getState().addField(fieldType, opts),
|
|
48
|
+
updateField: (fieldId, patch) => form.getState().updateField(fieldId, patch),
|
|
49
|
+
removeField: (fieldId) => form.getState().removeField(fieldId),
|
|
50
|
+
resolveFieldId,
|
|
51
|
+
fillField: (fieldId, value) => {
|
|
52
|
+
const { normalized } = form.getState();
|
|
53
|
+
const node = normalized.byId[fieldId];
|
|
54
|
+
if (!node)
|
|
55
|
+
return false;
|
|
56
|
+
const def = node.definition;
|
|
57
|
+
function normalizeTextValue(v, inputType) {
|
|
58
|
+
if (!v)
|
|
59
|
+
return v;
|
|
60
|
+
switch (inputType) {
|
|
61
|
+
case 'date':
|
|
62
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(v))
|
|
63
|
+
return v;
|
|
64
|
+
if (/^\d{4}-\d{2}$/.test(v))
|
|
65
|
+
return `${v}-01`;
|
|
66
|
+
if (/^\d{4}$/.test(v))
|
|
67
|
+
return `${v}-01-01`;
|
|
68
|
+
return null;
|
|
69
|
+
case 'month':
|
|
70
|
+
if (/^\d{4}-\d{2}$/.test(v))
|
|
71
|
+
return v;
|
|
72
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(v))
|
|
73
|
+
return v.slice(0, 7);
|
|
74
|
+
return null;
|
|
75
|
+
case 'datetime-local': {
|
|
76
|
+
const n = v.replace(' ', 'T');
|
|
77
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(n))
|
|
78
|
+
return n;
|
|
79
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(n))
|
|
80
|
+
return `${n}T00:00`;
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
case 'time': {
|
|
84
|
+
if (/^\d{2}:\d{2}$/.test(v))
|
|
85
|
+
return v;
|
|
86
|
+
const m = v.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)$/i);
|
|
87
|
+
if (m) {
|
|
88
|
+
let h = parseInt(m[1], 10);
|
|
89
|
+
if (m[3].toUpperCase() === 'AM' && h === 12)
|
|
90
|
+
h = 0;
|
|
91
|
+
if (m[3].toUpperCase() === 'PM' && h !== 12)
|
|
92
|
+
h += 12;
|
|
93
|
+
return `${String(h).padStart(2, '0')}:${m[2]}`;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
default:
|
|
98
|
+
return v;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
let response;
|
|
102
|
+
if (def.fieldType === 'singlematrix' &&
|
|
103
|
+
value !== null &&
|
|
104
|
+
typeof value === 'object') {
|
|
105
|
+
const rows = def.rows ?? [];
|
|
106
|
+
const columns = def.columns ?? [];
|
|
107
|
+
const existing = (form.getState().responses[fieldId]?.selected ??
|
|
108
|
+
{});
|
|
109
|
+
const selected = {
|
|
110
|
+
...existing,
|
|
111
|
+
};
|
|
112
|
+
const entries = Array.isArray(value)
|
|
113
|
+
? value.map((v, i) => [rows[i]?.value ?? String(i), v])
|
|
114
|
+
: Object.entries(value);
|
|
115
|
+
for (const [rowKey, colVal] of entries) {
|
|
116
|
+
const row = rows.find((r) => r.value.toLowerCase() === rowKey.toLowerCase() || r.id === rowKey);
|
|
117
|
+
if (!row)
|
|
118
|
+
continue;
|
|
119
|
+
const colStr = String(colVal);
|
|
120
|
+
const col = columns.find((c) => c.value.toLowerCase() === colStr.toLowerCase() ||
|
|
121
|
+
c.id === colStr) ?? columns[parseInt(colStr, 10)];
|
|
122
|
+
if (col)
|
|
123
|
+
selected[row.id] = { id: col.id, value: col.value };
|
|
124
|
+
}
|
|
125
|
+
response = { selected };
|
|
126
|
+
}
|
|
127
|
+
else if (def.fieldType === 'multimatrix' &&
|
|
128
|
+
value !== null &&
|
|
129
|
+
typeof value === 'object') {
|
|
130
|
+
const rows = def.rows ?? [];
|
|
131
|
+
const columns = def.columns ?? [];
|
|
132
|
+
const existing = (form.getState().responses[fieldId]?.selected ??
|
|
133
|
+
{});
|
|
134
|
+
const selected = {
|
|
135
|
+
...existing,
|
|
136
|
+
};
|
|
137
|
+
const entries = Array.isArray(value)
|
|
138
|
+
? value.map((v, i) => [rows[i]?.value ?? String(i), v])
|
|
139
|
+
: Object.entries(value);
|
|
140
|
+
for (const [rowKey, colVals] of entries) {
|
|
141
|
+
const row = rows.find((r) => r.value.toLowerCase() === rowKey.toLowerCase() || r.id === rowKey);
|
|
142
|
+
if (!row)
|
|
143
|
+
continue;
|
|
144
|
+
const colValArr = Array.isArray(colVals)
|
|
145
|
+
? colVals
|
|
146
|
+
: [colVals];
|
|
147
|
+
const matched = colValArr
|
|
148
|
+
.map((v) => {
|
|
149
|
+
const s = String(v);
|
|
150
|
+
return columns.find((c) => c.value.toLowerCase() === s.toLowerCase() || c.id === s);
|
|
151
|
+
})
|
|
152
|
+
.filter((c) => c != null);
|
|
153
|
+
if (matched.length > 0)
|
|
154
|
+
selected[row.id] = matched;
|
|
155
|
+
}
|
|
156
|
+
response = { selected };
|
|
157
|
+
}
|
|
158
|
+
else if (['radio', 'dropdown', 'boolean', 'rating', 'slider'].includes(def.fieldType)) {
|
|
159
|
+
const opts = def.options ?? [];
|
|
160
|
+
const match = opts.find((o) => o.value.toLowerCase() === String(value).toLowerCase() ||
|
|
161
|
+
o.id === value);
|
|
162
|
+
const numVal = Number(value);
|
|
163
|
+
const numMatch = !match && !isNaN(numVal)
|
|
164
|
+
? opts[numVal - 1] ?? opts[numVal]
|
|
165
|
+
: undefined;
|
|
166
|
+
const resolved = match ?? numMatch;
|
|
167
|
+
response = resolved
|
|
168
|
+
? { selected: { id: resolved.id, value: resolved.value } }
|
|
169
|
+
: { selected: undefined };
|
|
170
|
+
}
|
|
171
|
+
else if (def.fieldType === 'check' ||
|
|
172
|
+
def.fieldType === 'multiselectdropdown') {
|
|
173
|
+
const opts = def.options ?? [];
|
|
174
|
+
const vals = Array.isArray(value) ? value : [value];
|
|
175
|
+
const matches = opts.filter((o) => vals.some((v) => o.value.toLowerCase() === String(v).toLowerCase() || o.id === v));
|
|
176
|
+
response = {
|
|
177
|
+
selected: matches.map((o) => ({ id: o.id, value: o.value })),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
else if (def.fieldType === 'ranking') {
|
|
181
|
+
const opts = def.options ?? [];
|
|
182
|
+
const vals = Array.isArray(value) ? value : [value];
|
|
183
|
+
const ordered = vals
|
|
184
|
+
.map((v) => opts.find((o) => o.value.toLowerCase() === String(v).toLowerCase() || o.id === v))
|
|
185
|
+
.filter((o) => o != null);
|
|
186
|
+
const mentioned = new Set(ordered.map((o) => o.id));
|
|
187
|
+
const remaining = opts.filter((o) => !mentioned.has(o.id));
|
|
188
|
+
response = { selected: [...ordered, ...remaining] };
|
|
189
|
+
}
|
|
190
|
+
else if (def.fieldType === 'multitext') {
|
|
191
|
+
const opts = def.options ?? [];
|
|
192
|
+
const vals = Array.isArray(value)
|
|
193
|
+
? value
|
|
194
|
+
: typeof value === 'object' && value !== null
|
|
195
|
+
? Object.values(value)
|
|
196
|
+
: [value];
|
|
197
|
+
const multitextAnswers = {};
|
|
198
|
+
opts.forEach((opt, i) => {
|
|
199
|
+
if (vals[i] != null)
|
|
200
|
+
multitextAnswers[opt.id] = String(vals[i]);
|
|
201
|
+
});
|
|
202
|
+
response = { multitextAnswers };
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
if (value == null) {
|
|
206
|
+
response = { answer: undefined };
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
const normalized = normalizeTextValue(String(value), def.inputType);
|
|
210
|
+
if (normalized === null) {
|
|
211
|
+
const fmt = {
|
|
212
|
+
date: 'YYYY-MM-DD',
|
|
213
|
+
'datetime-local': 'YYYY-MM-DDTHH:mm',
|
|
214
|
+
month: 'YYYY-MM',
|
|
215
|
+
time: 'HH:mm',
|
|
216
|
+
};
|
|
217
|
+
const expected = fmt[def.inputType ?? ''];
|
|
218
|
+
return expected
|
|
219
|
+
? `Error: invalid value for inputType "${def.inputType}" — expected format ${expected}`
|
|
220
|
+
: `Error: invalid value for field`;
|
|
221
|
+
}
|
|
222
|
+
response = { answer: normalized };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
form
|
|
226
|
+
.getState()
|
|
227
|
+
.setResponse(fieldId, { ...response, _ai: true });
|
|
228
|
+
return true;
|
|
229
|
+
},
|
|
230
|
+
clearResponses: () => form.getState().resetResponses(),
|
|
231
|
+
getResponses: () => form.getState().responses,
|
|
232
|
+
option: {
|
|
233
|
+
add: (fieldId, value) => form.getState().addOption(fieldId, value),
|
|
234
|
+
update: (fieldId, optionId, value) => form.getState().updateOption(fieldId, optionId, value),
|
|
235
|
+
remove: (fieldId, optionId) => form.getState().removeOption(fieldId, optionId),
|
|
236
|
+
},
|
|
237
|
+
row: {
|
|
238
|
+
add: (fieldId, value) => form.getState().addRow(fieldId, value),
|
|
239
|
+
update: (fieldId, rowId, value) => form.getState().updateRow(fieldId, rowId, value),
|
|
240
|
+
remove: (fieldId, rowId) => form.getState().removeRow(fieldId, rowId),
|
|
241
|
+
},
|
|
242
|
+
column: {
|
|
243
|
+
add: (fieldId, value) => form.getState().addColumn(fieldId, value),
|
|
244
|
+
update: (fieldId, columnId, value) => form.getState().updateColumn(fieldId, columnId, value),
|
|
245
|
+
remove: (fieldId, columnId) => form.getState().removeColumn(fieldId, columnId),
|
|
246
|
+
},
|
|
247
|
+
moveField: (fieldId, toIndex, toParentId) => form.getState().moveField(fieldId, toIndex, toParentId),
|
|
248
|
+
getField: (fieldId) => form.getState().getField(fieldId),
|
|
249
|
+
getFieldTypes: () => getRegisteredFieldTypes().map((key) => {
|
|
250
|
+
const meta = getFieldTypeMeta(key);
|
|
251
|
+
return {
|
|
252
|
+
key,
|
|
253
|
+
label: meta.label,
|
|
254
|
+
category: meta.category,
|
|
255
|
+
hasOptions: meta.hasOptions,
|
|
256
|
+
hasMatrix: meta.hasMatrix,
|
|
257
|
+
};
|
|
258
|
+
}),
|
|
259
|
+
getFieldSpec: (fieldType) => getFieldTypeMeta(fieldType),
|
|
260
|
+
getDefinition: () => {
|
|
261
|
+
const { normalized, formId } = form.getState();
|
|
262
|
+
return {
|
|
263
|
+
id: formId,
|
|
264
|
+
fields: hydrateDefinition(normalized),
|
|
265
|
+
};
|
|
266
|
+
},
|
|
267
|
+
setFormId: (id) => form.getState().setFormId(id),
|
|
268
|
+
getFormSummary: () => {
|
|
269
|
+
const { normalized, formId } = form.getState();
|
|
270
|
+
const INPUT_TYPE_FORMAT = {
|
|
271
|
+
date: 'YYYY-MM-DD',
|
|
272
|
+
'datetime-local': 'YYYY-MM-DDTHH:mm',
|
|
273
|
+
month: 'YYYY-MM',
|
|
274
|
+
time: 'HH:mm',
|
|
275
|
+
};
|
|
276
|
+
function summarizeField(id) {
|
|
277
|
+
const node = normalized.byId[id];
|
|
278
|
+
const def = node.definition;
|
|
279
|
+
const isMatrix = def.fieldType === 'singlematrix' || def.fieldType === 'multimatrix';
|
|
280
|
+
const isSection = def.fieldType === 'section';
|
|
281
|
+
const base = {
|
|
282
|
+
id,
|
|
283
|
+
fieldType: def.fieldType,
|
|
284
|
+
question: isSection
|
|
285
|
+
? def.title
|
|
286
|
+
: def.question,
|
|
287
|
+
required: def.required ?? false,
|
|
288
|
+
options: def.options ?? [],
|
|
289
|
+
rows: def.rows ?? [],
|
|
290
|
+
columns: def.columns ?? [],
|
|
291
|
+
editWith: isMatrix
|
|
292
|
+
? 'add_row / add_column (NOT add_option)'
|
|
293
|
+
: 'add_option',
|
|
294
|
+
hasRules: (def.rules?.length ?? 0) > 0,
|
|
295
|
+
hasValue: (() => {
|
|
296
|
+
const r = form.getState().responses[id];
|
|
297
|
+
return (r != null &&
|
|
298
|
+
Object.values(r).some((v) => v != null &&
|
|
299
|
+
v !== '' &&
|
|
300
|
+
!(Array.isArray(v) && v.length === 0) &&
|
|
301
|
+
!(typeof v === 'object' &&
|
|
302
|
+
!Array.isArray(v) &&
|
|
303
|
+
Object.keys(v).length === 0)));
|
|
304
|
+
})(),
|
|
305
|
+
...(def.inputType && INPUT_TYPE_FORMAT[def.inputType]
|
|
306
|
+
? { valueFormat: INPUT_TYPE_FORMAT[def.inputType] }
|
|
307
|
+
: {}),
|
|
308
|
+
};
|
|
309
|
+
if (isSection && node.childIds.length > 0) {
|
|
310
|
+
return { ...base, children: node.childIds.map(summarizeField) };
|
|
311
|
+
}
|
|
312
|
+
return base;
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
formId,
|
|
316
|
+
fieldCount: Object.keys(normalized.byId).length,
|
|
317
|
+
fields: normalized.rootIds.map(summarizeField),
|
|
318
|
+
};
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|