@seed-ship/mcp-ui-solid 2.13.0 → 2.14.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 +2 -2
- package/dist/components/ScratchpadPanel.cjs +221 -2
- 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 +221 -2
- package/dist/components/ScratchpadPanel.js.map +1 -1
- package/dist/types/chat-bus.d.ts +3 -1
- package/dist/types/chat-bus.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/ScratchpadPanel.tsx +130 -2
- package/src/types/chat-bus.ts +3 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -20,6 +20,10 @@ export interface ScratchpadPanelProps {
|
|
|
20
20
|
/** Called when user clicks retry on error state */
|
|
21
21
|
onRetry?: () => void
|
|
22
22
|
onClose?: () => void
|
|
23
|
+
/** When true, action buttons show loading spinner and stay open until next server update */
|
|
24
|
+
asyncAction?: boolean
|
|
25
|
+
/** When true (set by server), scratchpad stays visible during stream */
|
|
26
|
+
pinned?: boolean
|
|
23
27
|
closable?: boolean
|
|
24
28
|
autoCloseDelay?: number
|
|
25
29
|
collapsible?: boolean
|
|
@@ -38,6 +42,7 @@ const STATUS_BADGES: Record<ScratchpadState['status'], { label: string; class: s
|
|
|
38
42
|
export const ScratchpadPanel: Component<ScratchpadPanelProps> = (props) => {
|
|
39
43
|
const [collapsed, setCollapsed] = createSignal(false)
|
|
40
44
|
const [localPreview, setLocalPreview] = createSignal<ScratchpadState['preview']>(undefined)
|
|
45
|
+
const [loadingAction, setLoadingAction] = createSignal<string | null>(null)
|
|
41
46
|
let previewTimer: ReturnType<typeof setTimeout> | null = null
|
|
42
47
|
const badge = () => STATUS_BADGES[props.state.status] || STATUS_BADGES.loading
|
|
43
48
|
const isClosable = () => props.closable !== false
|
|
@@ -49,18 +54,25 @@ export const ScratchpadPanel: Component<ScratchpadPanelProps> = (props) => {
|
|
|
49
54
|
const CLOSE_ALIASES = new Set(['done', 'close', 'dismiss', 'validate', 'cancel', 'sufficient'])
|
|
50
55
|
|
|
51
56
|
const handleAction = (action: string, data?: unknown) => {
|
|
57
|
+
if (props.asyncAction && !CLOSE_ALIASES.has(action)) {
|
|
58
|
+
setLoadingAction(action)
|
|
59
|
+
}
|
|
52
60
|
props.onAction?.(action, data)
|
|
53
61
|
if (CLOSE_ALIASES.has(action) && props.onClose) {
|
|
54
62
|
props.onClose()
|
|
55
63
|
}
|
|
56
64
|
}
|
|
57
65
|
|
|
58
|
-
// Auto-close on complete
|
|
66
|
+
// Auto-close on complete (unless pinned)
|
|
59
67
|
createEffect(() => {
|
|
60
|
-
if (props.state.status === 'complete' && props.autoCloseDelay) {
|
|
68
|
+
if (props.state.status === 'complete' && props.autoCloseDelay && !props.pinned) {
|
|
61
69
|
const timer = setTimeout(() => props.onClose?.(), props.autoCloseDelay)
|
|
62
70
|
onCleanup(() => clearTimeout(timer))
|
|
63
71
|
}
|
|
72
|
+
// Clear loading action when server responds
|
|
73
|
+
if (props.state.status !== 'processing' && loadingAction()) {
|
|
74
|
+
setLoadingAction(null)
|
|
75
|
+
}
|
|
64
76
|
})
|
|
65
77
|
|
|
66
78
|
// Preview auto-refresh when filters change
|
|
@@ -285,6 +297,9 @@ const SectionRenderer: Component<{
|
|
|
285
297
|
<Match when={props.section.type === 'feedback'}><FeedbackSection content={props.section.content} onAction={props.onAction} /></Match>
|
|
286
298
|
<Match when={props.section.type === 'prompt'}><PromptSection content={props.section.content} onAction={props.onAction} /></Match>
|
|
287
299
|
<Match when={props.section.type === 'stepper'}><StepperProgressSection content={props.section.content} /></Match>
|
|
300
|
+
<Match when={props.section.type === 'error'}><ErrorSectionRenderer content={props.section.content} onAction={props.onAction} /></Match>
|
|
301
|
+
<Match when={props.section.type === 'source_card'}><SourceCardSection content={props.section.content} /></Match>
|
|
302
|
+
<Match when={props.section.type === 'diff'}><DiffSection content={props.section.content} /></Match>
|
|
288
303
|
<Match when={true}><pre class="text-xs text-gray-500 overflow-auto">{JSON.stringify(props.section.content, null, 2)}</pre></Match>
|
|
289
304
|
</Switch>
|
|
290
305
|
</div>
|
|
@@ -798,3 +813,116 @@ const StepperProgressSection: Component<{ content: unknown }> = (props) => {
|
|
|
798
813
|
</Show>
|
|
799
814
|
)
|
|
800
815
|
}
|
|
816
|
+
|
|
817
|
+
// ─── Error Section (F6) ──────────────────────────────────────
|
|
818
|
+
|
|
819
|
+
const ErrorSectionRenderer: Component<{
|
|
820
|
+
content: unknown
|
|
821
|
+
onAction?: (action: string, data?: unknown) => void
|
|
822
|
+
}> = (props) => {
|
|
823
|
+
const [showDetails, setShowDetails] = createSignal(false)
|
|
824
|
+
const data = () => {
|
|
825
|
+
const c = props.content as any
|
|
826
|
+
return { message: c?.message || 'Error', severity: c?.severity || 'error', retryAction: c?.retryAction, retryLabel: c?.retryLabel || 'Retry', details: c?.details, timestamp: c?.timestamp }
|
|
827
|
+
}
|
|
828
|
+
const isWarning = () => data().severity === 'warning'
|
|
829
|
+
|
|
830
|
+
return (
|
|
831
|
+
<div class={`rounded-lg px-3 py-2 text-sm ${isWarning() ? 'bg-amber-50 dark:bg-amber-900/10 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-800' : 'bg-red-50 dark:bg-red-900/10 text-red-700 dark:text-red-400 border border-red-200 dark:border-red-800'}`}>
|
|
832
|
+
<div class="flex items-start gap-2">
|
|
833
|
+
<span class="flex-shrink-0">{isWarning() ? '⚠️' : '❌'}</span>
|
|
834
|
+
<div class="flex-1">
|
|
835
|
+
<p>{data().message}</p>
|
|
836
|
+
<div class="flex gap-2 mt-2">
|
|
837
|
+
<Show when={data().retryAction}>
|
|
838
|
+
<button type="button" onClick={() => props.onAction?.(data().retryAction!)} class={`px-2 py-1 text-xs font-medium rounded ${isWarning() ? 'bg-amber-600 text-white hover:bg-amber-700' : 'bg-red-600 text-white hover:bg-red-700'} transition-colors`}>🔄 {data().retryLabel}</button>
|
|
839
|
+
</Show>
|
|
840
|
+
<Show when={data().details}>
|
|
841
|
+
<button type="button" onClick={() => setShowDetails(!showDetails())} class="px-2 py-1 text-xs opacity-70 hover:opacity-100">▶ Details</button>
|
|
842
|
+
</Show>
|
|
843
|
+
</div>
|
|
844
|
+
<Show when={showDetails() && data().details}>
|
|
845
|
+
<pre class="mt-2 text-xs opacity-70 overflow-x-auto">{data().details}</pre>
|
|
846
|
+
</Show>
|
|
847
|
+
</div>
|
|
848
|
+
</div>
|
|
849
|
+
</div>
|
|
850
|
+
)
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// ─── Source Card Section (F9) ────────────────────────────────
|
|
854
|
+
|
|
855
|
+
const SourceCardSection: Component<{ content: unknown }> = (props) => {
|
|
856
|
+
const data = () => {
|
|
857
|
+
const c = props.content as any
|
|
858
|
+
return { name: c?.name || 'Source', status: c?.status || 'available', capabilities: c?.capabilities || [], latency_ms: c?.latency_ms, freshness: c?.freshness, row_count: c?.row_count }
|
|
859
|
+
}
|
|
860
|
+
const statusIcon = () => ({ queried: '✅', available: '📦', error: '❌' } as Record<string, string>)[data().status] || '📦'
|
|
861
|
+
|
|
862
|
+
return (
|
|
863
|
+
<div class="rounded-lg border border-gray-200 dark:border-gray-700 px-3 py-2">
|
|
864
|
+
<div class="flex items-center justify-between mb-1.5">
|
|
865
|
+
<span class="text-sm font-medium text-gray-900 dark:text-white">{statusIcon()} {data().name}</span>
|
|
866
|
+
<Show when={data().row_count !== undefined}>
|
|
867
|
+
<span class="text-xs font-bold text-blue-600 dark:text-blue-400">{data().row_count?.toLocaleString()} results</span>
|
|
868
|
+
</Show>
|
|
869
|
+
</div>
|
|
870
|
+
<div class="flex flex-wrap gap-1.5 mb-1">
|
|
871
|
+
<For each={data().capabilities}>
|
|
872
|
+
{(cap: any) => (
|
|
873
|
+
<span class={`text-[10px] px-1.5 py-0.5 rounded ${cap.supported ? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400' : 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400'}`}>
|
|
874
|
+
{cap.supported ? '✅' : '❌'} {cap.label}
|
|
875
|
+
</span>
|
|
876
|
+
)}
|
|
877
|
+
</For>
|
|
878
|
+
</div>
|
|
879
|
+
<Show when={data().freshness || data().latency_ms}>
|
|
880
|
+
<p class="text-[10px] text-gray-400">{[data().freshness, data().latency_ms ? `${data().latency_ms}ms` : ''].filter(Boolean).join(' · ')}</p>
|
|
881
|
+
</Show>
|
|
882
|
+
</div>
|
|
883
|
+
)
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// ─── Diff Section (F10) ──────────────────────────────────────
|
|
887
|
+
|
|
888
|
+
const DiffSection: Component<{ content: unknown }> = (props) => {
|
|
889
|
+
const data = () => {
|
|
890
|
+
const c = props.content as any
|
|
891
|
+
return { left: c?.left || { label: 'A', rows: [] }, right: c?.right || { label: 'B', rows: [] }, highlight: c?.highlight_columns || [] }
|
|
892
|
+
}
|
|
893
|
+
const allKeys = () => {
|
|
894
|
+
const l = data().left.rows[0] || {}
|
|
895
|
+
const r = data().right.rows[0] || {}
|
|
896
|
+
return [...new Set([...Object.keys(l), ...Object.keys(r)])]
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return (
|
|
900
|
+
<div class="overflow-x-auto">
|
|
901
|
+
<table class="min-w-full text-xs">
|
|
902
|
+
<thead>
|
|
903
|
+
<tr>
|
|
904
|
+
<th class="px-2 py-1 text-left text-gray-500 dark:text-gray-400"></th>
|
|
905
|
+
<th class="px-2 py-1 text-left font-medium text-blue-600 dark:text-blue-400">{data().left.label}</th>
|
|
906
|
+
<th class="px-2 py-1 text-left font-medium text-purple-600 dark:text-purple-400">{data().right.label}</th>
|
|
907
|
+
</tr>
|
|
908
|
+
</thead>
|
|
909
|
+
<tbody>
|
|
910
|
+
<For each={allKeys()}>
|
|
911
|
+
{(key) => {
|
|
912
|
+
const lVal = () => data().left.rows[0]?.[key]
|
|
913
|
+
const rVal = () => data().right.rows[0]?.[key]
|
|
914
|
+
const isDiff = () => String(lVal()) !== String(rVal()) && data().highlight.includes(key)
|
|
915
|
+
return (
|
|
916
|
+
<tr class={`border-t border-gray-100 dark:border-gray-700 ${isDiff() ? 'bg-yellow-50 dark:bg-yellow-900/10' : ''}`}>
|
|
917
|
+
<td class="px-2 py-1 font-mono text-gray-500 dark:text-gray-400">{key}</td>
|
|
918
|
+
<td class="px-2 py-1 text-gray-900 dark:text-white">{lVal() !== undefined ? String(lVal()) : '—'}</td>
|
|
919
|
+
<td class="px-2 py-1 text-gray-900 dark:text-white">{rVal() !== undefined ? String(rVal()) : '—'}</td>
|
|
920
|
+
</tr>
|
|
921
|
+
)
|
|
922
|
+
}}
|
|
923
|
+
</For>
|
|
924
|
+
</tbody>
|
|
925
|
+
</table>
|
|
926
|
+
</div>
|
|
927
|
+
)
|
|
928
|
+
}
|
package/src/types/chat-bus.ts
CHANGED
|
@@ -366,7 +366,7 @@ export interface ScratchpadState {
|
|
|
366
366
|
export interface ScratchpadSection {
|
|
367
367
|
id: string
|
|
368
368
|
title: string
|
|
369
|
-
type: 'data' | 'filter' | 'preview' | 'message' | 'action' | 'steps' | 'form' | 'understanding' | 'feedback' | 'prompt' | 'stepper'
|
|
369
|
+
type: 'data' | 'filter' | 'preview' | 'message' | 'action' | 'steps' | 'form' | 'understanding' | 'feedback' | 'prompt' | 'stepper' | 'error' | 'source_card' | 'diff'
|
|
370
370
|
content: unknown
|
|
371
371
|
/** Can the human edit this section? */
|
|
372
372
|
editable: boolean
|
|
@@ -385,6 +385,8 @@ export interface ScratchpadEvent {
|
|
|
385
385
|
sections?: ScratchpadSection[]
|
|
386
386
|
/** How to merge sections on update (default: 'replace') */
|
|
387
387
|
sectionMode?: 'replace' | 'append' | 'upsert'
|
|
388
|
+
/** If true, scratchpad stays visible during stream (no auto-close on complete) */
|
|
389
|
+
pinned?: boolean
|
|
388
390
|
filters?: Record<string, string | string[]>
|
|
389
391
|
preview?: { count: number; rows?: Record<string, unknown>[]; summary: string }
|
|
390
392
|
agentMessages?: Array<{ text: string; type: 'info' | 'question' | 'warning' }>
|