@seed-ship/mcp-ui-solid 2.10.3 → 2.12.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.
- package/README.md +10 -11
- package/dist/components/FormFieldRenderer.cjs +261 -223
- package/dist/components/FormFieldRenderer.cjs.map +1 -1
- package/dist/components/FormFieldRenderer.d.ts.map +1 -1
- package/dist/components/FormFieldRenderer.js +262 -224
- package/dist/components/FormFieldRenderer.js.map +1 -1
- package/dist/components/ScratchpadPanel.cjs +354 -247
- package/dist/components/ScratchpadPanel.cjs.map +1 -1
- package/dist/components/ScratchpadPanel.d.ts +4 -0
- package/dist/components/ScratchpadPanel.d.ts.map +1 -1
- package/dist/components/ScratchpadPanel.js +354 -247
- package/dist/components/ScratchpadPanel.js.map +1 -1
- package/dist/types/chat-bus.d.ts +11 -1
- package/dist/types/chat-bus.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types.d.cts +4 -0
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
- package/src/components/FormFieldRenderer.tsx +25 -3
- package/src/components/ScratchpadPanel.tsx +93 -32
- package/src/types/chat-bus.ts +7 -1
- package/src/types/index.ts +6 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -27,6 +27,10 @@ export const FormFieldRenderer: Component<FormFieldRendererProps> = (props) => {
|
|
|
27
27
|
formData: props.formData || (() => ({})),
|
|
28
28
|
})
|
|
29
29
|
|
|
30
|
+
const status = () => props.field.fieldStatus || 'optional'
|
|
31
|
+
const isUnsupported = () => status() === 'unsupported'
|
|
32
|
+
const isFieldDisabled = () => props.disabled || isUnsupported()
|
|
33
|
+
|
|
30
34
|
const baseInputClass = () => `
|
|
31
35
|
w-full px-3 py-2 border rounded-md
|
|
32
36
|
focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
|
@@ -35,6 +39,7 @@ export const FormFieldRenderer: Component<FormFieldRendererProps> = (props) => {
|
|
|
35
39
|
? 'border-red-500 focus:ring-red-500'
|
|
36
40
|
: 'border-gray-300 dark:border-gray-600'}
|
|
37
41
|
dark:bg-gray-700 dark:text-white
|
|
42
|
+
${isUnsupported() ? 'opacity-50' : ''}
|
|
38
43
|
`
|
|
39
44
|
|
|
40
45
|
const fieldId = () => `field-${props.field.name}`
|
|
@@ -46,12 +51,18 @@ export const FormFieldRenderer: Component<FormFieldRendererProps> = (props) => {
|
|
|
46
51
|
<Show when={props.field.label && props.field.type !== 'checkbox'}>
|
|
47
52
|
<label
|
|
48
53
|
for={fieldId()}
|
|
49
|
-
class=
|
|
54
|
+
class={`block text-sm font-medium ${isUnsupported() ? 'text-gray-400 dark:text-gray-500' : 'text-gray-700 dark:text-gray-300'}`}
|
|
50
55
|
>
|
|
51
56
|
{props.field.label}
|
|
52
|
-
<Show when={props.field.required}>
|
|
57
|
+
<Show when={props.field.required || status() === 'required'}>
|
|
53
58
|
<span class="text-red-500 ml-1" aria-hidden="true">*</span>
|
|
54
59
|
</Show>
|
|
60
|
+
<Show when={isUnsupported()}>
|
|
61
|
+
<span class="ml-2 text-[10px] font-medium bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400 px-1.5 py-0.5 rounded">Not supported</span>
|
|
62
|
+
</Show>
|
|
63
|
+
<Show when={status() === 'unknown'}>
|
|
64
|
+
<span class="ml-2 text-[10px] font-medium bg-yellow-100 dark:bg-yellow-900/30 text-yellow-600 dark:text-yellow-400 px-1.5 py-0.5 rounded">?</span>
|
|
65
|
+
</Show>
|
|
55
66
|
</label>
|
|
56
67
|
</Show>
|
|
57
68
|
|
|
@@ -251,7 +262,18 @@ export const FormFieldRenderer: Component<FormFieldRendererProps> = (props) => {
|
|
|
251
262
|
</Match>
|
|
252
263
|
</Switch>
|
|
253
264
|
|
|
254
|
-
<Show when={props.field.
|
|
265
|
+
<Show when={props.field.statusReason}>
|
|
266
|
+
<p class={`text-xs ${
|
|
267
|
+
isUnsupported() ? 'text-orange-500 dark:text-orange-400'
|
|
268
|
+
: status() === 'unknown' ? 'text-yellow-500 dark:text-yellow-400'
|
|
269
|
+
: status() === 'required' ? 'text-blue-500 dark:text-blue-400'
|
|
270
|
+
: 'text-gray-500 dark:text-gray-400'
|
|
271
|
+
}`}>
|
|
272
|
+
{props.field.statusReason}
|
|
273
|
+
</p>
|
|
274
|
+
</Show>
|
|
275
|
+
|
|
276
|
+
<Show when={props.field.helpText && !props.error && !props.field.statusReason}>
|
|
255
277
|
<p class="text-xs text-gray-500 dark:text-gray-400">{props.field.helpText}</p>
|
|
256
278
|
</Show>
|
|
257
279
|
|
|
@@ -15,6 +15,10 @@ export interface ScratchpadPanelProps {
|
|
|
15
15
|
onFilterChange?: (filters: Record<string, string | string[]>) => void
|
|
16
16
|
onAction?: (action: string, data?: unknown) => void
|
|
17
17
|
onSectionEdit?: (sectionId: string, content: unknown) => void
|
|
18
|
+
/** Dedicated callback for form submissions (cleaner than onAction) */
|
|
19
|
+
onSubmit?: (sectionId: string, values: Record<string, unknown>) => void
|
|
20
|
+
/** Called when user clicks retry on error state */
|
|
21
|
+
onRetry?: () => void
|
|
18
22
|
onClose?: () => void
|
|
19
23
|
closable?: boolean
|
|
20
24
|
autoCloseDelay?: number
|
|
@@ -28,6 +32,7 @@ const STATUS_BADGES: Record<ScratchpadState['status'], { label: string; class: s
|
|
|
28
32
|
waiting_human: { label: 'Your turn', class: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 animate-pulse' },
|
|
29
33
|
processing: { label: 'Processing...', class: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400' },
|
|
30
34
|
complete: { label: 'Complete', class: 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400' },
|
|
35
|
+
error: { label: 'Error', class: 'bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400' },
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
export const ScratchpadPanel: Component<ScratchpadPanelProps> = (props) => {
|
|
@@ -59,8 +64,8 @@ export const ScratchpadPanel: Component<ScratchpadPanelProps> = (props) => {
|
|
|
59
64
|
previewTimer = setTimeout(async () => {
|
|
60
65
|
try {
|
|
61
66
|
const res = await fetch(endpoint, {
|
|
62
|
-
method: 'POST',
|
|
63
|
-
headers: { 'Content-Type': 'application/json' },
|
|
67
|
+
method: props.state.previewMethod || 'POST',
|
|
68
|
+
headers: { 'Content-Type': 'application/json', ...props.state.previewHeaders },
|
|
64
69
|
credentials: 'include',
|
|
65
70
|
body: JSON.stringify({ filters }),
|
|
66
71
|
})
|
|
@@ -141,6 +146,7 @@ export const ScratchpadPanel: Component<ScratchpadPanelProps> = (props) => {
|
|
|
141
146
|
onFilterChange={props.onFilterChange}
|
|
142
147
|
onAction={props.onAction}
|
|
143
148
|
onSectionEdit={props.onSectionEdit}
|
|
149
|
+
onSubmit={props.onSubmit}
|
|
144
150
|
/>
|
|
145
151
|
)}
|
|
146
152
|
</For>
|
|
@@ -193,6 +199,35 @@ export const ScratchpadPanel: Component<ScratchpadPanelProps> = (props) => {
|
|
|
193
199
|
</div>
|
|
194
200
|
</Show>
|
|
195
201
|
|
|
202
|
+
{/* Error state with retry */}
|
|
203
|
+
<Show when={props.state.status === 'error' && props.state.error}>
|
|
204
|
+
<div class="px-4 py-3 border-t border-red-100 dark:border-red-900/30 bg-red-50 dark:bg-red-900/10">
|
|
205
|
+
<div class="flex items-start gap-2 text-sm text-red-700 dark:text-red-400">
|
|
206
|
+
<span class="flex-shrink-0 mt-0.5">⚠️</span>
|
|
207
|
+
<div class="flex-1">
|
|
208
|
+
<p class="font-medium">{props.state.error!.message}</p>
|
|
209
|
+
<Show when={props.state.error!.code}>
|
|
210
|
+
<p class="text-xs text-red-500 dark:text-red-500 mt-0.5">Code: {props.state.error!.code}</p>
|
|
211
|
+
</Show>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
<div class="flex gap-2 mt-2">
|
|
215
|
+
<Show when={props.state.error!.retryable !== false}>
|
|
216
|
+
<button type="button" onClick={() => props.onRetry?.()}
|
|
217
|
+
class="px-3 py-1.5 text-sm font-medium rounded-lg bg-red-600 text-white hover:bg-red-700 transition-colors flex items-center gap-1">
|
|
218
|
+
🔄 Retry
|
|
219
|
+
</button>
|
|
220
|
+
</Show>
|
|
221
|
+
<Show when={props.onClose}>
|
|
222
|
+
<button type="button" onClick={() => props.onClose?.()}
|
|
223
|
+
class="px-3 py-1.5 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">
|
|
224
|
+
Close
|
|
225
|
+
</button>
|
|
226
|
+
</Show>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</Show>
|
|
230
|
+
|
|
196
231
|
{/* Search button when waiting_human */}
|
|
197
232
|
<Show when={props.state.status === 'waiting_human' && hasFilters()}>
|
|
198
233
|
<div class="px-4 py-3 border-t border-gray-100 dark:border-gray-700">
|
|
@@ -224,6 +259,7 @@ const SectionRenderer: Component<{
|
|
|
224
259
|
onFilterChange?: (filters: Record<string, string | string[]>) => void
|
|
225
260
|
onAction?: (action: string, data?: unknown) => void
|
|
226
261
|
onSectionEdit?: (sectionId: string, content: unknown) => void
|
|
262
|
+
onSubmit?: (sectionId: string, values: Record<string, unknown>) => void
|
|
227
263
|
}> = (props) => {
|
|
228
264
|
return (
|
|
229
265
|
<div class="px-4 py-3">
|
|
@@ -234,7 +270,7 @@ const SectionRenderer: Component<{
|
|
|
234
270
|
<Match when={props.section.type === 'message'}><p class="text-sm text-gray-700 dark:text-gray-300">{String(props.section.content)}</p></Match>
|
|
235
271
|
<Match when={props.section.type === 'action'}><ActionSection content={props.section.content} onAction={props.onAction} /></Match>
|
|
236
272
|
<Match when={props.section.type === 'steps'}><EnrichedStepsSection content={props.section.content} onAction={props.onAction} onFilterChange={props.onFilterChange} /></Match>
|
|
237
|
-
<Match when={props.section.type === 'form'}><EmbeddedFormSection content={props.section.content} sectionId={props.section.id} onAction={props.onAction} /></Match>
|
|
273
|
+
<Match when={props.section.type === 'form'}><EmbeddedFormSection content={props.section.content} sectionId={props.section.id} onAction={props.onAction} onSubmit={props.onSubmit} /></Match>
|
|
238
274
|
<Match when={props.section.type === 'understanding'}><UnderstandingSection content={props.section.content} /></Match>
|
|
239
275
|
<Match when={props.section.type === 'feedback'}><FeedbackSection content={props.section.content} onAction={props.onAction} /></Match>
|
|
240
276
|
<Match when={props.section.type === 'prompt'}><PromptSection content={props.section.content} onAction={props.onAction} /></Match>
|
|
@@ -362,6 +398,7 @@ const EmbeddedFormSection: Component<{
|
|
|
362
398
|
content: unknown
|
|
363
399
|
sectionId: string
|
|
364
400
|
onAction?: (action: string, data?: unknown) => void
|
|
401
|
+
onSubmit?: (sectionId: string, values: Record<string, unknown>) => void
|
|
365
402
|
}> = (props) => {
|
|
366
403
|
const [formData, setFormData] = createSignal<Record<string, any>>({})
|
|
367
404
|
const [dynamicOptions, setDynamicOptions] = createSignal<Record<string, Array<{ label: string; value: string }>>>({})
|
|
@@ -403,7 +440,22 @@ const EmbeddedFormSection: Component<{
|
|
|
403
440
|
|
|
404
441
|
const handleSubmit = (e: Event) => {
|
|
405
442
|
e.preventDefault()
|
|
406
|
-
|
|
443
|
+
|
|
444
|
+
// Filter out unsupported fields, keep only values with content
|
|
445
|
+
const values = Object.fromEntries(
|
|
446
|
+
Object.entries(formData())
|
|
447
|
+
.filter(([key]) => {
|
|
448
|
+
const field = config().fields.find((f: any) => f.name === key)
|
|
449
|
+
return field?.fieldStatus !== 'unsupported'
|
|
450
|
+
})
|
|
451
|
+
.filter(([, v]) => v !== undefined && v !== '' && !(Array.isArray(v) && v.length === 0))
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
if (props.onSubmit) {
|
|
455
|
+
props.onSubmit(props.sectionId, values)
|
|
456
|
+
} else {
|
|
457
|
+
props.onAction?.('submit_form', { sectionId: props.sectionId, values })
|
|
458
|
+
}
|
|
407
459
|
}
|
|
408
460
|
|
|
409
461
|
return (
|
|
@@ -564,47 +616,56 @@ const FeedbackSection: Component<{
|
|
|
564
616
|
onAction?: (action: string, data?: unknown) => void
|
|
565
617
|
}> = (props) => {
|
|
566
618
|
const [comment, setComment] = createSignal('')
|
|
619
|
+
const [showComment, setShowComment] = createSignal(false)
|
|
567
620
|
const data = () => {
|
|
568
621
|
const c = props.content as any
|
|
622
|
+
// Support both formats: options array (universal) and approve/reject (simple)
|
|
623
|
+
const options = c?.options || [
|
|
624
|
+
{ value: c?.approve?.value || 'approve', label: c?.approve?.label || 'Yes', icon: '👍', variant: 'primary' },
|
|
625
|
+
{ value: c?.reject?.value || 'reject', label: c?.reject?.label || 'No', icon: '👎' },
|
|
626
|
+
]
|
|
569
627
|
return {
|
|
570
628
|
question: c?.question || '',
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
commentPlaceholder: c?.commentPlaceholder || 'Add a comment...',
|
|
629
|
+
options: options as Array<{ value: string; label: string; icon?: string; variant?: string; needsComment?: boolean }>,
|
|
630
|
+
allowFreeText: c?.allowFreeText ?? c?.allowComment ?? false,
|
|
631
|
+
placeholder: c?.placeholder || c?.commentPlaceholder || 'Add a comment...',
|
|
575
632
|
}
|
|
576
633
|
}
|
|
577
634
|
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
})
|
|
635
|
+
const handleOption = (option: any) => {
|
|
636
|
+
if (option.needsComment) {
|
|
637
|
+
setShowComment(true)
|
|
638
|
+
return
|
|
639
|
+
}
|
|
640
|
+
props.onAction?.('feedback', { option: option.value, comment: comment() })
|
|
585
641
|
}
|
|
586
642
|
|
|
587
643
|
return (
|
|
588
644
|
<div class="space-y-3">
|
|
589
645
|
<p class="text-sm text-gray-700 dark:text-gray-300">{data().question}</p>
|
|
590
|
-
<div class="flex gap-2">
|
|
591
|
-
<
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
646
|
+
<div class="flex flex-wrap gap-2">
|
|
647
|
+
<For each={data().options}>
|
|
648
|
+
{(option) => (
|
|
649
|
+
<button type="button" onClick={() => handleOption(option)}
|
|
650
|
+
class={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors flex items-center gap-1 ${
|
|
651
|
+
option.variant === 'primary' ? 'bg-blue-600 text-white hover:bg-blue-700'
|
|
652
|
+
: option.variant === 'danger' ? 'bg-red-600 text-white hover:bg-red-700'
|
|
653
|
+
: 'border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
|
654
|
+
}`}>
|
|
655
|
+
<Show when={option.icon}><span>{option.icon}</span></Show>
|
|
656
|
+
{option.label}
|
|
657
|
+
</button>
|
|
658
|
+
)}
|
|
659
|
+
</For>
|
|
599
660
|
</div>
|
|
600
|
-
<Show when={data().
|
|
601
|
-
<
|
|
602
|
-
type="text"
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
661
|
+
<Show when={data().allowFreeText || showComment()}>
|
|
662
|
+
<div class="flex gap-1">
|
|
663
|
+
<input type="text" value={comment()} onInput={(e) => setComment(e.currentTarget.value)}
|
|
664
|
+
placeholder={data().placeholder} autofocus={showComment()}
|
|
665
|
+
class="flex-1 px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:border-blue-400 outline-none" />
|
|
666
|
+
<button type="button" onClick={() => props.onAction?.('feedback', { option: 'comment', comment: comment() })}
|
|
667
|
+
class="px-3 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700">Send</button>
|
|
668
|
+
</div>
|
|
608
669
|
</Show>
|
|
609
670
|
</div>
|
|
610
671
|
)
|
package/src/types/chat-bus.ts
CHANGED
|
@@ -339,11 +339,17 @@ export interface ScratchpadState {
|
|
|
339
339
|
preview?: { count: number; rows?: Record<string, unknown>[]; summary: string }
|
|
340
340
|
/** Agent messages (explanations, questions) */
|
|
341
341
|
agentMessages: Array<{ text: string; type: 'info' | 'question' | 'warning' }>
|
|
342
|
-
status: 'loading' | 'ready' | 'waiting_human' | 'processing' | 'complete'
|
|
342
|
+
status: 'loading' | 'ready' | 'waiting_human' | 'processing' | 'complete' | 'error'
|
|
343
|
+
/** Error details when status is 'error' */
|
|
344
|
+
error?: { message: string; code?: string; retryable?: boolean }
|
|
343
345
|
/** Endpoint for auto-refresh preview when filters change */
|
|
344
346
|
previewEndpoint?: string
|
|
345
347
|
/** Debounce delay for preview refresh (ms, default 500) */
|
|
346
348
|
previewDebounce?: number
|
|
349
|
+
/** HTTP method for preview (default POST) */
|
|
350
|
+
previewMethod?: 'GET' | 'POST'
|
|
351
|
+
/** Extra headers for preview fetch */
|
|
352
|
+
previewHeaders?: Record<string, string>
|
|
347
353
|
/** Current turn number (multi-tour) */
|
|
348
354
|
turn?: number
|
|
349
355
|
/** Total expected turns */
|
package/src/types/index.ts
CHANGED
|
@@ -359,6 +359,12 @@ export interface FormFieldParams {
|
|
|
359
359
|
extraParams?: Record<string, string>
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
+
// Field status — API capability indicator
|
|
363
|
+
/** Whether this field is supported by the target API/dataset */
|
|
364
|
+
fieldStatus?: 'required' | 'optional' | 'unsupported' | 'unknown'
|
|
365
|
+
/** Human-readable explanation of the status */
|
|
366
|
+
statusReason?: string
|
|
367
|
+
|
|
362
368
|
// Checkbox specific
|
|
363
369
|
checkboxLabel?: string
|
|
364
370
|
|