@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.
Files changed (85) hide show
  1. package/dist/components/GenerativeUIErrorBoundary.cjs.map +1 -0
  2. package/dist/components/GenerativeUIErrorBoundary.js.map +1 -0
  3. package/dist/components/StreamingUIRenderer.cjs.map +1 -0
  4. package/dist/components/StreamingUIRenderer.js.map +1 -0
  5. package/dist/{mcp-ui-solid/src/components → components}/UIResourceRenderer.cjs +102 -97
  6. package/dist/components/UIResourceRenderer.cjs.map +1 -0
  7. package/dist/components/UIResourceRenderer.d.ts +0 -11
  8. package/dist/components/UIResourceRenderer.d.ts.map +1 -1
  9. package/dist/{mcp-ui-solid/src/components → components}/UIResourceRenderer.js +102 -97
  10. package/dist/components/UIResourceRenderer.js.map +1 -0
  11. package/dist/components.cjs +3 -3
  12. package/dist/components.d.ts +12 -0
  13. package/dist/components.js +3 -3
  14. package/dist/hooks/useStreamingUI.cjs.map +1 -0
  15. package/dist/hooks/useStreamingUI.js.map +1 -0
  16. package/dist/hooks.cjs +1 -1
  17. package/dist/hooks.d.ts +8 -0
  18. package/dist/hooks.js +1 -1
  19. package/dist/index.cjs +6 -6
  20. package/dist/index.js +6 -6
  21. package/dist/node_modules/.pnpm/dompurify@3.3.0/node_modules/dompurify/dist/purify.es.cjs +1006 -0
  22. package/dist/node_modules/.pnpm/dompurify@3.3.0/node_modules/dompurify/dist/purify.es.cjs.map +1 -0
  23. package/dist/node_modules/.pnpm/dompurify@3.3.0/node_modules/dompurify/dist/purify.es.js +1007 -0
  24. package/dist/node_modules/.pnpm/dompurify@3.3.0/node_modules/dompurify/dist/purify.es.js.map +1 -0
  25. package/dist/services/component-registry.cjs.map +1 -0
  26. package/dist/services/component-registry.js.map +1 -0
  27. package/dist/services/validation.cjs.map +1 -0
  28. package/dist/services/validation.js.map +1 -0
  29. package/dist/types.d.ts +265 -0
  30. package/dist/utils/logger.cjs.map +1 -0
  31. package/dist/utils/logger.js.map +1 -0
  32. package/dist/validation.cjs +1 -1
  33. package/dist/validation.js +1 -1
  34. package/package.json +20 -23
  35. package/src/components/ActionRenderer.tsx +33 -0
  36. package/src/components/ArtifactRenderer.tsx +54 -0
  37. package/src/components/CarouselRenderer.tsx +77 -0
  38. package/src/components/FooterRenderer.tsx +66 -0
  39. package/src/components/GenerativeUIErrorBoundary.tsx +259 -0
  40. package/src/components/StreamingUIRenderer.tsx +327 -0
  41. package/src/components/UIResourceRenderer.tsx +573 -0
  42. package/src/components/index.ts +14 -0
  43. package/src/hooks/index.ts +14 -0
  44. package/src/hooks/useStreamingUI.ts +447 -0
  45. package/src/index.test.ts +36 -0
  46. package/src/index.ts +70 -0
  47. package/src/services/component-registry.ts +378 -0
  48. package/src/services/index.ts +9 -0
  49. package/src/services/validation.ts +472 -0
  50. package/src/types/index.ts +320 -0
  51. package/src/types-export.ts +31 -0
  52. package/src/utils/logger.ts +74 -0
  53. package/src/validation.ts +38 -0
  54. package/src/vite-env.d.ts +11 -0
  55. package/tsconfig.json +20 -0
  56. package/tsconfig.tsbuildinfo +1 -0
  57. package/vite.config.ts +52 -0
  58. package/vite.config.ts.timestamp-1763266929437-a71eed80b91318.mjs +45 -0
  59. package/vitest.config.ts +10 -0
  60. package/dist/mcp-ui-solid/src/components/GenerativeUIErrorBoundary.cjs.map +0 -1
  61. package/dist/mcp-ui-solid/src/components/GenerativeUIErrorBoundary.js.map +0 -1
  62. package/dist/mcp-ui-solid/src/components/StreamingUIRenderer.cjs.map +0 -1
  63. package/dist/mcp-ui-solid/src/components/StreamingUIRenderer.js.map +0 -1
  64. package/dist/mcp-ui-solid/src/components/UIResourceRenderer.cjs.map +0 -1
  65. package/dist/mcp-ui-solid/src/components/UIResourceRenderer.js.map +0 -1
  66. package/dist/mcp-ui-solid/src/hooks/useStreamingUI.cjs.map +0 -1
  67. package/dist/mcp-ui-solid/src/hooks/useStreamingUI.js.map +0 -1
  68. package/dist/mcp-ui-solid/src/services/component-registry.cjs.map +0 -1
  69. package/dist/mcp-ui-solid/src/services/component-registry.js.map +0 -1
  70. package/dist/mcp-ui-solid/src/services/validation.cjs.map +0 -1
  71. package/dist/mcp-ui-solid/src/services/validation.js.map +0 -1
  72. package/dist/mcp-ui-solid/src/utils/logger.cjs.map +0 -1
  73. package/dist/mcp-ui-solid/src/utils/logger.js.map +0 -1
  74. /package/dist/{mcp-ui-solid/src/components → components}/GenerativeUIErrorBoundary.cjs +0 -0
  75. /package/dist/{mcp-ui-solid/src/components → components}/GenerativeUIErrorBoundary.js +0 -0
  76. /package/dist/{mcp-ui-solid/src/components → components}/StreamingUIRenderer.cjs +0 -0
  77. /package/dist/{mcp-ui-solid/src/components → components}/StreamingUIRenderer.js +0 -0
  78. /package/dist/{mcp-ui-solid/src/hooks → hooks}/useStreamingUI.cjs +0 -0
  79. /package/dist/{mcp-ui-solid/src/hooks → hooks}/useStreamingUI.js +0 -0
  80. /package/dist/{mcp-ui-solid/src/services → services}/component-registry.cjs +0 -0
  81. /package/dist/{mcp-ui-solid/src/services → services}/component-registry.js +0 -0
  82. /package/dist/{mcp-ui-solid/src/services → services}/validation.cjs +0 -0
  83. /package/dist/{mcp-ui-solid/src/services → services}/validation.js +0 -0
  84. /package/dist/{mcp-ui-solid/src/utils → utils}/logger.cjs +0 -0
  85. /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'