@esheet/renderer 0.0.3 → 0.0.4-1
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/README.md +3 -3
- package/dist/index.d.ts +9 -102
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5284 -4315
- package/dist/lib/EsheetRenderer.d.ts +116 -0
- package/dist/lib/EsheetRenderer.d.ts.map +1 -0
- package/dist/lib/EsheetRenderer.js +115 -0
- package/dist/lib/components/FieldNode.d.ts +17 -0
- package/dist/lib/components/FieldNode.d.ts.map +1 -0
- package/dist/lib/components/FieldNode.js +96 -0
- package/dist/lib/components/RendererBody.d.ts +14 -0
- package/dist/lib/components/RendererBody.d.ts.map +1 -0
- package/dist/lib/components/RendererBody.js +45 -0
- package/dist/lib/components/index.d.ts +3 -0
- package/dist/lib/components/index.d.ts.map +1 -0
- package/dist/lib/components/index.js +2 -0
- package/dist/lib/hooks/index.d.ts +2 -0
- package/dist/lib/hooks/index.d.ts.map +1 -0
- package/dist/lib/hooks/index.js +1 -0
- package/dist/lib/hooks/useRendererInit.d.ts +12 -0
- package/dist/lib/hooks/useRendererInit.d.ts.map +1 -0
- package/dist/lib/hooks/useRendererInit.js +87 -0
- package/dist/lib/mcp/index.d.ts +7 -0
- package/dist/lib/mcp/index.d.ts.map +1 -0
- package/dist/lib/mcp/index.js +4 -0
- package/dist/lib/mcp/system-prompt.d.ts +2 -0
- package/dist/lib/mcp/system-prompt.d.ts.map +1 -0
- package/dist/lib/mcp/system-prompt.js +24 -0
- package/dist/lib/mcp/tool-definitions.d.ts +10 -0
- package/dist/lib/mcp/tool-definitions.d.ts.map +1 -0
- package/dist/lib/mcp/tool-definitions.js +66 -0
- package/dist/lib/mcp/tool-executor.d.ts +5 -0
- package/dist/lib/mcp/tool-executor.d.ts.map +1 -0
- package/dist/lib/mcp/tool-executor.js +55 -0
- package/dist/lib/mcp/useRendererToolBridge.d.ts +28 -0
- package/dist/lib/mcp/useRendererToolBridge.d.ts.map +1 -0
- package/dist/lib/mcp/useRendererToolBridge.js +53 -0
- package/dist/lib/register-defaults.d.ts +2 -0
- package/dist/lib/register-defaults.d.ts.map +1 -0
- package/dist/lib/register-defaults.js +36 -0
- package/dist/lib/renderer-tools.d.ts +56 -0
- package/dist/lib/renderer-tools.d.ts.map +1 -0
- package/dist/lib/renderer-tools.js +305 -0
- package/dist/lib/renderer.d.ts +28 -0
- package/dist/lib/renderer.d.ts.map +1 -0
- package/dist/lib/renderer.js +33 -0
- package/package.json +5 -5
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type FormResponse, type FormStore, type UIStore, type ValidationError } from '@esheet/core';
|
|
3
|
+
import { type FhirQuestionnaireResponse, type ResponseExportOptions } from '@esheet/adapters';
|
|
4
|
+
import { type RendererTools } from './renderer-tools.js';
|
|
5
|
+
export interface EsheetRendererProps {
|
|
6
|
+
/** Form definition — accepts FormDefinition, SurveyJS schema, MCP elicitation envelope,
|
|
7
|
+
* or any of the above as a JSON/YAML string. Auto-detected and converted internally.
|
|
8
|
+
* Set `strict` to disable auto-conversion and require a valid FormDefinition directly. */
|
|
9
|
+
formDataInput: unknown;
|
|
10
|
+
/** Additional CSS classes for root container */
|
|
11
|
+
className?: string;
|
|
12
|
+
/** Initial form responses (pre-fill data) */
|
|
13
|
+
initialResponses?: FormResponse;
|
|
14
|
+
/** When true, disables auto-detection of MCP/SurveyJS formats.
|
|
15
|
+
* Only accepts a valid eSheet FormDefinition (or JSON/YAML string thereof). */
|
|
16
|
+
strict?: boolean;
|
|
17
|
+
/** Called after the form definition has been parsed and loaded into the store. */
|
|
18
|
+
onReady?: () => void;
|
|
19
|
+
/**
|
|
20
|
+
* Called once after the renderer mounts, providing a narrow `RendererTools`
|
|
21
|
+
* facade for MCP / AI tool integrations.
|
|
22
|
+
*/
|
|
23
|
+
onRendererToolsReady?: (tools: RendererTools) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Enable touch-optimized mode with larger touch targets.
|
|
26
|
+
* - `true`: Always enable touch mode
|
|
27
|
+
* - `false`: Never enable touch mode (CSS media query still applies)
|
|
28
|
+
* - `'auto'`: Enable based on viewport width (<980px) via JavaScript
|
|
29
|
+
* - `undefined`: Rely on CSS media query only (default)
|
|
30
|
+
*/
|
|
31
|
+
touchMode?: boolean | 'auto';
|
|
32
|
+
/**
|
|
33
|
+
* Called when touch mode changes (via auto-detection or programmatic toggle).
|
|
34
|
+
* Useful for syncing external UI with the renderer's touch state.
|
|
35
|
+
*/
|
|
36
|
+
onTouchModeChange?: (enabled: boolean) => void;
|
|
37
|
+
}
|
|
38
|
+
/** Response format options for getResponse() */
|
|
39
|
+
export type ResponseFormat = 'native' | 'fhir';
|
|
40
|
+
/** Options for getResponse() */
|
|
41
|
+
export interface GetResponseOptions {
|
|
42
|
+
/** Output format: 'native' (default) or 'fhir' (FHIR QuestionnaireResponse) */
|
|
43
|
+
format?: ResponseFormat;
|
|
44
|
+
/** FHIR export options (required when format is 'fhir') */
|
|
45
|
+
fhir?: Omit<ResponseExportOptions, 'questionnaireUrl'> & {
|
|
46
|
+
/** Canonical URL of the questionnaire. Falls back to form._sourceData metadata if available. */
|
|
47
|
+
questionnaireUrl?: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Return type for getResponse() — varies by format */
|
|
51
|
+
export type GetResponseResult<T extends ResponseFormat = 'native'> = T extends 'fhir' ? FhirQuestionnaireResponse : FormResponse;
|
|
52
|
+
export interface EsheetRendererHandle {
|
|
53
|
+
/** Get current form responses */
|
|
54
|
+
getRawResponse: () => FormResponse;
|
|
55
|
+
/**
|
|
56
|
+
* Get form responses in the specified format.
|
|
57
|
+
*
|
|
58
|
+
* @param options - Format and export options
|
|
59
|
+
* @returns Native FormResponse or FHIR QuestionnaireResponse
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* // Native format (default)
|
|
64
|
+
* const native = ref.current.getResponse();
|
|
65
|
+
*
|
|
66
|
+
* // FHIR QuestionnaireResponse
|
|
67
|
+
* const fhir = ref.current.getResponse({
|
|
68
|
+
* format: 'fhir',
|
|
69
|
+
* fhir: {
|
|
70
|
+
* questionnaireUrl: 'http://example.org/Questionnaire/my-form',
|
|
71
|
+
* status: 'completed',
|
|
72
|
+
* }
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
getResponse: <T extends ResponseFormat = 'native'>(options?: GetResponseOptions & {
|
|
77
|
+
format?: T;
|
|
78
|
+
}) => GetResponseResult<T>;
|
|
79
|
+
/** Get form store instance */
|
|
80
|
+
getFormStore: () => FormStore;
|
|
81
|
+
/** Get UI store instance */
|
|
82
|
+
getUIStore: () => UIStore;
|
|
83
|
+
/** Get validated form responses (returns null if invalid) */
|
|
84
|
+
getValidResponse: () => {
|
|
85
|
+
response: FormResponse | null;
|
|
86
|
+
errors: ValidationError[];
|
|
87
|
+
};
|
|
88
|
+
/** Returns true if touch mode is currently enabled */
|
|
89
|
+
isTouchModeEnabled: () => boolean;
|
|
90
|
+
/** Toggle touch mode on/off. Only works when touchMode prop is 'auto' or undefined. Overrides auto-detection. */
|
|
91
|
+
setTouchMode: (enabled: boolean) => void;
|
|
92
|
+
/** Reset to auto-detection mode (clears manual override). Only works when touchMode='auto'. */
|
|
93
|
+
resetTouchMode: () => void;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* EsheetRenderer - Read-only questionnaire form renderer
|
|
97
|
+
*
|
|
98
|
+
* Renders a form in fill-out mode with conditional visibility logic.
|
|
99
|
+
* Reuses all field components from @esheet/fields.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```tsx
|
|
103
|
+
* const rendererRef = useRef<EsheetRendererHandle>(null);
|
|
104
|
+
*
|
|
105
|
+
* <EsheetRenderer
|
|
106
|
+
* formData={myFormDefinition}
|
|
107
|
+
* initialResponses={{ field1: 'answer' }}
|
|
108
|
+
* ref={rendererRef}
|
|
109
|
+
* />
|
|
110
|
+
*
|
|
111
|
+
* // Later: get responses
|
|
112
|
+
* const responses = rendererRef.current?.getRawResponse();
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export declare const EsheetRenderer: React.ForwardRefExoticComponent<EsheetRendererProps & React.RefAttributes<EsheetRendererHandle>>;
|
|
116
|
+
//# sourceMappingURL=EsheetRenderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EsheetRenderer.d.ts","sourceRoot":"","sources":["../../src/lib/EsheetRenderer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAML,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,OAAO,EACZ,KAAK,eAAe,EACrB,MAAM,cAAc,CAAC;AACtB,OAAO,EAGL,KAAK,yBAAyB,EAC9B,KAAK,qBAAqB,EAC3B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAW9E,MAAM,WAAW,mBAAmB;IAClC;;+FAE2F;IAC3F,aAAa,EAAE,OAAO,CAAC;IACvB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,gBAAgB,CAAC,EAAE,YAAY,CAAC;IAChC;oFACgF;IAChF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IACtD;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC7B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CAChD;AAMD,gDAAgD;AAChD,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE/C,gCAAgC;AAChC,MAAM,WAAW,kBAAkB;IACjC,+EAA+E;IAC/E,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,2DAA2D;IAC3D,IAAI,CAAC,EAAE,IAAI,CAAC,qBAAqB,EAAE,kBAAkB,CAAC,GAAG;QACvD,gGAAgG;QAChG,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC3B,CAAC;CACH;AAED,uDAAuD;AACvD,MAAM,MAAM,iBAAiB,CAAC,CAAC,SAAS,cAAc,GAAG,QAAQ,IAC/D,CAAC,SAAS,MAAM,GAAG,yBAAyB,GAAG,YAAY,CAAC;AAE9D,MAAM,WAAW,oBAAoB;IACnC,iCAAiC;IACjC,cAAc,EAAE,MAAM,YAAY,CAAC;IACnC;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,WAAW,EAAE,CAAC,CAAC,SAAS,cAAc,GAAG,QAAQ,EAC/C,OAAO,CAAC,EAAE,kBAAkB,GAAG;QAAE,MAAM,CAAC,EAAE,CAAC,CAAA;KAAE,KAC1C,iBAAiB,CAAC,CAAC,CAAC,CAAC;IAC1B,8BAA8B;IAC9B,YAAY,EAAE,MAAM,SAAS,CAAC;IAC9B,4BAA4B;IAC5B,UAAU,EAAE,MAAM,OAAO,CAAC;IAC1B,6DAA6D;IAC7D,gBAAgB,EAAE,MAAM;QACtB,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;QAC9B,MAAM,EAAE,eAAe,EAAE,CAAC;KAC3B,CAAC;IACF,sDAAsD;IACtD,kBAAkB,EAAE,MAAM,OAAO,CAAC;IAClC,iHAAiH;IACjH,YAAY,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACzC,+FAA+F;IAC/F,cAAc,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,cAAc,kGA2BzB,CAAC"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { createFormStore, createUIStore, normalizeResponses, hydrateDefinition, validateForm, } from '@esheet/core';
|
|
4
|
+
import { exportResponseToFhir, } from '@esheet/adapters';
|
|
5
|
+
import { createRendererTools } from './renderer-tools.js';
|
|
6
|
+
import { FormStoreContext, UIContext, ZodIssuesPanel, useTouchMode, } from '@esheet/fields';
|
|
7
|
+
import { ensureDefaultFieldComponentsRegistered } from './register-defaults.js';
|
|
8
|
+
import { useRendererInit } from './hooks/useRendererInit.js';
|
|
9
|
+
import { RendererBody } from './components/RendererBody.js';
|
|
10
|
+
/**
|
|
11
|
+
* EsheetRenderer - Read-only questionnaire form renderer
|
|
12
|
+
*
|
|
13
|
+
* Renders a form in fill-out mode with conditional visibility logic.
|
|
14
|
+
* Reuses all field components from @esheet/fields.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* const rendererRef = useRef<EsheetRendererHandle>(null);
|
|
19
|
+
*
|
|
20
|
+
* <EsheetRenderer
|
|
21
|
+
* formData={myFormDefinition}
|
|
22
|
+
* initialResponses={{ field1: 'answer' }}
|
|
23
|
+
* ref={rendererRef}
|
|
24
|
+
* />
|
|
25
|
+
*
|
|
26
|
+
* // Later: get responses
|
|
27
|
+
* const responses = rendererRef.current?.getRawResponse();
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export const EsheetRenderer = React.forwardRef(function EsheetRenderer(props, ref) {
|
|
31
|
+
ensureDefaultFieldComponentsRegistered();
|
|
32
|
+
const formStore = React.useMemo(() => createFormStore(), []);
|
|
33
|
+
const uiStore = React.useMemo(() => createUIStore(), []);
|
|
34
|
+
React.useEffect(() => {
|
|
35
|
+
if (props.onRendererToolsReady)
|
|
36
|
+
props.onRendererToolsReady(createRendererTools(formStore));
|
|
37
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
38
|
+
}, [formStore]);
|
|
39
|
+
return (_jsx(FormStoreContext.Provider, { value: formStore, children: _jsx(UIContext.Provider, { value: uiStore, children: _jsx(EsheetRendererInner, { ...props, formStore: formStore, uiStore: uiStore, ref: ref }) }) }));
|
|
40
|
+
});
|
|
41
|
+
const EsheetRendererInner = React.forwardRef(function EsheetRendererInner({ formDataInput: formData, className = '', initialResponses, strict = false, onReady, formStore, uiStore, touchMode, onTouchModeChange, }, ref) {
|
|
42
|
+
const [validationErrors, setValidationErrors] = React.useState([]);
|
|
43
|
+
// Touch mode state using shared hook
|
|
44
|
+
const { isTouchEnabled, isManualOverride, setTouchMode: setTouchModeInternal, resetTouchMode: resetTouchModeInternal, } = useTouchMode({ mode: touchMode, onChange: onTouchModeChange });
|
|
45
|
+
// Initialize form definition and set preview mode
|
|
46
|
+
useRendererInit(formStore, uiStore, formData, initialResponses, setValidationErrors, strict, onReady);
|
|
47
|
+
// Expose ref API
|
|
48
|
+
React.useImperativeHandle(ref, () => ({
|
|
49
|
+
getRawResponse: () => formStore.getState().responses,
|
|
50
|
+
getResponse: (options) => {
|
|
51
|
+
const state = formStore.getState();
|
|
52
|
+
const responses = state.responses;
|
|
53
|
+
if (options?.format === 'fhir') {
|
|
54
|
+
// Build FHIR QuestionnaireResponse
|
|
55
|
+
const normalizedAnswers = normalizeResponses(responses);
|
|
56
|
+
// Try to get questionnaire URL from options or formSourceData
|
|
57
|
+
const fhirMeta = state.formSourceData;
|
|
58
|
+
const questionnaireUrl = options.fhir?.questionnaireUrl ??
|
|
59
|
+
fhirMeta?.url ??
|
|
60
|
+
`urn:uuid:${state.formId}`;
|
|
61
|
+
const exportOptions = {
|
|
62
|
+
questionnaireUrl,
|
|
63
|
+
status: options.fhir?.status ?? 'completed',
|
|
64
|
+
subject: options.fhir?.subject,
|
|
65
|
+
author: options.fhir?.author,
|
|
66
|
+
resourceId: options.fhir?.resourceId,
|
|
67
|
+
};
|
|
68
|
+
// Rebuild form definition from normalized state
|
|
69
|
+
const formDefinition = {
|
|
70
|
+
id: state.formId,
|
|
71
|
+
fields: hydrateDefinition(state.normalized),
|
|
72
|
+
_sourceData: state.formSourceData,
|
|
73
|
+
};
|
|
74
|
+
return exportResponseToFhir(formDefinition, normalizedAnswers, exportOptions);
|
|
75
|
+
}
|
|
76
|
+
// Native format (default)
|
|
77
|
+
return responses;
|
|
78
|
+
},
|
|
79
|
+
getFormStore: () => formStore,
|
|
80
|
+
getUIStore: () => uiStore,
|
|
81
|
+
getValidResponse: () => {
|
|
82
|
+
const state = formStore.getState();
|
|
83
|
+
const errors = validateForm(state.normalized, state.responses);
|
|
84
|
+
return {
|
|
85
|
+
response: errors.length === 0 ? state.responses : null,
|
|
86
|
+
errors,
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
isTouchModeEnabled: () => isTouchEnabled,
|
|
90
|
+
setTouchMode: setTouchModeInternal,
|
|
91
|
+
resetTouchMode: resetTouchModeInternal,
|
|
92
|
+
}), [
|
|
93
|
+
formStore,
|
|
94
|
+
uiStore,
|
|
95
|
+
isTouchEnabled,
|
|
96
|
+
setTouchModeInternal,
|
|
97
|
+
resetTouchModeInternal,
|
|
98
|
+
]);
|
|
99
|
+
// Determine if touch mode class should be applied
|
|
100
|
+
// true: always apply, 'auto'/undefined: apply based on isTouchEnabled state, false: never apply (CSS only)
|
|
101
|
+
const applyTouchClass = touchMode === true ||
|
|
102
|
+
((touchMode === 'auto' || touchMode === undefined) && isTouchEnabled);
|
|
103
|
+
// Apply disabled class when user explicitly disabled touch mode (prevents CSS media query)
|
|
104
|
+
const applyDisabledClass = isManualOverride && !isTouchEnabled;
|
|
105
|
+
const rootClasses = [
|
|
106
|
+
'esheet-renderer-root',
|
|
107
|
+
applyTouchClass && 'esheet-touch-active',
|
|
108
|
+
applyDisabledClass && 'touch-mode-disabled',
|
|
109
|
+
'ms:w-full ms:max-w-2xl ms:mx-auto ms:p-4 ms:text-mstext',
|
|
110
|
+
className,
|
|
111
|
+
]
|
|
112
|
+
.filter(Boolean)
|
|
113
|
+
.join(' ');
|
|
114
|
+
return (_jsxs("div", { className: rootClasses, children: [_jsx(ZodIssuesPanel, { issues: validationErrors }), _jsx(RendererBody, { form: formStore, ui: uiStore })] }));
|
|
115
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { FormStore, UIStore } from '@esheet/core';
|
|
3
|
+
export interface FieldNodeProps {
|
|
4
|
+
id: string;
|
|
5
|
+
form: FormStore;
|
|
6
|
+
ui: UIStore;
|
|
7
|
+
/** Nesting depth for visual styling (default: 0) */
|
|
8
|
+
depth?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* FieldNode - Renders a single field or section with recursive children
|
|
12
|
+
*
|
|
13
|
+
* Looks up the field component from the registry and passes field props.
|
|
14
|
+
* For sections, recursively renders visible children.
|
|
15
|
+
*/
|
|
16
|
+
export declare const FieldNode: React.NamedExoticComponent<FieldNodeProps>;
|
|
17
|
+
//# sourceMappingURL=FieldNode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FieldNode.d.ts","sourceRoot":"","sources":["../../../src/lib/components/FieldNode.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,KAAK,EAAuB,SAAS,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAG5E,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,CAAC;IAChB,EAAE,EAAE,OAAO,CAAC;IACZ,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,eAAO,MAAM,SAAS,4CAiKpB,CAAC"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { getFieldComponent } from '@esheet/fields';
|
|
4
|
+
/**
|
|
5
|
+
* FieldNode - Renders a single field or section with recursive children
|
|
6
|
+
*
|
|
7
|
+
* Looks up the field component from the registry and passes field props.
|
|
8
|
+
* For sections, recursively renders visible children.
|
|
9
|
+
*/
|
|
10
|
+
export const FieldNode = React.memo(function FieldNode({ id, form, ui, depth = 1, }) {
|
|
11
|
+
// Subscribe to field data
|
|
12
|
+
const field = React.useSyncExternalStore((cb) => form.subscribe(cb), () => form.getState().getField(id), () => form.getState().getField(id));
|
|
13
|
+
const normalized = React.useSyncExternalStore((cb) => form.subscribe(cb), () => form.getState().normalized, () => form.getState().normalized);
|
|
14
|
+
const responses = React.useSyncExternalStore((cb) => form.subscribe(cb), () => form.getState().responses, () => form.getState().responses);
|
|
15
|
+
// Get visible children for sections
|
|
16
|
+
const visibleChildIds = React.useMemo(() => {
|
|
17
|
+
if (!field || field.definition.fieldType !== 'section')
|
|
18
|
+
return [];
|
|
19
|
+
const node = normalized.byId[id];
|
|
20
|
+
if (!node || node.childIds.length === 0)
|
|
21
|
+
return [];
|
|
22
|
+
const cache = new Map();
|
|
23
|
+
const isFieldRenderable = (fieldId) => {
|
|
24
|
+
const cached = cache.get(fieldId);
|
|
25
|
+
if (cached !== undefined)
|
|
26
|
+
return cached;
|
|
27
|
+
const isVisible = form.getState().isVisible(fieldId);
|
|
28
|
+
if (!isVisible) {
|
|
29
|
+
cache.set(fieldId, false);
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const childNode = normalized.byId[fieldId];
|
|
33
|
+
if (!childNode) {
|
|
34
|
+
cache.set(fieldId, false);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (childNode.definition.fieldType !== 'section') {
|
|
38
|
+
cache.set(fieldId, true);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
const hasRenderableChild = childNode.childIds.some((cid) => isFieldRenderable(cid));
|
|
42
|
+
cache.set(fieldId, hasRenderableChild);
|
|
43
|
+
return hasRenderableChild;
|
|
44
|
+
};
|
|
45
|
+
return node.childIds.filter((childId) => isFieldRenderable(childId));
|
|
46
|
+
}, [field, id, form, normalized, responses]);
|
|
47
|
+
// Render nested children for sections
|
|
48
|
+
const nestedChildren = React.useMemo(() => {
|
|
49
|
+
if (!field ||
|
|
50
|
+
field.definition.fieldType !== 'section' ||
|
|
51
|
+
visibleChildIds.length === 0) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const containerClass = depth === 1
|
|
55
|
+
? 'section-children ms:space-y-2'
|
|
56
|
+
: 'section-children ms:space-y-2 ms:border-l ms:border-msborder ms:pl-3';
|
|
57
|
+
return (_jsx("div", { className: containerClass, "data-depth": depth, children: visibleChildIds.map((childId) => (_jsx(FieldNode, { id: childId, form: form, ui: ui, depth: depth + 1 }, childId))) }));
|
|
58
|
+
}, [field, visibleChildIds, id, form, ui, depth]);
|
|
59
|
+
if (!field)
|
|
60
|
+
return null;
|
|
61
|
+
const Component = getFieldComponent(field.definition.fieldType);
|
|
62
|
+
// Check if field is enabled/required via conditional logic
|
|
63
|
+
const isVisible = form.getState().isVisible(field.definition.id);
|
|
64
|
+
const isEnabled = form.getState().isEnabled(field.definition.id);
|
|
65
|
+
const isRequired = form.getState().isRequired(field.definition.id);
|
|
66
|
+
const response = form.getState().getResponse(field.definition.id);
|
|
67
|
+
if (!isVisible)
|
|
68
|
+
return null;
|
|
69
|
+
const props = {
|
|
70
|
+
field,
|
|
71
|
+
form,
|
|
72
|
+
ui,
|
|
73
|
+
isSelected: false,
|
|
74
|
+
isPreview: true,
|
|
75
|
+
isEnabled,
|
|
76
|
+
isRequired,
|
|
77
|
+
response,
|
|
78
|
+
onRemove: () => undefined, // No-op in renderer
|
|
79
|
+
onUpdate: () => undefined, // No-op in renderer
|
|
80
|
+
onResponse: (value) => form.getState().setResponse(field.definition.id, value),
|
|
81
|
+
};
|
|
82
|
+
const isSection = field.definition.fieldType === 'section';
|
|
83
|
+
const parentNode = field.parentId
|
|
84
|
+
? form.getState().getField(field.parentId)
|
|
85
|
+
: null;
|
|
86
|
+
const isChildOfSection = parentNode?.definition.fieldType === 'section';
|
|
87
|
+
const wrapperClass = `field-wrapper ms:bg-mssurface${isSection ? ' ms:mb-2 ms:border ms:border-msborder ms:rounded' : ''}${!isSection && !isChildOfSection
|
|
88
|
+
? ' ms:mb-2 ms:p-6 ms:border ms:border-msborder ms:rounded'
|
|
89
|
+
: ''}${isChildOfSection
|
|
90
|
+
? ' ms:p-6 ms:border-b ms:border-msborder ms:last:border-b-0'
|
|
91
|
+
: ''}${!isEnabled ? ' ms:opacity-50 ms:pointer-events-none' : ''}`;
|
|
92
|
+
return (_jsx("div", { className: wrapperClass, "data-field-type": field.definition.fieldType, "data-field-id": field.definition.id, "aria-disabled": !isEnabled || undefined, children: field.definition.fieldType === 'section' ? ((() => {
|
|
93
|
+
const SectionComponent = Component;
|
|
94
|
+
return (_jsx(SectionComponent, { ...props, nestedChildren: nestedChildren }));
|
|
95
|
+
})()) : (_jsx(Component, { ...props })) }));
|
|
96
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FormStore, UIStore } from '@esheet/core';
|
|
2
|
+
export interface RendererBodyProps {
|
|
3
|
+
form: FormStore;
|
|
4
|
+
ui: UIStore;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* RendererBody - Iterates over visible root fields and renders them
|
|
8
|
+
*
|
|
9
|
+
* Respects conditional visibility logic from form store.
|
|
10
|
+
* Only renders fields where isVisible() returns true.
|
|
11
|
+
* Sections recursively render their visible children.
|
|
12
|
+
*/
|
|
13
|
+
export declare function RendererBody({ form, ui }: RendererBodyProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
//# sourceMappingURL=RendererBody.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RendererBody.d.ts","sourceRoot":"","sources":["../../../src/lib/components/RendererBody.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,SAAS,CAAC;IAChB,EAAE,EAAE,OAAO,CAAC;CACb;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,iBAAiB,2CAyD3D"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { FieldNode } from './FieldNode.js';
|
|
4
|
+
/**
|
|
5
|
+
* RendererBody - Iterates over visible root fields and renders them
|
|
6
|
+
*
|
|
7
|
+
* Respects conditional visibility logic from form store.
|
|
8
|
+
* Only renders fields where isVisible() returns true.
|
|
9
|
+
* Sections recursively render their visible children.
|
|
10
|
+
*/
|
|
11
|
+
export function RendererBody({ form, ui }) {
|
|
12
|
+
// Subscribe to form state for visibility updates and responses
|
|
13
|
+
const normalized = React.useSyncExternalStore((cb) => form.subscribe(cb), () => form.getState().normalized, () => form.getState().normalized);
|
|
14
|
+
const responses = React.useSyncExternalStore((cb) => form.subscribe(cb), () => form.getState().responses, () => form.getState().responses);
|
|
15
|
+
// Compute visible root fields
|
|
16
|
+
const visibleRootIds = React.useMemo(() => {
|
|
17
|
+
const cache = new Map();
|
|
18
|
+
const isFieldRenderable = (fieldId) => {
|
|
19
|
+
const cached = cache.get(fieldId);
|
|
20
|
+
if (cached !== undefined)
|
|
21
|
+
return cached;
|
|
22
|
+
const isVisible = form.getState().isVisible(fieldId);
|
|
23
|
+
if (!isVisible) {
|
|
24
|
+
cache.set(fieldId, false);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const node = normalized.byId[fieldId];
|
|
28
|
+
if (!node) {
|
|
29
|
+
cache.set(fieldId, false);
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
// Non-section fields are renderable if visible
|
|
33
|
+
if (node.definition.fieldType !== 'section') {
|
|
34
|
+
cache.set(fieldId, true);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
// Sections are renderable only if they have at least one renderable child
|
|
38
|
+
const hasRenderableChild = node.childIds.some((childId) => isFieldRenderable(childId));
|
|
39
|
+
cache.set(fieldId, hasRenderableChild);
|
|
40
|
+
return hasRenderableChild;
|
|
41
|
+
};
|
|
42
|
+
return normalized.rootIds.filter((id) => isFieldRenderable(id));
|
|
43
|
+
}, [form, normalized, responses]);
|
|
44
|
+
return (_jsx("div", { className: "canvas-fields renderer-body ms:space-y-0", children: visibleRootIds.map((id) => (_jsx(FieldNode, { id: id, form: form, ui: ui }, id))) }));
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useRendererInit } from './useRendererInit.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type FormResponse, type FormStore, type UIStore } from '@esheet/core';
|
|
2
|
+
/**
|
|
3
|
+
* Initialize renderer with form definition.
|
|
4
|
+
*
|
|
5
|
+
* Auto-detects and converts the following input formats:
|
|
6
|
+
* - eSheet FormDefinition (object or JSON/YAML string)
|
|
7
|
+
* - FHIR R4 Questionnaire resource
|
|
8
|
+
* - MCP elicitation/create envelope
|
|
9
|
+
* - SurveyJS schema (has top-level `pages` or `elements` array)
|
|
10
|
+
*/
|
|
11
|
+
export declare function useRendererInit(form: FormStore, ui: UIStore, formData: unknown, initialResponses?: FormResponse, onValidationError?: (errors: string[]) => void, strict?: boolean, onReady?: () => void): void;
|
|
12
|
+
//# sourceMappingURL=useRendererInit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useRendererInit.d.ts","sourceRoot":"","sources":["../../../src/lib/hooks/useRendererInit.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,OAAO,EACb,MAAM,cAAc,CAAC;AAYtB;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,SAAS,EACf,EAAE,EAAE,OAAO,EACX,QAAQ,EAAE,OAAO,EACjB,gBAAgB,CAAC,EAAE,YAAY,EAC/B,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,EAC9C,MAAM,UAAQ,EACd,OAAO,CAAC,EAAE,MAAM,IAAI,GACnB,IAAI,CAqFN"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import YAML from 'js-yaml';
|
|
3
|
+
import { formatZodValidationError, formDefinitionSchema, } from '@esheet/core';
|
|
4
|
+
import { importFromMcp, convertSurveyJS, isSurveyJSSchema, isMcpElicitationRequest, importFromFhir, isFhirQuestionnaire, } from '@esheet/adapters';
|
|
5
|
+
/**
|
|
6
|
+
* Initialize renderer with form definition.
|
|
7
|
+
*
|
|
8
|
+
* Auto-detects and converts the following input formats:
|
|
9
|
+
* - eSheet FormDefinition (object or JSON/YAML string)
|
|
10
|
+
* - FHIR R4 Questionnaire resource
|
|
11
|
+
* - MCP elicitation/create envelope
|
|
12
|
+
* - SurveyJS schema (has top-level `pages` or `elements` array)
|
|
13
|
+
*/
|
|
14
|
+
export function useRendererInit(form, ui, formData, initialResponses, onValidationError, strict = false, onReady) {
|
|
15
|
+
// Store onReady in a ref so it never causes the effect to re-run
|
|
16
|
+
const onReadyRef = React.useRef(onReady);
|
|
17
|
+
onReadyRef.current = onReady;
|
|
18
|
+
React.useEffect(() => {
|
|
19
|
+
try {
|
|
20
|
+
// Parse input if string
|
|
21
|
+
let parsed;
|
|
22
|
+
if (typeof formData === 'string') {
|
|
23
|
+
const trimmed = formData.trim();
|
|
24
|
+
// Detect format: YAML if starts with non-brace, JSON otherwise
|
|
25
|
+
const isYaml = !trimmed.startsWith('{') && !trimmed.startsWith('[');
|
|
26
|
+
parsed = isYaml ? YAML.load(trimmed) : JSON.parse(trimmed);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
parsed = formData;
|
|
30
|
+
}
|
|
31
|
+
// Auto-detect and convert FHIR, MCP or SurveyJS input (skipped in strict mode)
|
|
32
|
+
if (!strict && isFhirQuestionnaire(parsed)) {
|
|
33
|
+
parsed = importFromFhir(parsed);
|
|
34
|
+
}
|
|
35
|
+
else if (!strict && isMcpElicitationRequest(parsed)) {
|
|
36
|
+
const mcpReq = parsed;
|
|
37
|
+
if (mcpReq.params.mode !== 'url') {
|
|
38
|
+
parsed = importFromMcp(mcpReq.params.requestedSchema, {
|
|
39
|
+
mcpId: mcpReq.id,
|
|
40
|
+
mcpMessage: mcpReq.params.message,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (!strict && isSurveyJSSchema(parsed)) {
|
|
45
|
+
parsed = convertSurveyJS(parsed);
|
|
46
|
+
}
|
|
47
|
+
// Validate schema
|
|
48
|
+
const validated = formDefinitionSchema.safeParse(parsed);
|
|
49
|
+
if (!validated.success) {
|
|
50
|
+
const errors = validated.error.issues.map(formatZodValidationError);
|
|
51
|
+
console.error('[EsheetRenderer] Invalid form definition:', validated.error.issues);
|
|
52
|
+
onValidationError?.(errors);
|
|
53
|
+
// Load empty form instead of crashing
|
|
54
|
+
form.getState().loadDefinition({
|
|
55
|
+
id: 'invalid-form',
|
|
56
|
+
title: 'Invalid Form',
|
|
57
|
+
fields: [],
|
|
58
|
+
});
|
|
59
|
+
ui.getState().setMode('preview');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Clear any previous validation errors on success
|
|
63
|
+
onValidationError?.([]);
|
|
64
|
+
// Load validated definition
|
|
65
|
+
form.getState().loadDefinition(validated.data);
|
|
66
|
+
// Apply initial responses if provided
|
|
67
|
+
if (initialResponses && Object.keys(initialResponses).length > 0) {
|
|
68
|
+
for (const [fieldId, value] of Object.entries(initialResponses)) {
|
|
69
|
+
form.getState().setResponse(fieldId, value);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Set preview mode
|
|
73
|
+
ui.getState().setMode('preview');
|
|
74
|
+
onReadyRef.current?.();
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.error('[EsheetRenderer] Failed to initialize:', error);
|
|
78
|
+
// Load empty form as fallback
|
|
79
|
+
form.getState().loadDefinition({
|
|
80
|
+
id: 'error-form',
|
|
81
|
+
title: 'Error',
|
|
82
|
+
fields: [],
|
|
83
|
+
});
|
|
84
|
+
ui.getState().setMode('preview');
|
|
85
|
+
}
|
|
86
|
+
}, [form, ui, formData, initialResponses, onValidationError, strict]);
|
|
87
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { executeToolCall } from './tool-executor.js';
|
|
2
|
+
export { RENDERER_TOOL_DEFINITIONS } from './tool-definitions.js';
|
|
3
|
+
export { RENDERER_SYSTEM_PROMPT } from './system-prompt.js';
|
|
4
|
+
export type { ToolDefinition } from './tool-definitions.js';
|
|
5
|
+
export { useRendererMcpToolHandler } from './useRendererToolBridge.js';
|
|
6
|
+
export type { UseRendererMcpToolHandlerOptions } from './useRendererToolBridge.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/mcp/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,YAAY,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACvE,YAAY,EAAE,gCAAgC,EAAE,MAAM,4BAA4B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system-prompt.d.ts","sourceRoot":"","sources":["../../../src/lib/mcp/system-prompt.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,sBAAsB,QAwByC,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const RENDERER_SYSTEM_PROMPT = 'Form renderer assistant. You can read the current form state, inspect field values, fill in responses, and clear answers.\n\n' +
|
|
2
|
+
'FILL WORKFLOW:\n' +
|
|
3
|
+
'1. Call get_form — returns {totalFields, filledCount, unfilledFields, filledFieldIds}.\n' +
|
|
4
|
+
'2. Fill every field in unfilledFields using fill_field.\n' +
|
|
5
|
+
'3. After each fill, check unfilledFields in the response for remaining empty fields. Fill them immediately.\n' +
|
|
6
|
+
'4. You are done when unfilledFields is empty.\n' +
|
|
7
|
+
'NEVER skip a field because you are unsure of its value — generate a realistic mock value for every field including dates, times, and appointment times.\n' +
|
|
8
|
+
'NEVER plan all fills upfront — conditional logic reveals new fields dynamically and you must respond to unfilledFields after each fill.\n\n' +
|
|
9
|
+
'TEXT FIELD FORMAT RULES — each text field includes a valueFormat property; use it exactly:\n' +
|
|
10
|
+
' valueFormat "YYYY-MM-DD" → date, e.g. "2026-05-21"\n' +
|
|
11
|
+
' valueFormat "YYYY-MM-DDTHH:mm" → datetime-local, e.g. "2026-05-21T14:30"\n' +
|
|
12
|
+
' valueFormat "YYYY-MM" → month, e.g. "2026-05"\n' +
|
|
13
|
+
' valueFormat "HH:mm" → time (24-hour), e.g. "14:30"\n' +
|
|
14
|
+
'Wrong formats are rejected with an error — always use the valueFormat from the field schema.\n\n' +
|
|
15
|
+
'OTHER FIELD RULES:\n' +
|
|
16
|
+
' radio/dropdown/boolean/rating/slider → single string matching an option value\n' +
|
|
17
|
+
' check/multiselectdropdown → array of strings matching option values\n' +
|
|
18
|
+
' ranking → ordered array of option value strings\n' +
|
|
19
|
+
' multitext → array of strings, one per option slot\n' +
|
|
20
|
+
' singlematrix → { "Row Label": "Column Label" }\n' +
|
|
21
|
+
' multimatrix → { "Row Label": ["Col1", "Col2"] }\n\n' +
|
|
22
|
+
'Use get_form_raw to see ALL fields. Use get_form_tree for visibility/enabled/required state. ' +
|
|
23
|
+
'Use get_valid_response before submitting. Use clear_responses to reset. ' +
|
|
24
|
+
'For conversational questions reply in plain text without calling a tool.';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ToolDefinition {
|
|
2
|
+
type: string;
|
|
3
|
+
function: {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
parameters: Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export declare const RENDERER_TOOL_DEFINITIONS: readonly ToolDefinition[];
|
|
10
|
+
//# sourceMappingURL=tool-definitions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-definitions.d.ts","sourceRoot":"","sources":["../../../src/lib/mcp/tool-definitions.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACrC,CAAC;CACH;AAED,eAAO,MAAM,yBAAyB,EAAE,SAAS,cAAc,EAsE9D,CAAC"}
|