@seed-ship/mcp-ui-solid 2.7.0 → 2.8.1
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 +61 -10
- package/dist/components/ScratchpadPanel.cjs +339 -0
- package/dist/components/ScratchpadPanel.cjs.map +1 -0
- package/dist/components/ScratchpadPanel.d.ts +22 -0
- package/dist/components/ScratchpadPanel.d.ts.map +1 -0
- package/dist/components/ScratchpadPanel.js +339 -0
- package/dist/components/ScratchpadPanel.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/types/chat-bus.d.ts +58 -0
- package/dist/types/chat-bus.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/ScratchpadPanel.tsx +298 -0
- package/src/index.ts +5 -0
- package/src/types/chat-bus.ts +49 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScratchpadPanel — HITL shared workspace between agent and human
|
|
3
|
+
* v2.7.0: Renders scratchpad sections, editable filters, preview, agent messages
|
|
4
|
+
*
|
|
5
|
+
* @experimental — This component may change without major bump until v2.5.0 stabilization.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Component, Show, For, Switch, Match, createSignal } from 'solid-js'
|
|
9
|
+
import type { ScratchpadState, ScratchpadSection } from '../types/chat-bus'
|
|
10
|
+
|
|
11
|
+
export interface ScratchpadPanelProps {
|
|
12
|
+
state: ScratchpadState
|
|
13
|
+
/** Called when human modifies filters */
|
|
14
|
+
onFilterChange?: (filters: Record<string, string | string[]>) => void
|
|
15
|
+
/** Called when human clicks an action button */
|
|
16
|
+
onAction?: (action: string, data?: unknown) => void
|
|
17
|
+
/** Called when human edits a section */
|
|
18
|
+
onSectionEdit?: (sectionId: string, content: unknown) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const STATUS_BADGES: Record<ScratchpadState['status'], { label: string; class: string }> = {
|
|
22
|
+
loading: { label: 'Loading...', class: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400' },
|
|
23
|
+
ready: { label: 'Ready', class: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400' },
|
|
24
|
+
waiting_human: { label: 'Your turn', class: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 animate-pulse' },
|
|
25
|
+
processing: { label: 'Processing...', class: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400' },
|
|
26
|
+
complete: { label: 'Complete', class: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400' },
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @experimental
|
|
31
|
+
*/
|
|
32
|
+
export const ScratchpadPanel: Component<ScratchpadPanelProps> = (props) => {
|
|
33
|
+
const badge = () => STATUS_BADGES[props.state.status] || STATUS_BADGES.loading
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div class="w-full bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-lg overflow-visible">
|
|
37
|
+
{/* Header */}
|
|
38
|
+
<div class="flex items-center justify-between px-4 py-3 border-b border-gray-100 dark:border-gray-700">
|
|
39
|
+
<div class="flex items-center gap-2">
|
|
40
|
+
<span class="text-base">📝</span>
|
|
41
|
+
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">{props.state.title}</h3>
|
|
42
|
+
</div>
|
|
43
|
+
<span class={`px-2 py-0.5 text-xs font-medium rounded-full ${badge().class}`}>
|
|
44
|
+
{badge().label}
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
{/* Sections */}
|
|
49
|
+
<div class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
50
|
+
<For each={props.state.sections}>
|
|
51
|
+
{(section) => (
|
|
52
|
+
<SectionRenderer
|
|
53
|
+
section={section}
|
|
54
|
+
filters={props.state.filters}
|
|
55
|
+
onFilterChange={props.onFilterChange}
|
|
56
|
+
onAction={props.onAction}
|
|
57
|
+
onSectionEdit={props.onSectionEdit}
|
|
58
|
+
/>
|
|
59
|
+
)}
|
|
60
|
+
</For>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{/* Agent messages */}
|
|
64
|
+
<Show when={props.state.agentMessages.length > 0}>
|
|
65
|
+
<div class="px-4 py-3 border-t border-gray-100 dark:border-gray-700 space-y-2">
|
|
66
|
+
<For each={props.state.agentMessages}>
|
|
67
|
+
{(msg) => (
|
|
68
|
+
<div class={`flex items-start gap-2 text-sm ${
|
|
69
|
+
msg.type === 'warning' ? 'text-amber-600 dark:text-amber-400'
|
|
70
|
+
: msg.type === 'question' ? 'text-blue-600 dark:text-blue-400'
|
|
71
|
+
: 'text-gray-600 dark:text-gray-400'
|
|
72
|
+
}`}>
|
|
73
|
+
<span class="flex-shrink-0 mt-0.5">
|
|
74
|
+
{msg.type === 'warning' ? '⚠️' : msg.type === 'question' ? '💬' : 'ℹ️'}
|
|
75
|
+
</span>
|
|
76
|
+
<p>{msg.text}</p>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
</For>
|
|
80
|
+
</div>
|
|
81
|
+
</Show>
|
|
82
|
+
|
|
83
|
+
{/* Preview */}
|
|
84
|
+
<Show when={props.state.preview}>
|
|
85
|
+
<div class="px-4 py-3 border-t border-gray-100 dark:border-gray-700">
|
|
86
|
+
<div class="flex items-center gap-2 mb-2">
|
|
87
|
+
<span class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Preview</span>
|
|
88
|
+
<span class="px-1.5 py-0.5 text-xs font-bold bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded">
|
|
89
|
+
{props.state.preview!.count.toLocaleString()}
|
|
90
|
+
</span>
|
|
91
|
+
</div>
|
|
92
|
+
<p class="text-sm text-gray-700 dark:text-gray-300">{props.state.preview!.summary}</p>
|
|
93
|
+
<Show when={props.state.preview!.rows && props.state.preview!.rows!.length > 0}>
|
|
94
|
+
<div class="mt-2 overflow-x-auto">
|
|
95
|
+
<table class="min-w-full text-xs">
|
|
96
|
+
<thead>
|
|
97
|
+
<tr>
|
|
98
|
+
<For each={Object.keys(props.state.preview!.rows![0])}>
|
|
99
|
+
{(key) => <th class="px-2 py-1 text-left font-medium text-gray-500 dark:text-gray-400">{key}</th>}
|
|
100
|
+
</For>
|
|
101
|
+
</tr>
|
|
102
|
+
</thead>
|
|
103
|
+
<tbody>
|
|
104
|
+
<For each={props.state.preview!.rows!.slice(0, 5)}>
|
|
105
|
+
{(row) => (
|
|
106
|
+
<tr class="border-t border-gray-100 dark:border-gray-700">
|
|
107
|
+
<For each={Object.values(row)}>
|
|
108
|
+
{(val) => <td class="px-2 py-1 text-gray-700 dark:text-gray-300">{String(val)}</td>}
|
|
109
|
+
</For>
|
|
110
|
+
</tr>
|
|
111
|
+
)}
|
|
112
|
+
</For>
|
|
113
|
+
</tbody>
|
|
114
|
+
</table>
|
|
115
|
+
</div>
|
|
116
|
+
</Show>
|
|
117
|
+
</div>
|
|
118
|
+
</Show>
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── Section Renderer ────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
const SectionRenderer: Component<{
|
|
126
|
+
section: ScratchpadSection
|
|
127
|
+
filters: Record<string, string | string[]>
|
|
128
|
+
onFilterChange?: (filters: Record<string, string | string[]>) => void
|
|
129
|
+
onAction?: (action: string, data?: unknown) => void
|
|
130
|
+
onSectionEdit?: (sectionId: string, content: unknown) => void
|
|
131
|
+
}> = (props) => {
|
|
132
|
+
return (
|
|
133
|
+
<div class="px-4 py-3">
|
|
134
|
+
<div class="flex items-center gap-2 mb-2">
|
|
135
|
+
<h4 class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">{props.section.title}</h4>
|
|
136
|
+
<Show when={props.section.editable}>
|
|
137
|
+
<span class="text-[10px] text-blue-500 dark:text-blue-400">(editable)</span>
|
|
138
|
+
</Show>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<Switch>
|
|
142
|
+
{/* Data section — key-value or compact table */}
|
|
143
|
+
<Match when={props.section.type === 'data'}>
|
|
144
|
+
<DataSection content={props.section.content} />
|
|
145
|
+
</Match>
|
|
146
|
+
|
|
147
|
+
{/* Filter section — editable chips */}
|
|
148
|
+
<Match when={props.section.type === 'filter'}>
|
|
149
|
+
<FilterSection
|
|
150
|
+
filters={props.filters}
|
|
151
|
+
onFilterChange={props.onFilterChange}
|
|
152
|
+
/>
|
|
153
|
+
</Match>
|
|
154
|
+
|
|
155
|
+
{/* Message section */}
|
|
156
|
+
<Match when={props.section.type === 'message'}>
|
|
157
|
+
<p class="text-sm text-gray-700 dark:text-gray-300">{String(props.section.content)}</p>
|
|
158
|
+
</Match>
|
|
159
|
+
|
|
160
|
+
{/* Action section — buttons */}
|
|
161
|
+
<Match when={props.section.type === 'action'}>
|
|
162
|
+
<ActionSection content={props.section.content} onAction={props.onAction} />
|
|
163
|
+
</Match>
|
|
164
|
+
|
|
165
|
+
{/* Steps section */}
|
|
166
|
+
<Match when={props.section.type === 'steps'}>
|
|
167
|
+
<StepsSection content={props.section.content} />
|
|
168
|
+
</Match>
|
|
169
|
+
|
|
170
|
+
{/* Fallback */}
|
|
171
|
+
<Match when={true}>
|
|
172
|
+
<pre class="text-xs text-gray-500 dark:text-gray-400 overflow-auto">
|
|
173
|
+
{JSON.stringify(props.section.content, null, 2)}
|
|
174
|
+
</pre>
|
|
175
|
+
</Match>
|
|
176
|
+
</Switch>
|
|
177
|
+
</div>
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ─── Sub-components ──────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
const DataSection: Component<{ content: unknown }> = (props) => {
|
|
184
|
+
const entries = () => {
|
|
185
|
+
if (typeof props.content !== 'object' || !props.content) return []
|
|
186
|
+
return Object.entries(props.content as Record<string, unknown>)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<div class="space-y-1">
|
|
191
|
+
<For each={entries()}>
|
|
192
|
+
{([key, value]) => (
|
|
193
|
+
<div class="flex gap-2 text-sm">
|
|
194
|
+
<span class="text-gray-500 dark:text-gray-400 font-mono text-xs min-w-[120px]">{key}:</span>
|
|
195
|
+
<span class="text-gray-900 dark:text-white text-xs">
|
|
196
|
+
{Array.isArray(value) ? value.join(', ') : String(value)}
|
|
197
|
+
</span>
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
</For>
|
|
201
|
+
</div>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const FilterSection: Component<{
|
|
206
|
+
filters: Record<string, string | string[]>
|
|
207
|
+
onFilterChange?: (filters: Record<string, string | string[]>) => void
|
|
208
|
+
}> = (props) => {
|
|
209
|
+
const removeFilter = (key: string) => {
|
|
210
|
+
const next = { ...props.filters }
|
|
211
|
+
delete next[key]
|
|
212
|
+
props.onFilterChange?.(next)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const entries = () => Object.entries(props.filters)
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<div class="flex flex-wrap gap-1.5">
|
|
219
|
+
<For each={entries()}>
|
|
220
|
+
{([key, value]) => (
|
|
221
|
+
<span class="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded-full">
|
|
222
|
+
<span class="text-blue-500 dark:text-blue-400">{key}:</span>
|
|
223
|
+
{Array.isArray(value) ? value.join(', ') : String(value)}
|
|
224
|
+
<button
|
|
225
|
+
type="button"
|
|
226
|
+
onClick={() => removeFilter(key)}
|
|
227
|
+
class="ml-0.5 hover:text-blue-900 dark:hover:text-blue-100"
|
|
228
|
+
aria-label={`Remove filter ${key}`}
|
|
229
|
+
>
|
|
230
|
+
×
|
|
231
|
+
</button>
|
|
232
|
+
</span>
|
|
233
|
+
)}
|
|
234
|
+
</For>
|
|
235
|
+
</div>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const ActionSection: Component<{
|
|
240
|
+
content: unknown
|
|
241
|
+
onAction?: (action: string, data?: unknown) => void
|
|
242
|
+
}> = (props) => {
|
|
243
|
+
const actions = () => {
|
|
244
|
+
if (Array.isArray(props.content)) return props.content as Array<{ label: string; action: string; variant?: string }>
|
|
245
|
+
return []
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<div class="flex flex-wrap gap-2">
|
|
250
|
+
<For each={actions()}>
|
|
251
|
+
{(item) => (
|
|
252
|
+
<button
|
|
253
|
+
type="button"
|
|
254
|
+
onClick={() => props.onAction?.(item.action)}
|
|
255
|
+
class={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
|
|
256
|
+
item.variant === 'primary'
|
|
257
|
+
? 'bg-blue-600 text-white hover:bg-blue-700'
|
|
258
|
+
: item.variant === 'danger'
|
|
259
|
+
? 'bg-red-600 text-white hover:bg-red-700'
|
|
260
|
+
: 'border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
|
261
|
+
}`}
|
|
262
|
+
>
|
|
263
|
+
{item.label}
|
|
264
|
+
</button>
|
|
265
|
+
)}
|
|
266
|
+
</For>
|
|
267
|
+
</div>
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const StepsSection: Component<{ content: unknown }> = (props) => {
|
|
272
|
+
const steps = () => {
|
|
273
|
+
if (Array.isArray(props.content)) return props.content as Array<{ label: string; status: 'done' | 'active' | 'pending' }>
|
|
274
|
+
return []
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<div class="flex items-center gap-1">
|
|
279
|
+
<For each={steps()}>
|
|
280
|
+
{(step, i) => (
|
|
281
|
+
<>
|
|
282
|
+
<Show when={i() > 0}>
|
|
283
|
+
<div class={`w-6 h-px ${step.status === 'pending' ? 'bg-gray-300 dark:bg-gray-600' : 'bg-blue-400'}`} />
|
|
284
|
+
</Show>
|
|
285
|
+
<div class={`flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium ${
|
|
286
|
+
step.status === 'done' ? 'text-green-600 dark:text-green-400'
|
|
287
|
+
: step.status === 'active' ? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20'
|
|
288
|
+
: 'text-gray-400'
|
|
289
|
+
}`}>
|
|
290
|
+
{step.status === 'done' ? '✓' : step.status === 'active' ? '●' : '○'}
|
|
291
|
+
{step.label}
|
|
292
|
+
</div>
|
|
293
|
+
</>
|
|
294
|
+
)}
|
|
295
|
+
</For>
|
|
296
|
+
</div>
|
|
297
|
+
)
|
|
298
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ export { ComponentToolbar } from './components/ComponentToolbar'
|
|
|
41
41
|
// Chat Bus (v2.4.0 — @experimental)
|
|
42
42
|
export { ChatBusProvider, useChatBus } from './hooks/useChatBus'
|
|
43
43
|
export { ChatPrompt } from './components/ChatPrompt'
|
|
44
|
+
export { ScratchpadPanel } from './components/ScratchpadPanel'
|
|
44
45
|
|
|
45
46
|
// Autocomplete Components
|
|
46
47
|
export { GhostText, GhostTextInput } from './components/GhostText'
|
|
@@ -59,6 +60,7 @@ export type { EditableUIResourceRendererProps } from './components/EditableUIRes
|
|
|
59
60
|
export type { ExpandableWrapperProps } from './components/ExpandableWrapper'
|
|
60
61
|
export type { ComponentToolbarProps, ToolbarAction, ToolbarIcon } from './components/ComponentToolbar'
|
|
61
62
|
export type { ChatPromptProps } from './components/ChatPrompt'
|
|
63
|
+
export type { ScratchpadPanelProps } from './components/ScratchpadPanel'
|
|
62
64
|
export type { GhostTextProps, GhostTextInputProps } from './components/GhostText'
|
|
63
65
|
export type { AutocompleteDropdownProps } from './components/AutocompleteDropdown'
|
|
64
66
|
export type { AutocompleteFormFieldProps, AutocompleteFormFieldParams } from './components/AutocompleteFormField'
|
|
@@ -234,6 +236,9 @@ export type {
|
|
|
234
236
|
AgentContext,
|
|
235
237
|
BriefingEvent,
|
|
236
238
|
BriefingSection,
|
|
239
|
+
ScratchpadState,
|
|
240
|
+
ScratchpadSection,
|
|
241
|
+
ScratchpadEvent,
|
|
237
242
|
StreamDoneMetadata,
|
|
238
243
|
ChatError,
|
|
239
244
|
Citation,
|
package/src/types/chat-bus.ts
CHANGED
|
@@ -58,6 +58,9 @@ export interface ChatEvents {
|
|
|
58
58
|
onBriefing: (event: ChatEventBase & { briefing: BriefingEvent }) => void
|
|
59
59
|
onCapabilityChange: (event: ChatEventBase & { capabilities: string[] }) => void
|
|
60
60
|
|
|
61
|
+
// --- Scratchpad (HITL shared workspace) ---
|
|
62
|
+
onScratchpad: (event: ChatEventBase & { scratchpad: ScratchpadEvent }) => void
|
|
63
|
+
|
|
61
64
|
// --- Fallback ---
|
|
62
65
|
onCustomEvent: (type: string, event: ChatEventBase & { data: unknown }) => void
|
|
63
66
|
}
|
|
@@ -314,6 +317,52 @@ export interface BriefingSection {
|
|
|
314
317
|
components?: UIComponent[]
|
|
315
318
|
}
|
|
316
319
|
|
|
320
|
+
// ─── Scratchpad types ────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* @experimental
|
|
324
|
+
* Scratchpad state — shared workspace between agent and human.
|
|
325
|
+
* The agent fills sections, the human can edit filters and validate.
|
|
326
|
+
*/
|
|
327
|
+
export interface ScratchpadState {
|
|
328
|
+
id: string
|
|
329
|
+
title: string
|
|
330
|
+
sections: ScratchpadSection[]
|
|
331
|
+
/** Active filters — human can add/remove */
|
|
332
|
+
filters: Record<string, string | string[]>
|
|
333
|
+
/** Live preview (auto-updated when filters change) */
|
|
334
|
+
preview?: { count: number; rows?: Record<string, unknown>[]; summary: string }
|
|
335
|
+
/** Agent messages (explanations, questions) */
|
|
336
|
+
agentMessages: Array<{ text: string; type: 'info' | 'question' | 'warning' }>
|
|
337
|
+
status: 'loading' | 'ready' | 'waiting_human' | 'processing' | 'complete'
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export interface ScratchpadSection {
|
|
341
|
+
id: string
|
|
342
|
+
title: string
|
|
343
|
+
type: 'data' | 'filter' | 'preview' | 'message' | 'action' | 'steps' | 'form'
|
|
344
|
+
content: unknown
|
|
345
|
+
/** Can the human edit this section? */
|
|
346
|
+
editable: boolean
|
|
347
|
+
/** Who filled this section */
|
|
348
|
+
source: 'agent' | 'human' | 'api'
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* @experimental
|
|
353
|
+
* SSE event for scratchpad create/update/close.
|
|
354
|
+
*/
|
|
355
|
+
export interface ScratchpadEvent {
|
|
356
|
+
id: string
|
|
357
|
+
action: 'create' | 'update' | 'close'
|
|
358
|
+
title?: string
|
|
359
|
+
sections?: ScratchpadSection[]
|
|
360
|
+
filters?: Record<string, string | string[]>
|
|
361
|
+
preview?: { count: number; rows?: Record<string, unknown>[]; summary: string }
|
|
362
|
+
agentMessages?: Array<{ text: string; type: 'info' | 'question' | 'warning' }>
|
|
363
|
+
status?: ScratchpadState['status']
|
|
364
|
+
}
|
|
365
|
+
|
|
317
366
|
// ─── SSE / Stream types ──────────────────────────────────────
|
|
318
367
|
|
|
319
368
|
export interface StreamDoneMetadata {
|