@seed-ship/mcp-ui-solid 2.2.11 → 2.4.0

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 (52) hide show
  1. package/dist/components/ChatPrompt.cjs +271 -0
  2. package/dist/components/ChatPrompt.cjs.map +1 -0
  3. package/dist/components/ChatPrompt.d.ts +33 -0
  4. package/dist/components/ChatPrompt.d.ts.map +1 -0
  5. package/dist/components/ChatPrompt.js +271 -0
  6. package/dist/components/ChatPrompt.js.map +1 -0
  7. package/dist/components/UIResourceRenderer.cjs +29 -27
  8. package/dist/components/UIResourceRenderer.cjs.map +1 -1
  9. package/dist/components/UIResourceRenderer.js +30 -28
  10. package/dist/components/UIResourceRenderer.js.map +1 -1
  11. package/dist/hooks/useChatBus.cjs +28 -0
  12. package/dist/hooks/useChatBus.cjs.map +1 -0
  13. package/dist/hooks/useChatBus.d.ts +56 -0
  14. package/dist/hooks/useChatBus.d.ts.map +1 -0
  15. package/dist/hooks/useChatBus.js +28 -0
  16. package/dist/hooks/useChatBus.js.map +1 -0
  17. package/dist/index.cjs +11 -0
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +5 -1
  20. package/dist/index.d.ts +5 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +12 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/services/chat-bus.cjs +118 -0
  25. package/dist/services/chat-bus.cjs.map +1 -0
  26. package/dist/services/chat-bus.d.ts +43 -0
  27. package/dist/services/chat-bus.d.ts.map +1 -0
  28. package/dist/services/chat-bus.js +118 -0
  29. package/dist/services/chat-bus.js.map +1 -0
  30. package/dist/services/index.d.ts +2 -1
  31. package/dist/services/index.d.ts.map +1 -1
  32. package/dist/services/validation.cjs +71 -1
  33. package/dist/services/validation.cjs.map +1 -1
  34. package/dist/services/validation.d.ts +21 -0
  35. package/dist/services/validation.d.ts.map +1 -1
  36. package/dist/services/validation.js +71 -1
  37. package/dist/services/validation.js.map +1 -1
  38. package/dist/types/chat-bus.d.ts +286 -0
  39. package/dist/types/chat-bus.d.ts.map +1 -0
  40. package/package.json +1 -1
  41. package/src/components/ChatPrompt.test.tsx +280 -0
  42. package/src/components/ChatPrompt.tsx +263 -0
  43. package/src/components/UIResourceRenderer.tsx +2 -2
  44. package/src/hooks/useChatBus.tsx +81 -0
  45. package/src/index.ts +36 -0
  46. package/src/services/chat-bus.test.ts +306 -0
  47. package/src/services/chat-bus.ts +183 -0
  48. package/src/services/index.ts +4 -0
  49. package/src/services/validation.test.ts +56 -1
  50. package/src/services/validation.ts +100 -0
  51. package/src/types/chat-bus.ts +320 -0
  52. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,263 @@
