@vertesia/fusion-ux 1.3.0 → 1.4.0-dev.20260614.160504Z
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/LICENSE +198 -10
- package/lib/fusion-fragment/ChartRenderer.d.ts.map +1 -0
- package/lib/{esm/fusion-fragment → fusion-fragment}/ChartRenderer.js +8 -6
- package/lib/fusion-fragment/ChartRenderer.js.map +1 -0
- package/lib/{types/fusion-fragment → fusion-fragment}/FieldRenderer.d.ts +1 -1
- package/lib/fusion-fragment/FieldRenderer.d.ts.map +1 -0
- package/lib/{esm/fusion-fragment → fusion-fragment}/FieldRenderer.js +14 -8
- package/lib/fusion-fragment/FieldRenderer.js.map +1 -0
- package/lib/{types/fusion-fragment → fusion-fragment}/FusionFragmentContext.d.ts +3 -3
- package/lib/fusion-fragment/FusionFragmentContext.d.ts.map +1 -0
- package/lib/{esm/fusion-fragment → fusion-fragment}/FusionFragmentContext.js +2 -2
- package/lib/fusion-fragment/FusionFragmentContext.js.map +1 -0
- package/lib/fusion-fragment/FusionFragmentHandler.d.ts.map +1 -0
- package/lib/{esm/fusion-fragment → fusion-fragment}/FusionFragmentHandler.js +7 -14
- package/lib/fusion-fragment/FusionFragmentHandler.js.map +1 -0
- package/lib/fusion-fragment/FusionFragmentRenderer.d.ts.map +1 -0
- package/lib/{esm/fusion-fragment → fusion-fragment}/FusionFragmentRenderer.js +2 -2
- package/lib/fusion-fragment/FusionFragmentRenderer.js.map +1 -0
- package/lib/{types/fusion-fragment → fusion-fragment}/SectionRenderer.d.ts +1 -1
- package/lib/fusion-fragment/SectionRenderer.d.ts.map +1 -0
- package/lib/{esm/fusion-fragment → fusion-fragment}/SectionRenderer.js +12 -5
- package/lib/fusion-fragment/SectionRenderer.js.map +1 -0
- package/lib/fusion-fragment/TableRenderer.d.ts.map +1 -0
- package/lib/{esm/fusion-fragment → fusion-fragment}/TableRenderer.js +9 -7
- package/lib/fusion-fragment/TableRenderer.js.map +1 -0
- package/lib/{types/fusion-fragment → fusion-fragment}/index.d.ts +4 -4
- package/lib/fusion-fragment/index.d.ts.map +1 -0
- package/lib/{esm/fusion-fragment → fusion-fragment}/index.js +4 -4
- package/lib/fusion-fragment/index.js.map +1 -0
- package/lib/{types/index.d.ts → index.d.ts} +4 -4
- package/lib/index.d.ts.map +1 -0
- package/lib/{esm/index.js → index.js} +4 -4
- package/lib/index.js.map +1 -0
- package/lib/{types/render → render}/index.d.ts +1 -1
- package/lib/render/index.d.ts.map +1 -0
- package/lib/{esm/render → render}/index.js +1 -1
- package/lib/render/index.js.map +1 -0
- package/lib/render/textPreview.d.ts.map +1 -0
- package/lib/{esm/render → render}/textPreview.js +12 -9
- package/lib/render/textPreview.js.map +1 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js.map +1 -0
- package/lib/validation/formatErrors.d.ts.map +1 -0
- package/lib/{esm/validation → validation}/formatErrors.js +1 -1
- package/lib/validation/formatErrors.js.map +1 -0
- package/lib/validation/fuzzyMatch.d.ts.map +1 -0
- package/lib/{esm/validation → validation}/fuzzyMatch.js +3 -4
- package/lib/validation/fuzzyMatch.js.map +1 -0
- package/lib/validation/index.d.ts +8 -0
- package/lib/validation/index.d.ts.map +1 -0
- package/lib/validation/index.js +8 -0
- package/lib/validation/index.js.map +1 -0
- package/lib/{types/validation → validation}/schemas.d.ts +1 -1
- package/lib/validation/schemas.d.ts.map +1 -0
- package/lib/{esm/validation → validation}/schemas.js +25 -25
- package/lib/validation/schemas.js.map +1 -0
- package/lib/validation/validateTemplate.d.ts.map +1 -0
- package/lib/{esm/validation → validation}/validateTemplate.js +23 -21
- package/lib/validation/validateTemplate.js.map +1 -0
- package/lib/vertesia-fusion-ux.js +1 -1
- package/lib/vertesia-fusion-ux.js.map +1 -1
- package/package.json +23 -35
- package/src/fusion-fragment/ChartRenderer.tsx +95 -96
- package/src/fusion-fragment/FieldRenderer.tsx +173 -174
- package/src/fusion-fragment/FusionFragmentContext.tsx +31 -37
- package/src/fusion-fragment/FusionFragmentHandler.tsx +214 -223
- package/src/fusion-fragment/FusionFragmentRenderer.tsx +102 -107
- package/src/fusion-fragment/SectionRenderer.tsx +174 -169
- package/src/fusion-fragment/TableRenderer.tsx +175 -171
- package/src/fusion-fragment/index.ts +11 -11
- package/src/index.ts +42 -45
- package/src/render/index.ts +3 -3
- package/src/render/textPreview.ts +183 -186
- package/src/types.ts +174 -174
- package/src/validation/formatErrors.ts +86 -86
- package/src/validation/fuzzyMatch.ts +69 -77
- package/src/validation/index.ts +3 -3
- package/src/validation/schemas.ts +120 -120
- package/src/validation/validateTemplate.ts +225 -226
- package/lib/esm/fusion-fragment/ChartRenderer.js.map +0 -1
- package/lib/esm/fusion-fragment/FieldRenderer.js.map +0 -1
- package/lib/esm/fusion-fragment/FusionFragmentContext.js.map +0 -1
- package/lib/esm/fusion-fragment/FusionFragmentHandler.js.map +0 -1
- package/lib/esm/fusion-fragment/FusionFragmentRenderer.js.map +0 -1
- package/lib/esm/fusion-fragment/SectionRenderer.js.map +0 -1
- package/lib/esm/fusion-fragment/TableRenderer.js.map +0 -1
- package/lib/esm/fusion-fragment/index.js.map +0 -1
- package/lib/esm/index.js.map +0 -1
- package/lib/esm/render/index.js.map +0 -1
- package/lib/esm/render/textPreview.js.map +0 -1
- package/lib/esm/types.js.map +0 -1
- package/lib/esm/validation/formatErrors.js.map +0 -1
- package/lib/esm/validation/fuzzyMatch.js.map +0 -1
- package/lib/esm/validation/index.js +0 -8
- package/lib/esm/validation/index.js.map +0 -1
- package/lib/esm/validation/schemas.js.map +0 -1
- package/lib/esm/validation/validateTemplate.js.map +0 -1
- package/lib/tsconfig.tsbuildinfo +0 -1
- package/lib/types/fusion-fragment/ChartRenderer.d.ts.map +0 -1
- package/lib/types/fusion-fragment/FieldRenderer.d.ts.map +0 -1
- package/lib/types/fusion-fragment/FusionFragmentContext.d.ts.map +0 -1
- package/lib/types/fusion-fragment/FusionFragmentHandler.d.ts.map +0 -1
- package/lib/types/fusion-fragment/FusionFragmentRenderer.d.ts.map +0 -1
- package/lib/types/fusion-fragment/SectionRenderer.d.ts.map +0 -1
- package/lib/types/fusion-fragment/TableRenderer.d.ts.map +0 -1
- package/lib/types/fusion-fragment/index.d.ts.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
- package/lib/types/render/index.d.ts.map +0 -1
- package/lib/types/render/textPreview.d.ts.map +0 -1
- package/lib/types/types.d.ts.map +0 -1
- package/lib/types/validation/formatErrors.d.ts.map +0 -1
- package/lib/types/validation/fuzzyMatch.d.ts.map +0 -1
- package/lib/types/validation/index.d.ts +0 -8
- package/lib/types/validation/index.d.ts.map +0 -1
- package/lib/types/validation/schemas.d.ts.map +0 -1
- package/lib/types/validation/validateTemplate.d.ts.map +0 -1
- /package/lib/{types/fusion-fragment → fusion-fragment}/ChartRenderer.d.ts +0 -0
- /package/lib/{types/fusion-fragment → fusion-fragment}/FusionFragmentHandler.d.ts +0 -0
- /package/lib/{types/fusion-fragment → fusion-fragment}/FusionFragmentRenderer.d.ts +0 -0
- /package/lib/{types/fusion-fragment → fusion-fragment}/TableRenderer.d.ts +0 -0
- /package/lib/{types/render → render}/textPreview.d.ts +0 -0
- /package/lib/{types/types.d.ts → types.d.ts} +0 -0
- /package/lib/{esm/types.js → types.js} +0 -0
- /package/lib/{types/validation → validation}/formatErrors.d.ts +0 -0
- /package/lib/{types/validation → validation}/fuzzyMatch.d.ts +0 -0
- /package/lib/{types/validation → validation}/validateTemplate.d.ts +0 -0
|
@@ -3,24 +3,24 @@
|
|
|
3
3
|
* Provides data and update handlers to nested components
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createContext,
|
|
7
|
-
import type {
|
|
6
|
+
import { createContext, type ReactElement, type ReactNode, useContext, useMemo } from 'react';
|
|
7
|
+
import type { ChartComponentProps, FusionFragmentContextValue } from '../types.js';
|
|
8
8
|
|
|
9
9
|
const FusionFragmentContext = createContext<FusionFragmentContextValue | null>(null);
|
|
10
10
|
|
|
11
11
|
export interface FusionFragmentProviderProps {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
/** Data to display in fragments */
|
|
13
|
+
data: Record<string, unknown>;
|
|
14
|
+
/** Callback when a field is updated (direct mode) */
|
|
15
|
+
onUpdate?: (key: string, value: unknown) => Promise<void>;
|
|
16
|
+
/** Send message to conversation (agent mode) */
|
|
17
|
+
sendMessage?: (message: string) => void;
|
|
18
|
+
/** Chart component to render Vega-Lite charts (injected to avoid circular deps) */
|
|
19
|
+
ChartComponent?: React.ComponentType<ChartComponentProps>;
|
|
20
|
+
/** Artifact run ID for resolving artifact references */
|
|
21
|
+
artifactRunId?: string;
|
|
22
|
+
/** Children components */
|
|
23
|
+
children: ReactNode;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
@@ -40,23 +40,19 @@ export interface FusionFragmentProviderProps {
|
|
|
40
40
|
* ```
|
|
41
41
|
*/
|
|
42
42
|
export function FusionFragmentProvider({
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
data,
|
|
44
|
+
onUpdate,
|
|
45
|
+
sendMessage,
|
|
46
|
+
ChartComponent,
|
|
47
|
+
artifactRunId,
|
|
48
|
+
children,
|
|
49
49
|
}: FusionFragmentProviderProps): ReactElement {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
const value = useMemo<FusionFragmentContextValue>(
|
|
51
|
+
() => ({ data, onUpdate, sendMessage, ChartComponent, artifactRunId }),
|
|
52
|
+
[data, onUpdate, sendMessage, ChartComponent, artifactRunId],
|
|
53
|
+
);
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
<FusionFragmentContext.Provider value={value}>
|
|
57
|
-
{children}
|
|
58
|
-
</FusionFragmentContext.Provider>
|
|
59
|
-
);
|
|
55
|
+
return <FusionFragmentContext.Provider value={value}>{children}</FusionFragmentContext.Provider>;
|
|
60
56
|
}
|
|
61
57
|
|
|
62
58
|
/**
|
|
@@ -64,15 +60,13 @@ export function FusionFragmentProvider({
|
|
|
64
60
|
* @throws Error if used outside of FusionFragmentProvider
|
|
65
61
|
*/
|
|
66
62
|
export function useFusionFragmentContext(): FusionFragmentContextValue {
|
|
67
|
-
|
|
63
|
+
const context = useContext(FusionFragmentContext);
|
|
68
64
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
);
|
|
73
|
-
}
|
|
65
|
+
if (!context) {
|
|
66
|
+
throw new Error('useFusionFragmentContext must be used within a FusionFragmentProvider');
|
|
67
|
+
}
|
|
74
68
|
|
|
75
|
-
|
|
69
|
+
return context;
|
|
76
70
|
}
|
|
77
71
|
|
|
78
72
|
/**
|
|
@@ -80,5 +74,5 @@ export function useFusionFragmentContext(): FusionFragmentContextValue {
|
|
|
80
74
|
* Returns null if not within a provider (useful for optional context)
|
|
81
75
|
*/
|
|
82
76
|
export function useFusionFragmentContextSafe(): FusionFragmentContextValue | null {
|
|
83
|
-
|
|
77
|
+
return useContext(FusionFragmentContext);
|
|
84
78
|
}
|
|
@@ -3,77 +3,77 @@
|
|
|
3
3
|
* Parses fusion-fragment code blocks and renders them
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { type ReactElement, useMemo } from 'react';
|
|
7
7
|
import type { FragmentTemplate } from '../types.js';
|
|
8
8
|
import { parseAndValidateTemplate } from '../validation/validateTemplate.js';
|
|
9
|
-
import { FusionFragmentRenderer } from './FusionFragmentRenderer.js';
|
|
10
9
|
import { useFusionFragmentContextSafe } from './FusionFragmentContext.js';
|
|
10
|
+
import { FusionFragmentRenderer } from './FusionFragmentRenderer.js';
|
|
11
11
|
|
|
12
12
|
const styles = {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
13
|
+
error: {
|
|
14
|
+
backgroundColor: 'var(--red-2, #fef2f2)',
|
|
15
|
+
border: '1px solid var(--red-6, #fca5a5)',
|
|
16
|
+
borderRadius: '8px',
|
|
17
|
+
padding: '16px',
|
|
18
|
+
fontFamily: 'var(--font-mono, ui-monospace, monospace)',
|
|
19
|
+
fontSize: '13px',
|
|
20
|
+
},
|
|
21
|
+
errorTitle: {
|
|
22
|
+
fontWeight: 600,
|
|
23
|
+
color: 'var(--red-11, #dc2626)',
|
|
24
|
+
marginBottom: '8px',
|
|
25
|
+
},
|
|
26
|
+
errorMessage: {
|
|
27
|
+
color: 'var(--red-10, #ef4444)',
|
|
28
|
+
whiteSpace: 'pre-wrap' as const,
|
|
29
|
+
},
|
|
30
|
+
noContext: {
|
|
31
|
+
backgroundColor: 'var(--yellow-2, #fefce8)',
|
|
32
|
+
border: '1px solid var(--yellow-6, #fde047)',
|
|
33
|
+
borderRadius: '8px',
|
|
34
|
+
padding: '16px',
|
|
35
|
+
},
|
|
36
|
+
noContextTitle: {
|
|
37
|
+
fontSize: '14px',
|
|
38
|
+
fontWeight: 600,
|
|
39
|
+
color: 'var(--yellow-11, #ca8a04)',
|
|
40
|
+
marginBottom: '8px',
|
|
41
|
+
},
|
|
42
|
+
noContextMessage: {
|
|
43
|
+
fontSize: '13px',
|
|
44
|
+
color: 'var(--gray-11, #6b7280)',
|
|
45
|
+
},
|
|
46
|
+
loading: {
|
|
47
|
+
display: 'flex',
|
|
48
|
+
flexDirection: 'column' as const,
|
|
49
|
+
alignItems: 'center',
|
|
50
|
+
justifyContent: 'center',
|
|
51
|
+
gap: '12px',
|
|
52
|
+
minHeight: '150px',
|
|
53
|
+
borderRadius: '8px',
|
|
54
|
+
border: '1px solid var(--gray-6, #e5e7eb)',
|
|
55
|
+
backgroundColor: 'var(--gray-2, #f9fafb)',
|
|
56
|
+
},
|
|
57
|
+
loadingIcon: {
|
|
58
|
+
width: '32px',
|
|
59
|
+
height: '32px',
|
|
60
|
+
color: 'var(--gray-9, #9ca3af)',
|
|
61
|
+
},
|
|
62
|
+
loadingText: {
|
|
63
|
+
fontSize: '14px',
|
|
64
|
+
color: 'var(--gray-9, #9ca3af)',
|
|
65
|
+
},
|
|
66
|
+
loadingDots: {
|
|
67
|
+
display: 'flex',
|
|
68
|
+
gap: '4px',
|
|
69
|
+
},
|
|
70
|
+
loadingDot: {
|
|
71
|
+
width: '8px',
|
|
72
|
+
height: '8px',
|
|
73
|
+
borderRadius: '50%',
|
|
74
|
+
backgroundColor: 'var(--gray-6, #d1d5db)',
|
|
75
|
+
animation: 'fusion-fragment-bounce 1s infinite',
|
|
76
|
+
},
|
|
77
77
|
};
|
|
78
78
|
|
|
79
79
|
/**
|
|
@@ -81,146 +81,141 @@ const styles = {
|
|
|
81
81
|
* vs actually invalid JSON structure
|
|
82
82
|
*/
|
|
83
83
|
function isIncompleteJson(code: string): boolean {
|
|
84
|
-
|
|
84
|
+
const trimmed = code.trim();
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
// Empty or very short content is likely incomplete
|
|
87
|
+
if (trimmed.length < 2) return true;
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
// Must start with { for a valid JSON object
|
|
90
|
+
if (!trimmed.startsWith('{')) return false;
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
// Try to parse - if it succeeds, it's not incomplete
|
|
93
|
+
try {
|
|
94
|
+
JSON.parse(trimmed);
|
|
95
|
+
return false; // Valid JSON
|
|
96
|
+
} catch (e) {
|
|
97
|
+
const message = e instanceof Error ? e.message : '';
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
'unexpected end',
|
|
102
|
-
'unterminated string',
|
|
103
|
-
'expected',
|
|
104
|
-
'unexpected token',
|
|
105
|
-
];
|
|
99
|
+
// Common indicators of incomplete JSON during streaming
|
|
100
|
+
const incompleteIndicators = ['unexpected end', 'unterminated string', 'expected', 'unexpected token'];
|
|
106
101
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
102
|
+
const lowerMessage = message.toLowerCase();
|
|
103
|
+
if (incompleteIndicators.some((ind) => lowerMessage.includes(ind))) {
|
|
104
|
+
// Additional check: count brackets to see if they're unbalanced
|
|
105
|
+
let braceCount = 0;
|
|
106
|
+
let bracketCount = 0;
|
|
107
|
+
let inString = false;
|
|
108
|
+
let escaped = false;
|
|
114
109
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
110
|
+
for (const char of trimmed) {
|
|
111
|
+
if (escaped) {
|
|
112
|
+
escaped = false;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (char === '\\') {
|
|
116
|
+
escaped = true;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (char === '"') {
|
|
120
|
+
inString = !inString;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!inString) {
|
|
124
|
+
if (char === '{') braceCount++;
|
|
125
|
+
else if (char === '}') braceCount--;
|
|
126
|
+
else if (char === '[') bracketCount++;
|
|
127
|
+
else if (char === ']') bracketCount--;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// If brackets are unbalanced or we're in an unclosed string, it's incomplete
|
|
132
|
+
return braceCount > 0 || bracketCount > 0 || inString;
|
|
133
133
|
}
|
|
134
|
-
}
|
|
135
134
|
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
// For other parse errors, consider it invalid rather than incomplete
|
|
136
|
+
return false;
|
|
138
137
|
}
|
|
139
|
-
|
|
140
|
-
// For other parse errors, consider it invalid rather than incomplete
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
138
|
}
|
|
144
139
|
|
|
145
140
|
export interface FusionFragmentHandlerProps {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
141
|
+
/** The JSON code from the fusion-fragment code block */
|
|
142
|
+
code: string;
|
|
143
|
+
/** Optional data to use instead of context */
|
|
144
|
+
data?: Record<string, unknown>;
|
|
145
|
+
/** Optional update handler */
|
|
146
|
+
onUpdate?: (key: string, value: unknown) => Promise<void>;
|
|
152
147
|
}
|
|
153
148
|
|
|
154
149
|
/**
|
|
155
150
|
* Loading placeholder shown while streaming incomplete JSON
|
|
156
151
|
*/
|
|
157
152
|
function LoadingPlaceholder(): ReactElement {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
153
|
+
return (
|
|
154
|
+
<div style={styles.loading}>
|
|
155
|
+
<style>{`
|
|
161
156
|
@keyframes fusion-fragment-bounce {
|
|
162
157
|
0%, 80%, 100% { transform: translateY(0); }
|
|
163
158
|
40% { transform: translateY(-6px); }
|
|
164
159
|
}
|
|
165
160
|
`}</style>
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
161
|
+
<svg
|
|
162
|
+
style={styles.loadingIcon}
|
|
163
|
+
viewBox="0 0 24 24"
|
|
164
|
+
fill="none"
|
|
165
|
+
stroke="currentColor"
|
|
166
|
+
strokeWidth="2"
|
|
167
|
+
strokeLinecap="round"
|
|
168
|
+
strokeLinejoin="round"
|
|
169
|
+
role="img"
|
|
170
|
+
>
|
|
171
|
+
<title>Loading</title>
|
|
172
|
+
<polyline points="16 18 22 12 16 6" />
|
|
173
|
+
<polyline points="8 6 2 12 8 18" />
|
|
174
|
+
</svg>
|
|
175
|
+
<span style={styles.loadingText}>Loading fragment...</span>
|
|
176
|
+
<div style={styles.loadingDots}>
|
|
177
|
+
{[0, 1, 2].map((i) => (
|
|
178
|
+
<div
|
|
179
|
+
key={i}
|
|
180
|
+
style={{
|
|
181
|
+
...styles.loadingDot,
|
|
182
|
+
animationDelay: `${i * 150}ms`,
|
|
183
|
+
}}
|
|
184
|
+
/>
|
|
185
|
+
))}
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
192
189
|
}
|
|
193
190
|
|
|
194
191
|
/**
|
|
195
192
|
* Error display component
|
|
196
193
|
*/
|
|
197
194
|
function ParseError({ message }: { message: string }): ReactElement {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
195
|
+
return (
|
|
196
|
+
<div style={styles.error}>
|
|
197
|
+
<div style={styles.errorTitle}>Failed to parse fusion-fragment</div>
|
|
198
|
+
<div style={styles.errorMessage}>{message}</div>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
204
201
|
}
|
|
205
202
|
|
|
206
203
|
/**
|
|
207
204
|
* Warning when no context is available
|
|
208
205
|
*/
|
|
209
206
|
function NoContextWarning({ template }: { template: FragmentTemplate }): ReactElement {
|
|
210
|
-
|
|
211
|
-
.flatMap(s => s.fields || [])
|
|
212
|
-
.map(f => f.key);
|
|
207
|
+
const requiredKeys = template.sections.flatMap((s) => s.fields || []).map((f) => f.key);
|
|
213
208
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
209
|
+
return (
|
|
210
|
+
<div style={styles.noContext}>
|
|
211
|
+
<div style={styles.noContextTitle}>No data context available</div>
|
|
212
|
+
<div style={styles.noContextMessage}>
|
|
213
|
+
This fusion-fragment requires data with keys: {requiredKeys.join(', ')}.
|
|
214
|
+
<br />
|
|
215
|
+
Wrap this component in a FusionFragmentProvider with the required data.
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
224
219
|
}
|
|
225
220
|
|
|
226
221
|
/**
|
|
@@ -239,66 +234,62 @@ function NoContextWarning({ template }: { template: FragmentTemplate }): ReactEl
|
|
|
239
234
|
* ```
|
|
240
235
|
*/
|
|
241
236
|
export function FusionFragmentHandler({
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
237
|
+
code,
|
|
238
|
+
data: propData,
|
|
239
|
+
onUpdate: propOnUpdate,
|
|
245
240
|
}: FusionFragmentHandlerProps): ReactElement {
|
|
246
|
-
|
|
247
|
-
|
|
241
|
+
// Try to get context (may be null if no provider)
|
|
242
|
+
const context = useFusionFragmentContextSafe();
|
|
248
243
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
244
|
+
// Use prop data or context data
|
|
245
|
+
const data = propData ?? context?.data;
|
|
246
|
+
const onUpdate = propOnUpdate ?? context?.onUpdate;
|
|
247
|
+
const sendMessage = context?.sendMessage;
|
|
253
248
|
|
|
254
|
-
|
|
255
|
-
|
|
249
|
+
// Check if JSON is incomplete (streaming) - show loading placeholder
|
|
250
|
+
const incomplete = useMemo(() => isIncompleteJson(code), [code]);
|
|
256
251
|
|
|
257
|
-
|
|
258
|
-
|
|
252
|
+
// Parse and validate the template (only if not obviously incomplete)
|
|
253
|
+
const result = useMemo(() => {
|
|
254
|
+
if (incomplete) {
|
|
255
|
+
return { valid: false, errors: [], template: undefined };
|
|
256
|
+
}
|
|
257
|
+
const dataKeys = data ? Object.keys(data) : [];
|
|
258
|
+
return parseAndValidateTemplate(code, dataKeys);
|
|
259
|
+
}, [code, data, incomplete]);
|
|
260
|
+
|
|
261
|
+
// Show loading placeholder for incomplete JSON (streaming in progress)
|
|
259
262
|
if (incomplete) {
|
|
260
|
-
|
|
263
|
+
return <LoadingPlaceholder />;
|
|
261
264
|
}
|
|
262
|
-
const dataKeys = data ? Object.keys(data) : [];
|
|
263
|
-
return parseAndValidateTemplate(code, dataKeys);
|
|
264
|
-
}, [code, data, incomplete]);
|
|
265
|
-
|
|
266
|
-
// Show loading placeholder for incomplete JSON (streaming in progress)
|
|
267
|
-
if (incomplete) {
|
|
268
|
-
return <LoadingPlaceholder />;
|
|
269
|
-
}
|
|
270
265
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
266
|
+
// Parse error
|
|
267
|
+
if (!result.valid && !result.template) {
|
|
268
|
+
const parseError = result.errors.find((e) => e.path === 'root' && e.message.includes('JSON'));
|
|
269
|
+
if (parseError) {
|
|
270
|
+
return <ParseError message={parseError.message} />;
|
|
271
|
+
}
|
|
276
272
|
}
|
|
277
|
-
}
|
|
278
273
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
274
|
+
// Template parsed but no data available
|
|
275
|
+
if (result.template && !data) {
|
|
276
|
+
return <NoContextWarning template={result.template} />;
|
|
277
|
+
}
|
|
283
278
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
279
|
+
// Template parsed but validation errors (show errors in renderer)
|
|
280
|
+
if (result.template && data) {
|
|
281
|
+
return (
|
|
282
|
+
<FusionFragmentRenderer
|
|
283
|
+
template={result.template}
|
|
284
|
+
data={data}
|
|
285
|
+
onUpdate={onUpdate}
|
|
286
|
+
agentMode={sendMessage ? { enabled: true, sendMessage } : undefined}
|
|
287
|
+
/>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
295
290
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
<ParseError
|
|
299
|
-
message={result.errors.map(e => `${e.path}: ${e.message}`).join('\n')}
|
|
300
|
-
/>
|
|
301
|
-
);
|
|
291
|
+
// Fallback for any other error case
|
|
292
|
+
return <ParseError message={result.errors.map((e) => `${e.path}: ${e.message}`).join('\n')} />;
|
|
302
293
|
}
|
|
303
294
|
|
|
304
295
|
/**
|
|
@@ -306,8 +297,8 @@ export function FusionFragmentHandler({
|
|
|
306
297
|
* for use with markdown renderers that support custom code blocks
|
|
307
298
|
*/
|
|
308
299
|
export function createFusionFragmentCodeBlockRenderer() {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
300
|
+
return {
|
|
301
|
+
language: 'fusion-fragment',
|
|
302
|
+
component: FusionFragmentHandler,
|
|
303
|
+
};
|
|
313
304
|
}
|