@seed-ship/mcp-ui-solid 4.0.6 → 4.2.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/dist/components/AgentCard.cjs +122 -0
- package/dist/components/AgentCard.cjs.map +1 -0
- package/dist/components/AgentCard.d.ts +20 -0
- package/dist/components/AgentCard.d.ts.map +1 -0
- package/dist/components/AgentCard.js +122 -0
- package/dist/components/AgentCard.js.map +1 -0
- package/dist/components/AgentHandoff.cjs +49 -0
- package/dist/components/AgentHandoff.cjs.map +1 -0
- package/dist/components/AgentHandoff.d.ts +12 -0
- package/dist/components/AgentHandoff.d.ts.map +1 -0
- package/dist/components/AgentHandoff.js +49 -0
- package/dist/components/AgentHandoff.js.map +1 -0
- package/dist/components/BriefingDiff.cjs +165 -0
- package/dist/components/BriefingDiff.cjs.map +1 -0
- package/dist/components/BriefingDiff.d.ts +12 -0
- package/dist/components/BriefingDiff.d.ts.map +1 -0
- package/dist/components/BriefingDiff.js +165 -0
- package/dist/components/BriefingDiff.js.map +1 -0
- package/dist/components/FormFieldRenderer.cjs +314 -264
- package/dist/components/FormFieldRenderer.cjs.map +1 -1
- package/dist/components/FormFieldRenderer.d.ts.map +1 -1
- package/dist/components/FormFieldRenderer.js +315 -265
- package/dist/components/FormFieldRenderer.js.map +1 -1
- package/dist/components/FormRenderer.cjs +74 -17
- package/dist/components/FormRenderer.cjs.map +1 -1
- package/dist/components/FormRenderer.d.ts.map +1 -1
- package/dist/components/FormRenderer.js +76 -19
- package/dist/components/FormRenderer.js.map +1 -1
- package/dist/components/ScratchpadPanel.cjs +618 -411
- package/dist/components/ScratchpadPanel.cjs.map +1 -1
- package/dist/components/ScratchpadPanel.d.ts.map +1 -1
- package/dist/components/ScratchpadPanel.js +619 -412
- package/dist/components/ScratchpadPanel.js.map +1 -1
- package/dist/components/SplitStepper.cjs +121 -0
- package/dist/components/SplitStepper.cjs.map +1 -0
- package/dist/components/SplitStepper.d.ts +12 -0
- package/dist/components/SplitStepper.d.ts.map +1 -0
- package/dist/components/SplitStepper.js +121 -0
- package/dist/components/SplitStepper.js.map +1 -0
- package/dist/components/index.d.ts +8 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components.cjs +9 -0
- package/dist/components.cjs.map +1 -1
- package/dist/components.d.cts +8 -0
- package/dist/components.d.ts +8 -0
- package/dist/components.js +9 -0
- package/dist/components.js.map +1 -1
- package/dist/index.cjs +9 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/types/chat-bus.d.ts +81 -1
- package/dist/types/chat-bus.d.ts.map +1 -1
- package/dist/types/index.d.ts +15 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types.d.cts +15 -0
- package/dist/types.d.ts +15 -0
- package/package.json +1 -1
- package/src/components/AgentCard.tsx +109 -0
- package/src/components/AgentHandoff.tsx +64 -0
- package/src/components/BriefingDiff.tsx +93 -0
- package/src/components/FormFieldRenderer.tsx +35 -4
- package/src/components/FormRenderer.tsx +74 -4
- package/src/components/ScratchpadPanel.tsx +131 -49
- package/src/components/SplitStepper.tsx +111 -0
- package/src/components/index.ts +13 -0
- package/src/index.ts +15 -0
- package/src/types/chat-bus.ts +70 -1
- package/src/types/index.ts +18 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Component, Show, For, Switch, Match, Accessor, createSignal, createEffect, onCleanup } from 'solid-js'
|
|
8
|
-
import type { FormFieldParams } from '../types'
|
|
8
|
+
import type { FormFieldParams, PrefillSource } from '../types'
|
|
9
9
|
import { useConditionalField } from '../hooks/useConditionalField'
|
|
10
10
|
|
|
11
11
|
export interface FormFieldRendererProps {
|
|
@@ -20,6 +20,14 @@ export interface FormFieldRendererProps {
|
|
|
20
20
|
formData?: Accessor<Record<string, any>>
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Badge config by prefill source */
|
|
24
|
+
const SOURCE_BADGES: Record<PrefillSource, { icon: string; title: string } | null> = {
|
|
25
|
+
detected: { icon: '\u2705', title: 'Detected from message' },
|
|
26
|
+
inferred: { icon: '\uD83D\uDD17', title: 'Inferred from context' },
|
|
27
|
+
user: { icon: '\u270F\uFE0F', title: 'Previously provided' },
|
|
28
|
+
default: null,
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
export const FormFieldRenderer: Component<FormFieldRendererProps> = (props) => {
|
|
24
32
|
// Conditional visibility based on showWhen
|
|
25
33
|
const { isVisible } = useConditionalField({
|
|
@@ -27,10 +35,23 @@ export const FormFieldRenderer: Component<FormFieldRendererProps> = (props) => {
|
|
|
27
35
|
formData: props.formData || (() => ({})),
|
|
28
36
|
})
|
|
29
37
|
|
|
38
|
+
// Muted state — starts muted if field says so, clears on focus/click
|
|
39
|
+
const [isMuted, setIsMuted] = createSignal(!!props.field.muted)
|
|
40
|
+
|
|
41
|
+
const handleFieldFocus = () => {
|
|
42
|
+
if (isMuted()) setIsMuted(false)
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
const status = () => props.field.fieldStatus || 'optional'
|
|
31
46
|
const isUnsupported = () => status() === 'unsupported'
|
|
32
47
|
const isFieldDisabled = () => props.disabled || isUnsupported()
|
|
33
48
|
|
|
49
|
+
const sourceBadge = () => {
|
|
50
|
+
const src = props.field.source
|
|
51
|
+
if (!src) return null
|
|
52
|
+
return SOURCE_BADGES[src]
|
|
53
|
+
}
|
|
54
|
+
|
|
34
55
|
const baseInputClass = () => `
|
|
35
56
|
w-full px-3 py-2 border rounded-md
|
|
36
57
|
focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
|
@@ -40,6 +61,7 @@ export const FormFieldRenderer: Component<FormFieldRendererProps> = (props) => {
|
|
|
40
61
|
: 'border-gray-300 dark:border-gray-600'}
|
|
41
62
|
dark:bg-gray-700 dark:text-white
|
|
42
63
|
${isUnsupported() ? 'opacity-50' : ''}
|
|
64
|
+
${isMuted() ? 'bg-gray-50 dark:bg-gray-700/50 text-gray-500 dark:text-gray-400' : ''}
|
|
43
65
|
`
|
|
44
66
|
|
|
45
67
|
const fieldId = () => `field-${props.field.name}`
|
|
@@ -47,12 +69,15 @@ export const FormFieldRenderer: Component<FormFieldRendererProps> = (props) => {
|
|
|
47
69
|
|
|
48
70
|
return (
|
|
49
71
|
<Show when={isVisible()}>
|
|
50
|
-
<div class="space-y-1">
|
|
72
|
+
<div class="space-y-1" onFocusIn={handleFieldFocus} onClick={handleFieldFocus}>
|
|
51
73
|
<Show when={props.field.label && props.field.type !== 'checkbox'}>
|
|
52
74
|
<label
|
|
53
75
|
for={fieldId()}
|
|
54
|
-
class={`block text-sm font-medium ${isUnsupported() ? 'text-gray-400 dark:text-gray-500' : 'text-gray-700 dark:text-gray-300'}`}
|
|
76
|
+
class={`block text-sm font-medium ${isUnsupported() ? 'text-gray-400 dark:text-gray-500' : isMuted() ? 'text-gray-400 dark:text-gray-500' : 'text-gray-700 dark:text-gray-300'}`}
|
|
55
77
|
>
|
|
78
|
+
<Show when={sourceBadge()}>
|
|
79
|
+
<span class="mr-1" title={sourceBadge()!.title}>{sourceBadge()!.icon}</span>
|
|
80
|
+
</Show>
|
|
56
81
|
{props.field.label}
|
|
57
82
|
<Show when={props.field.required || status() === 'required'}>
|
|
58
83
|
<span class="text-red-500 ml-1" aria-hidden="true">*</span>
|
|
@@ -322,7 +347,13 @@ export const FormFieldRenderer: Component<FormFieldRendererProps> = (props) => {
|
|
|
322
347
|
</p>
|
|
323
348
|
</Show>
|
|
324
349
|
|
|
325
|
-
<Show when={props.field.
|
|
350
|
+
<Show when={props.field.displayHint && props.field.prefill != null}>
|
|
351
|
+
<p class={`text-xs italic ${isMuted() ? 'text-gray-400 dark:text-gray-500' : 'text-gray-500 dark:text-gray-400'}`}>
|
|
352
|
+
{props.field.displayHint}
|
|
353
|
+
</p>
|
|
354
|
+
</Show>
|
|
355
|
+
|
|
356
|
+
<Show when={props.field.helpText && !props.error && !props.field.statusReason && !props.field.displayHint}>
|
|
326
357
|
<p class="text-xs text-gray-500 dark:text-gray-400">{props.field.helpText}</p>
|
|
327
358
|
</Show>
|
|
328
359
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Sprint 4: Form state persistence
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { Component, createSignal, For, Show, onMount, createEffect } from 'solid-js'
|
|
8
|
+
import { Component, createSignal, For, Show, onMount, createEffect, onCleanup } from 'solid-js'
|
|
9
9
|
import { FormFieldRenderer } from './FormFieldRenderer'
|
|
10
10
|
import type { UIComponent, FormComponentParams, FormFieldParams } from '../types'
|
|
11
11
|
import { useAction } from '../hooks/useAction'
|
|
@@ -56,11 +56,43 @@ export const FormRenderer: Component<FormRendererProps> = (props) => {
|
|
|
56
56
|
})
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
//
|
|
59
|
+
// Auto-submit countdown state (v4.2.0)
|
|
60
|
+
const [countdown, setCountdown] = createSignal<number | null>(null)
|
|
61
|
+
let countdownTimer: ReturnType<typeof setInterval> | null = null
|
|
62
|
+
const [userInteracted, setUserInteracted] = createSignal(false)
|
|
63
|
+
|
|
64
|
+
const cancelCountdown = () => {
|
|
65
|
+
if (countdownTimer) {
|
|
66
|
+
clearInterval(countdownTimer)
|
|
67
|
+
countdownTimer = null
|
|
68
|
+
}
|
|
69
|
+
setCountdown(null)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const handleUserInteraction = () => {
|
|
73
|
+
if (!userInteracted()) {
|
|
74
|
+
setUserInteracted(true)
|
|
75
|
+
cancelCountdown()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
onCleanup(() => cancelCountdown())
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if all required fields have prefill values
|
|
83
|
+
*/
|
|
84
|
+
const allRequiredPrefilled = (): boolean => {
|
|
85
|
+
return params().fields
|
|
86
|
+
.filter((f) => f.required)
|
|
87
|
+
.every((f) => f.prefill != null)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Initialize form data with default values, applying prefill (v4.2.0)
|
|
60
91
|
const initializeForm = (clearStorage = false) => {
|
|
61
92
|
const initial: Record<string, any> = {}
|
|
62
93
|
for (const field of params().fields) {
|
|
63
|
-
|
|
94
|
+
// prefill takes priority over defaultValue
|
|
95
|
+
initial[field.name] = field.prefill ?? field.defaultValue ?? getFieldDefault(field.type)
|
|
64
96
|
}
|
|
65
97
|
setFormData(initial)
|
|
66
98
|
setErrors({})
|
|
@@ -91,7 +123,29 @@ export const FormRenderer: Component<FormRendererProps> = (props) => {
|
|
|
91
123
|
}
|
|
92
124
|
})
|
|
93
125
|
|
|
126
|
+
// Auto-submit countdown (v4.2.0)
|
|
127
|
+
createEffect(() => {
|
|
128
|
+
const delay = params().autoSubmitDelay
|
|
129
|
+
if (!delay || !allRequiredPrefilled() || userInteracted()) return
|
|
130
|
+
|
|
131
|
+
let remaining = Math.ceil(delay / 1000)
|
|
132
|
+
setCountdown(remaining)
|
|
133
|
+
|
|
134
|
+
countdownTimer = setInterval(() => {
|
|
135
|
+
remaining--
|
|
136
|
+
if (remaining <= 0) {
|
|
137
|
+
cancelCountdown()
|
|
138
|
+
// Trigger submit programmatically
|
|
139
|
+
const form = document.querySelector(`#form-${props.component.id}`) as HTMLFormElement | null
|
|
140
|
+
if (form) form.requestSubmit()
|
|
141
|
+
} else {
|
|
142
|
+
setCountdown(remaining)
|
|
143
|
+
}
|
|
144
|
+
}, 1000)
|
|
145
|
+
})
|
|
146
|
+
|
|
94
147
|
const handleFieldChange = (name: string, value: any) => {
|
|
148
|
+
handleUserInteraction()
|
|
95
149
|
setFormData((prev) => ({ ...prev, [name]: value }))
|
|
96
150
|
// Clear error on change
|
|
97
151
|
if (errors()[name]) {
|
|
@@ -179,7 +233,7 @@ export const FormRenderer: Component<FormRendererProps> = (props) => {
|
|
|
179
233
|
</h3>
|
|
180
234
|
</Show>
|
|
181
235
|
|
|
182
|
-
<form onSubmit={handleSubmit} noValidate>
|
|
236
|
+
<form id={`form-${props.component.id}`} onSubmit={handleSubmit} noValidate>
|
|
183
237
|
<div class={layoutClass()}>
|
|
184
238
|
<For each={params().fields}>
|
|
185
239
|
{(field) => (
|
|
@@ -203,6 +257,22 @@ export const FormRenderer: Component<FormRendererProps> = (props) => {
|
|
|
203
257
|
</div>
|
|
204
258
|
</Show>
|
|
205
259
|
|
|
260
|
+
{/* Auto-submit countdown (v4.2.0) */}
|
|
261
|
+
<Show when={countdown() != null}>
|
|
262
|
+
<div class="mt-4 flex items-center gap-3 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md">
|
|
263
|
+
<span class="text-sm text-blue-700 dark:text-blue-300">
|
|
264
|
+
{params().submitLabel || 'Submit'} in {countdown()}s...
|
|
265
|
+
</span>
|
|
266
|
+
<button
|
|
267
|
+
type="button"
|
|
268
|
+
onClick={() => { cancelCountdown(); setUserInteracted(true) }}
|
|
269
|
+
class="text-sm text-blue-600 dark:text-blue-400 underline hover:text-blue-800 dark:hover:text-blue-200"
|
|
270
|
+
>
|
|
271
|
+
Cancel
|
|
272
|
+
</button>
|
|
273
|
+
</div>
|
|
274
|
+
</Show>
|
|
275
|
+
|
|
206
276
|
<div class="flex gap-2 pt-4 mt-4 border-t border-gray-200 dark:border-gray-700">
|
|
207
277
|
<button
|
|
208
278
|
type="submit"
|
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { Component, Show, For, Switch, Match, createSignal, createEffect, onCleanup } from 'solid-js'
|
|
9
|
-
import type { ScratchpadState, ScratchpadSection, VerifiedTextContent, DataPreviewContent, MapSectionContent } from '../types/chat-bus'
|
|
9
|
+
import type { ScratchpadState, ScratchpadSection, VerifiedTextContent, DataPreviewContent, MapSectionContent, AgentCardContent, SplitStepperContent, AgentHandoffContent, BriefingDiffContent } from '../types/chat-bus'
|
|
10
10
|
import type { FormFieldParams, ChartComponentParams } from '../types'
|
|
11
11
|
import { FormFieldRenderer } from './FormFieldRenderer'
|
|
12
12
|
import { VerifiedText } from './VerifiedText'
|
|
13
13
|
import { DataPreviewSection } from './DataPreviewSection'
|
|
14
14
|
import { MapRenderer } from './MapRenderer'
|
|
15
15
|
import { ChartJSRenderer } from './ChartJSRenderer'
|
|
16
|
+
import { AgentCard, AgentStatusBadge } from './AgentCard'
|
|
17
|
+
import { SplitStepper } from './SplitStepper'
|
|
18
|
+
import { AgentHandoff } from './AgentHandoff'
|
|
19
|
+
import { BriefingDiff } from './BriefingDiff'
|
|
16
20
|
|
|
17
21
|
export interface ScratchpadPanelProps {
|
|
18
22
|
state: ScratchpadState
|
|
@@ -177,6 +181,14 @@ export const ScratchpadPanel: Component<ScratchpadPanelProps> = (props) => {
|
|
|
177
181
|
<div class="flex items-center gap-2">
|
|
178
182
|
<span class="text-base">📝</span>
|
|
179
183
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">{props.state.title}</h3>
|
|
184
|
+
{/* AgentStatusBadge — auto-detected from agent_card sections (v4.1.0) */}
|
|
185
|
+
{(() => {
|
|
186
|
+
const agentSection = props.state.sections.find(s => s.type === 'agent_card')
|
|
187
|
+
if (!agentSection) return null
|
|
188
|
+
const ac = parseContent(agentSection.content) as AgentCardContent | null
|
|
189
|
+
if (!ac?.name) return null
|
|
190
|
+
return <AgentStatusBadge agentName={ac.name} status={ac.status || 'idle'} />
|
|
191
|
+
})()}
|
|
180
192
|
<Show when={isCollapsible()}>
|
|
181
193
|
<svg class={`w-3.5 h-3.5 text-gray-400 transition-transform ${collapsed() ? '-rotate-90' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
182
194
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
@@ -363,7 +375,7 @@ const SectionRenderer: Component<{
|
|
|
363
375
|
onSubmit?: (sectionId: string, values: Record<string, unknown>) => void
|
|
364
376
|
}> = (props) => {
|
|
365
377
|
return (
|
|
366
|
-
<div class="px-4 py-3">
|
|
378
|
+
<div class="px-4 py-3 animate-[slideDown_0.2s_ease-out]">
|
|
367
379
|
<h4 class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">{props.section.title}</h4>
|
|
368
380
|
<Switch>
|
|
369
381
|
<Match when={props.section.type === 'data'}><DataSection content={parseContent(props.section.content)} /></Match>
|
|
@@ -383,6 +395,10 @@ const SectionRenderer: Component<{
|
|
|
383
395
|
<Match when={props.section.type === 'data_preview'}><DataPreviewSection content={parseContent(props.section.content) as DataPreviewContent} /></Match>
|
|
384
396
|
<Match when={props.section.type === 'map'}>{(() => { const c = parseContent(props.section.content) as MapSectionContent; return <MapRenderer params={{ geojson: c.geojson, center: c.center, zoom: c.zoom, geojsonStyle: c.style, popup: c.popup, layers: c.layers, height: c.height || '300px', fitBounds: true }} /> })()}</Match>
|
|
385
397
|
<Match when={props.section.type === 'chart'}>{(() => { const c = parseContent(props.section.content) as ChartComponentParams; return <ChartJSRenderer component={{ id: props.section.id, type: 'chart', position: { colStart: 1, colSpan: 12 }, params: { ...c, renderer: 'native', height: (c as any)?.height || '250px' } }} /> })()}</Match>
|
|
398
|
+
<Match when={props.section.type === 'agent_card'}><AgentCard content={parseContent(props.section.content) as AgentCardContent} /></Match>
|
|
399
|
+
<Match when={props.section.type === 'split_stepper'}><SplitStepper content={parseContent(props.section.content) as SplitStepperContent} /></Match>
|
|
400
|
+
<Match when={props.section.type === 'agent_handoff'}><AgentHandoff content={parseContent(props.section.content) as AgentHandoffContent} /></Match>
|
|
401
|
+
<Match when={props.section.type === 'briefing_diff'}><BriefingDiff content={parseContent(props.section.content) as BriefingDiffContent} /></Match>
|
|
386
402
|
<Match when={true}><pre class="text-xs text-gray-500 overflow-auto">{JSON.stringify(props.section.content, null, 2)}</pre></Match>
|
|
387
403
|
</Switch>
|
|
388
404
|
</div>
|
|
@@ -654,30 +670,63 @@ const ActionSection: Component<{
|
|
|
654
670
|
content: unknown
|
|
655
671
|
onAction?: (action: string, data?: unknown) => void
|
|
656
672
|
}> = (props) => {
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
if (
|
|
661
|
-
|
|
662
|
-
return obj.actions as Array<{ label: string; value?: string; action?: string; variant?: string; icon?: string }>
|
|
663
|
-
}
|
|
664
|
-
return []
|
|
673
|
+
const data = () => {
|
|
674
|
+
const c = props.content as any
|
|
675
|
+
if (Array.isArray(c)) return { actions: c, title: undefined, preview: undefined, validation: undefined }
|
|
676
|
+
if (c && Array.isArray(c.actions)) return { actions: c.actions, title: c.title, preview: c.preview, validation: c.validation }
|
|
677
|
+
return { actions: [], title: undefined, preview: undefined, validation: undefined }
|
|
665
678
|
}
|
|
679
|
+
|
|
666
680
|
return (
|
|
667
|
-
<div
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
681
|
+
<div>
|
|
682
|
+
{/* Confirm checkpoint: title + preview (v4.1.0) */}
|
|
683
|
+
<Show when={data().title}>
|
|
684
|
+
<p class="text-sm font-medium text-gray-800 dark:text-gray-200 mb-2">{data().title}</p>
|
|
685
|
+
</Show>
|
|
686
|
+
<Show when={data().preview}>
|
|
687
|
+
{(preview) => (
|
|
688
|
+
<div class="mb-2 p-2 rounded bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-xs text-gray-600 dark:text-gray-400">
|
|
689
|
+
<Show when={preview().count != null}>
|
|
690
|
+
<span class="font-medium text-gray-800 dark:text-gray-200">{preview().count}</span> items
|
|
691
|
+
</Show>
|
|
692
|
+
<Show when={preview().summary}>
|
|
693
|
+
<span class="ml-1">— {preview().summary}</span>
|
|
694
|
+
</Show>
|
|
695
|
+
</div>
|
|
679
696
|
)}
|
|
680
|
-
</
|
|
697
|
+
</Show>
|
|
698
|
+
<Show when={data().validation && data().validation.confidence != null}>
|
|
699
|
+
<div class="mb-2 flex items-center gap-2 text-xs">
|
|
700
|
+
<span classList={{
|
|
701
|
+
'text-green-600 dark:text-green-400': data().validation.confidence >= 0.8,
|
|
702
|
+
'text-amber-600 dark:text-amber-400': data().validation.confidence >= 0.5 && data().validation.confidence < 0.8,
|
|
703
|
+
'text-red-600 dark:text-red-400': data().validation.confidence < 0.5,
|
|
704
|
+
}}>
|
|
705
|
+
{Math.round(data().validation.confidence * 100)}% verified
|
|
706
|
+
</span>
|
|
707
|
+
<Show when={data().validation.hallucinated?.length > 0}>
|
|
708
|
+
<span class="text-amber-600">({data().validation.hallucinated.length} unverified)</span>
|
|
709
|
+
</Show>
|
|
710
|
+
</div>
|
|
711
|
+
</Show>
|
|
712
|
+
|
|
713
|
+
{/* Action buttons */}
|
|
714
|
+
<div class="flex flex-wrap gap-2">
|
|
715
|
+
<For each={data().actions as Array<{ label: string; value?: string; action?: string; variant?: string; icon?: string }>}>
|
|
716
|
+
{(item) => (
|
|
717
|
+
<button type="button" on:click={() => props.onAction?.(item.value || item.action || item.label, item)}
|
|
718
|
+
class={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
|
|
719
|
+
item.variant === 'primary' ? 'bg-blue-600 text-white hover:bg-blue-700'
|
|
720
|
+
: item.variant === 'danger' ? 'bg-red-600 text-white hover:bg-red-700'
|
|
721
|
+
: item.variant === 'secondary' ? 'border border-blue-300 dark:border-blue-600 text-blue-700 dark:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/20'
|
|
722
|
+
: 'border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
|
723
|
+
}`}>
|
|
724
|
+
<Show when={item.icon}><span class="mr-1">{item.icon}</span></Show>
|
|
725
|
+
{item.label}
|
|
726
|
+
</button>
|
|
727
|
+
)}
|
|
728
|
+
</For>
|
|
729
|
+
</div>
|
|
681
730
|
</div>
|
|
682
731
|
)
|
|
683
732
|
}
|
|
@@ -737,18 +786,21 @@ const FeedbackSection: Component<{
|
|
|
737
786
|
}> = (props) => {
|
|
738
787
|
const [comment, setComment] = createSignal('')
|
|
739
788
|
const [showComment, setShowComment] = createSignal(false)
|
|
789
|
+
const [submitted, setSubmitted] = createSignal<string | null>(null)
|
|
740
790
|
const data = () => {
|
|
741
791
|
const c = props.content as any
|
|
742
|
-
// Support both formats: options array (universal) and approve/reject (simple)
|
|
743
792
|
const options = c?.options || [
|
|
744
|
-
{ value: c?.approve?.value || 'approve', label: c?.approve?.label || 'Yes', icon: '
|
|
745
|
-
{ value: c?.reject?.value || 'reject', label: c?.reject?.label || 'No', icon: '
|
|
793
|
+
{ value: c?.approve?.value || 'approve', label: c?.approve?.label || 'Yes', icon: '\uD83D\uDC4D', variant: 'primary' },
|
|
794
|
+
{ value: c?.reject?.value || 'reject', label: c?.reject?.label || 'No', icon: '\uD83D\uDC4E' },
|
|
746
795
|
]
|
|
747
796
|
return {
|
|
748
797
|
question: c?.question || '',
|
|
749
798
|
options: options as Array<{ value: string; label: string; icon?: string; variant?: string; needsComment?: boolean }>,
|
|
750
799
|
allowFreeText: c?.allowFreeText ?? c?.allowComment ?? false,
|
|
751
800
|
placeholder: c?.placeholder || c?.commentPlaceholder || 'Add a comment...',
|
|
801
|
+
// v4.1.0: per-step feedback
|
|
802
|
+
agentId: c?.agentId as string | undefined,
|
|
803
|
+
stepId: c?.stepId as string | undefined,
|
|
752
804
|
}
|
|
753
805
|
}
|
|
754
806
|
|
|
@@ -757,35 +809,65 @@ const FeedbackSection: Component<{
|
|
|
757
809
|
setShowComment(true)
|
|
758
810
|
return
|
|
759
811
|
}
|
|
760
|
-
|
|
812
|
+
setSubmitted(option.value)
|
|
813
|
+
const payload = {
|
|
814
|
+
option: option.value,
|
|
815
|
+
comment: comment(),
|
|
816
|
+
...(data().agentId ? { agentId: data().agentId } : {}),
|
|
817
|
+
...(data().stepId ? { stepId: data().stepId } : {}),
|
|
818
|
+
}
|
|
819
|
+
console.info('[MCP-UI:HITL] user responded', {
|
|
820
|
+
agentId: data().agentId, stepId: data().stepId, action: option.value,
|
|
821
|
+
})
|
|
822
|
+
props.onAction?.('feedback', payload)
|
|
761
823
|
}
|
|
762
824
|
|
|
763
825
|
return (
|
|
764
826
|
<div class="space-y-3">
|
|
765
827
|
<p class="text-sm text-gray-700 dark:text-gray-300">{data().question}</p>
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
<
|
|
787
|
-
|
|
828
|
+
|
|
829
|
+
{/* Already submitted — show micro-badge */}
|
|
830
|
+
<Show when={submitted()}>
|
|
831
|
+
<div class="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
|
832
|
+
<span classList={{
|
|
833
|
+
'text-green-600': submitted() === 'approve',
|
|
834
|
+
'text-red-600': submitted() === 'reject',
|
|
835
|
+
'text-blue-600': submitted() !== 'approve' && submitted() !== 'reject',
|
|
836
|
+
}}>
|
|
837
|
+
{submitted() === 'approve' ? '\u2705' : submitted() === 'reject' ? '\u274C' : '\uD83D\uDCAC'} {submitted()}
|
|
838
|
+
</span>
|
|
839
|
+
<Show when={comment()}>
|
|
840
|
+
<span class="italic">— {comment()}</span>
|
|
841
|
+
</Show>
|
|
842
|
+
</div>
|
|
843
|
+
</Show>
|
|
844
|
+
|
|
845
|
+
{/* Buttons — hidden after submit */}
|
|
846
|
+
<Show when={!submitted()}>
|
|
847
|
+
<div class="flex flex-wrap gap-2">
|
|
848
|
+
<For each={data().options}>
|
|
849
|
+
{(option) => (
|
|
850
|
+
<button type="button" on:click={() => handleOption(option)}
|
|
851
|
+
class={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors flex items-center gap-1 ${
|
|
852
|
+
option.variant === 'primary' ? 'bg-blue-600 text-white hover:bg-blue-700'
|
|
853
|
+
: option.variant === 'danger' ? 'bg-red-600 text-white hover:bg-red-700'
|
|
854
|
+
: 'border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
|
855
|
+
}`}>
|
|
856
|
+
<Show when={option.icon}><span>{option.icon}</span></Show>
|
|
857
|
+
{option.label}
|
|
858
|
+
</button>
|
|
859
|
+
)}
|
|
860
|
+
</For>
|
|
788
861
|
</div>
|
|
862
|
+
<Show when={data().allowFreeText || showComment()}>
|
|
863
|
+
<div class="flex gap-1">
|
|
864
|
+
<input type="text" value={comment()} onInput={(e) => setComment(e.currentTarget.value)}
|
|
865
|
+
placeholder={data().placeholder} autofocus={showComment()}
|
|
866
|
+
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" />
|
|
867
|
+
<button type="button" on:click={() => { setSubmitted('comment'); props.onAction?.('feedback', { option: 'comment', comment: comment(), agentId: data().agentId, stepId: data().stepId }) }}
|
|
868
|
+
class="px-3 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700">Send</button>
|
|
869
|
+
</div>
|
|
870
|
+
</Show>
|
|
789
871
|
</Show>
|
|
790
872
|
</div>
|
|
791
873
|
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SplitStepper — parallel agent steppers side by side
|
|
3
|
+
* v4.1.0: AITL sprint — 2-3 columns, synthesis row at bottom
|
|
4
|
+
*
|
|
5
|
+
* @experimental
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { For, Show } from 'solid-js'
|
|
9
|
+
import type { SplitStepperContent } from '../types/chat-bus'
|
|
10
|
+
|
|
11
|
+
export interface SplitStepperProps {
|
|
12
|
+
content: SplitStepperContent
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const STEP_ICONS: Record<string, string> = {
|
|
16
|
+
done: '\u2705',
|
|
17
|
+
active: '\uD83D\uDD04',
|
|
18
|
+
pending: '\u23F3',
|
|
19
|
+
skipped: '\u23ED\uFE0F',
|
|
20
|
+
error: '\u274C',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const AGENT_STATUS_COLORS: Record<string, string> = {
|
|
24
|
+
done: 'border-green-400 dark:border-green-600',
|
|
25
|
+
active: 'border-blue-400 dark:border-blue-500',
|
|
26
|
+
pending: 'border-gray-300 dark:border-gray-600',
|
|
27
|
+
error: 'border-red-400 dark:border-red-600',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function SplitStepper(props: SplitStepperProps) {
|
|
31
|
+
const c = () => props.content
|
|
32
|
+
|
|
33
|
+
if (typeof console !== 'undefined') {
|
|
34
|
+
console.info('[MCP-UI:SplitStepper] mounted', {
|
|
35
|
+
agents: c().agents.map(a => `${a.id}:${a.status}`),
|
|
36
|
+
synthesis: c().synthesis?.status,
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div class="split-stepper">
|
|
42
|
+
{/* Agent columns */}
|
|
43
|
+
<div class="grid gap-3" style={{ "grid-template-columns": `repeat(${Math.min(c().agents.length, 3)}, 1fr)` }}>
|
|
44
|
+
<For each={c().agents}>
|
|
45
|
+
{(agent) => (
|
|
46
|
+
<div class={`rounded-lg border-2 ${AGENT_STATUS_COLORS[agent.status] || AGENT_STATUS_COLORS.pending} p-3`}>
|
|
47
|
+
{/* Agent header */}
|
|
48
|
+
<div class="flex items-center gap-2 mb-2">
|
|
49
|
+
<span class="font-medium text-sm text-gray-900 dark:text-white truncate">{agent.name}</span>
|
|
50
|
+
<span class={`w-2 h-2 rounded-full flex-shrink-0 ${
|
|
51
|
+
agent.status === 'done' ? 'bg-green-500' :
|
|
52
|
+
agent.status === 'active' ? 'bg-blue-500 animate-pulse' :
|
|
53
|
+
agent.status === 'error' ? 'bg-red-500' :
|
|
54
|
+
'bg-gray-400'
|
|
55
|
+
}`} />
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{/* Steps */}
|
|
59
|
+
<div class="space-y-1">
|
|
60
|
+
<For each={agent.steps}>
|
|
61
|
+
{(step) => (
|
|
62
|
+
<div class="flex items-center gap-2 text-xs">
|
|
63
|
+
<span class="flex-shrink-0 w-4 text-center">{STEP_ICONS[step.status] || STEP_ICONS.pending}</span>
|
|
64
|
+
<span classList={{
|
|
65
|
+
'text-gray-900 dark:text-white font-medium': step.status === 'active',
|
|
66
|
+
'text-gray-500 dark:text-gray-400': step.status !== 'active',
|
|
67
|
+
'line-through opacity-50': step.status === 'skipped',
|
|
68
|
+
}}>
|
|
69
|
+
{step.label}
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
</For>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
</For>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* Synthesis row */}
|
|
81
|
+
<Show when={c().synthesis}>
|
|
82
|
+
{(syn) => (
|
|
83
|
+
<div class={`mt-3 p-3 rounded-lg border-2 text-center ${
|
|
84
|
+
syn().status === 'done' ? 'border-green-400 dark:border-green-600 bg-green-50 dark:bg-green-900/10' :
|
|
85
|
+
syn().status === 'active' ? 'border-blue-400 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/10' :
|
|
86
|
+
'border-dashed border-gray-300 dark:border-gray-600'
|
|
87
|
+
}`}>
|
|
88
|
+
<div class="flex items-center justify-center gap-2 text-sm">
|
|
89
|
+
<Show when={syn().status === 'active'}>
|
|
90
|
+
<div class="w-3 h-3 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
|
91
|
+
</Show>
|
|
92
|
+
<Show when={syn().status === 'done'}>
|
|
93
|
+
<span>{STEP_ICONS.done}</span>
|
|
94
|
+
</Show>
|
|
95
|
+
<Show when={syn().status === 'pending'}>
|
|
96
|
+
<span class="text-gray-400">{STEP_ICONS.pending}</span>
|
|
97
|
+
</Show>
|
|
98
|
+
<span classList={{
|
|
99
|
+
'font-medium text-blue-700 dark:text-blue-300': syn().status === 'active',
|
|
100
|
+
'font-medium text-green-700 dark:text-green-300': syn().status === 'done',
|
|
101
|
+
'text-gray-500 dark:text-gray-400': syn().status === 'pending',
|
|
102
|
+
}}>
|
|
103
|
+
{syn().label}
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
</Show>
|
|
109
|
+
</div>
|
|
110
|
+
)
|
|
111
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -64,6 +64,19 @@ export type { CodeBlockRendererProps } from './CodeBlockRenderer'
|
|
|
64
64
|
export { MapRenderer } from './MapRenderer'
|
|
65
65
|
export type { MapRendererProps } from './MapRenderer'
|
|
66
66
|
|
|
67
|
+
// Agent AITL (v4.1.0)
|
|
68
|
+
export { AgentCard, AgentStatusBadge } from './AgentCard'
|
|
69
|
+
export type { AgentCardProps, AgentStatusBadgeProps } from './AgentCard'
|
|
70
|
+
|
|
71
|
+
export { SplitStepper } from './SplitStepper'
|
|
72
|
+
export type { SplitStepperProps } from './SplitStepper'
|
|
73
|
+
|
|
74
|
+
export { AgentHandoff } from './AgentHandoff'
|
|
75
|
+
export type { AgentHandoffProps } from './AgentHandoff'
|
|
76
|
+
|
|
77
|
+
export { BriefingDiff } from './BriefingDiff'
|
|
78
|
+
export type { BriefingDiffProps } from './BriefingDiff'
|
|
79
|
+
|
|
67
80
|
// Data Verification (v3.1.0 — anti-hallucination)
|
|
68
81
|
export { VerifiedText } from './VerifiedText'
|
|
69
82
|
export type { VerifiedTextProps } from './VerifiedText'
|
package/src/index.ts
CHANGED
|
@@ -48,6 +48,12 @@ export { dispatchScratchpad, useScratchpadState } from './stores/scratchpad-stor
|
|
|
48
48
|
export { VerifiedText } from './components/VerifiedText'
|
|
49
49
|
export { DataPreviewSection } from './components/DataPreviewSection'
|
|
50
50
|
|
|
51
|
+
// Agent AITL Components (v4.1.0)
|
|
52
|
+
export { AgentCard, AgentStatusBadge } from './components/AgentCard'
|
|
53
|
+
export { SplitStepper } from './components/SplitStepper'
|
|
54
|
+
export { AgentHandoff } from './components/AgentHandoff'
|
|
55
|
+
export { BriefingDiff } from './components/BriefingDiff'
|
|
56
|
+
|
|
51
57
|
// Autocomplete Components
|
|
52
58
|
export { GhostText, GhostTextInput } from './components/GhostText'
|
|
53
59
|
export { AutocompleteDropdown } from './components/AutocompleteDropdown'
|
|
@@ -68,6 +74,10 @@ export type { ChatPromptProps } from './components/ChatPrompt'
|
|
|
68
74
|
export type { ScratchpadPanelProps } from './components/ScratchpadPanel'
|
|
69
75
|
export type { VerifiedTextProps } from './components/VerifiedText'
|
|
70
76
|
export type { DataPreviewSectionProps } from './components/DataPreviewSection'
|
|
77
|
+
export type { AgentCardProps, AgentStatusBadgeProps } from './components/AgentCard'
|
|
78
|
+
export type { SplitStepperProps } from './components/SplitStepper'
|
|
79
|
+
export type { AgentHandoffProps } from './components/AgentHandoff'
|
|
80
|
+
export type { BriefingDiffProps } from './components/BriefingDiff'
|
|
71
81
|
export type { GhostTextProps, GhostTextInputProps } from './components/GhostText'
|
|
72
82
|
export type { AutocompleteDropdownProps } from './components/AutocompleteDropdown'
|
|
73
83
|
export type { AutocompleteFormFieldProps, AutocompleteFormFieldParams } from './components/AutocompleteFormField'
|
|
@@ -271,4 +281,9 @@ export type {
|
|
|
271
281
|
DataPreviewColumn,
|
|
272
282
|
DataPreviewContent,
|
|
273
283
|
MapSectionContent,
|
|
284
|
+
// Agent AITL types (v4.1.0)
|
|
285
|
+
AgentCardContent,
|
|
286
|
+
SplitStepperContent,
|
|
287
|
+
AgentHandoffContent,
|
|
288
|
+
BriefingDiffContent,
|
|
274
289
|
} from './types/chat-bus'
|