1
+ /**
2
+ * ChatPrompt — Ephemeral structured interaction above chat input
3
+ * v2.4.0: choice, confirm, form subtypes
4
+ *
5
+ * @experimental — This component may change without major bump until v2.5.0.
6
+ *
7
+ * Renders above the chat input. User responds → Promise resolves → prompt disappears.
8
+ * Supports AbortSignal for cleanup on navigation (C4).
9
+ */
10
+
11
+ import { Component, Show, For, createSignal, onCleanup, Switch, Match } from 'solid-js'
12
+ import type {
13
+ ChatPromptConfig,
14
+ ChatPromptResponse,
15
+ ChoicePromptConfig,
16
+ ConfirmPromptConfig,
17
+ FormPromptConfig,
18
+ } from '../types/chat-bus'
19
+
20
+ export interface ChatPromptProps {
21
+ /** Prompt configuration */
22
+ config: ChatPromptConfig
23
+ /** Called when user responds */
24
+ onSubmit: (response: ChatPromptResponse) => void
25
+ /** Called when user dismisses */
26
+ onDismiss?: () => void
27
+ }
28
+
29
+ /**
30
+ * @experimental
31
+ * Ephemeral interaction component — choice buttons, confirmation dialog, or quick form.
32
+ * Designed to sit between the chat messages and the input area.
33
+ *
34
+ * @example
35
+ * <ChatPrompt
36
+ * config={{ type: 'choice', title: 'Format?', config: { options: [...] } }}
37
+ * onSubmit={(r) => bus.events.emit('onChatPromptResponse', { streamKey, response: r })}
38
+ * onDismiss={() => setActivePrompt(null)}
39
+ * />
40
+ */
41
+ export const ChatPrompt: Component<ChatPromptProps> = (props) => {
42
+ return (
43
+ <div
44
+ class="w-full max-w-2xl mx-auto mb-2 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-lg overflow-hidden"
45
+ style={{ animation: 'chat-prompt-slide-up 0.2s ease-out' }}
46
+ role="dialog"
47
+ aria-label={props.config.title}
48
+ >
49
+ {/* Header */}
50
+ <div class="flex items-center justify-between px-4 py-2.5 border-b border-gray-100 dark:border-gray-700">
51
+ <p class="text-sm font-medium text-gray-900 dark:text-white">{props.config.title}</p>
52
+ <button
53
+ onClick={() => {
54
+ props.onDismiss?.()
55
+ props.onSubmit({ type: props.config.type, value: '', label: '', dismissed: true })
56
+ }}
57
+ class="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
58
+ aria-label="Dismiss"
59
+ >
60
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
61
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
62
+ </svg>
63
+ </button>
64
+ </div>
65
+
66
+ {/* Body — type-specific */}
67
+ <div class="px-4 py-3">
68
+ <Switch>
69
+ <Match when={props.config.type === 'choice'}>
70
+ <ChoiceBody
71
+ config={props.config.config as ChoicePromptConfig}
72
+ onSelect={(value, label) => props.onSubmit({ type: 'choice', value, label })}
73
+ />
74
+ </Match>
75
+ <Match when={props.config.type === 'confirm'}>
76
+ <ConfirmBody
77
+ config={props.config.config as ConfirmPromptConfig}
78
+ onConfirm={() => props.onSubmit({ type: 'confirm', value: 'confirmed', label: (props.config.config as ConfirmPromptConfig).confirmLabel || 'Confirmed' })}
79
+ onCancel={() => {
80
+ props.onDismiss?.()
81
+ props.onSubmit({ type: 'confirm', value: 'cancelled', label: (props.config.config as ConfirmPromptConfig).cancelLabel || 'Cancelled', dismissed: true })
82
+ }}
83
+ />
84
+ </Match>
85
+ <Match when={props.config.type === 'form'}>
86
+ <FormBody
87
+ config={props.config.config as FormPromptConfig}
88
+ onSubmit={(data, label) => props.onSubmit({ type: 'form', value: data, label })}
89
+ />
90
+ </Match>
91
+ </Switch>
92
+ </div>
93
+
94
+ <style>{`
95
+ @keyframes chat-prompt-slide-up {
96
+ from { opacity: 0; transform: translateY(8px); }
97
+ to { opacity: 1; transform: translateY(0); }
98
+ }
99
+ `}</style>
100
+ </div>
101
+ )
102
+ }
103
+
104
+ // ─── Choice ──────────────────────────────────────────────────
105
+
106
+ const ChoiceBody: Component<{
107
+ config: ChoicePromptConfig
108
+ onSelect: (value: string, label: string) => void
109
+ }> = (props) => {
110
+ const layoutClass = () => {
111
+ switch (props.config.layout) {
112
+ case 'vertical': return 'flex flex-col gap-2'
113
+ case 'grid': return 'grid grid-cols-2 gap-2'
114
+ default: return 'flex flex-wrap gap-2'
115
+ }
116
+ }
117
+
118
+ return (
119
+ <div class={layoutClass()}>
120
+ <For each={props.config.options}>
121
+ {(option) => (
122
+ <button
123
+ onClick={() => props.onSelect(option.value, option.label)}
124
+ class="px-4 py-2 text-sm font-medium rounded-lg border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white hover:bg-blue-50 hover:border-blue-300 dark:hover:bg-blue-900/30 dark:hover:border-blue-600 transition-colors text-left"
125
+ >
126
+ <Show when={option.icon}>
127
+ <span class="mr-2">{option.icon}</span>
128
+ </Show>
129
+ {option.label}
130
+ <Show when={option.description}>
131
+ <span class="block text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-normal">{option.description}</span>
132
+ </Show>
133
+ </button>
134
+ )}
135
+ </For>
136
+ </div>
137
+ )
138
+ }
139
+
140
+ // ─── Confirm ─────────────────────────────────────────────────
141
+
142
+ const ConfirmBody: Component<{
143
+ config: ConfirmPromptConfig
144
+ onConfirm: () => void
145
+ onCancel: () => void
146
+ }> = (props) => {
147
+ const isDanger = () => props.config.variant === 'danger'
148
+
149
+ return (
150
+ <div>
151
+ <Show when={props.config.message}>
152
+ <p class="text-sm text-gray-600 dark:text-gray-400 mb-3">{props.config.message}</p>
153
+ </Show>
154
+ <div class="flex gap-2 justify-end">
155
+ <button
156
+ onClick={props.onCancel}
157
+ class="px-4 py-2 text-sm font-medium rounded-lg border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
158
+ >
159
+ {props.config.cancelLabel || 'Cancel'}
160
+ </button>
161
+ <button
162
+ onClick={props.onConfirm}
163
+ class={`px-4 py-2 text-sm font-medium rounded-lg text-white transition-colors ${
164
+ isDanger()
165
+ ? 'bg-red-600 hover:bg-red-700'
166
+ : 'bg-blue-600 hover:bg-blue-700'
167
+ }`}
168
+ >
169
+ {props.config.confirmLabel || 'Confirm'}
170
+ </button>
171
+ </div>
172
+ </div>
173
+ )
174
+ }
175
+
176
+ // ─── Form ────────────────────────────────────────────────────
177
+
178
+ const FormBody: Component<{
179
+ config: FormPromptConfig
180
+ onSubmit: (data: Record<string, unknown>, label: string) => void
181
+ }> = (props) => {
182
+ const [formData, setFormData] = createSignal<Record<string, string>>({})
183
+
184
+ const updateField = (name: string, value: string) => {
185
+ setFormData((prev) => ({ ...prev, [name]: value }))
186
+ }
187
+
188
+ const handleSubmit = (e: Event) => {
189
+ e.preventDefault()
190
+ const data = formData()
191
+ // Build a human-readable label from the form values
192
+ const label = Object.entries(data)
193
+ .filter(([, v]) => v)
194
+ .map(([k, v]) => `${k}: ${v}`)
195
+ .join(', ')
196
+ props.onSubmit(data, label || 'Form submitted')
197
+ }
198
+
199
+ const isValid = () => {
200
+ const data = formData()
201
+ return (props.config.fields || [])
202
+ .filter((f) => f.required)
203
+ .every((f) => data[f.name]?.trim())
204
+ }
205
+
206
+ return (
207
+ <form onSubmit={handleSubmit} class="flex flex-col gap-3">
208
+ <For each={props.config.fields}>
209
+ {(field) => (
210
+ <div>
211
+ <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
212
+ {field.label}
213
+ <Show when={field.required}>
214
+ <span class="text-red-500 ml-0.5">*</span>
215
+ </Show>
216
+ </label>
217
+ <Switch>
218
+ <Match when={field.type === 'textarea'}>
219
+ <textarea
220
+ value={formData()[field.name] || ''}
221
+ onInput={(e) => updateField(field.name, e.currentTarget.value)}
222
+ placeholder={field.placeholder}
223
+ rows={3}
224
+ class="w-full px-3 py-2 text-sm rounded-lg border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:border-blue-400 focus:ring-1 focus:ring-blue-400 outline-none transition-colors"
225
+ />
226
+ </Match>
227
+ <Match when={field.type === 'select'}>
228
+ <select
229
+ value={formData()[field.name] || ''}
230
+ onChange={(e) => updateField(field.name, e.currentTarget.value)}
231
+ class="w-full px-3 py-2 text-sm rounded-lg border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:border-blue-400 focus:ring-1 focus:ring-blue-400 outline-none transition-colors"
232
+ >
233
+ <option value="">{field.placeholder || 'Select...'}</option>
234
+ <For each={field.options}>
235
+ {(opt) => <option value={opt.value}>{opt.label}</option>}
236
+ </For>
237
+ </select>
238
+ </Match>
239
+ <Match when={true}>
240
+ <input
241
+ type={field.type === 'number' ? 'number' : 'text'}
242
+ value={formData()[field.name] || ''}
243
+ onInput={(e) => updateField(field.name, e.currentTarget.value)}
244
+ placeholder={field.placeholder}
245
+ class="w-full px-3 py-2 text-sm rounded-lg border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:border-blue-400 focus:ring-1 focus:ring-blue-400 outline-none transition-colors"
246
+ />
247
+ </Match>
248
+ </Switch>
249
+ </div>
250
+ )}
251
+ </For>
252
+ <div class="flex justify-end">
253
+ <button
254
+ type="submit"
255
+ disabled={!isValid()}
256
+ class="px-4 py-2 text-sm font-medium rounded-lg text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
257
+ >
258
+ {props.config.submitLabel || 'Submit'}
259
+ </button>
260
+ </div>
261
+ </form>
262
+ )
263
+ }
@@ -7,7 +7,7 @@ import DOMPurify from 'dompurify'
7
7
  import { Component, createSignal, Show, For, createMemo, createEffect } from 'solid-js'
