@seed-ship/mcp-ui-solid 2.13.0 → 2.15.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 +517 -260
- package/dist/components/ScratchpadPanel.cjs.map +1 -1
- package/dist/components/ScratchpadPanel.d.ts +8 -0
- package/dist/components/ScratchpadPanel.d.ts.map +1 -1
- package/dist/components/ScratchpadPanel.js +517 -260
- 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 +156 -2
- package/src/types/chat-bus.ts +3 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -20,6 +20,14 @@ 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
|
|
27
|
+
/** Log events/actions to console */
|
|
28
|
+
debug?: boolean
|
|
29
|
+
/** Show mini debug overlay */
|
|
30
|
+
debugOverlay?: boolean
|
|
23
31
|
closable?: boolean
|
|
24
32
|
autoCloseDelay?: number
|
|
25
33
|
collapsible?: boolean
|
|
@@ -38,29 +46,52 @@ const STATUS_BADGES: Record<ScratchpadState['status'], { label: string; class: s
|
|
|
38
46
|
export const ScratchpadPanel: Component<ScratchpadPanelProps> = (props) => {
|
|
39
47
|
const [collapsed, setCollapsed] = createSignal(false)
|
|
40
48
|
const [localPreview, setLocalPreview] = createSignal<ScratchpadState['preview']>(undefined)
|
|
49
|
+
const [loadingAction, setLoadingAction] = createSignal<string | null>(null)
|
|
41
50
|
let previewTimer: ReturnType<typeof setTimeout> | null = null
|
|
42
51
|
const badge = () => STATUS_BADGES[props.state.status] || STATUS_BADGES.loading
|
|
43
52
|
const isClosable = () => props.closable !== false
|
|
44
53
|
const isCollapsible = () => props.collapsible !== false
|
|
45
54
|
const preview = () => localPreview() || props.state.preview
|
|
46
55
|
const hasFilters = () => Object.keys(props.state.filters || {}).length > 0
|
|
56
|
+
const [eventCount, setEventCount] = createSignal(0)
|
|
57
|
+
const [lastEvent, setLastEvent] = createSignal('')
|
|
58
|
+
|
|
59
|
+
const debugLog = (event: string, data?: any) => {
|
|
60
|
+
if (!props.debug) return
|
|
61
|
+
setEventCount(c => c + 1)
|
|
62
|
+
setLastEvent(event)
|
|
63
|
+
console.log(`[ScratchpadPanel:${props.state.id}] ${event}`, data || '')
|
|
64
|
+
}
|
|
47
65
|
|
|
48
66
|
// Action aliases that auto-close the scratchpad
|
|
49
67
|
const CLOSE_ALIASES = new Set(['done', 'close', 'dismiss', 'validate', 'cancel', 'sufficient'])
|
|
50
68
|
|
|
51
69
|
const handleAction = (action: string, data?: unknown) => {
|
|
70
|
+
debugLog('onAction', { action, asyncAction: props.asyncAction, data })
|
|
71
|
+
if (props.asyncAction && !CLOSE_ALIASES.has(action)) {
|
|
72
|
+
setLoadingAction(action)
|
|
73
|
+
}
|
|
52
74
|
props.onAction?.(action, data)
|
|
53
75
|
if (CLOSE_ALIASES.has(action) && props.onClose) {
|
|
54
76
|
props.onClose()
|
|
55
77
|
}
|
|
56
78
|
}
|
|
57
79
|
|
|
58
|
-
//
|
|
80
|
+
// Debug: log state changes
|
|
59
81
|
createEffect(() => {
|
|
60
|
-
|
|
82
|
+
debugLog('state', { status: props.state.status, sections: props.state.sections?.length, filters: Object.keys(props.state.filters || {}), turn: props.state.turn })
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Auto-close on complete (unless pinned)
|
|
86
|
+
createEffect(() => {
|
|
87
|
+
if (props.state.status === 'complete' && props.autoCloseDelay && !props.pinned) {
|
|
61
88
|
const timer = setTimeout(() => props.onClose?.(), props.autoCloseDelay)
|
|
62
89
|
onCleanup(() => clearTimeout(timer))
|
|
63
90
|
}
|
|
91
|
+
// Clear loading action when server responds
|
|
92
|
+
if (props.state.status !== 'processing' && loadingAction()) {
|
|
93
|
+
setLoadingAction(null)
|
|
94
|
+
}
|
|
64
95
|
})
|
|
65
96
|
|
|
66
97
|
// Preview auto-refresh when filters change
|
|
@@ -257,6 +288,13 @@ export const ScratchpadPanel: Component<ScratchpadPanelProps> = (props) => {
|
|
|
257
288
|
<style>{`
|
|
258
289
|
@keyframes scratchpad-slide-down { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } }
|
|
259
290
|
`}</style>
|
|
291
|
+
|
|
292
|
+
{/* Debug overlay */}
|
|
293
|
+
<Show when={props.debugOverlay}>
|
|
294
|
+
<div class="absolute bottom-1 right-1 px-2 py-1 text-[9px] font-mono bg-black/80 text-green-400 rounded z-50 pointer-events-none">
|
|
295
|
+
{props.state.id} | ev:{eventCount()} | sec:{props.state.sections?.length || 0} | {props.state.status} | last:{lastEvent()}
|
|
296
|
+
</div>
|
|
297
|
+
</Show>
|
|
260
298
|
</div>
|
|
261
299
|
)
|
|
262
300
|
}
|
|
@@ -285,6 +323,9 @@ const SectionRenderer: Component<{
|
|
|
285
323
|
<Match when={props.section.type === 'feedback'}><FeedbackSection content={props.section.content} onAction={props.onAction} /></Match>
|
|
286
324
|
<Match when={props.section.type === 'prompt'}><PromptSection content={props.section.content} onAction={props.onAction} /></Match>
|
|
287
325
|
<Match when={props.section.type === 'stepper'}><StepperProgressSection content={props.section.content} /></Match>
|
|
326
|
+
<Match when={props.section.type === 'error'}><ErrorSectionRenderer content={props.section.content} onAction={props.onAction} /></Match>
|
|
327
|
+
<Match when={props.section.type === 'source_card'}><SourceCardSection content={props.section.content} /></Match>
|
|
328
|
+
<Match when={props.section.type === 'diff'}><DiffSection content={props.section.content} /></Match>
|
|
288
329
|
<Match when={true}><pre class="text-xs text-gray-500 overflow-auto">{JSON.stringify(props.section.content, null, 2)}</pre></Match>
|
|
289
330
|
</Switch>
|
|
290
331
|
</div>
|
|
@@ -798,3 +839,116 @@ const StepperProgressSection: Component<{ content: unknown }> = (props) => {
|
|
|
798
839
|
</Show>
|
|
799
840
|
)
|
|
800
841
|
}
|
|
842
|
+
|
|
843
|
+
// ─── Error Section (F6) ──────────────────────────────────────
|
|
844
|
+
|
|
845
|
+
const ErrorSectionRenderer: Component<{
|
|
846
|
+
content: unknown
|
|
847
|
+
onAction?: (action: string, data?: unknown) => void
|
|
848
|
+
}> = (props) => {
|
|
849
|
+
const [showDetails, setShowDetails] = createSignal(false)
|
|
850
|
+
const data = () => {
|
|
851
|
+
const c = props.content as any
|
|
852
|
+
return { message: c?.message || 'Error', severity: c?.severity || 'error', retryAction: c?.retryAction, retryLabel: c?.retryLabel || 'Retry', details: c?.details, timestamp: c?.timestamp }
|
|
853
|
+
}
|
|
854
|
+
const isWarning = () => data().severity === 'warning'
|
|
855
|
+
|
|
856
|
+
return (
|
|
857
|
+
<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'}`}>
|
|
858
|
+
<div class="flex items-start gap-2">
|
|
859
|
+
<span class="flex-shrink-0">{isWarning() ? '⚠️' : '❌'}</span>
|
|
860
|
+
<div class="flex-1">
|
|
861
|
+
<p>{data().message}</p>
|
|
862
|
+
<div class="flex gap-2 mt-2">
|
|
863
|
+
<Show when={data().retryAction}>
|
|
864
|
+
<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>
|
|
865
|
+
</Show>
|
|
866
|
+
<Show when={data().details}>
|
|
867
|
+
<button type="button" onClick={() => setShowDetails(!showDetails())} class="px-2 py-1 text-xs opacity-70 hover:opacity-100">▶ Details</button>
|
|
868
|
+
</Show>
|
|
869
|
+
</div>
|
|
870
|
+
<Show when={showDetails() && data().details}>
|
|
871
|
+
<pre class="mt-2 text-xs opacity-70 overflow-x-auto">{data().details}</pre>
|
|
872
|
+
</Show>
|
|
873
|
+
</div>
|
|
874
|
+
</div>
|
|
875
|
+
</div>
|
|
876
|
+
)
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// ─── Source Card Section (F9) ────────────────────────────────
|
|
880
|
+
|
|
881
|
+
const SourceCardSection: Component<{ content: unknown }> = (props) => {
|
|
882
|
+
const data = () => {
|
|
883
|
+
const c = props.content as any
|
|
884
|
+
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 }
|
|
885
|
+
}
|
|
886
|
+
const statusIcon = () => ({ queried: '✅', available: '📦', error: '❌' } as Record<string, string>)[data().status] || '📦'
|
|
887
|
+
|
|
888
|
+
return (
|
|
889
|
+
<div class="rounded-lg border border-gray-200 dark:border-gray-700 px-3 py-2">
|
|
890
|
+
<div class="flex items-center justify-between mb-1.5">
|
|
891
|
+
<span class="text-sm font-medium text-gray-900 dark:text-white">{statusIcon()} {data().name}</span>
|
|
892
|
+
<Show when={data().row_count !== undefined}>
|
|
893
|
+
<span class="text-xs font-bold text-blue-600 dark:text-blue-400">{data().row_count?.toLocaleString()} results</span>
|
|
894
|
+
</Show>
|
|
895
|
+
</div>
|
|
896
|
+
<div class="flex flex-wrap gap-1.5 mb-1">
|
|
897
|
+
<For each={data().capabilities}>
|
|
898
|
+
{(cap: any) => (
|
|
899
|
+
<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'}`}>
|
|
900
|
+
{cap.supported ? '✅' : '❌'} {cap.label}
|
|
901
|
+
</span>
|
|
902
|
+
)}
|
|
903
|
+
</For>
|
|
904
|
+
</div>
|
|
905
|
+
<Show when={data().freshness || data().latency_ms}>
|
|
906
|
+
<p class="text-[10px] text-gray-400">{[data().freshness, data().latency_ms ? `${data().latency_ms}ms` : ''].filter(Boolean).join(' · ')}</p>
|
|
907
|
+
</Show>
|
|
908
|
+
</div>
|
|
909
|
+
)
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// ─── Diff Section (F10) ──────────────────────────────────────
|
|
913
|
+
|
|
914
|
+
const DiffSection: Component<{ content: unknown }> = (props) => {
|
|
915
|
+
const data = () => {
|
|
916
|
+
const c = props.content as any
|
|
917
|
+
return { left: c?.left || { label: 'A', rows: [] }, right: c?.right || { label: 'B', rows: [] }, highlight: c?.highlight_columns || [] }
|
|
918
|
+
}
|
|
919
|
+
const allKeys = () => {
|
|
920
|
+
const l = data().left.rows[0] || {}
|
|
921
|
+
const r = data().right.rows[0] || {}
|
|
922
|
+
return [...new Set([...Object.keys(l), ...Object.keys(r)])]
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
return (
|
|
926
|
+
<div class="overflow-x-auto">
|
|
927
|
+
<table class="min-w-full text-xs">
|
|
928
|
+
<thead>
|
|
929
|
+
<tr>
|
|
930
|
+
<th class="px-2 py-1 text-left text-gray-500 dark:text-gray-400"></th>
|
|
931
|
+
<th class="px-2 py-1 text-left font-medium text-blue-600 dark:text-blue-400">{data().left.label}</th>
|
|
932
|
+
<th class="px-2 py-1 text-left font-medium text-purple-600 dark:text-purple-400">{data().right.label}</th>
|
|
933
|
+
</tr>
|
|
934
|
+
</thead>
|
|
935
|
+
<tbody>
|
|
936
|
+
<For each={allKeys()}>
|
|
937
|
+
{(key) => {
|
|
938
|
+
const lVal = () => data().left.rows[0]?.[key]
|
|
939
|
+
const rVal = () => data().right.rows[0]?.[key]
|
|
940
|
+
const isDiff = () => String(lVal()) !== String(rVal()) && data().highlight.includes(key)
|
|
941
|
+
return (
|
|
942
|
+
<tr class={`border-t border-gray-100 dark:border-gray-700 ${isDiff() ? 'bg-yellow-50 dark:bg-yellow-900/10' : ''}`}>
|
|
943
|
+
<td class="px-2 py-1 font-mono text-gray-500 dark:text-gray-400">{key}</td>
|
|
944
|
+
<td class="px-2 py-1 text-gray-900 dark:text-white">{lVal() !== undefined ? String(lVal()) : '—'}</td>
|
|
945
|
+
<td class="px-2 py-1 text-gray-900 dark:text-white">{rVal() !== undefined ? String(rVal()) : '—'}</td>
|
|
946
|
+
</tr>
|
|
947
|
+
)
|
|
948
|
+
}}
|
|
949
|
+
</For>
|
|
950
|
+
</tbody>
|
|
951
|
+
</table>
|
|
952
|
+
</div>
|
|
953
|
+
)
|
|
954
|
+
}
|
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' }>
|