@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.
@@ -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="block text-sm font-medium text-gray-700 dark:text-gray-300"
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.helpText && !props.error}>
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
+ &#128260; 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
- props.onAction?.('submit_form', { sectionId: props.sectionId, values: formData() })
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
- approve: c?.approve || { label: 'Yes', value: 'approve' },
572
- reject: c?.reject || { label: 'No', value: 'reject' },
573
- allowComment: c?.allowComment ?? false,
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 handleFeedback = (approved: boolean) => {
579
- const d = data()
580
- props.onAction?.('feedback', {
581
- approved,
582
- value: approved ? d.approve.value : d.reject.value,
583
- comment: comment(),
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
- <button type="button" onClick={() => handleFeedback(true)}
592
- class="px-3 py-1.5 text-sm font-medium rounded-lg bg-green-600 text-white hover:bg-green-700 transition-colors flex items-center gap-1">
593
- &#128077; {data().approve.label}
594
- </button>
595
- <button type="button" onClick={() => handleFeedback(false)}
596
- 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 flex items-center gap-1">
597
- &#128078; {data().reject.label}
598
- </button>
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().allowComment}>
601
- <input
602
- type="text"
603
- value={comment()}
604
- onInput={(e) => setComment(e.currentTarget.value)}
605
- placeholder={data().commentPlaceholder}
606
- class="w-full 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"
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
  )
@@ -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 */
@@ -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