8
8
  import { isServer } from 'solid-js/web'
9
9
  import type { UIComponent, UILayout, RendererError, TableVirtualizeOptions } from '../types'
10
- import { validateComponent, DEFAULT_RESOURCE_LIMITS } from '../services/validation'
10
+ import { validateComponent, DEFAULT_RESOURCE_LIMITS, getIframeSandbox } from '../services/validation'
11
11
  import { GenerativeUIErrorBoundary } from './GenerativeUIErrorBoundary'
12
12
  import { GridRenderer } from './GridRenderer'
13
13
  import { FooterRenderer } from './FooterRenderer'
@@ -775,7 +775,7 @@ function IframeRenderer(props: { component: UIComponent }) {
775
775
  title={params.title || 'Embedded content'}
776
776
  class="w-full border-0 flex-1"
777
777
  style={`height: ${params.height || '400px'}; min-height: 300px;`}
778
- sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
778
+ sandbox={getIframeSandbox(params.url)}
779
779
  loading="lazy"
780
780
  />
781
781
  </div>
@@ -0,0 +1,81 @@
1
+ /**
2
+ * useChatBus — SolidJS hook + context provider for the Chat Bus
3
+ * v2.4.0: Event-driven chat toolkit
4
+ *
5
+ * @experimental — This API may change without major bump until v2.5.0.
6
+ */
7
+
8
+ import { createContext, useContext, onCleanup, type ParentComponent } from 'solid-js'
9
+ import { createChatBus } from '../services/chat-bus'
10
+ import type { ChatBus } from '../types/chat-bus'
11
+
12
+ // ─── Context ─────────────────────────────────────────────────
13
+
14
+ const ChatBusContext = createContext<ChatBus>()
15
+
16
+ /**
17
+ * @experimental
18
+ * Provider that creates and shares a ChatBus with all children.
19
+ * Cleans up all listeners on unmount.
20
+ *
21
+ * @example
22
+ * <ChatBusProvider>
23
+ * <ChatInterfaceStreaming />
24
+ * <BriefingPanel />
25
+ * <AgentRouter />
26
+ * </ChatBusProvider>
27
+ */
28
+ export const ChatBusProvider: ParentComponent<{ bus?: ChatBus }> = (props) => {
29
+ const bus = props.bus ?? createChatBus()
30
+
31
+ onCleanup(() => {
32
+ bus.events.clear()
33
+ })
34
+
35
+ return (
36
+ <ChatBusContext.Provider value={bus}>
37
+ {props.children}
38
+ </ChatBusContext.Provider>
39
+ )
40
+ }
41
+
42
+ // ─── Hook ────────────────────────────────────────────────────
43
+
44
+ /**
45
+ * @experimental
46
+ * Access the ChatBus from any child component.
47
+ * Must be used within a `<ChatBusProvider>`.
48
+ *
49
+ * @example
50
+ * function BriefingPanel() {
51
+ * const bus = useChatBus()
52
+ *
53
+ * bus.events.on('onBriefing', (event) => {
54
+ * addBriefing(event.briefing)
55
+ * })
56
+ *
57
+ * return <div>...</div>
58
+ * }
59
+ *
60
+ * @example
61
+ * function AgentRouter() {
62
+ * const bus = useChatBus()
63
+ *
64
+ * bus.events.on('onStreamEnd', (event) => {
65
+ * if (event.metadata.needs_clarification) {
66
+ * bus.commands.exec('showChatPrompt', {
67
+ * type: 'choice',
68
+ * title: 'Quelle periode ?',
69
+ * config: { options: [...] }
70
+ * })
71
+ * }
72
+ * })
73
+ * }
74
+ */
75
+ export function useChatBus(): ChatBus {
76
+ const bus = useContext(ChatBusContext)
77
+ if (!bus) {
78
+ throw new Error('useChatBus must be used within a <ChatBusProvider>')
79
+ }
80
+ return bus
81
+ }
package/src/index.ts CHANGED
@@ -38,6 +38,10 @@ export { EditableUIResourceRenderer } from './components/EditableUIResourceRende
38
38
  export { ExpandableWrapper } from './components/ExpandableWrapper'
39
39
  export { ComponentToolbar } from './components/ComponentToolbar'
40
40
 
41
+ // Chat Bus (v2.4.0 — @experimental)
42
+ export { ChatBusProvider, useChatBus } from './hooks/useChatBus'
43
+ export { ChatPrompt } from './components/ChatPrompt'
44
+
41
45
  // Autocomplete Components
42
46
  export { GhostText, GhostTextInput } from './components/GhostText'
43
47
  export { AutocompleteDropdown } from './components/AutocompleteDropdown'
@@ -54,6 +58,7 @@ export type { ResizeHandleProps as ResizeHandleComponentProps } from './componen
54
58
  export type { EditableUIResourceRendererProps } from './components/EditableUIResourceRenderer'
55
59
  export type { ExpandableWrapperProps } from './components/ExpandableWrapper'
56
60
  export type { ComponentToolbarProps, ToolbarAction, ToolbarIcon } from './components/ComponentToolbar'
61
+ export type { ChatPromptProps } from './components/ChatPrompt'
57
62
  export type { GhostTextProps, GhostTextInputProps } from './components/GhostText'
58
63
  export type { AutocompleteDropdownProps } from './components/AutocompleteDropdown'
59
64
  export type { AutocompleteFormFieldProps, AutocompleteFormFieldParams } from './components/AutocompleteFormField'
@@ -200,7 +205,38 @@ export {
200
205
  validateComponent,
201
206
  validateLayout,
202
207
  validateIframeDomain,
208
+ getIframeSandbox,
203
209
  DEFAULT_RESOURCE_LIMITS,
204
210
  DEFAULT_IFRAME_DOMAINS,
211
+ TRUSTED_IFRAME_DOMAINS,
205
212
  ComponentRegistry,
213
+ createEventEmitter,
214
+ createCommandHandler,
215
+ createChatBus,
206
216
  } from './services'
217
+
218
+ // Chat Bus Types (v2.4.0 — @experimental)
219
+ export type {
220
+ ChatEventBase,
221
+ ChatEvents,
222
+ ChatCommands,
223
+ ChatBus,
224
+ ChatEventEmitter,
225
+ ChatCommandHandler,
226
+ EventSubscribeOptions,
227
+ ChatPromptConfig,
228
+ ChatPromptResponse,
229
+ ChoicePromptConfig,
230
+ ConfirmPromptConfig,
231
+ FormPromptConfig,
232
+ SelectPromptConfig,
233
+ SuggestionItem,
234
+ AgentContext,
235
+ BriefingEvent,
236
+ BriefingSection,
237
+ StreamDoneMetadata,
238
+ ChatError,
239
+ Citation,
240
+ ToolCallEvent,
241
+ ClarificationEvent,
242
+ } from './types/chat-bus'