@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,259 @@
1
+ /**
2
+ * Generative UI Error Boundary with Telemetry
3
+ * Phase 0: Error isolation + structured logging
4
+ *
5
+ * Features:
6
+ * - Component-level error isolation
7
+ * - Structured logging with context
8
+ * - Performance timing
9
+ * - Retry mechanism
10
+ * - User-friendly fallback UI
11
+ */
12
+
13
+ import { Component, ErrorBoundary, createSignal, Show } from 'solid-js'
14
+ import { isServer } from 'solid-js/web'
15
+ import { createLogger } from '../utils/logger'
16
+ import type { RendererError } from '../types'
17
+
18
+ const logger = createLogger('generative-ui')
19
+
20
+ /**
21
+ * Props for GenerativeUIErrorBoundary
22
+ */
23
+ export interface GenerativeUIErrorBoundaryProps {
24
+ /**
25
+ * Component identifier for telemetry
26
+ */
27
+ componentId: string
28
+
29
+ /**
30
+ * Component type for context
31
+ */
32
+ componentType: string
33
+
34
+ /**
35
+ * Error callback
36
+ */
37
+ onError?: (error: RendererError) => void
38
+
39
+ /**
40
+ * Allow retry on error
41
+ */
42
+ allowRetry?: boolean
43
+
44
+ /**
45
+ * Child components to wrap
46
+ */
47
+ children: any
48
+
49
+ /**
50
+ * Custom fallback UI (optional)
51
+ */
52
+ fallback?: (error: Error, retry?: () => void) => any
53
+ }
54
+
55
+ /**
56
+ * Default fallback UI for errors
57
+ */
58
+ function DefaultErrorFallback(props: {
59
+ error: Error
60
+ componentId: string
61
+ componentType: string
62
+ allowRetry?: boolean
63
+ onRetry?: () => void
64
+ }) {
65
+ return (
66
+ <div class="w-full h-full bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
67
+ <div class="flex items-start gap-3">
68
+ <div class="flex-shrink-0">
69
+ <svg
70
+ class="w-5 h-5 text-yellow-600 dark:text-yellow-400"
71
+ fill="none"
72
+ stroke="currentColor"
73
+ viewBox="0 0 24 24"
74
+ >
75
+ <path
76
+ stroke-linecap="round"
77
+ stroke-linejoin="round"
78
+ stroke-width="2"
79
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
80
+ />
81
+ </svg>
82
+ </div>
83
+ <div class="flex-1 min-w-0">
84
+ <p class="text-sm font-medium text-yellow-900 dark:text-yellow-100">
85
+ Component Failed to Render
86
+ </p>
87
+ <p class="text-xs text-yellow-700 dark:text-yellow-300 mt-1">
88
+ Type: {props.componentType || 'unknown'} | ID: {props.componentId?.slice(0, 8) || 'unknown'}...
89
+ </p>
90
+ <Show when={import.meta.env.DEV}>
91
+ <p class="text-xs text-yellow-600 dark:text-yellow-400 mt-2 font-mono">
92
+ {props.error.message}
93
+ </p>
94
+ </Show>
95
+ <Show when={props.allowRetry}>
96
+ <button
97
+ onClick={props.onRetry}
98
+ class="mt-3 text-xs font-medium text-yellow-800 dark:text-yellow-200 hover:text-yellow-900 dark:hover:text-yellow-100 underline"
99
+ >
100
+ Retry Rendering
101
+ </button>
102
+ </Show>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ )
107
+ }
108
+
109
+ /**
110
+ * Generative UI Error Boundary Component
111
+ */
112
+ export const GenerativeUIErrorBoundary: Component<GenerativeUIErrorBoundaryProps> = (props) => {
113
+ const [retryKey, setRetryKey] = createSignal(0)
114
+ const [renderStartTime] = createSignal(isServer ? 0 : performance.now())
115
+
116
+ // Handle error with telemetry
117
+ const handleError = (error: Error) => {
118
+ const renderEndTime = isServer ? 0 : performance.now()
119
+ const renderDuration = renderEndTime - renderStartTime()
120
+
121
+ // Structure error context
122
+ const errorContext = {
123
+ componentId: props.componentId,
124
+ componentType: props.componentType,
125
+ errorMessage: error.message,
126
+ errorStack: error.stack,
127
+ renderDuration,
128
+ retryCount: retryKey(),
129
+ timestamp: new Date().toISOString(),
130
+ userAgent: isServer ? 'server' : navigator.userAgent,
131
+ viewport: isServer
132
+ ? { width: 0, height: 0 }
133
+ : { width: window.innerWidth, height: window.innerHeight },
134
+ }
135
+
136
+ // Log to structured logger
137
+ logger.error(`Component render failed: ${props.componentType}`, errorContext)
138
+
139
+ // Call error callback
140
+ props.onError?.({
141
+ type: 'render',
142
+ message: error.message,
143
+ componentId: props.componentId,
144
+ details: errorContext,
145
+ })
146
+
147
+ // In production, send to monitoring service
148
+ if (import.meta.env.PROD) {
149
+ // Future: Send to Sentry or other APM
150
+ // Sentry.captureException(error, { contexts: { component: errorContext } })
151
+ }
152
+ }
153
+
154
+ // Retry mechanism
155
+ const handleRetry = () => {
156
+ const newRetryCount = retryKey() + 1
157
+ logger.info(`Retrying component render: ${props.componentType}`, {
158
+ componentId: props.componentId,
159
+ retryCount: newRetryCount,
160
+ })
161
+ setRetryKey(newRetryCount)
162
+ }
163
+
164
+ return (
165
+ <ErrorBoundary
166
+ fallback={(error) => {
167
+ handleError(error)
168
+
169
+ // Use custom fallback if provided
170
+ if (props.fallback) {
171
+ return props.fallback(error, props.allowRetry ? handleRetry : undefined)
172
+ }
173
+
174
+ // Default fallback
175
+ return (
176
+ <DefaultErrorFallback
177
+ error={error}
178
+ componentId={props.componentId}
179
+ componentType={props.componentType}
180
+ allowRetry={props.allowRetry}
181
+ onRetry={handleRetry}
182
+ />
183
+ )
184
+ }}
185
+ >
186
+ {/* Key prop for forcing remount on retry */}
187
+ {(() => {
188
+ const _ = retryKey() // Access signal to track changes
189
+ return <>{props.children}</>
190
+ })()}
191
+ </ErrorBoundary>
192
+ )
193
+ }
194
+
195
+ /**
196
+ * Performance monitoring wrapper
197
+ * Logs render times for performance analysis
198
+ */
199
+ export function withPerformanceMonitoring<P extends { componentId: string; componentType: string }>(
200
+ WrappedComponent: Component<P>
201
+ ) {
202
+ return (props: P) => {
203
+ const renderStart = isServer ? 0 : performance.now()
204
+
205
+ // Log render start
206
+ logger.debug(`Component render start: ${props.componentType}`, {
207
+ componentId: props.componentId,
208
+ timestamp: new Date().toISOString(),
209
+ })
210
+
211
+ // Measure on mount completion (client-side only)
212
+ if (!isServer && typeof window !== 'undefined') {
213
+ requestAnimationFrame(() => {
214
+ const renderEnd = performance.now()
215
+ const duration = renderEnd - renderStart
216
+
217
+ logger.info(`Component rendered: ${props.componentType}`, {
218
+ componentId: props.componentId,
219
+ renderDuration: duration,
220
+ timestamp: new Date().toISOString(),
221
+ })
222
+
223
+ // Warn if render is slow (>50ms target)
224
+ if (duration > 50) {
225
+ logger.warn(`Slow component render: ${props.componentType}`, {
226
+ componentId: props.componentId,
227
+ renderDuration: duration,
228
+ threshold: 50,
229
+ })
230
+ }
231
+ })
232
+ }
233
+
234
+ return <WrappedComponent {...props} />
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Hook to track component lifecycle events
240
+ */
241
+ export function useComponentTelemetry(componentId: string, componentType: string) {
242
+ const mountTime = isServer ? 0 : performance.now()
243
+
244
+ // Log mount
245
+ logger.debug(`Component mounted: ${componentType}`, {
246
+ componentId,
247
+ timestamp: new Date().toISOString(),
248
+ })
249
+
250
+ // Return cleanup function for unmount
251
+ return () => {
252
+ const lifetime = isServer ? 0 : performance.now() - mountTime
253
+ logger.debug(`Component unmounted: ${componentType}`, {
254
+ componentId,
255
+ lifetime,
256
+ timestamp: new Date().toISOString(),
257
+ })
258
+ }
259
+ }
@@ -0,0 +1,327 @@
1
+ /**
2
+ * StreamingUIRenderer Component - Phase 2
3
+ *
4
+ * Renders streaming dashboard components with skeleton states and progress indicators.
5
+ * Uses the useStreamingUI hook for SSE connection and state management.
6
+ *
7
+ * Features:
8
+ * - Skeleton loading states while components stream
9
+ * - Progress bar and status messages
10
+ * - Smooth component animations on arrival
11
+ * - Error handling with retry capability
12
+ * - Responsive 12-column grid layout
13
+ *
14
+ * Usage:
15
+ * ```tsx
16
+ * <StreamingUIRenderer
17
+ * query="Show me revenue trends"
18
+ * spaceIds={['uuid1', 'uuid2']}
19
+ * onComplete={(metadata) => console.log('Done!', metadata)}
20
+ * />
21
+ * ```
22
+ */
23
+
24
+ import { Show, For, createSignal, onMount } from 'solid-js'
25
+ import { useStreamingUI, type UseStreamingUIOptions } from '../hooks/useStreamingUI'
26
+ import type { UIComponent, RendererError } from '../types'
27
+ import { validateComponent } from '../services/validation'
28
+ import { GenerativeUIErrorBoundary } from './GenerativeUIErrorBoundary'
29
+
30
+ export interface StreamingUIRendererProps extends UseStreamingUIOptions {
31
+ class?: string
32
+ showProgress?: boolean
33
+ showMetadata?: boolean
34
+ onRenderError?: (error: RendererError) => void
35
+ }
36
+
37
+ /**
38
+ * Component Renderer - Inline lightweight version
39
+ * (Full implementation in UIResourceRenderer)
40
+ */
41
+ function StreamingComponentRenderer(props: {
42
+ component: UIComponent
43
+ onError?: (error: RendererError) => void
44
+ }) {
45
+ // Validate component before rendering
46
+ const validation = validateComponent(props.component)
47
+ if (!validation.valid) {
48
+ props.onError?.({
49
+ type: 'validation',
50
+ message: 'Component validation failed',
51
+ componentId: props.component.id,
52
+ details: validation.errors,
53
+ })
54
+
55
+ return (
56
+ <div class="w-full bg-error-subtle border border-border-error rounded-lg p-4">
57
+ <p class="text-sm font-medium text-error-primary">Validation Error</p>
58
+ <p class="text-xs text-text-secondary mt-1">
59
+ {validation.errors?.[0]?.message || 'Unknown validation error'}
60
+ </p>
61
+ </div>
62
+ )
63
+ }
64
+
65
+ // Simplified renderer - just show component type and title
66
+ // Full rendering logic in UIResourceRenderer
67
+ const params = props.component.params as any
68
+
69
+ return (
70
+ <GenerativeUIErrorBoundary
71
+ componentId={props.component.id}
72
+ componentType={props.component.type}
73
+ onError={props.onError}
74
+ allowRetry={false}
75
+ >
76
+ <div class="w-full bg-surface-secondary border border-border-subtle rounded-lg p-4">
77
+ <div class="flex items-center gap-2 mb-2">
78
+ <span class="text-xs font-medium text-text-tertiary uppercase">
79
+ {props.component.type}
80
+ </span>
81
+ </div>
82
+ <Show when={params?.title}>
83
+ <h3 class="text-sm font-semibold text-text-primary">{params.title}</h3>
84
+ </Show>
85
+ <Show when={props.component.type === 'metric' && params?.value}>
86
+ <div class="mt-2">
87
+ <p class="text-2xl font-semibold text-text-primary">{params.value}</p>
88
+ <Show when={params.unit}>
89
+ <span class="text-sm text-text-secondary">{params.unit}</span>
90
+ </Show>
91
+ </div>
92
+ </Show>
93
+ <div class="mt-3 text-xs text-text-tertiary">
94
+ Component ID: {props.component.id.slice(0, 8)}...
95
+ </div>
96
+ </div>
97
+ </GenerativeUIErrorBoundary>
98
+ )
99
+ }
100
+
101
+ export function StreamingUIRenderer(props: StreamingUIRendererProps) {
102
+ const { components, isLoading, isStreaming, error, progress, metadata, startStreaming } =
103
+ useStreamingUI({
104
+ query: props.query,
105
+ spaceIds: props.spaceIds,
106
+ sessionId: props.sessionId,
107
+ options: props.options,
108
+ onComplete: props.onComplete,
109
+ onError: props.onError,
110
+ onComponentReceived: props.onComponentReceived,
111
+ })
112
+
113
+ const [animatingComponents, setAnimatingComponents] = createSignal<Set<string>>(new Set())
114
+
115
+ // Track new components for animation
116
+ const handleComponentRender = (componentId: string) => {
117
+ setAnimatingComponents((prev) => new Set([...prev, componentId]))
118
+
119
+ // Remove from animating set after animation completes
120
+ setTimeout(() => {
121
+ setAnimatingComponents((prev) => {
122
+ const next = new Set(prev)
123
+ next.delete(componentId)
124
+ return next
125
+ })
126
+ }, 500)
127
+ }
128
+
129
+ return (
130
+ <div class={`streaming-ui-renderer ${props.class || ''}`}>
131
+ {/* Progress Bar */}
132
+ <Show when={props.showProgress !== false && (isLoading() || isStreaming())}>
133
+ <div class="mb-4 rounded-lg border border-border-subtle bg-surface-secondary p-4">
134
+ {/* Status Message */}
135
+ <div class="mb-2 flex items-center justify-between">
136
+ <span class="text-sm font-medium text-text-primary">{progress().message}</span>
137
+ <Show when={progress().totalCount !== null}>
138
+ <span class="text-sm text-text-secondary">
139
+ {progress().receivedCount} / {progress().totalCount}
140
+ </span>
141
+ </Show>
142
+ </div>
143
+
144
+ {/* Progress Bar */}
145
+ <div class="h-2 w-full overflow-hidden rounded-full bg-surface-tertiary">
146
+ <div
147
+ class="h-full bg-brand-primary transition-all duration-300 ease-out"
148
+ style={
149
+ progress().totalCount !== null
150
+ ? `width: ${(progress().receivedCount / progress().totalCount!) * 100}%`
151
+ : 'width: 0%'
152
+ }
153
+ />
154
+ </div>
155
+
156
+ {/* Indeterminate Progress (when totalCount unknown) */}
157
+ <Show when={progress().totalCount === null && isStreaming()}>
158
+ <div class="mt-2">
159
+ <div class="h-1 w-full overflow-hidden rounded-full bg-surface-tertiary">
160
+ <div class="animate-progress-indeterminate h-full w-1/3 bg-brand-primary" />
161
+ </div>
162
+ </div>
163
+ </Show>
164
+ </div>
165
+ </Show>
166
+
167
+ {/* Error State */}
168
+ <Show when={error()}>
169
+ <div class="mb-4 rounded-lg border border-border-error bg-error-subtle p-4">
170
+ <div class="mb-2 flex items-center gap-2">
171
+ <svg
172
+ class="h-5 w-5 text-error-primary"
173
+ fill="none"
174
+ viewBox="0 0 24 24"
175
+ stroke="currentColor"
176
+ >
177
+ <path
178
+ stroke-linecap="round"
179
+ stroke-linejoin="round"
180
+ stroke-width="2"
181
+ d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
182
+ />
183
+ </svg>
184
+ <span class="font-medium text-error-primary">{error()?.error}</span>
185
+ </div>
186
+ <p class="text-sm text-text-secondary">{error()?.message}</p>
187
+
188
+ {/* Retry Button (if recoverable) */}
189
+ <Show when={error()?.recoverable}>
190
+ <button
191
+ type="button"
192
+ class="mt-3 rounded-md bg-error-primary px-3 py-1.5 text-sm font-medium text-white hover:bg-error-hover"
193
+ onClick={() => startStreaming()}
194
+ >
195
+ Retry
196
+ </button>
197
+ </Show>
198
+ </div>
199
+ </Show>
200
+
201
+ {/* Components Grid */}
202
+ <div class="grid grid-cols-12 gap-4">
203
+ {/* Render received components */}
204
+ <For each={components()}>
205
+ {(component) => {
206
+ // Trigger animation on mount (SSR-safe, no 'use' directive needed)
207
+ onMount(() => handleComponentRender(component.id))
208
+
209
+ return (
210
+ <div
211
+ class={`
212
+ col-span-${component.position.colSpan}
213
+ ${animatingComponents().has(component.id) ? 'animate-fade-in-up' : ''}
214
+ `}
215
+ style={`grid-column-start: ${component.position.colStart}; grid-column-end: ${component.position.colStart + component.position.colSpan}`}
216
+ >
217
+ <StreamingComponentRenderer component={component} onError={props.onRenderError} />
218
+ </div>
219
+ )
220
+ }}
221
+ </For>
222
+
223
+ {/* Skeleton placeholders (if streaming and expecting more) */}
224
+ <Show when={isStreaming() && progress().totalCount !== null}>
225
+ <For
226
+ each={Array.from({
227
+ length: progress().totalCount! - progress().receivedCount,
228
+ })}
229
+ >
230
+ {() => <SkeletonComponent />}
231
+ </For>
232
+ </Show>
233
+ </div>
234
+
235
+ {/* Metadata Display */}
236
+ <Show when={props.showMetadata !== false && metadata()}>
237
+ <div class="mt-6 rounded-lg border border-border-subtle bg-surface-secondary p-4 text-sm text-text-secondary">
238
+ <div class="grid grid-cols-2 gap-4 md:grid-cols-4">
239
+ <div>
240
+ <div class="font-medium text-text-primary">Provider</div>
241
+ <div>{metadata()?.provider}</div>
242
+ </div>
243
+ <div>
244
+ <div class="font-medium text-text-primary">Model</div>
245
+ <div>{metadata()?.model}</div>
246
+ </div>
247
+ <div>
248
+ <div class="font-medium text-text-primary">Execution Time</div>
249
+ <div>{metadata()?.executionTimeMs}ms</div>
250
+ </div>
251
+ <Show when={metadata()?.costUSD !== undefined}>
252
+ <div>
253
+ <div class="font-medium text-text-primary">Cost</div>
254
+ <div>${metadata()?.costUSD?.toFixed(4)}</div>
255
+ </div>
256
+ </Show>
257
+ <div>
258
+ <div class="font-medium text-text-primary">TTFB</div>
259
+ <div>{metadata()?.firstTokenMs}ms</div>
260
+ </div>
261
+ <Show when={metadata()?.cached}>
262
+ <div>
263
+ <div class="font-medium text-text-primary">Cached</div>
264
+ <div class="text-success-primary">Yes</div>
265
+ </div>
266
+ </Show>
267
+ </div>
268
+ </div>
269
+ </Show>
270
+ </div>
271
+ )
272
+ }
273
+
274
+ /**
275
+ * Skeleton Component - Placeholder while components load
276
+ */
277
+ function SkeletonComponent() {
278
+ return (
279
+ <div class="col-span-12 md:col-span-6 lg:col-span-4">
280
+ <div class="animate-pulse rounded-lg border border-border-subtle bg-surface-secondary p-4">
281
+ {/* Header skeleton */}
282
+ <div class="mb-4 h-6 w-1/2 rounded bg-surface-tertiary" />
283
+
284
+ {/* Content skeleton */}
285
+ <div class="space-y-3">
286
+ <div class="h-4 rounded bg-surface-tertiary" />
287
+ <div class="h-4 w-5/6 rounded bg-surface-tertiary" />
288
+ <div class="h-4 w-4/6 rounded bg-surface-tertiary" />
289
+ </div>
290
+
291
+ {/* Chart/visual skeleton */}
292
+ <div class="mt-4 h-32 rounded bg-surface-tertiary" />
293
+ </div>
294
+ </div>
295
+ )
296
+ }
297
+
298
+ // CSS Animations (add to global styles or Tailwind config)
299
+ /*
300
+ @keyframes fade-in-up {
301
+ from {
302
+ opacity: 0;
303
+ transform: translateY(10px);
304
+ }
305
+ to {
306
+ opacity: 1;
307
+ transform: translateY(0);
308
+ }
309
+ }
310
+
311
+ @keyframes progress-indeterminate {
312
+ 0% {
313
+ transform: translateX(-100%);
314
+ }
315
+ 100% {
316
+ transform: translateX(400%);
317
+ }
318
+ }
319
+
320
+ .animate-fade-in-up {
321
+ animation: fade-in-up 0.5s ease-out;
322
+ }
323
+
324
+ .animate-progress-indeterminate {
325
+ animation: progress-indeterminate 1.5s infinite ease-in-out;
326
+ }
327
+ */