@seed-ship/mcp-ui-solid 6.17.0 → 6.18.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/CHANGELOG.md +28 -0
- package/README.md +50 -3
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/presentation.cjs +141 -0
- package/dist/adapters/presentation.cjs.map +1 -0
- package/dist/adapters/presentation.d.ts +68 -0
- package/dist/adapters/presentation.d.ts.map +1 -0
- package/dist/adapters/presentation.js +141 -0
- package/dist/adapters/presentation.js.map +1 -0
- package/dist/adapters.cjs +4 -0
- package/dist/adapters.cjs.map +1 -1
- package/dist/adapters.d.cts +2 -0
- package/dist/adapters.d.ts +2 -0
- package/dist/adapters.js +4 -0
- package/dist/adapters.js.map +1 -1
- package/dist/components/ChartJSRenderer.cjs +165 -65
- package/dist/components/ChartJSRenderer.cjs.map +1 -1
- package/dist/components/ChartJSRenderer.d.ts.map +1 -1
- package/dist/components/ChartJSRenderer.js +168 -68
- package/dist/components/ChartJSRenderer.js.map +1 -1
- package/dist/components/ComponentToolbar.cjs.map +1 -1
- package/dist/components/ComponentToolbar.d.ts.map +1 -1
- package/dist/components/ComponentToolbar.js.map +1 -1
- package/dist/components/EditableLayoutContext.cjs +17 -0
- package/dist/components/EditableLayoutContext.cjs.map +1 -0
- package/dist/components/EditableLayoutContext.d.ts +5 -0
- package/dist/components/EditableLayoutContext.d.ts.map +1 -0
- package/dist/components/EditableLayoutContext.js +17 -0
- package/dist/components/EditableLayoutContext.js.map +1 -0
- package/dist/components/EditableUIResourceRenderer.cjs +87 -84
- package/dist/components/EditableUIResourceRenderer.cjs.map +1 -1
- package/dist/components/EditableUIResourceRenderer.d.ts.map +1 -1
- package/dist/components/EditableUIResourceRenderer.js +87 -84
- package/dist/components/EditableUIResourceRenderer.js.map +1 -1
- package/dist/components/ElicitationForm.cjs.map +1 -1
- package/dist/components/ElicitationForm.js.map +1 -1
- package/dist/components/FeedbackInline.cjs +0 -1
- package/dist/components/FeedbackInline.cjs.map +1 -1
- package/dist/components/FeedbackInline.js +0 -1
- package/dist/components/FeedbackInline.js.map +1 -1
- package/dist/components/GraphRenderer.cjs.map +1 -1
- package/dist/components/GraphRenderer.d.ts.map +1 -1
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/components/GridRenderer.cjs +12 -6
- package/dist/components/GridRenderer.cjs.map +1 -1
- package/dist/components/GridRenderer.d.ts.map +1 -1
- package/dist/components/GridRenderer.js +13 -7
- package/dist/components/GridRenderer.js.map +1 -1
- package/dist/components/MapRenderer.cjs.map +1 -1
- package/dist/components/MapRenderer.d.ts.map +1 -1
- package/dist/components/MapRenderer.js.map +1 -1
- package/dist/components/PresentationFeedback.cjs +0 -1
- package/dist/components/PresentationFeedback.cjs.map +1 -1
- package/dist/components/PresentationFeedback.js +0 -1
- package/dist/components/PresentationFeedback.js.map +1 -1
- package/dist/components/UIResourceRenderer.cjs +37 -22
- package/dist/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/components/UIResourceRenderer.d.ts.map +1 -1
- package/dist/components/UIResourceRenderer.js +38 -23
- package/dist/components/UIResourceRenderer.js.map +1 -1
- package/dist/components/VerifiedText.cjs +0 -4
- package/dist/components/VerifiedText.cjs.map +1 -1
- package/dist/components/VerifiedText.d.ts.map +1 -1
- package/dist/components/VerifiedText.js +0 -4
- package/dist/components/VerifiedText.js.map +1 -1
- package/dist/components/chart-data-table.cjs +73 -0
- package/dist/components/chart-data-table.cjs.map +1 -0
- package/dist/components/chart-data-table.d.ts +23 -0
- package/dist/components/chart-data-table.d.ts.map +1 -0
- package/dist/components/chart-data-table.js +73 -0
- package/dist/components/chart-data-table.js.map +1 -0
- package/dist/context/MCPUIStringsContext.cjs +10 -9
- package/dist/context/MCPUIStringsContext.cjs.map +1 -1
- package/dist/context/MCPUIStringsContext.d.ts +17 -5
- package/dist/context/MCPUIStringsContext.d.ts.map +1 -1
- package/dist/context/MCPUIStringsContext.js +11 -10
- package/dist/context/MCPUIStringsContext.js.map +1 -1
- package/dist/hooks/createResponsiveGrid.cjs +37 -0
- package/dist/hooks/createResponsiveGrid.cjs.map +1 -0
- package/dist/hooks/createResponsiveGrid.d.ts +6 -0
- package/dist/hooks/createResponsiveGrid.d.ts.map +1 -0
- package/dist/hooks/createResponsiveGrid.js +37 -0
- package/dist/hooks/createResponsiveGrid.js.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-ui-spec/dist/schemas.cjs +13 -1
- package/dist/mcp-ui-spec/dist/schemas.cjs.map +1 -1
- package/dist/mcp-ui-spec/dist/schemas.js +13 -1
- package/dist/mcp-ui-spec/dist/schemas.js.map +1 -1
- package/dist/services/chat-bus.cjs.map +1 -1
- package/dist/services/chat-bus.d.ts.map +1 -1
- package/dist/services/chat-bus.js.map +1 -1
- package/dist/services/chat-prompt-controller.cjs.map +1 -1
- package/dist/services/chat-prompt-controller.d.ts.map +1 -1
- package/dist/services/chat-prompt-controller.js.map +1 -1
- package/dist/services/component-registry.cjs +162 -22
- package/dist/services/component-registry.cjs.map +1 -1
- package/dist/services/component-registry.d.ts +6 -3
- package/dist/services/component-registry.d.ts.map +1 -1
- package/dist/services/component-registry.js +162 -22
- package/dist/services/component-registry.js.map +1 -1
- package/dist/services/telemetry.cjs +0 -1
- package/dist/services/telemetry.cjs.map +1 -1
- package/dist/services/telemetry.js +0 -1
- package/dist/services/telemetry.js.map +1 -1
- package/dist/services/validation.cjs +21 -7
- package/dist/services/validation.cjs.map +1 -1
- package/dist/services/validation.d.ts.map +1 -1
- package/dist/services/validation.js +21 -7
- package/dist/services/validation.js.map +1 -1
- package/dist/types/index.d.ts +43 -8
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types.d.cts +43 -8
- package/dist/types.d.ts +43 -8
- package/docs/briefs/VISUALIZATION-FOUNDATION-2026-09-14.md +105 -0
- package/package.json +4 -4
- package/src/adapters/index.ts +18 -0
- package/src/adapters/presentation.test.ts +114 -0
- package/src/adapters/presentation.ts +229 -0
- package/src/components/ChartJSRenderer.test.tsx +325 -0
- package/src/components/ChartJSRenderer.tsx +162 -27
- package/src/components/ChatPrompt.test.tsx +1 -1
- package/src/components/ComponentToolbar.tsx +1 -1
- package/src/components/EditableLayoutContext.tsx +12 -0
- package/src/components/EditableUIResourceRenderer.tsx +11 -8
- package/src/components/ElicitationForm.tsx +1 -1
- package/src/components/FeedbackInline.tsx +1 -1
- package/src/components/GraphRenderer.test.tsx +1 -2
- package/src/components/GraphRenderer.tsx +0 -2
- package/src/components/GridRenderer.tsx +6 -4
- package/src/components/MapRenderer.tsx +0 -3
- package/src/components/PresentationFeedback.tsx +1 -1
- package/src/components/ResponsiveGrid.test.tsx +166 -0
- package/src/components/TableRenderer.pagination.test.tsx +31 -0
- package/src/components/UIResourceRenderer.tsx +14 -9
- package/src/components/VerifiedText.tsx +0 -5
- package/src/components/chart-data-table.ts +97 -0
- package/src/context/MCPUIStringsContext.test.tsx +60 -0
- package/src/context/MCPUIStringsContext.tsx +31 -12
- package/src/hooks/createResponsiveGrid.ts +41 -0
- package/src/services/chat-bus.test.ts +1 -1
- package/src/services/chat-bus.ts +0 -2
- package/src/services/chat-prompt-controller.ts +0 -8
- package/src/services/component-registry.test.ts +81 -2
- package/src/services/component-registry.ts +167 -23
- package/src/services/telemetry.ts +1 -1
- package/src/services/validation.test.ts +1 -14
- package/src/services/validation.ts +23 -6
- package/src/services/visualization-validation.test.ts +42 -0
- package/src/types/index.ts +50 -9
- package/src/types/visualization-contracts.test.ts +66 -0
- package/src/utils/perf.test.ts +3 -3
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { cleanup, fireEvent, render } from '@solidjs/testing-library'
|
|
3
|
+
import { createSignal } from 'solid-js'
|
|
4
|
+
import { UIResourceRenderer } from './UIResourceRenderer'
|
|
5
|
+
import type { UIComponent } from '../types'
|
|
6
|
+
|
|
7
|
+
afterEach(cleanup)
|
|
8
|
+
|
|
9
|
+
describe('table page bounds', () => {
|
|
10
|
+
it('clamps an oversized initial page and remains navigable after data shrinks', () => {
|
|
11
|
+
const [count, setCount] = createSignal(30)
|
|
12
|
+
const component: UIComponent = {
|
|
13
|
+
id: 'paged-table', type: 'table', position: { colStart: 1, colSpan: 12 },
|
|
14
|
+
params: {
|
|
15
|
+
columns: [{ key: 'name', label: 'Name' }],
|
|
16
|
+
get rows() { return Array.from({ length: count() }, (_, i) => ({ name: `row-${i}` })) },
|
|
17
|
+
initialPage: 99, chatPageSize: 10, className: 'custom-table',
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
const { container, getByText } = render(() => <UIResourceRenderer content={component} />)
|
|
21
|
+
expect(container.querySelector('.custom-table')).not.toBeNull()
|
|
22
|
+
expect(container.querySelector('tbody')?.textContent).toContain('row-29')
|
|
23
|
+
expect(getByText('3 / 3')).toBeTruthy()
|
|
24
|
+
setCount(15)
|
|
25
|
+
expect(container.querySelector('tbody')?.textContent).toContain('row-14')
|
|
26
|
+
expect(getByText('2 / 2')).toBeTruthy()
|
|
27
|
+
fireEvent.click(getByText('◀'))
|
|
28
|
+
expect(getByText('1 / 2')).toBeTruthy()
|
|
29
|
+
expect(container.querySelector('tbody')?.textContent).toContain('row-0')
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -54,6 +54,7 @@ import { ExpandableWrapper, useExpanded } from './ExpandableWrapper'
|
|
|
54
54
|
import { PortalDropdownMenu } from './PortalDropdownMenu'
|
|
55
55
|
import { RenderProvider } from './RenderContext'
|
|
56
56
|
import { useAction } from '../hooks/useAction'
|
|
57
|
+
import { createResponsiveGrid } from '../hooks/createResponsiveGrid'
|
|
57
58
|
import { marked } from 'marked'
|
|
58
59
|
|
|
59
60
|
/**
|
|
@@ -711,8 +712,12 @@ function TableRenderer(props: {
|
|
|
711
712
|
const hasServerPagination = () => !!tableParams.pagination
|
|
712
713
|
const needsClientPagination = () =>
|
|
713
714
|
!hasServerPagination() && !showAll() && clientPageSize() > 0 && filteredRows().length > clientPageSize()
|
|
714
|
-
const [
|
|
715
|
+
const [requestedClientPage, setClientPage] = createSignal(tableParams.initialPage ?? 0)
|
|
715
716
|
const clientTotalPages = () => needsClientPagination() ? Math.ceil(filteredRows().length / clientPageSize()) : 1
|
|
717
|
+
const clientPage = createMemo(() => Math.max(0, Math.min(requestedClientPage(), clientTotalPages() - 1)))
|
|
718
|
+
createEffect(() => {
|
|
719
|
+
if (requestedClientPage() !== clientPage()) setClientPage(clientPage())
|
|
720
|
+
})
|
|
716
721
|
const clientVisibleRows = createMemo(() => {
|
|
717
722
|
if (showAll() || !needsClientPagination()) return filteredRows()
|
|
718
723
|
const start = clientPage() * clientPageSize()
|
|
@@ -939,7 +944,7 @@ function TableRenderer(props: {
|
|
|
939
944
|
<ExpandableWrapper title={tableParams.title || 'Table'} copyData={getTableCSV()} copyLabel="Copy table (CSV)" toolbarVariant={props.toolbarVariant}>
|
|
940
945
|
<div class={`relative w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden group ${
|
|
941
946
|
isExpanded() ? 'flex-1 min-h-0 flex flex-col' : 'h-full'
|
|
942
|
-
}`}>
|
|
947
|
+
} ${tableParams.className || ''}`}>
|
|
943
948
|
<Show when={exportable} fallback={<CopyButton getText={getTableCSV} title="Copy table (CSV)" position="top-right" />}>
|
|
944
949
|
<div class="absolute right-10 top-2 z-10">
|
|
945
950
|
<button
|
|
@@ -1866,16 +1871,17 @@ export const UIResourceRenderer: Component<UIResourceRendererProps> = (props) =>
|
|
|
1866
1871
|
}
|
|
1867
1872
|
|
|
1868
1873
|
// Convert grid styles to CSS string
|
|
1874
|
+
const responsiveGrid = createResponsiveGrid()
|
|
1869
1875
|
const gridContainerStyle = () => {
|
|
1870
1876
|
const layoutData = layout()
|
|
1871
|
-
return `grid-template-columns: repeat(${layoutData.grid.columns}, 1fr); gap: ${layoutData.grid.gap}`
|
|
1877
|
+
return `display: grid; grid-template-columns: repeat(${responsiveGrid.stacked() ? 1 : layoutData.grid.columns}, minmax(0, 1fr)); gap: ${layoutData.grid.gap}`
|
|
1872
1878
|
}
|
|
1873
1879
|
|
|
1874
1880
|
// Convert component grid styles to CSS string
|
|
1875
1881
|
const getGridStyleString = (component: UIComponent) => {
|
|
1876
1882
|
// Defensive check for position field - default to full width
|
|
1877
|
-
if (!component.position) {
|
|
1878
|
-
return 'grid-column: 1 /
|
|
1883
|
+
if (responsiveGrid.stacked() || !component.position) {
|
|
1884
|
+
return 'grid-column: 1 / -1; grid-row: auto'
|
|
1879
1885
|
}
|
|
1880
1886
|
const { colStart, colSpan, rowStart, rowSpan = 1 } = component.position
|
|
1881
1887
|
return `grid-column: ${colStart} / span ${colSpan}; grid-row: ${rowStart ? `${rowStart} / span ${rowSpan}` : 'auto'}`
|
|
@@ -1921,8 +1927,6 @@ export const UIResourceRenderer: Component<UIResourceRendererProps> = (props) =>
|
|
|
1921
1927
|
}
|
|
1922
1928
|
})
|
|
1923
1929
|
|
|
1924
|
-
const layoutData = layout()
|
|
1925
|
-
|
|
1926
1930
|
// ── Identity + duplicate-mount detection (v6.5.0) ─────────────
|
|
1927
1931
|
// `isLayoutContent` distinguishes a real composite/layout payload from
|
|
1928
1932
|
// the synthetic single-component wrapping above. Drives whether the
|
|
@@ -1960,10 +1964,11 @@ export const UIResourceRenderer: Component<UIResourceRendererProps> = (props) =>
|
|
|
1960
1964
|
? { 'data-mcp-ui-layout-id': outerKey() }
|
|
1961
1965
|
: { 'data-mcp-ui-component-id': outerKey() })}
|
|
1962
1966
|
>
|
|
1963
|
-
<div class="grid gap-4" style={gridContainerStyle()}>
|
|
1964
|
-
<For each={
|
|
1967
|
+
<div ref={responsiveGrid.ref} class="grid gap-4" style={gridContainerStyle()} data-mcp-ui-grid data-mcp-ui-stacked={responsiveGrid.stacked()}>
|
|
1968
|
+
<For each={layout().components}>
|
|
1965
1969
|
{(component) => (
|
|
1966
1970
|
<div
|
|
1971
|
+
class="min-w-0"
|
|
1967
1972
|
style={getGridStyleString(component)}
|
|
1968
1973
|
data-mcp-ui-component-id={getUiResourceStableKey(component)}
|
|
1969
1974
|
>
|
|
@@ -46,11 +46,6 @@ function buildAnnotatedSegments(text: string, validation: DataValidation): TextS
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// Build all number positions sorted
|
|
50
|
-
const allPositions = validation.llmNumbers
|
|
51
|
-
.map(n => ({ position: n.position, length: n.context.length - 20 })) // approximate original match length
|
|
52
|
-
.sort((a, b) => a.position - b.position)
|
|
53
|
-
|
|
54
49
|
// Re-extract number lengths from text for precise splitting
|
|
55
50
|
const numberRegex = /\d[\d\s,.]*\d|\d+/g
|
|
56
51
|
const matches: Array<{ start: number; end: number }> = []
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export interface ChartDataTable {
|
|
2
|
+
columns: string[];
|
|
3
|
+
rows: Array<Array<string | number>>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface ChartDatasetLike {
|
|
7
|
+
label?: string;
|
|
8
|
+
data?: unknown[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ChartDataLike {
|
|
12
|
+
type?: string;
|
|
13
|
+
data?: {
|
|
14
|
+
labels?: Array<string | number>;
|
|
15
|
+
datasets?: ChartDatasetLike[];
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function tableCell(value: unknown): string | number {
|
|
20
|
+
if (value == null) return '';
|
|
21
|
+
if (typeof value === 'string' || typeof value === 'number') return value;
|
|
22
|
+
if (typeof value === 'boolean') return String(value);
|
|
23
|
+
try {
|
|
24
|
+
return JSON.stringify(value);
|
|
25
|
+
} catch {
|
|
26
|
+
return String(value);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isPoint(value: unknown): value is Record<string, unknown> {
|
|
31
|
+
return (
|
|
32
|
+
value !== null &&
|
|
33
|
+
typeof value === 'object' &&
|
|
34
|
+
!Array.isArray(value) &&
|
|
35
|
+
('x' in value || 'y' in value || 'r' in value)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Project Chart.js data into an accessible table without flattening point
|
|
41
|
+
* coordinates. Categorical charts use a compact wide table; scatter, bubble,
|
|
42
|
+
* and object-valued line charts use one row per point so x/y/r remain distinct.
|
|
43
|
+
*/
|
|
44
|
+
export function chartToDataTable(params: ChartDataLike): ChartDataTable {
|
|
45
|
+
const datasets = params.data?.datasets ?? [];
|
|
46
|
+
const labels = params.data?.labels ?? [];
|
|
47
|
+
const pointData =
|
|
48
|
+
params.type === 'scatter' ||
|
|
49
|
+
params.type === 'bubble' ||
|
|
50
|
+
datasets.some((dataset) => dataset.data?.some(isPoint));
|
|
51
|
+
|
|
52
|
+
if (pointData) {
|
|
53
|
+
const hasLabels = labels.length > 0;
|
|
54
|
+
const hasRadius =
|
|
55
|
+
params.type === 'bubble' ||
|
|
56
|
+
datasets.some((dataset) => dataset.data?.some((value) => isPoint(value) && 'r' in value));
|
|
57
|
+
const columns = [
|
|
58
|
+
'Series',
|
|
59
|
+
'Point',
|
|
60
|
+
...(hasLabels ? ['Label'] : []),
|
|
61
|
+
'x',
|
|
62
|
+
'y',
|
|
63
|
+
...(hasRadius ? ['r'] : []),
|
|
64
|
+
];
|
|
65
|
+
const rows: Array<Array<string | number>> = [];
|
|
66
|
+
|
|
67
|
+
datasets.forEach((dataset, datasetIndex) => {
|
|
68
|
+
const series = dataset.label || `Series ${datasetIndex + 1}`;
|
|
69
|
+
(dataset.data ?? []).forEach((value, pointIndex) => {
|
|
70
|
+
const point = isPoint(value) ? value : { y: value };
|
|
71
|
+
rows.push([
|
|
72
|
+
series,
|
|
73
|
+
pointIndex + 1,
|
|
74
|
+
...(hasLabels ? [tableCell(labels[pointIndex])] : []),
|
|
75
|
+
tableCell(point.x),
|
|
76
|
+
tableCell(point.y),
|
|
77
|
+
...(hasRadius ? [tableCell(point.r)] : []),
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return { columns, rows };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const rowCount = Math.max(labels.length, ...datasets.map((dataset) => dataset.data?.length ?? 0), 0);
|
|
86
|
+
const columns = ['Label', ...datasets.map((dataset, index) => dataset.label || `Series ${index + 1}`)];
|
|
87
|
+
const rows: Array<Array<string | number>> = [];
|
|
88
|
+
|
|
89
|
+
for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
|
|
90
|
+
rows.push([
|
|
91
|
+
tableCell(labels[rowIndex] ?? rowIndex + 1),
|
|
92
|
+
...datasets.map((dataset) => tableCell(dataset.data?.[rowIndex])),
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { columns, rows };
|
|
97
|
+
}
|
|
@@ -11,10 +11,13 @@
|
|
|
11
11
|
|
|
12
12
|
import { describe, it, expect, beforeEach } from 'vitest'
|
|
13
13
|
import { render, cleanup, fireEvent } from '@solidjs/testing-library'
|
|
14
|
+
import { createSignal } from 'solid-js'
|
|
14
15
|
import {
|
|
16
|
+
MCPUIStringsContext,
|
|
15
17
|
MCPUIStringsProvider,
|
|
16
18
|
useMCPUIStrings,
|
|
17
19
|
DEFAULT_MCPUI_STRINGS,
|
|
20
|
+
type MCPUIStrings,
|
|
18
21
|
} from './MCPUIStringsContext'
|
|
19
22
|
import { FeedbackInline } from '../components/FeedbackInline'
|
|
20
23
|
import { ExpandableWrapper } from '../components/ExpandableWrapper'
|
|
@@ -27,6 +30,8 @@ describe('MCPUIStringsContext (v6.6.0)', () => {
|
|
|
27
30
|
expect(DEFAULT_MCPUI_STRINGS.feedbackUseful).toBe('Useful')
|
|
28
31
|
expect(DEFAULT_MCPUI_STRINGS.feedbackPositiveAck).toBe('Thanks!')
|
|
29
32
|
expect(DEFAULT_MCPUI_STRINGS.retry).toBe('Retry')
|
|
33
|
+
expect(DEFAULT_MCPUI_STRINGS.chartView).toBe('Chart')
|
|
34
|
+
expect(DEFAULT_MCPUI_STRINGS.chartDataView).toBe('Data')
|
|
30
35
|
})
|
|
31
36
|
|
|
32
37
|
it('useMCPUIStrings returns the EN defaults with no provider mounted', () => {
|
|
@@ -58,6 +63,61 @@ describe('MCPUIStringsContext (v6.6.0)', () => {
|
|
|
58
63
|
expect(captured!.closeExpandedView).toBe('Close expanded view')
|
|
59
64
|
})
|
|
60
65
|
|
|
66
|
+
it('accepts a legacy typed dictionary and resolves chart defaults through the direct context', () => {
|
|
67
|
+
const legacyStrings: MCPUIStrings = {
|
|
68
|
+
expand: 'Legacy expand',
|
|
69
|
+
expandedView: 'Legacy expanded view',
|
|
70
|
+
copyToClipboard: 'Legacy copy',
|
|
71
|
+
closeExpandedView: 'Legacy close',
|
|
72
|
+
feedbackUseful: 'Legacy useful',
|
|
73
|
+
feedbackNotUseful: 'Legacy not useful',
|
|
74
|
+
feedbackPositiveAck: 'Legacy positive',
|
|
75
|
+
feedbackNegativeAck: 'Legacy negative',
|
|
76
|
+
retry: 'Legacy retry',
|
|
77
|
+
}
|
|
78
|
+
let captured: ReturnType<typeof useMCPUIStrings> | undefined
|
|
79
|
+
const Probe = () => {
|
|
80
|
+
captured = useMCPUIStrings()
|
|
81
|
+
return <span>probe</span>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
render(() => (
|
|
85
|
+
<MCPUIStringsContext.Provider value={legacyStrings}>
|
|
86
|
+
<Probe />
|
|
87
|
+
</MCPUIStringsContext.Provider>
|
|
88
|
+
))
|
|
89
|
+
|
|
90
|
+
expect(captured!.expand).toBe('Legacy expand')
|
|
91
|
+
expect(captured!.chartView).toBe('Chart')
|
|
92
|
+
expect(captured!.chartDataView).toBe('Data')
|
|
93
|
+
expect(captured!.chartViewSelector).toBe('Chart or data view')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('keeps replacement provider overrides reactive while resolving omitted defaults', () => {
|
|
97
|
+
const [overrides, setOverrides] = createSignal<Partial<MCPUIStrings>>({
|
|
98
|
+
chartView: 'Diagram',
|
|
99
|
+
})
|
|
100
|
+
const Probe = () => {
|
|
101
|
+
const strings = useMCPUIStrings()
|
|
102
|
+
return (
|
|
103
|
+
<button type="button" onClick={() => setOverrides({ chartView: 'Graphique' })}>
|
|
104
|
+
{strings.chartView}|{strings.chartDataView}
|
|
105
|
+
</button>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { getByRole } = render(() => (
|
|
110
|
+
<MCPUIStringsProvider strings={overrides()}>
|
|
111
|
+
<Probe />
|
|
112
|
+
</MCPUIStringsProvider>
|
|
113
|
+
))
|
|
114
|
+
const button = getByRole('button')
|
|
115
|
+
|
|
116
|
+
expect(button.textContent).toBe('Diagram|Data')
|
|
117
|
+
fireEvent.click(button)
|
|
118
|
+
expect(button.textContent).toBe('Graphique|Data')
|
|
119
|
+
})
|
|
120
|
+
|
|
61
121
|
it('FeedbackInline reads its ack from the provider (FR override)', () => {
|
|
62
122
|
const { getByText, container } = render(() => (
|
|
63
123
|
<MCPUIStringsProvider
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
* ```
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
-
import { createContext, useContext, type JSX } from 'solid-js'
|
|
42
|
+
import { createContext, mergeProps, useContext, type JSX } from 'solid-js'
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* The library's own chrome strings. Flat map, no interpolation.
|
|
46
46
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* The original chrome keys remain required. Later additions are optional
|
|
48
|
+
* so existing typed dictionaries remain valid across minor releases.
|
|
49
|
+
* `useMCPUIStrings` resolves omitted keys to their English defaults.
|
|
50
50
|
*/
|
|
51
51
|
export interface MCPUIStrings {
|
|
52
52
|
// ── ExpandableWrapper toolbar ──────────────────────────────
|
|
@@ -72,13 +72,27 @@ export interface MCPUIStrings {
|
|
|
72
72
|
// ── Generic chrome ────────────────────────────────────────
|
|
73
73
|
/** Label of the streaming retry button. */
|
|
74
74
|
retry: string
|
|
75
|
+
|
|
76
|
+
// ── Chart data access ─────────────────────────────────────
|
|
77
|
+
/** Label of the native chart view button. */
|
|
78
|
+
chartView?: string
|
|
79
|
+
/** Label of the exact-data table view button. */
|
|
80
|
+
chartDataView?: string
|
|
81
|
+
/** Accessible name of the chart/data view selector. */
|
|
82
|
+
chartViewSelector?: string
|
|
83
|
+
/** Caption and accessible name for the chart's data table. */
|
|
84
|
+
chartDataTable?: string
|
|
85
|
+
/** Screen-reader description associated with the chart canvas. */
|
|
86
|
+
chartDataSummary?: string
|
|
87
|
+
/** Empty state shown when a chart has no data points. */
|
|
88
|
+
chartNoData?: string
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
/**
|
|
78
92
|
* English defaults. A published library ships no hardcoded non-English
|
|
79
93
|
* chrome — consumers localize via `<MCPUIStringsProvider>`.
|
|
80
94
|
*/
|
|
81
|
-
export const DEFAULT_MCPUI_STRINGS: MCPUIStrings = {
|
|
95
|
+
export const DEFAULT_MCPUI_STRINGS: Required<MCPUIStrings> = {
|
|
82
96
|
expand: 'Expand',
|
|
83
97
|
expandedView: 'Expanded view',
|
|
84
98
|
copyToClipboard: 'Copy to clipboard',
|
|
@@ -88,6 +102,12 @@ export const DEFAULT_MCPUI_STRINGS: MCPUIStrings = {
|
|
|
88
102
|
feedbackPositiveAck: 'Thanks!',
|
|
89
103
|
feedbackNegativeAck: "Noted — we'll improve",
|
|
90
104
|
retry: 'Retry',
|
|
105
|
+
chartView: 'Chart',
|
|
106
|
+
chartDataView: 'Data',
|
|
107
|
+
chartViewSelector: 'Chart or data view',
|
|
108
|
+
chartDataTable: 'Chart data',
|
|
109
|
+
chartDataSummary: 'Exact values are available in the data view.',
|
|
110
|
+
chartNoData: 'No chart data',
|
|
91
111
|
}
|
|
92
112
|
|
|
93
113
|
export const MCPUIStringsContext = createContext<MCPUIStrings>(DEFAULT_MCPUI_STRINGS)
|
|
@@ -97,8 +117,8 @@ export const MCPUIStringsContext = createContext<MCPUIStrings>(DEFAULT_MCPUI_STR
|
|
|
97
117
|
* `<MCPUIStringsProvider>` is mounted above — every renderer works
|
|
98
118
|
* standalone with English chrome.
|
|
99
119
|
*/
|
|
100
|
-
export function useMCPUIStrings(): MCPUIStrings {
|
|
101
|
-
return useContext(MCPUIStringsContext)
|
|
120
|
+
export function useMCPUIStrings(): Required<MCPUIStrings> {
|
|
121
|
+
return mergeProps(DEFAULT_MCPUI_STRINGS, useContext(MCPUIStringsContext))
|
|
102
122
|
}
|
|
103
123
|
|
|
104
124
|
export interface MCPUIStringsProviderProps {
|
|
@@ -116,12 +136,11 @@ export interface MCPUIStringsProviderProps {
|
|
|
116
136
|
* Merges the partial `strings` override over the English defaults.
|
|
117
137
|
*/
|
|
118
138
|
export function MCPUIStringsProvider(props: MCPUIStringsProviderProps): JSX.Element {
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
const value = (): MCPUIStrings => ({ ...DEFAULT_MCPUI_STRINGS, ...props.strings })
|
|
139
|
+
// The function source keeps replacement `strings` objects reactive while
|
|
140
|
+
// mergeProps fills any omitted key without taking a one-time snapshot.
|
|
141
|
+
const value = mergeProps(DEFAULT_MCPUI_STRINGS, () => props.strings)
|
|
123
142
|
return (
|
|
124
|
-
<MCPUIStringsContext.Provider value={value
|
|
143
|
+
<MCPUIStringsContext.Provider value={value}>
|
|
125
144
|
{props.children}
|
|
126
145
|
</MCPUIStringsContext.Provider>
|
|
127
146
|
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createSignal, onCleanup, onMount } from 'solid-js'
|
|
2
|
+
import { useEditableLayoutPositions } from '../components/EditableLayoutContext'
|
|
3
|
+
|
|
4
|
+
/** Stack read-only layouts in narrow containers, including desktop chat panels. */
|
|
5
|
+
export function createResponsiveGrid(breakpoint = 640) {
|
|
6
|
+
const [stacked, setStacked] = createSignal(false)
|
|
7
|
+
const preserveEditablePositions = useEditableLayoutPositions()
|
|
8
|
+
let container: HTMLElement | undefined
|
|
9
|
+
|
|
10
|
+
const ref = (element: HTMLElement) => { container = element }
|
|
11
|
+
|
|
12
|
+
// Editor canvases preserve authored coordinates at every width. This is a
|
|
13
|
+
// context-only opt-out, so ordinary/read-only renderers retain responsiveness.
|
|
14
|
+
if (preserveEditablePositions) return { stacked: () => false, ref }
|
|
15
|
+
|
|
16
|
+
onMount(() => {
|
|
17
|
+
if (!container) return
|
|
18
|
+
const element = container
|
|
19
|
+
const updateWidth = (width: number) => {
|
|
20
|
+
// Hidden/unmeasured containers retain their last layout until measurable.
|
|
21
|
+
if (Number.isFinite(width) && width > 0) setStacked(width < breakpoint)
|
|
22
|
+
}
|
|
23
|
+
const measure = () => updateWidth(element.getBoundingClientRect().width)
|
|
24
|
+
measure()
|
|
25
|
+
|
|
26
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
27
|
+
const observer = new ResizeObserver((entries) => {
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (entry.target === element) updateWidth(entry.contentRect.width)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
observer.observe(element)
|
|
33
|
+
onCleanup(() => observer.disconnect())
|
|
34
|
+
} else if (typeof window !== 'undefined') {
|
|
35
|
+
window.addEventListener('resize', measure)
|
|
36
|
+
onCleanup(() => window.removeEventListener('resize', measure))
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return { stacked, ref }
|
|
41
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
6
6
|
import { createEventEmitter, createCommandHandler, createChatBus, clarificationToPromptConfig, elicitationToPromptConfig } from './chat-bus'
|
|
7
|
-
import type {
|
|
7
|
+
import type { ClarificationEvent, ElicitationEvent } from '../types/chat-bus'
|
|
8
8
|
|
|
9
9
|
describe('createEventEmitter', () => {
|
|
10
10
|
it('emits events to subscribed listeners', () => {
|
package/src/services/chat-bus.ts
CHANGED
|
@@ -124,14 +124,6 @@ export function createChatPromptController(): ChatPromptController {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
function clearPending(): void {
|
|
128
|
-
if (pending) {
|
|
129
|
-
cleanupAbort(pending)
|
|
130
|
-
pending = null
|
|
131
|
-
}
|
|
132
|
-
setActivePrompt(null)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
127
|
function handle(
|
|
136
128
|
config: ChatPromptConfig,
|
|
137
129
|
signal?: AbortSignal
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for ComponentRegistry — all
|
|
2
|
+
* Tests for ComponentRegistry — all 20 component types registered
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, it, expect } from 'vitest'
|
|
@@ -7,13 +7,28 @@ import {
|
|
|
7
7
|
validateAgainstRegistry,
|
|
8
8
|
getComponentEntry,
|
|
9
9
|
ComponentRegistry,
|
|
10
|
+
QuickchartRegistry,
|
|
11
|
+
TableRegistry,
|
|
10
12
|
MapRegistry,
|
|
13
|
+
GraphRegistry,
|
|
11
14
|
} from './component-registry'
|
|
12
15
|
import type { ComponentType } from '../types'
|
|
13
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
ChartComponentParamsSchema,
|
|
18
|
+
ChartTimeAxisSchema,
|
|
19
|
+
ChartTypeSchema,
|
|
20
|
+
ComponentTypeSchema,
|
|
21
|
+
GraphComponentParamsSchema,
|
|
22
|
+
GraphEdgeSchema,
|
|
23
|
+
GraphLayoutNameSchema,
|
|
24
|
+
GraphNodeSchema,
|
|
25
|
+
MapComponentParamsSchema,
|
|
26
|
+
TableComponentParamsSchema,
|
|
27
|
+
} from '@seed-ship/mcp-ui-spec'
|
|
14
28
|
|
|
15
29
|
interface RegistrySchemaNode {
|
|
16
30
|
type?: string
|
|
31
|
+
description?: string
|
|
17
32
|
enum?: string[]
|
|
18
33
|
oneOf?: RegistrySchemaNode[]
|
|
19
34
|
anyOf?: RegistrySchemaNode[]
|
|
@@ -24,6 +39,7 @@ interface RegistrySchemaNode {
|
|
|
24
39
|
minItems?: number
|
|
25
40
|
maxItems?: number
|
|
26
41
|
minLength?: number
|
|
42
|
+
minimum?: number
|
|
27
43
|
}
|
|
28
44
|
|
|
29
45
|
/** All 20 component types in the registry */
|
|
@@ -143,6 +159,69 @@ describe('MapRegistry ↔ map spec parity (UI-MAP-0a)', () => {
|
|
|
143
159
|
})
|
|
144
160
|
})
|
|
145
161
|
|
|
162
|
+
describe('visualization registry ↔ runtime spec parity', () => {
|
|
163
|
+
it('keeps the historical QuickchartRegistry export but describes the native renderer', () => {
|
|
164
|
+
expect(QuickchartRegistry.name).toBe('Chart.js')
|
|
165
|
+
expect(QuickchartRegistry.description).toContain('locally with Chart.js')
|
|
166
|
+
expect(QuickchartRegistry.description).toContain('host explicitly opts in')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('advertises the complete chart params surface and all supported chart types', () => {
|
|
170
|
+
const properties = QuickchartRegistry.schema.properties as Record<string, RegistrySchemaNode>
|
|
171
|
+
expect(Object.keys(properties).sort()).toEqual(Object.keys(ChartComponentParamsSchema.shape).sort())
|
|
172
|
+
expect(properties.type.enum).toEqual(ChartTypeSchema.options)
|
|
173
|
+
|
|
174
|
+
const dataProperties = properties.data.properties!
|
|
175
|
+
const dataset = dataProperties.datasets.items as RegistrySchemaNode
|
|
176
|
+
const datasetProperties = dataset.properties!
|
|
177
|
+
expect(datasetProperties.data).not.toHaveProperty('oneOf')
|
|
178
|
+
expect(datasetProperties.data.anyOf).toHaveLength(2)
|
|
179
|
+
expect(ChartComponentParamsSchema.safeParse({
|
|
180
|
+
type: 'bar', data: { labels: [], datasets: [{ label: 'Empty series', data: [] }] },
|
|
181
|
+
}).success).toBe(true)
|
|
182
|
+
const pointBranch = datasetProperties.data.anyOf?.[1].items as RegistrySchemaNode
|
|
183
|
+
expect(pointBranch.required).toEqual(['x', 'y'])
|
|
184
|
+
expect(Object.keys(pointBranch.properties!).sort()).toEqual(['r', 'x', 'y'])
|
|
185
|
+
expect(pointBranch.properties?.r.minimum).toBe(0)
|
|
186
|
+
|
|
187
|
+
expect(Object.keys(properties.timeAxis.properties!).sort()).toEqual(
|
|
188
|
+
Object.keys(ChartTimeAxisSchema.shape).sort()
|
|
189
|
+
)
|
|
190
|
+
expect(properties.timeAxis.properties?.unit.enum).toEqual([
|
|
191
|
+
'day', 'week', 'month', 'quarter', 'year',
|
|
192
|
+
])
|
|
193
|
+
expect(properties.timeAxis.description).toContain('date adapter')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('advertises the complete table surface including search and client paging controls', () => {
|
|
197
|
+
const properties = TableRegistry.schema.properties as Record<string, RegistrySchemaNode>
|
|
198
|
+
expect(Object.keys(properties).sort()).toEqual(Object.keys(TableComponentParamsSchema.shape).sort())
|
|
199
|
+
expect(properties.searchable.type).toBe('boolean')
|
|
200
|
+
expect(properties.searchPlaceholder.type).toBe('string')
|
|
201
|
+
expect(properties.pageSize).toMatchObject({ type: 'integer', minimum: 0 })
|
|
202
|
+
expect(properties.chatPageSize).toMatchObject({ type: 'integer', minimum: 0 })
|
|
203
|
+
expect(properties.initialPage).toMatchObject({ type: 'integer', minimum: 0 })
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('advertises only the seven canonical graph layouts, including object options', () => {
|
|
207
|
+
const properties = GraphRegistry.schema.properties as Record<string, RegistrySchemaNode>
|
|
208
|
+
expect(Object.keys(properties).sort()).toEqual(Object.keys(GraphComponentParamsSchema.shape).sort())
|
|
209
|
+
expect(properties).not.toHaveProperty('directed')
|
|
210
|
+
|
|
211
|
+
const [shorthand, objectForm] = properties.layout.oneOf!
|
|
212
|
+
expect(shorthand.enum).toEqual(GraphLayoutNameSchema.options)
|
|
213
|
+
expect(objectForm.properties?.type.enum).toEqual(GraphLayoutNameSchema.options)
|
|
214
|
+
expect(objectForm.properties).toHaveProperty('options')
|
|
215
|
+
|
|
216
|
+
const node = properties.nodes.items as RegistrySchemaNode
|
|
217
|
+
const edge = properties.edges.items as RegistrySchemaNode
|
|
218
|
+
expect(Object.keys(node.properties!).sort()).toEqual(Object.keys(GraphNodeSchema.shape).sort())
|
|
219
|
+
expect(Object.keys(edge.properties!).sort()).toEqual(Object.keys(GraphEdgeSchema.shape).sort())
|
|
220
|
+
expect(GraphRegistry.description).not.toContain('Degrades to an edge table')
|
|
221
|
+
expect(GraphRegistry.examples[0].component.params).not.toHaveProperty('directed')
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
146
225
|
describe('ComponentRegistry', () => {
|
|
147
226
|
describe('registry completeness', () => {
|
|
148
227
|
it('has exactly 20 registered types', () => {
|