@seed-ship/mcp-ui-solid 2.10.3 → 2.11.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.
@@ -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,12 @@ 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
+ // Use dedicated onSubmit if provided, fallback to onAction
444
+ if (props.onSubmit) {
445
+ props.onSubmit(props.sectionId, formData())
446
+ } else {
447
+ props.onAction?.('submit_form', { sectionId: props.sectionId, values: formData() })
448
+ }
407
449
  }
408
450
 
409
451
  return (
@@ -564,47 +606,56 @@ const FeedbackSection: Component<{
564
606
  onAction?: (action: string, data?: unknown) => void
565
607
  }> = (props) => {
566
608
  const [comment, setComment] = createSignal('')
609
+ const [showComment, setShowComment] = createSignal(false)
567
610
  const data = () => {
568
611
  const c = props.content as any
612
+ // Support both formats: options array (universal) and approve/reject (simple)
613
+ const options = c?.options || [
614
+ { value: c?.approve?.value || 'approve', label: c?.approve?.label || 'Yes', icon: '👍', variant: 'primary' },
615
+ { value: c?.reject?.value || 'reject', label: c?.reject?.label || 'No', icon: '👎' },
616
+ ]
569
617
  return {
570
618
  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...',
619
+ options: options as Array<{ value: string; label: string; icon?: string; variant?: string; needsComment?: boolean }>,
620
+ allowFreeText: c?.allowFreeText ?? c?.allowComment ?? false,
621
+ placeholder: c?.placeholder || c?.commentPlaceholder || 'Add a comment...',
575
622
  }
576
623
  }
577
624
 
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
- })
625
+ const handleOption = (option: any) => {
626
+ if (option.needsComment) {
627
+ setShowComment(true)
628
+ return
629
+ }
630
+ props.onAction?.('feedback', { option: option.value, comment: comment() })
585
631
  }
586
632
 
587
633
  return (
588
634
  <div class="space-y-3">
589
635
  <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>
636
+ <div class="flex flex-wrap gap-2">
637
+ <For each={data().options}>
638
+ {(option) => (
639
+ <button type="button" onClick={() => handleOption(option)}
640
+ class={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors flex items-center gap-1 ${
641
+ option.variant === 'primary' ? 'bg-blue-600 text-white hover:bg-blue-700'
642
+ : option.variant === 'danger' ? 'bg-red-600 text-white hover:bg-red-700'
643
+ : 'border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
644
+ }`}>
645
+ <Show when={option.icon}><span>{option.icon}</span></Show>
646
+ {option.label}
647
+ </button>
648
+ )}
649
+ </For>
599
650
  </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
- />
651
+ <Show when={data().allowFreeText || showComment()}>
652
+ <div class="flex gap-1">
653
+ <input type="text" value={comment()} onInput={(e) => setComment(e.currentTarget.value)}
654
+ placeholder={data().placeholder} autofocus={showComment()}
655
+ 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" />
656
+ <button type="button" onClick={() => props.onAction?.('feedback', { option: 'comment', comment: comment() })}
657
+ class="px-3 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700">Send</button>
658
+ </div>
608
659
  </Show>
609
660
  </div>
610
661
  )
@@ -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 */