@seed-ship/mcp-ui-solid 1.0.29 → 1.0.31
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/components/GenerativeUIErrorBoundary.cjs.map +1 -0
- package/dist/components/GenerativeUIErrorBoundary.js.map +1 -0
- package/dist/components/StreamingUIRenderer.cjs.map +1 -0
- package/dist/components/StreamingUIRenderer.js.map +1 -0
- package/dist/{mcp-ui-solid/src/components → components}/UIResourceRenderer.cjs +102 -97
- package/dist/components/UIResourceRenderer.cjs.map +1 -0
- package/dist/components/UIResourceRenderer.d.ts +0 -11
- package/dist/components/UIResourceRenderer.d.ts.map +1 -1
- package/dist/{mcp-ui-solid/src/components → components}/UIResourceRenderer.js +102 -97
- package/dist/components/UIResourceRenderer.js.map +1 -0
- package/dist/components.cjs +3 -3
- package/dist/components.d.ts +12 -0
- package/dist/components.js +3 -3
- package/dist/hooks/useStreamingUI.cjs.map +1 -0
- package/dist/hooks/useStreamingUI.js.map +1 -0
- package/dist/hooks.cjs +1 -1
- package/dist/hooks.d.ts +8 -0
- package/dist/hooks.js +1 -1
- package/dist/index.cjs +6 -6
- package/dist/index.js +6 -6
- package/dist/node_modules/.pnpm/dompurify@3.3.0/node_modules/dompurify/dist/purify.es.cjs +1006 -0
- package/dist/node_modules/.pnpm/dompurify@3.3.0/node_modules/dompurify/dist/purify.es.cjs.map +1 -0
- package/dist/node_modules/.pnpm/dompurify@3.3.0/node_modules/dompurify/dist/purify.es.js +1007 -0
- package/dist/node_modules/.pnpm/dompurify@3.3.0/node_modules/dompurify/dist/purify.es.js.map +1 -0
- package/dist/services/component-registry.cjs.map +1 -0
- package/dist/services/component-registry.js.map +1 -0
- package/dist/services/validation.cjs.map +1 -0
- package/dist/services/validation.js.map +1 -0
- package/dist/types.d.ts +265 -0
- package/dist/utils/logger.cjs.map +1 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/validation.cjs +1 -1
- package/dist/validation.js +1 -1
- package/package.json +20 -23
- package/src/components/ActionRenderer.tsx +33 -0
- package/src/components/ArtifactRenderer.tsx +54 -0
- package/src/components/CarouselRenderer.tsx +77 -0
- package/src/components/FooterRenderer.tsx +66 -0
- package/src/components/GenerativeUIErrorBoundary.tsx +259 -0
- package/src/components/StreamingUIRenderer.tsx +327 -0
- package/src/components/UIResourceRenderer.tsx +573 -0
- package/src/components/index.ts +14 -0
- package/src/hooks/index.ts +14 -0
- package/src/hooks/useStreamingUI.ts +447 -0
- package/src/index.test.ts +36 -0
- package/src/index.ts +70 -0
- package/src/services/component-registry.ts +378 -0
- package/src/services/index.ts +9 -0
- package/src/services/validation.ts +472 -0
- package/src/types/index.ts +320 -0
- package/src/types-export.ts +31 -0
- package/src/utils/logger.ts +74 -0
- package/src/validation.ts +38 -0
- package/src/vite-env.d.ts +11 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vite.config.ts +52 -0
- package/vite.config.ts.timestamp-1763266929437-a71eed80b91318.mjs +45 -0
- package/vitest.config.ts +10 -0
- package/dist/mcp-ui-solid/src/components/GenerativeUIErrorBoundary.cjs.map +0 -1
- package/dist/mcp-ui-solid/src/components/GenerativeUIErrorBoundary.js.map +0 -1
- package/dist/mcp-ui-solid/src/components/StreamingUIRenderer.cjs.map +0 -1
- package/dist/mcp-ui-solid/src/components/StreamingUIRenderer.js.map +0 -1
- package/dist/mcp-ui-solid/src/components/UIResourceRenderer.cjs.map +0 -1
- package/dist/mcp-ui-solid/src/components/UIResourceRenderer.js.map +0 -1
- package/dist/mcp-ui-solid/src/hooks/useStreamingUI.cjs.map +0 -1
- package/dist/mcp-ui-solid/src/hooks/useStreamingUI.js.map +0 -1
- package/dist/mcp-ui-solid/src/services/component-registry.cjs.map +0 -1
- package/dist/mcp-ui-solid/src/services/component-registry.js.map +0 -1
- package/dist/mcp-ui-solid/src/services/validation.cjs.map +0 -1
- package/dist/mcp-ui-solid/src/services/validation.js.map +0 -1
- package/dist/mcp-ui-solid/src/utils/logger.cjs.map +0 -1
- package/dist/mcp-ui-solid/src/utils/logger.js.map +0 -1
- /package/dist/{mcp-ui-solid/src/components → components}/GenerativeUIErrorBoundary.cjs +0 -0
- /package/dist/{mcp-ui-solid/src/components → components}/GenerativeUIErrorBoundary.js +0 -0
- /package/dist/{mcp-ui-solid/src/components → components}/StreamingUIRenderer.cjs +0 -0
- /package/dist/{mcp-ui-solid/src/components → components}/StreamingUIRenderer.js +0 -0
- /package/dist/{mcp-ui-solid/src/hooks → hooks}/useStreamingUI.cjs +0 -0
- /package/dist/{mcp-ui-solid/src/hooks → hooks}/useStreamingUI.js +0 -0
- /package/dist/{mcp-ui-solid/src/services → services}/component-registry.cjs +0 -0
- /package/dist/{mcp-ui-solid/src/services → services}/component-registry.js +0 -0
- /package/dist/{mcp-ui-solid/src/services → services}/validation.cjs +0 -0
- /package/dist/{mcp-ui-solid/src/services → services}/validation.js +0 -0
- /package/dist/{mcp-ui-solid/src/utils → utils}/logger.cjs +0 -0
- /package/dist/{mcp-ui-solid/src/utils → utils}/logger.js +0 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useStreamingUI Hook - Phase 2
|
|
3
|
+
*
|
|
4
|
+
* Client-side hook for consuming the streaming generative UI endpoint.
|
|
5
|
+
* Handles SSE connection, component buffering, reordering, and error handling.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - SSE connection with automatic reconnection
|
|
9
|
+
* - Component buffering and reordering by sequenceId
|
|
10
|
+
* - Progress tracking and loading states
|
|
11
|
+
* - Error handling with recovery attempts
|
|
12
|
+
* - Cleanup on unmount
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* ```tsx
|
|
16
|
+
* const { components, isLoading, error, progress } = useStreamingUI({
|
|
17
|
+
* query: 'Show me revenue trends',
|
|
18
|
+
* spaceIds: ['uuid1', 'uuid2'],
|
|
19
|
+
* onComplete: (metadata) => console.log('Done!', metadata),
|
|
20
|
+
* })
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { createSignal, onCleanup } from 'solid-js'
|
|
25
|
+
import type { UIComponent } from '../types'
|
|
26
|
+
import { createLogger } from '../utils/logger'
|
|
27
|
+
|
|
28
|
+
const logger = createLogger('useStreamingUI')
|
|
29
|
+
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// Types
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
34
|
+
export interface UseStreamingUIOptions {
|
|
35
|
+
query: string
|
|
36
|
+
spaceIds?: string[]
|
|
37
|
+
sessionId?: string
|
|
38
|
+
options?: {
|
|
39
|
+
useCache?: boolean
|
|
40
|
+
useLLM?: boolean
|
|
41
|
+
maxComponents?: number
|
|
42
|
+
preferredComponents?: Array<'chart' | 'table' | 'metric' | 'text'>
|
|
43
|
+
}
|
|
44
|
+
onComplete?: (metadata: CompleteMetadata) => void
|
|
45
|
+
onError?: (error: StreamError) => void
|
|
46
|
+
onComponentReceived?: (component: UIComponent) => void
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface StreamingUIState {
|
|
50
|
+
components: UIComponent[]
|
|
51
|
+
isLoading: boolean
|
|
52
|
+
isStreaming: boolean
|
|
53
|
+
error: StreamError | null
|
|
54
|
+
progress: StreamProgress
|
|
55
|
+
metadata: CompleteMetadata | null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface StreamProgress {
|
|
59
|
+
receivedCount: number
|
|
60
|
+
totalCount: number | null
|
|
61
|
+
message: string
|
|
62
|
+
timestamp: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface CompleteMetadata {
|
|
66
|
+
layoutId: string
|
|
67
|
+
componentsCount: number
|
|
68
|
+
executionTimeMs: number
|
|
69
|
+
firstTokenMs: number
|
|
70
|
+
provider: 'groq' | 'mock'
|
|
71
|
+
model: string
|
|
72
|
+
tokensUsed?: number
|
|
73
|
+
costUSD?: number
|
|
74
|
+
cached: boolean
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface StreamError {
|
|
78
|
+
error: string
|
|
79
|
+
message: string
|
|
80
|
+
componentId?: string
|
|
81
|
+
recoverable: boolean
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface ComponentBuffer {
|
|
85
|
+
[sequenceId: number]: {
|
|
86
|
+
component: UIComponent
|
|
87
|
+
position: { colStart: number; colSpan: number; rowStart?: number; rowSpan?: number }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// SSE Event Types (must match server)
|
|
93
|
+
// ============================================================================
|
|
94
|
+
|
|
95
|
+
type SSEEventType = 'status' | 'component-start' | 'component' | 'complete' | 'error'
|
|
96
|
+
|
|
97
|
+
interface StatusEvent {
|
|
98
|
+
message: string
|
|
99
|
+
timestamp: string
|
|
100
|
+
totalComponents?: number
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface ComponentStartEvent {
|
|
104
|
+
componentId: string
|
|
105
|
+
type: 'chart' | 'table' | 'metric' | 'text'
|
|
106
|
+
sequenceId: number
|
|
107
|
+
position: { colStart: number; colSpan: number }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface ComponentEvent {
|
|
111
|
+
componentId: string
|
|
112
|
+
sequenceId: number
|
|
113
|
+
component: UIComponent
|
|
114
|
+
position: { colStart: number; colSpan: number; rowStart?: number; rowSpan?: number }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// Hook Implementation
|
|
119
|
+
// ============================================================================
|
|
120
|
+
|
|
121
|
+
export function useStreamingUI(options: UseStreamingUIOptions) {
|
|
122
|
+
// State
|
|
123
|
+
const [components, setComponents] = createSignal<UIComponent[]>([])
|
|
124
|
+
const [isLoading, setIsLoading] = createSignal(false)
|
|
125
|
+
const [isStreaming, setIsStreaming] = createSignal(false)
|
|
126
|
+
const [error, setError] = createSignal<StreamError | null>(null)
|
|
127
|
+
const [progress, setProgress] = createSignal<StreamProgress>({
|
|
128
|
+
receivedCount: 0,
|
|
129
|
+
totalCount: null,
|
|
130
|
+
message: 'Initializing...',
|
|
131
|
+
timestamp: new Date().toISOString(),
|
|
132
|
+
})
|
|
133
|
+
const [metadata, setMetadata] = createSignal<CompleteMetadata | null>(null)
|
|
134
|
+
|
|
135
|
+
// Component buffer for reordering
|
|
136
|
+
let componentBuffer: ComponentBuffer = {}
|
|
137
|
+
let nextSequenceId = 0
|
|
138
|
+
let eventSource: EventSource | null = null
|
|
139
|
+
let reconnectAttempts = 0
|
|
140
|
+
const maxReconnectAttempts = 3
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Flush components from buffer in sequence order
|
|
144
|
+
*/
|
|
145
|
+
const flushBuffer = () => {
|
|
146
|
+
const flushed: UIComponent[] = []
|
|
147
|
+
|
|
148
|
+
while (componentBuffer[nextSequenceId]) {
|
|
149
|
+
const { component } = componentBuffer[nextSequenceId]
|
|
150
|
+
flushed.push(component)
|
|
151
|
+
delete componentBuffer[nextSequenceId]
|
|
152
|
+
nextSequenceId++
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (flushed.length > 0) {
|
|
156
|
+
setComponents((prev) => [...prev, ...flushed])
|
|
157
|
+
|
|
158
|
+
setProgress((prev) => ({
|
|
159
|
+
...prev,
|
|
160
|
+
receivedCount: prev.receivedCount + flushed.length,
|
|
161
|
+
}))
|
|
162
|
+
|
|
163
|
+
logger.debug('Flushed components from buffer', {
|
|
164
|
+
count: flushed.length,
|
|
165
|
+
nextSequenceId,
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Handle SSE status event
|
|
172
|
+
*/
|
|
173
|
+
const handleStatusEvent = (data: StatusEvent) => {
|
|
174
|
+
logger.debug('Status event received', data as unknown as Record<string, unknown>)
|
|
175
|
+
|
|
176
|
+
setProgress({
|
|
177
|
+
receivedCount: progress().receivedCount,
|
|
178
|
+
totalCount: data.totalComponents ?? progress().totalCount,
|
|
179
|
+
message: data.message,
|
|
180
|
+
timestamp: data.timestamp,
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Handle SSE component-start event
|
|
186
|
+
*/
|
|
187
|
+
const handleComponentStartEvent = (data: ComponentStartEvent) => {
|
|
188
|
+
logger.debug('Component-start event received', data as unknown as Record<string, unknown>)
|
|
189
|
+
|
|
190
|
+
setProgress((prev) => ({
|
|
191
|
+
...prev,
|
|
192
|
+
message: `Loading ${data.type} component...`,
|
|
193
|
+
timestamp: new Date().toISOString(),
|
|
194
|
+
}))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Handle SSE component event
|
|
199
|
+
*/
|
|
200
|
+
const handleComponentEvent = (data: ComponentEvent) => {
|
|
201
|
+
logger.debug('Component event received', {
|
|
202
|
+
componentId: data.componentId,
|
|
203
|
+
sequenceId: data.sequenceId,
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// Add to buffer
|
|
207
|
+
componentBuffer[data.sequenceId] = {
|
|
208
|
+
component: data.component,
|
|
209
|
+
position: data.position,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Flush buffer in sequence
|
|
213
|
+
flushBuffer()
|
|
214
|
+
|
|
215
|
+
// Notify callback
|
|
216
|
+
if (options.onComponentReceived) {
|
|
217
|
+
options.onComponentReceived(data.component)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Handle SSE complete event
|
|
223
|
+
*/
|
|
224
|
+
const handleCompleteEvent = (data: CompleteMetadata) => {
|
|
225
|
+
logger.info('Stream completed', data as unknown as Record<string, unknown>)
|
|
226
|
+
|
|
227
|
+
setIsStreaming(false)
|
|
228
|
+
setIsLoading(false)
|
|
229
|
+
setMetadata(data)
|
|
230
|
+
|
|
231
|
+
// Flush any remaining buffered components
|
|
232
|
+
flushBuffer()
|
|
233
|
+
|
|
234
|
+
setProgress((prev) => ({
|
|
235
|
+
...prev,
|
|
236
|
+
message: 'Dashboard loaded',
|
|
237
|
+
timestamp: new Date().toISOString(),
|
|
238
|
+
}))
|
|
239
|
+
|
|
240
|
+
// Notify callback
|
|
241
|
+
if (options.onComplete) {
|
|
242
|
+
options.onComplete(data)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Handle SSE error event
|
|
248
|
+
*/
|
|
249
|
+
const handleErrorEvent = (data: StreamError) => {
|
|
250
|
+
logger.error('Stream error received', data as unknown as Record<string, unknown>)
|
|
251
|
+
|
|
252
|
+
setError(data)
|
|
253
|
+
setIsStreaming(false)
|
|
254
|
+
setIsLoading(false)
|
|
255
|
+
|
|
256
|
+
setProgress((prev) => ({
|
|
257
|
+
...prev,
|
|
258
|
+
message: `Error: ${data.message}`,
|
|
259
|
+
timestamp: new Date().toISOString(),
|
|
260
|
+
}))
|
|
261
|
+
|
|
262
|
+
// Notify callback
|
|
263
|
+
if (options.onError) {
|
|
264
|
+
options.onError(data)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Try to reconnect if recoverable
|
|
268
|
+
if (data.recoverable && reconnectAttempts < maxReconnectAttempts) {
|
|
269
|
+
reconnectAttempts++
|
|
270
|
+
logger.warn('Attempting to reconnect', { attempt: reconnectAttempts })
|
|
271
|
+
setTimeout(() => startStreaming(), 1000 * reconnectAttempts)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Parse SSE event
|
|
277
|
+
*/
|
|
278
|
+
const parseSSEEvent = (event: MessageEvent, eventType: SSEEventType) => {
|
|
279
|
+
try {
|
|
280
|
+
const data = JSON.parse(event.data)
|
|
281
|
+
|
|
282
|
+
switch (eventType) {
|
|
283
|
+
case 'status':
|
|
284
|
+
handleStatusEvent(data as StatusEvent)
|
|
285
|
+
break
|
|
286
|
+
case 'component-start':
|
|
287
|
+
handleComponentStartEvent(data as ComponentStartEvent)
|
|
288
|
+
break
|
|
289
|
+
case 'component':
|
|
290
|
+
handleComponentEvent(data as ComponentEvent)
|
|
291
|
+
break
|
|
292
|
+
case 'complete':
|
|
293
|
+
handleCompleteEvent(data as CompleteMetadata)
|
|
294
|
+
break
|
|
295
|
+
case 'error':
|
|
296
|
+
handleErrorEvent(data as StreamError)
|
|
297
|
+
break
|
|
298
|
+
default:
|
|
299
|
+
logger.warn('Unknown SSE event type', { eventType })
|
|
300
|
+
}
|
|
301
|
+
} catch (error) {
|
|
302
|
+
logger.error('Failed to parse SSE event', {
|
|
303
|
+
error: error instanceof Error ? error.message : String(error),
|
|
304
|
+
eventType,
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Start SSE streaming
|
|
311
|
+
*/
|
|
312
|
+
const startStreaming = () => {
|
|
313
|
+
// Reset state
|
|
314
|
+
setComponents([])
|
|
315
|
+
setError(null)
|
|
316
|
+
setIsLoading(true)
|
|
317
|
+
setIsStreaming(true)
|
|
318
|
+
componentBuffer = {}
|
|
319
|
+
nextSequenceId = 0
|
|
320
|
+
|
|
321
|
+
setProgress({
|
|
322
|
+
receivedCount: 0,
|
|
323
|
+
totalCount: null,
|
|
324
|
+
message: 'Connecting to server...',
|
|
325
|
+
timestamp: new Date().toISOString(),
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
logger.info('Starting SSE stream', {
|
|
329
|
+
query: options.query,
|
|
330
|
+
spaceIds: options.spaceIds,
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
// Build request body
|
|
334
|
+
const requestBody = {
|
|
335
|
+
query: options.query,
|
|
336
|
+
spaceIds: options.spaceIds,
|
|
337
|
+
sessionId: options.sessionId,
|
|
338
|
+
options: options.options,
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Create EventSource (SSE connection)
|
|
342
|
+
// Note: EventSource doesn't support POST, so we need to use fetch + ReadableStream
|
|
343
|
+
fetch('/api/mcp/generative-ui-stream', {
|
|
344
|
+
method: 'POST',
|
|
345
|
+
headers: {
|
|
346
|
+
'Content-Type': 'application/json',
|
|
347
|
+
},
|
|
348
|
+
body: JSON.stringify(requestBody),
|
|
349
|
+
})
|
|
350
|
+
.then(async (response) => {
|
|
351
|
+
if (!response.ok) {
|
|
352
|
+
const errorData = await response.json()
|
|
353
|
+
throw new Error(errorData.message || 'Stream request failed')
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (!response.body) {
|
|
357
|
+
throw new Error('Response body is null')
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const reader = response.body.getReader()
|
|
361
|
+
const decoder = new TextDecoder()
|
|
362
|
+
|
|
363
|
+
let buffer = ''
|
|
364
|
+
|
|
365
|
+
// Read stream
|
|
366
|
+
const readChunk = async (): Promise<void> => {
|
|
367
|
+
const { done, value } = await reader.read()
|
|
368
|
+
|
|
369
|
+
if (done) {
|
|
370
|
+
logger.info('Stream ended')
|
|
371
|
+
return
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
buffer += decoder.decode(value, { stream: true })
|
|
375
|
+
|
|
376
|
+
// Process SSE messages
|
|
377
|
+
const lines = buffer.split('\n')
|
|
378
|
+
buffer = lines.pop() || '' // Keep incomplete line in buffer
|
|
379
|
+
|
|
380
|
+
let currentEvent: SSEEventType | null = null
|
|
381
|
+
|
|
382
|
+
for (const line of lines) {
|
|
383
|
+
if (line.startsWith('event: ')) {
|
|
384
|
+
currentEvent = line.slice(7) as SSEEventType
|
|
385
|
+
} else if (line.startsWith('data: ') && currentEvent) {
|
|
386
|
+
const data = line.slice(6)
|
|
387
|
+
parseSSEEvent({ data } as MessageEvent, currentEvent)
|
|
388
|
+
currentEvent = null
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Continue reading
|
|
393
|
+
return readChunk()
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
await readChunk()
|
|
397
|
+
})
|
|
398
|
+
.catch((err) => {
|
|
399
|
+
logger.error('Stream fetch failed', {
|
|
400
|
+
error: err instanceof Error ? err.message : String(err),
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
handleErrorEvent({
|
|
404
|
+
error: 'Stream connection failed',
|
|
405
|
+
message: err instanceof Error ? err.message : 'Unknown error',
|
|
406
|
+
recoverable: true,
|
|
407
|
+
})
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Stop streaming
|
|
413
|
+
*/
|
|
414
|
+
const stopStreaming = () => {
|
|
415
|
+
if (eventSource) {
|
|
416
|
+
eventSource.close()
|
|
417
|
+
eventSource = null
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
setIsStreaming(false)
|
|
421
|
+
setIsLoading(false)
|
|
422
|
+
|
|
423
|
+
logger.info('Streaming stopped')
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Cleanup on unmount
|
|
428
|
+
*/
|
|
429
|
+
onCleanup(() => {
|
|
430
|
+
stopStreaming()
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
// Auto-start streaming
|
|
434
|
+
startStreaming()
|
|
435
|
+
|
|
436
|
+
// Return state accessors and controls
|
|
437
|
+
return {
|
|
438
|
+
components,
|
|
439
|
+
isLoading,
|
|
440
|
+
isStreaming,
|
|
441
|
+
error,
|
|
442
|
+
progress,
|
|
443
|
+
metadata,
|
|
444
|
+
startStreaming,
|
|
445
|
+
stopStreaming,
|
|
446
|
+
}
|
|
447
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Basic module export tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import * as mcpUiSolid from './index';
|
|
7
|
+
|
|
8
|
+
describe('@seed-ship/mcp-ui-solid', () => {
|
|
9
|
+
it('should export UIResourceRenderer', () => {
|
|
10
|
+
expect(mcpUiSolid.UIResourceRenderer).toBeDefined();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should export StreamingUIRenderer', () => {
|
|
14
|
+
expect(mcpUiSolid.StreamingUIRenderer).toBeDefined();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should export GenerativeUIErrorBoundary', () => {
|
|
18
|
+
expect(mcpUiSolid.GenerativeUIErrorBoundary).toBeDefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should export useStreamingUI hook', () => {
|
|
22
|
+
expect(mcpUiSolid.useStreamingUI).toBeDefined();
|
|
23
|
+
expect(typeof mcpUiSolid.useStreamingUI).toBe('function');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should export validation functions', () => {
|
|
27
|
+
expect(mcpUiSolid.validateComponent).toBeDefined();
|
|
28
|
+
expect(typeof mcpUiSolid.validateComponent).toBe('function');
|
|
29
|
+
expect(mcpUiSolid.validateLayout).toBeDefined();
|
|
30
|
+
expect(typeof mcpUiSolid.validateLayout).toBe('function');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should export ComponentRegistry', () => {
|
|
34
|
+
expect(mcpUiSolid.ComponentRegistry).toBeDefined();
|
|
35
|
+
});
|
|
36
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @seed-ship/mcp-ui-solid
|
|
3
|
+
*
|
|
4
|
+
* SolidJS components and hooks for rendering MCP-generated UI resources
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* import { UIResourceRenderer, StreamingUIRenderer } from '@seed-ship/mcp-ui-solid'
|
|
9
|
+
* import { useStreamingUI } from '@seed-ship/mcp-ui-solid/hooks'
|
|
10
|
+
* import type { UIComponent, UILayout } from '@seed-ship/mcp-ui-solid/types'
|
|
11
|
+
*
|
|
12
|
+
* // Static rendering
|
|
13
|
+
* function Dashboard() {
|
|
14
|
+
* const layout = { components: [...] }
|
|
15
|
+
* return <UIResourceRenderer content={layout} />
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* // Streaming rendering
|
|
19
|
+
* function StreamingDashboard() {
|
|
20
|
+
* return (
|
|
21
|
+
* <StreamingUIRenderer
|
|
22
|
+
* query="Show me revenue trends"
|
|
23
|
+
* spaceIds={['space-1']}
|
|
24
|
+
* onComplete={(metadata) => console.log('Done!', metadata)}
|
|
25
|
+
* />
|
|
26
|
+
* )
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
// Components
|
|
32
|
+
export { UIResourceRenderer, StreamingUIRenderer, GenerativeUIErrorBoundary } from './components'
|
|
33
|
+
|
|
34
|
+
export type {
|
|
35
|
+
UIResourceRendererProps,
|
|
36
|
+
StreamingUIRendererProps,
|
|
37
|
+
GenerativeUIErrorBoundaryProps,
|
|
38
|
+
} from './components'
|
|
39
|
+
|
|
40
|
+
// Hooks
|
|
41
|
+
export { useStreamingUI } from './hooks'
|
|
42
|
+
|
|
43
|
+
export type {
|
|
44
|
+
UseStreamingUIOptions,
|
|
45
|
+
StreamingUIState,
|
|
46
|
+
StreamProgress,
|
|
47
|
+
StreamError,
|
|
48
|
+
CompleteMetadata,
|
|
49
|
+
} from './hooks'
|
|
50
|
+
|
|
51
|
+
// Types
|
|
52
|
+
export type {
|
|
53
|
+
UIComponent,
|
|
54
|
+
UILayout,
|
|
55
|
+
GridPosition,
|
|
56
|
+
ComponentType,
|
|
57
|
+
RendererError,
|
|
58
|
+
ChartComponentParams,
|
|
59
|
+
TableComponentParams,
|
|
60
|
+
MetricComponentParams,
|
|
61
|
+
TextComponentParams,
|
|
62
|
+
} from './types'
|
|
63
|
+
|
|
64
|
+
// Services
|
|
65
|
+
export {
|
|
66
|
+
validateComponent,
|
|
67
|
+
validateLayout,
|
|
68
|
+
DEFAULT_RESOURCE_LIMITS,
|
|
69
|
+
ComponentRegistry,
|
|
70
|
+
} from './services'
|