@seed-ship/mcp-ui-solid 3.0.5 → 4.0.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/CHANGELOG.md +115 -0
- package/README.md +253 -280
- package/dist/components/ChartJSRenderer.cjs +37 -15
- package/dist/components/ChartJSRenderer.cjs.map +1 -1
- package/dist/components/ChartJSRenderer.d.ts.map +1 -1
- package/dist/components/ChartJSRenderer.js +37 -15
- package/dist/components/ChartJSRenderer.js.map +1 -1
- package/dist/components/DataPreviewSection.cjs +213 -0
- package/dist/components/DataPreviewSection.cjs.map +1 -0
- package/dist/components/DataPreviewSection.d.ts +19 -0
- package/dist/components/DataPreviewSection.d.ts.map +1 -0
- package/dist/components/DataPreviewSection.js +213 -0
- package/dist/components/DataPreviewSection.js.map +1 -0
- package/dist/components/MapRenderer.cjs +168 -26
- package/dist/components/MapRenderer.cjs.map +1 -1
- package/dist/components/MapRenderer.d.ts +2 -2
- package/dist/components/MapRenderer.d.ts.map +1 -1
- package/dist/components/MapRenderer.js +169 -27
- package/dist/components/MapRenderer.js.map +1 -1
- package/dist/components/ScratchpadPanel.cjs +83 -1
- package/dist/components/ScratchpadPanel.cjs.map +1 -1
- package/dist/components/ScratchpadPanel.d.ts.map +1 -1
- package/dist/components/ScratchpadPanel.js +84 -2
- package/dist/components/ScratchpadPanel.js.map +1 -1
- package/dist/components/VerifiedText.cjs +166 -0
- package/dist/components/VerifiedText.cjs.map +1 -0
- package/dist/components/VerifiedText.d.ts +22 -0
- package/dist/components/VerifiedText.d.ts.map +1 -0
- package/dist/components/VerifiedText.js +166 -0
- package/dist/components/VerifiedText.js.map +1 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components.cjs +4 -0
- package/dist/components.cjs.map +1 -1
- package/dist/components.d.cts +4 -0
- package/dist/components.d.ts +4 -0
- package/dist/components.js +4 -0
- package/dist/components.js.map +1 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useDataValidator.cjs +31 -0
- package/dist/hooks/useDataValidator.cjs.map +1 -0
- package/dist/hooks/useDataValidator.d.ts +42 -0
- package/dist/hooks/useDataValidator.d.ts.map +1 -0
- package/dist/hooks/useDataValidator.js +31 -0
- package/dist/hooks/useDataValidator.js.map +1 -0
- package/dist/hooks.cjs +2 -0
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.cts +2 -0
- package/dist/hooks.d.ts +2 -0
- package/dist/hooks.js +2 -0
- package/dist/hooks.js.map +1 -1
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -5
- package/dist/index.d.ts +9 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/node_modules/.pnpm/@mapbox_point-geometry@1.1.0/node_modules/@mapbox/point-geometry/index.cjs +290 -0
- package/dist/node_modules/.pnpm/@mapbox_point-geometry@1.1.0/node_modules/@mapbox/point-geometry/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/@mapbox_point-geometry@1.1.0/node_modules/@mapbox/point-geometry/index.js +291 -0
- package/dist/node_modules/.pnpm/@mapbox_point-geometry@1.1.0/node_modules/@mapbox/point-geometry/index.js.map +1 -0
- package/dist/node_modules/.pnpm/@mapbox_vector-tile@2.0.4/node_modules/@mapbox/vector-tile/index.cjs +243 -0
- package/dist/node_modules/.pnpm/@mapbox_vector-tile@2.0.4/node_modules/@mapbox/vector-tile/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/@mapbox_vector-tile@2.0.4/node_modules/@mapbox/vector-tile/index.js +243 -0
- package/dist/node_modules/.pnpm/@mapbox_vector-tile@2.0.4/node_modules/@mapbox/vector-tile/index.js.map +1 -0
- package/dist/node_modules/.pnpm/color2k@2.0.3/node_modules/color2k/dist/index.exports.import.es.cjs +137 -0
- package/dist/node_modules/.pnpm/color2k@2.0.3/node_modules/color2k/dist/index.exports.import.es.cjs.map +1 -0
- package/dist/node_modules/.pnpm/color2k@2.0.3/node_modules/color2k/dist/index.exports.import.es.js +137 -0
- package/dist/node_modules/.pnpm/color2k@2.0.3/node_modules/color2k/dist/index.exports.import.es.js.map +1 -0
- package/dist/node_modules/.pnpm/pbf@4.0.1/node_modules/pbf/index.cjs +686 -0
- package/dist/node_modules/.pnpm/pbf@4.0.1/node_modules/pbf/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/pbf@4.0.1/node_modules/pbf/index.js +687 -0
- package/dist/node_modules/.pnpm/pbf@4.0.1/node_modules/pbf/index.js.map +1 -0
- package/dist/node_modules/.pnpm/pmtiles@3.2.1/node_modules/pmtiles/dist/index.cjs +1366 -0
- package/dist/node_modules/.pnpm/pmtiles@3.2.1/node_modules/pmtiles/dist/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/pmtiles@3.2.1/node_modules/pmtiles/dist/index.js +1366 -0
- package/dist/node_modules/.pnpm/pmtiles@3.2.1/node_modules/pmtiles/dist/index.js.map +1 -0
- package/dist/node_modules/.pnpm/potpack@1.0.2/node_modules/potpack/index.cjs +54 -0
- package/dist/node_modules/.pnpm/potpack@1.0.2/node_modules/potpack/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/potpack@1.0.2/node_modules/potpack/index.js +55 -0
- package/dist/node_modules/.pnpm/potpack@1.0.2/node_modules/potpack/index.js.map +1 -0
- package/dist/node_modules/.pnpm/protomaps-leaflet@4.1.1/node_modules/protomaps-leaflet/dist/esm/index.cjs +1256 -0
- package/dist/node_modules/.pnpm/protomaps-leaflet@4.1.1/node_modules/protomaps-leaflet/dist/esm/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/protomaps-leaflet@4.1.1/node_modules/protomaps-leaflet/dist/esm/index.js +1256 -0
- package/dist/node_modules/.pnpm/protomaps-leaflet@4.1.1/node_modules/protomaps-leaflet/dist/esm/index.js.map +1 -0
- package/dist/node_modules/.pnpm/quickselect@2.0.0/node_modules/quickselect/index.cjs +47 -0
- package/dist/node_modules/.pnpm/quickselect@2.0.0/node_modules/quickselect/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/quickselect@2.0.0/node_modules/quickselect/index.js +48 -0
- package/dist/node_modules/.pnpm/quickselect@2.0.0/node_modules/quickselect/index.js.map +1 -0
- package/dist/node_modules/.pnpm/rbush@3.0.1/node_modules/rbush/index.cjs +378 -0
- package/dist/node_modules/.pnpm/rbush@3.0.1/node_modules/rbush/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/rbush@3.0.1/node_modules/rbush/index.js +379 -0
- package/dist/node_modules/.pnpm/rbush@3.0.1/node_modules/rbush/index.js.map +1 -0
- package/dist/services/data-validator.cjs +85 -0
- package/dist/services/data-validator.cjs.map +1 -0
- package/dist/services/data-validator.d.ts +28 -0
- package/dist/services/data-validator.d.ts.map +1 -0
- package/dist/services/data-validator.js +85 -0
- package/dist/services/data-validator.js.map +1 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/types/chat-bus.d.ts +88 -1
- package/dist/types/chat-bus.d.ts.map +1 -1
- package/dist/types/index.d.ts +135 -6
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types.d.cts +135 -6
- package/dist/types.d.ts +135 -6
- package/package.json +5 -1
- package/src/components/ChartJSRenderer.tsx +35 -13
- package/src/components/DataPreviewSection.tsx +251 -0
- package/src/components/MapRenderer.test.tsx +94 -5
- package/src/components/MapRenderer.tsx +246 -45
- package/src/components/ScratchpadPanel.tsx +19 -3
- package/src/components/VerifiedText.tsx +187 -0
- package/src/components/index.ts +7 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useDataValidator.ts +68 -0
- package/src/index.ts +26 -1
- package/src/services/data-validator.test.ts +151 -0
- package/src/services/data-validator.ts +149 -0
- package/src/services/index.ts +2 -0
- package/src/types/chat-bus.ts +98 -1
- package/src/types/index.ts +145 -6
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MapRenderer - Interactive map Component
|
|
3
|
-
* Sprint 6:
|
|
4
|
-
*
|
|
3
|
+
* Sprint 6: Markers + clustering
|
|
4
|
+
* v3.1.0: GeoJSON, choropleth, popups, multi-layer, PMTiles
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Component, createEffect, onCleanup, createSignal, Show } from 'solid-js'
|
|
8
8
|
import { isServer } from 'solid-js/web'
|
|
9
|
-
import type { UIComponent, MapComponentParams, MapClusterOptions } from '../types'
|
|
9
|
+
import type { UIComponent, MapComponentParams, MapClusterOptions, MapGeoJSONStyle, MapPopupConfig, MapLayer, MapPMTilesConfig } from '../types'
|
|
10
10
|
|
|
11
11
|
// Lazy load leaflet (it doesn't support SSR well)
|
|
12
12
|
let L: any = null
|
|
@@ -25,6 +25,146 @@ export interface MapRendererProps {
|
|
|
25
25
|
params?: MapComponentParams
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// ─── Helpers ────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve choropleth color for a feature based on property value and scale stops.
|
|
32
|
+
*/
|
|
33
|
+
function getChoroplethColor(
|
|
34
|
+
value: unknown,
|
|
35
|
+
scale: Array<[number, string]>,
|
|
36
|
+
fallback: string
|
|
37
|
+
): string {
|
|
38
|
+
if (value == null || typeof value !== 'number' || !isFinite(value)) return fallback
|
|
39
|
+
|
|
40
|
+
// Scale is sorted ascending: [[0, '#eff3ff'], [100, '#084594']]
|
|
41
|
+
if (scale.length === 0) return fallback
|
|
42
|
+
if (value <= scale[0][0]) return scale[0][1]
|
|
43
|
+
if (value >= scale[scale.length - 1][0]) return scale[scale.length - 1][1]
|
|
44
|
+
|
|
45
|
+
// Find surrounding stops and interpolate (use upper bracket color)
|
|
46
|
+
for (let i = 1; i < scale.length; i++) {
|
|
47
|
+
if (value <= scale[i][0]) return scale[i][1]
|
|
48
|
+
}
|
|
49
|
+
return scale[scale.length - 1][1]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build a Leaflet style function from MapGeoJSONStyle config.
|
|
54
|
+
*/
|
|
55
|
+
function buildStyleFn(style: MapGeoJSONStyle | undefined): (feature: any) => Record<string, unknown> {
|
|
56
|
+
if (!style) {
|
|
57
|
+
return () => ({
|
|
58
|
+
fillColor: '#3388ff',
|
|
59
|
+
fillOpacity: 0.6,
|
|
60
|
+
color: '#333',
|
|
61
|
+
weight: 1,
|
|
62
|
+
opacity: 1,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (feature: any) => {
|
|
67
|
+
let fillColor = style.fillColor || '#3388ff'
|
|
68
|
+
|
|
69
|
+
// Choropleth: override fillColor based on feature property
|
|
70
|
+
if (style.choroplethField && style.choroplethScale && feature?.properties) {
|
|
71
|
+
const val = feature.properties[style.choroplethField]
|
|
72
|
+
fillColor = getChoroplethColor(val, style.choroplethScale, style.choroplethFallback || '#ccc')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
fillColor,
|
|
77
|
+
fillOpacity: style.fillOpacity ?? 0.6,
|
|
78
|
+
color: style.strokeColor || '#333',
|
|
79
|
+
weight: style.strokeWeight ?? 1,
|
|
80
|
+
opacity: style.strokeOpacity ?? 1,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build popup HTML from a feature's properties using popup config.
|
|
87
|
+
*/
|
|
88
|
+
function buildPopupContent(feature: any, popup: MapPopupConfig | undefined): string | null {
|
|
89
|
+
if (!popup || !feature?.properties) return null
|
|
90
|
+
const props = feature.properties
|
|
91
|
+
|
|
92
|
+
// Custom template
|
|
93
|
+
if (popup.template) {
|
|
94
|
+
return popup.template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
95
|
+
const val = props[key]
|
|
96
|
+
return val != null ? String(val) : ''
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Auto-generated popup
|
|
101
|
+
const parts: string[] = []
|
|
102
|
+
|
|
103
|
+
if (popup.titleField && props[popup.titleField] != null) {
|
|
104
|
+
parts.push(`<strong>${escapeHtml(String(props[popup.titleField]))}</strong>`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const fields = popup.fields || Object.keys(props).slice(0, 8)
|
|
108
|
+
for (const key of fields) {
|
|
109
|
+
if (key === popup.titleField) continue
|
|
110
|
+
const val = props[key]
|
|
111
|
+
if (val == null) continue
|
|
112
|
+
const formatted = typeof val === 'number' ? val.toLocaleString('fr-FR') : String(val)
|
|
113
|
+
parts.push(`<span style="color:#666;font-size:11px">${escapeHtml(key)}</span>: ${escapeHtml(formatted)}`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return parts.join('<br/>')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function escapeHtml(str: string): string {
|
|
120
|
+
return str
|
|
121
|
+
.replace(/&/g, '&')
|
|
122
|
+
.replace(/</g, '<')
|
|
123
|
+
.replace(/>/g, '>')
|
|
124
|
+
.replace(/"/g, '"')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Add a GeoJSON layer to the map with style and popup support.
|
|
129
|
+
* Returns the layer for bounds calculation.
|
|
130
|
+
*/
|
|
131
|
+
function addGeoJSONLayer(
|
|
132
|
+
mapInst: any,
|
|
133
|
+
leaflet: any,
|
|
134
|
+
geojson: unknown,
|
|
135
|
+
style?: MapGeoJSONStyle,
|
|
136
|
+
popup?: MapPopupConfig
|
|
137
|
+
): any {
|
|
138
|
+
const styleFn = buildStyleFn(style)
|
|
139
|
+
|
|
140
|
+
const layer = leaflet.geoJSON(geojson, {
|
|
141
|
+
style: styleFn,
|
|
142
|
+
pointToLayer: (feature: any, latlng: any) => {
|
|
143
|
+
// Render points as circle markers for consistency
|
|
144
|
+
const s = styleFn(feature)
|
|
145
|
+
return leaflet.circleMarker(latlng, {
|
|
146
|
+
radius: 6,
|
|
147
|
+
fillColor: s.fillColor,
|
|
148
|
+
fillOpacity: s.fillOpacity,
|
|
149
|
+
color: s.color,
|
|
150
|
+
weight: s.weight,
|
|
151
|
+
opacity: s.opacity,
|
|
152
|
+
})
|
|
153
|
+
},
|
|
154
|
+
onEachFeature: (feature: any, featureLayer: any) => {
|
|
155
|
+
const html = buildPopupContent(feature, popup)
|
|
156
|
+
if (html) {
|
|
157
|
+
featureLayer.bindPopup(html, { maxWidth: 300 })
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
layer.addTo(mapInst)
|
|
163
|
+
return layer
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── Component ──────────────────────────────────────────────
|
|
167
|
+
|
|
28
168
|
export const MapRenderer: Component<MapRendererProps> = (props) => {
|
|
29
169
|
let mapContainer: HTMLDivElement | undefined
|
|
30
170
|
let mapInstance: any = null
|
|
@@ -82,38 +222,33 @@ export const MapRenderer: Component<MapRendererProps> = (props) => {
|
|
|
82
222
|
})
|
|
83
223
|
}
|
|
84
224
|
|
|
85
|
-
// Update markers and view
|
|
225
|
+
// Update markers and view
|
|
86
226
|
if (mapInstance && L) {
|
|
87
227
|
const p = params()
|
|
228
|
+
const allBoundsLayers: any[] = []
|
|
88
229
|
|
|
89
|
-
// Clear existing layers (markers
|
|
230
|
+
// Clear existing layers (markers, cluster groups, GeoJSON)
|
|
90
231
|
mapInstance.eachLayer((layer: any) => {
|
|
91
|
-
if (layer instanceof L.Marker || layer
|
|
232
|
+
if (layer instanceof L.Marker || layer instanceof L.GeoJSON
|
|
233
|
+
|| layer instanceof L.CircleMarker
|
|
234
|
+
|| layer._group || layer._featureGroup) {
|
|
92
235
|
mapInstance.removeLayer(layer)
|
|
93
236
|
}
|
|
94
237
|
})
|
|
95
238
|
|
|
239
|
+
// ─── Markers (legacy) ────────────────────────
|
|
96
240
|
const markers: any[] = []
|
|
97
241
|
const shouldCluster = p?.clustering && p?.markers && p.markers.length > 0
|
|
98
242
|
|
|
99
243
|
if (shouldCluster) {
|
|
100
|
-
// Sprint Ultimate U.2: Use marker clustering
|
|
101
244
|
try {
|
|
102
|
-
// Lazy load markercluster plugin
|
|
103
|
-
// Import markercluster plugin for side-effects (registers with Leaflet)
|
|
104
245
|
await import('leaflet.markercluster')
|
|
105
|
-
|
|
106
|
-
// Load cluster CSS if not already loaded
|
|
107
246
|
if (!clusterCssLoaded) {
|
|
108
247
|
await import('leaflet.markercluster/dist/MarkerCluster.css')
|
|
109
248
|
await import('leaflet.markercluster/dist/MarkerCluster.Default.css')
|
|
110
249
|
clusterCssLoaded = true
|
|
111
250
|
}
|
|
112
|
-
|
|
113
|
-
// Get cluster options
|
|
114
251
|
const clusterOpts: MapClusterOptions = typeof p.clustering === 'object' ? p.clustering : {}
|
|
115
|
-
|
|
116
|
-
// Create cluster group with options
|
|
117
252
|
const clusterGroup = (L as any).markerClusterGroup({
|
|
118
253
|
maxClusterRadius: clusterOpts.maxClusterRadius ?? 80,
|
|
119
254
|
spiderfyOnMaxZoom: clusterOpts.spiderfyOnMaxZoom ?? true,
|
|
@@ -121,53 +256,119 @@ export const MapRenderer: Component<MapRendererProps> = (props) => {
|
|
|
121
256
|
disableClusteringAtZoom: clusterOpts.disableClusteringAtZoom,
|
|
122
257
|
animate: clusterOpts.animateAddingMarkers ?? true
|
|
123
258
|
})
|
|
124
|
-
|
|
125
|
-
// Add markers to cluster group
|
|
126
259
|
p?.markers?.forEach(marker => {
|
|
127
260
|
const m = L.marker(marker.position)
|
|
128
|
-
if (marker.tooltip)
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
if (marker.popup) {
|
|
132
|
-
m.bindPopup(marker.popup)
|
|
133
|
-
}
|
|
261
|
+
if (marker.tooltip) m.bindTooltip(marker.tooltip)
|
|
262
|
+
if (marker.popup) m.bindPopup(marker.popup)
|
|
134
263
|
clusterGroup.addLayer(m)
|
|
135
264
|
markers.push(m)
|
|
136
265
|
})
|
|
137
|
-
|
|
138
266
|
mapInstance.addLayer(clusterGroup)
|
|
139
|
-
} catch
|
|
140
|
-
console.warn('Failed to load leaflet.markercluster, falling back to regular markers', e)
|
|
141
|
-
// Fallback to regular markers
|
|
267
|
+
} catch {
|
|
142
268
|
p?.markers?.forEach(marker => {
|
|
143
269
|
const m = L.marker(marker.position).addTo(mapInstance)
|
|
144
|
-
if (marker.tooltip)
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
if (marker.popup) {
|
|
148
|
-
m.bindPopup(marker.popup)
|
|
149
|
-
}
|
|
270
|
+
if (marker.tooltip) m.bindTooltip(marker.tooltip)
|
|
271
|
+
if (marker.popup) m.bindPopup(marker.popup)
|
|
150
272
|
markers.push(m)
|
|
151
273
|
})
|
|
152
274
|
}
|
|
153
275
|
} else {
|
|
154
|
-
// Standard marker rendering (no clustering)
|
|
155
276
|
p?.markers?.forEach(marker => {
|
|
156
277
|
const m = L.marker(marker.position).addTo(mapInstance)
|
|
157
|
-
if (marker.tooltip)
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
if (marker.popup) {
|
|
161
|
-
m.bindPopup(marker.popup)
|
|
162
|
-
}
|
|
278
|
+
if (marker.tooltip) m.bindTooltip(marker.tooltip)
|
|
279
|
+
if (marker.popup) m.bindPopup(marker.popup)
|
|
163
280
|
markers.push(m)
|
|
164
281
|
})
|
|
165
282
|
}
|
|
166
283
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
284
|
+
if (markers.length > 0) {
|
|
285
|
+
allBoundsLayers.push(...markers)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ─── GeoJSON (v3.1.0) ───────────────────────
|
|
289
|
+
if (p?.geojson) {
|
|
290
|
+
const geoLayer = addGeoJSONLayer(mapInstance, L, p.geojson, p.geojsonStyle, p.popup)
|
|
291
|
+
allBoundsLayers.push(geoLayer)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ─── Named layers (v3.1.0) ──────────────────
|
|
295
|
+
if (p?.layers && p.layers.length > 0) {
|
|
296
|
+
const overlays: Record<string, any> = {}
|
|
297
|
+
|
|
298
|
+
for (const layerDef of p.layers) {
|
|
299
|
+
const geoLayer = addGeoJSONLayer(
|
|
300
|
+
mapInstance, L,
|
|
301
|
+
layerDef.geojson,
|
|
302
|
+
layerDef.style || p?.geojsonStyle,
|
|
303
|
+
layerDef.popup || p?.popup
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
overlays[layerDef.name] = geoLayer
|
|
307
|
+
allBoundsLayers.push(geoLayer)
|
|
308
|
+
|
|
309
|
+
// Respect initial visibility
|
|
310
|
+
if (layerDef.visible === false) {
|
|
311
|
+
mapInstance.removeLayer(geoLayer)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Add layer control if multiple layers
|
|
316
|
+
if (Object.keys(overlays).length > 1) {
|
|
317
|
+
L.control.layers(null, overlays, { collapsed: true }).addTo(mapInstance)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ─── PMTiles (v3.1.0) ────────────────────────
|
|
322
|
+
if (p?.pmtiles) {
|
|
323
|
+
try {
|
|
324
|
+
// @ts-ignore — optional peer dependency, may not be installed
|
|
325
|
+
const protomaps = await import(/* @vite-ignore */ 'protomaps-leaflet')
|
|
326
|
+
const pmConfig = p.pmtiles
|
|
327
|
+
|
|
328
|
+
const paintRules = pmConfig.paintRules?.map(rule => ({
|
|
329
|
+
dataLayer: rule.dataLayer,
|
|
330
|
+
symbolizer: new (protomaps as any)[
|
|
331
|
+
rule.symbolizer === 'polygon' ? 'PolygonSymbolizer' :
|
|
332
|
+
rule.symbolizer === 'line' ? 'LineSymbolizer' :
|
|
333
|
+
'CircleSymbolizer'
|
|
334
|
+
]({
|
|
335
|
+
fill: rule.color || '#3388ff',
|
|
336
|
+
stroke: rule.color || '#333',
|
|
337
|
+
width: rule.width ?? 1,
|
|
338
|
+
opacity: rule.opacity ?? 0.6,
|
|
339
|
+
}),
|
|
340
|
+
})) || []
|
|
341
|
+
|
|
342
|
+
const labelRules = pmConfig.labelRules?.map(rule => ({
|
|
343
|
+
dataLayer: rule.dataLayer,
|
|
344
|
+
symbolizer: new (protomaps as any).TextSymbolizer({
|
|
345
|
+
label_props: [rule.textField],
|
|
346
|
+
fontSize: rule.fontSize ?? 12,
|
|
347
|
+
}),
|
|
348
|
+
})) || []
|
|
349
|
+
|
|
350
|
+
const pmLayer = (protomaps as any).leafletLayer({
|
|
351
|
+
url: pmConfig.url,
|
|
352
|
+
attribution: pmConfig.attribution,
|
|
353
|
+
paintRules,
|
|
354
|
+
labelRules,
|
|
355
|
+
maxZoom: pmConfig.maxZoom,
|
|
356
|
+
minZoom: pmConfig.minZoom,
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
pmLayer.addTo(mapInstance)
|
|
360
|
+
} catch (e) {
|
|
361
|
+
console.warn('[MCP-UI] Failed to load protomaps-leaflet for PMTiles:', e)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ─── Fit bounds ─────────────────────────────
|
|
366
|
+
if (p?.fitBounds && allBoundsLayers.length > 0) {
|
|
367
|
+
const group = L.featureGroup(allBoundsLayers)
|
|
368
|
+
const bounds = group.getBounds()
|
|
369
|
+
if (bounds.isValid()) {
|
|
370
|
+
mapInstance.fitBounds(bounds.pad(0.1))
|
|
371
|
+
}
|
|
171
372
|
} else if (p?.center) {
|
|
172
373
|
mapInstance.setView(p.center, p.zoom || mapInstance.getZoom())
|
|
173
374
|
}
|
|
@@ -183,7 +384,7 @@ export const MapRenderer: Component<MapRendererProps> = (props) => {
|
|
|
183
384
|
})
|
|
184
385
|
|
|
185
386
|
return (
|
|
186
|
-
<div class=
|
|
387
|
+
<div class={`w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden ${params()?.className || ''}`}>
|
|
187
388
|
<Show when={error()}>
|
|
188
389
|
<div class="p-4 text-red-500 bg-red-50 dark:bg-red-900/20 text-center">
|
|
189
390
|
{error()}
|
|
@@ -6,9 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { Component, Show, For, Switch, Match, createSignal, createEffect, onCleanup } from 'solid-js'
|
|
9
|
-
import type { ScratchpadState, ScratchpadSection } from '../types/chat-bus'
|
|
10
|
-
import type { FormFieldParams } from '../types'
|
|
9
|
+
import type { ScratchpadState, ScratchpadSection, VerifiedTextContent, DataPreviewContent, MapSectionContent } from '../types/chat-bus'
|
|
10
|
+
import type { FormFieldParams, ChartComponentParams } from '../types'
|
|
11
11
|
import { FormFieldRenderer } from './FormFieldRenderer'
|
|
12
|
+
import { VerifiedText } from './VerifiedText'
|
|
13
|
+
import { DataPreviewSection } from './DataPreviewSection'
|
|
14
|
+
import { MapRenderer } from './MapRenderer'
|
|
15
|
+
import { ChartJSRenderer } from './ChartJSRenderer'
|
|
12
16
|
|
|
13
17
|
export interface ScratchpadPanelProps {
|
|
14
18
|
state: ScratchpadState
|
|
@@ -365,6 +369,10 @@ const SectionRenderer: Component<{
|
|
|
365
369
|
<Match when={props.section.type === 'error'}><ErrorSectionRenderer content={props.section.content} onAction={props.onAction} /></Match>
|
|
366
370
|
<Match when={props.section.type === 'source_card'}><SourceCardSection content={props.section.content} /></Match>
|
|
367
371
|
<Match when={props.section.type === 'diff'}><DiffSection content={props.section.content} /></Match>
|
|
372
|
+
<Match when={props.section.type === 'verified_text'}><VerifiedText {...(props.section.content as VerifiedTextContent)} onHallucinationClick={(h) => props.onAction?.('hallucination_click', h)} /></Match>
|
|
373
|
+
<Match when={props.section.type === 'data_preview'}><DataPreviewSection content={props.section.content as DataPreviewContent} /></Match>
|
|
374
|
+
<Match when={props.section.type === 'map'}>{(() => { const c = 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>
|
|
375
|
+
<Match when={props.section.type === 'chart'}><ChartJSRenderer component={{ id: props.section.id, type: 'chart', position: { colStart: 1, colSpan: 12 }, params: { ...(props.section.content as ChartComponentParams), renderer: 'native', height: (props.section.content as any)?.height || '250px' } }} /></Match>
|
|
368
376
|
<Match when={true}><pre class="text-xs text-gray-500 overflow-auto">{JSON.stringify(props.section.content, null, 2)}</pre></Match>
|
|
369
377
|
</Switch>
|
|
370
378
|
</div>
|
|
@@ -636,7 +644,15 @@ const ActionSection: Component<{
|
|
|
636
644
|
content: unknown
|
|
637
645
|
onAction?: (action: string, data?: unknown) => void
|
|
638
646
|
}> = (props) => {
|
|
639
|
-
const actions = () =>
|
|
647
|
+
const actions = () => {
|
|
648
|
+
if (Array.isArray(props.content)) return props.content as Array<{ label: string; value?: string; action?: string; variant?: string; icon?: string }>
|
|
649
|
+
const obj = props.content as Record<string, unknown> | null
|
|
650
|
+
if (obj && Array.isArray(obj.actions)) {
|
|
651
|
+
console.warn('[MCP-UI] ActionSection: content should be an array, got { actions: [...] }. Unwrapping automatically.')
|
|
652
|
+
return obj.actions as Array<{ label: string; value?: string; action?: string; variant?: string; icon?: string }>
|
|
653
|
+
}
|
|
654
|
+
return []
|
|
655
|
+
}
|
|
640
656
|
return (
|
|
641
657
|
<div class="flex flex-wrap gap-2">
|
|
642
658
|
<For each={actions()}>
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VerifiedText — renders LLM text with inline verification badges
|
|
3
|
+
* v3.1.0: Highlights verified vs hallucinated numbers
|
|
4
|
+
*
|
|
5
|
+
* @experimental
|
|
6
|
+
*
|
|
7
|
+
* Modes:
|
|
8
|
+
* - highlight: ✅/⚠️ badges next to numbers (default)
|
|
9
|
+
* - strip: replaces hallucinated numbers with [non vérifié]
|
|
10
|
+
* - annotate: tooltip on hover with closest source number
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createMemo, For } from 'solid-js'
|
|
14
|
+
import type { DataValidation, HallucinatedNumber } from '../types/chat-bus'
|
|
15
|
+
|
|
16
|
+
export interface VerifiedTextProps {
|
|
17
|
+
text: string
|
|
18
|
+
validation: DataValidation
|
|
19
|
+
/** Display mode (default: 'highlight') */
|
|
20
|
+
mode?: 'highlight' | 'strip' | 'annotate'
|
|
21
|
+
/** Callback when a hallucinated number is clicked */
|
|
22
|
+
onHallucinationClick?: (item: HallucinatedNumber) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface TextSegment {
|
|
26
|
+
type: 'text' | 'verified' | 'hallucinated'
|
|
27
|
+
content: string
|
|
28
|
+
item?: HallucinatedNumber
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build annotated segments by splitting text at number positions.
|
|
33
|
+
* Verified numbers get ✅, hallucinated get ⚠️.
|
|
34
|
+
*/
|
|
35
|
+
function buildAnnotatedSegments(text: string, validation: DataValidation): TextSegment[] {
|
|
36
|
+
// Build position maps
|
|
37
|
+
const hallucinatedPositions = new Map<number, HallucinatedNumber>()
|
|
38
|
+
for (const h of validation.hallucinated) {
|
|
39
|
+
hallucinatedPositions.set(h.position, h)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const verifiedPositions = new Set<number>()
|
|
43
|
+
for (const n of validation.llmNumbers) {
|
|
44
|
+
if (!hallucinatedPositions.has(n.position)) {
|
|
45
|
+
verifiedPositions.add(n.position)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
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
|
+
// Re-extract number lengths from text for precise splitting
|
|
55
|
+
const numberRegex = /\d[\d\s,.]*\d|\d+/g
|
|
56
|
+
const matches: Array<{ start: number; end: number }> = []
|
|
57
|
+
let m: RegExpExecArray | null
|
|
58
|
+
while ((m = numberRegex.exec(text)) !== null) {
|
|
59
|
+
matches.push({ start: m.index, end: m.index + m[0].length })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Build a lookup from position → match
|
|
63
|
+
const matchByPosition = new Map<number, { start: number; end: number }>()
|
|
64
|
+
for (const match of matches) {
|
|
65
|
+
matchByPosition.set(match.start, match)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Build segments
|
|
69
|
+
const segments: TextSegment[] = []
|
|
70
|
+
let cursor = 0
|
|
71
|
+
|
|
72
|
+
for (const num of validation.llmNumbers) {
|
|
73
|
+
const match = matchByPosition.get(num.position)
|
|
74
|
+
if (!match) continue
|
|
75
|
+
|
|
76
|
+
// Text before this number
|
|
77
|
+
if (match.start > cursor) {
|
|
78
|
+
segments.push({ type: 'text', content: text.slice(cursor, match.start) })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const numberText = text.slice(match.start, match.end)
|
|
82
|
+
const hallucinated = hallucinatedPositions.get(num.position)
|
|
83
|
+
|
|
84
|
+
if (hallucinated) {
|
|
85
|
+
segments.push({ type: 'hallucinated', content: numberText, item: hallucinated })
|
|
86
|
+
} else {
|
|
87
|
+
segments.push({ type: 'verified', content: numberText })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
cursor = match.end
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Remaining text
|
|
94
|
+
if (cursor < text.length) {
|
|
95
|
+
segments.push({ type: 'text', content: text.slice(cursor) })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return segments
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function VerifiedText(props: VerifiedTextProps) {
|
|
102
|
+
const mode = () => props.mode || 'highlight'
|
|
103
|
+
|
|
104
|
+
const segments = createMemo<TextSegment[]>(() => {
|
|
105
|
+
if (!props.validation || props.validation.valid) {
|
|
106
|
+
return [{ type: 'text' as const, content: props.text }]
|
|
107
|
+
}
|
|
108
|
+
return buildAnnotatedSegments(props.text, props.validation)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div class="verified-text text-sm leading-relaxed">
|
|
113
|
+
<For each={segments()}>
|
|
114
|
+
{(seg) => {
|
|
115
|
+
if (seg.type === 'text') {
|
|
116
|
+
return <span>{seg.content}</span>
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (seg.type === 'verified') {
|
|
120
|
+
return (
|
|
121
|
+
<span
|
|
122
|
+
class="inline-flex items-center gap-0.5 px-0.5 rounded bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-300"
|
|
123
|
+
title="Verified against source data"
|
|
124
|
+
>
|
|
125
|
+
{seg.content}
|
|
126
|
+
<span class="text-xs opacity-70" aria-label="verified">✅</span>
|
|
127
|
+
</span>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// hallucinated
|
|
132
|
+
const h = seg.item!
|
|
133
|
+
const tooltipText = () => {
|
|
134
|
+
if (h.closest != null && h.distance != null) {
|
|
135
|
+
return `Not found in source data. Closest: ${h.closest} (${Math.round(h.distance * 100)}% off)`
|
|
136
|
+
}
|
|
137
|
+
return 'Not found in source data'
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (mode() === 'strip') {
|
|
141
|
+
return (
|
|
142
|
+
<span
|
|
143
|
+
class="inline-flex items-center px-1 rounded bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-300 italic text-xs"
|
|
144
|
+
title={tooltipText()}
|
|
145
|
+
>
|
|
146
|
+
[non vérifié]
|
|
147
|
+
</span>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<span
|
|
153
|
+
class="inline-flex items-center gap-0.5 px-0.5 rounded bg-amber-50 dark:bg-amber-900/20 text-amber-800 dark:text-amber-300 cursor-help"
|
|
154
|
+
title={tooltipText()}
|
|
155
|
+
onClick={() => props.onHallucinationClick?.(h)}
|
|
156
|
+
role={props.onHallucinationClick ? 'button' : undefined}
|
|
157
|
+
>
|
|
158
|
+
{seg.content}
|
|
159
|
+
<span class="text-xs" aria-label="unverified">⚠️</span>
|
|
160
|
+
</span>
|
|
161
|
+
)
|
|
162
|
+
}}
|
|
163
|
+
</For>
|
|
164
|
+
|
|
165
|
+
{/* Confidence bar */}
|
|
166
|
+
{props.validation && !props.validation.valid && (
|
|
167
|
+
<div class="mt-2 flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
|
168
|
+
<div class="flex-1 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
|
169
|
+
<div
|
|
170
|
+
class="h-full rounded-full transition-all"
|
|
171
|
+
classList={{
|
|
172
|
+
'bg-green-500': props.validation.confidence >= 0.8,
|
|
173
|
+
'bg-amber-500': props.validation.confidence >= 0.5 && props.validation.confidence < 0.8,
|
|
174
|
+
'bg-red-500': props.validation.confidence < 0.5,
|
|
175
|
+
}}
|
|
176
|
+
style={{ width: `${Math.round(props.validation.confidence * 100)}%` }}
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
<span>
|
|
180
|
+
{Math.round(props.validation.confidence * 100)}% verified
|
|
181
|
+
({props.validation.hallucinated.length} unverified)
|
|
182
|
+
</span>
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
)
|
|
187
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -64,6 +64,13 @@ export type { CodeBlockRendererProps } from './CodeBlockRenderer'
|
|
|
64
64
|
export { MapRenderer } from './MapRenderer'
|
|
65
65
|
export type { MapRendererProps } from './MapRenderer'
|
|
66
66
|
|
|
67
|
+
// Data Verification (v3.1.0 — anti-hallucination)
|
|
68
|
+
export { VerifiedText } from './VerifiedText'
|
|
69
|
+
export type { VerifiedTextProps } from './VerifiedText'
|
|
70
|
+
|
|
71
|
+
export { DataPreviewSection } from './DataPreviewSection'
|
|
72
|
+
export type { DataPreviewSectionProps } from './DataPreviewSection'
|
|
73
|
+
|
|
67
74
|
// Sprint Ultimate: RenderContext for circular dependency resolution
|
|
68
75
|
export { RenderContext, RenderProvider, useRenderContext } from './RenderContext'
|
|
69
76
|
export type { RenderContextValue, RenderComponentFn } from './RenderContext'
|
package/src/hooks/index.ts
CHANGED
|
@@ -57,3 +57,10 @@ export type {
|
|
|
57
57
|
UseAutocompleteOptions,
|
|
58
58
|
UseAutocompleteReturn,
|
|
59
59
|
} from './useAutocomplete'
|
|
60
|
+
|
|
61
|
+
// Data Validator hooks (v3.1.0 — anti-hallucination)
|
|
62
|
+
export { useDataValidator } from './useDataValidator'
|
|
63
|
+
export type {
|
|
64
|
+
UseDataValidatorOptions,
|
|
65
|
+
UseDataValidatorReturn,
|
|
66
|
+
} from './useDataValidator'
|