@seed-ship/mcp-ui-solid 3.0.5 → 4.0.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 +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 +172 -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 +172 -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 +74 -0
- package/dist/components/ScratchpadPanel.cjs.map +1 -1
- package/dist/components/ScratchpadPanel.d.ts.map +1 -1
- package/dist/components/ScratchpadPanel.js +75 -1
- 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 +206 -0
- package/src/components/MapRenderer.test.tsx +94 -5
- package/src/components/MapRenderer.tsx +246 -45
- package/src/components/ScratchpadPanel.tsx +10 -2
- 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
package/src/index.ts
CHANGED
|
@@ -44,6 +44,10 @@ export { ChatPrompt } from './components/ChatPrompt'
|
|
|
44
44
|
export { ScratchpadPanel } from './components/ScratchpadPanel'
|
|
45
45
|
export { dispatchScratchpad, useScratchpadState } from './stores/scratchpad-store'
|
|
46
46
|
|
|
47
|
+
// Data Verification Components (v3.1.0)
|
|
48
|
+
export { VerifiedText } from './components/VerifiedText'
|
|
49
|
+
export { DataPreviewSection } from './components/DataPreviewSection'
|
|
50
|
+
|
|
47
51
|
// Autocomplete Components
|
|
48
52
|
export { GhostText, GhostTextInput } from './components/GhostText'
|
|
49
53
|
export { AutocompleteDropdown } from './components/AutocompleteDropdown'
|
|
@@ -62,6 +66,8 @@ export type { ExpandableWrapperProps } from './components/ExpandableWrapper'
|
|
|
62
66
|
export type { ComponentToolbarProps, ToolbarAction, ToolbarIcon } from './components/ComponentToolbar'
|
|
63
67
|
export type { ChatPromptProps } from './components/ChatPrompt'
|
|
64
68
|
export type { ScratchpadPanelProps } from './components/ScratchpadPanel'
|
|
69
|
+
export type { VerifiedTextProps } from './components/VerifiedText'
|
|
70
|
+
export type { DataPreviewSectionProps } from './components/DataPreviewSection'
|
|
65
71
|
export type { GhostTextProps, GhostTextInputProps } from './components/GhostText'
|
|
66
72
|
export type { AutocompleteDropdownProps } from './components/AutocompleteDropdown'
|
|
67
73
|
export type { AutocompleteFormFieldProps, AutocompleteFormFieldParams } from './components/AutocompleteFormField'
|
|
@@ -81,6 +87,8 @@ export {
|
|
|
81
87
|
useResize,
|
|
82
88
|
// Autocomplete hooks
|
|
83
89
|
useAutocomplete,
|
|
90
|
+
// Data Validator hooks (v3.1.0)
|
|
91
|
+
useDataValidator,
|
|
84
92
|
} from './hooks'
|
|
85
93
|
|
|
86
94
|
export type {
|
|
@@ -107,6 +115,9 @@ export type {
|
|
|
107
115
|
// Autocomplete types
|
|
108
116
|
UseAutocompleteOptions,
|
|
109
117
|
UseAutocompleteReturn,
|
|
118
|
+
// Data Validator types (v3.1.0)
|
|
119
|
+
UseDataValidatorOptions,
|
|
120
|
+
UseDataValidatorReturn,
|
|
110
121
|
} from './hooks'
|
|
111
122
|
|
|
112
123
|
// Context (Phase 5.0)
|
|
@@ -177,10 +188,14 @@ export type {
|
|
|
177
188
|
GalleryImage,
|
|
178
189
|
ImageGalleryParams,
|
|
179
190
|
VideoComponentParams,
|
|
180
|
-
// Code & Maps types (Sprint 6)
|
|
191
|
+
// Code & Maps types (Sprint 6 + v3.1.0)
|
|
181
192
|
CodeComponentParams,
|
|
182
193
|
MapMarker,
|
|
183
194
|
MapComponentParams,
|
|
195
|
+
MapPopupConfig,
|
|
196
|
+
MapGeoJSONStyle,
|
|
197
|
+
MapLayer,
|
|
198
|
+
MapPMTilesConfig,
|
|
184
199
|
// Validation options (v2.0.0)
|
|
185
200
|
IframePolicy,
|
|
186
201
|
ValidationOptions,
|
|
@@ -217,6 +232,7 @@ export {
|
|
|
217
232
|
createCommandHandler,
|
|
218
233
|
createChatBus,
|
|
219
234
|
mergeScratchpadSections,
|
|
235
|
+
validateAgainstSource,
|
|
220
236
|
} from './services'
|
|
221
237
|
|
|
222
238
|
// Chat Bus Types (v2.4.0 — @experimental)
|
|
@@ -246,4 +262,13 @@ export type {
|
|
|
246
262
|
Citation,
|
|
247
263
|
ToolCallEvent,
|
|
248
264
|
ClarificationEvent,
|
|
265
|
+
// Data Validation types (v3.1.0)
|
|
266
|
+
DataValidation,
|
|
267
|
+
LLMNumber,
|
|
268
|
+
HallucinatedNumber,
|
|
269
|
+
DataValidationOptions,
|
|
270
|
+
VerifiedTextContent,
|
|
271
|
+
DataPreviewColumn,
|
|
272
|
+
DataPreviewContent,
|
|
273
|
+
MapSectionContent,
|
|
249
274
|
} from './types/chat-bus'
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for validateAgainstSource — anti-hallucination data validator
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest'
|
|
6
|
+
import { validateAgainstSource } from './data-validator'
|
|
7
|
+
|
|
8
|
+
const SAMPLE_ROWS = [
|
|
9
|
+
{ type: 'Appartement', ventes: 22306, prix_m2: 3337 },
|
|
10
|
+
{ type: 'Maison', ventes: 2492, prix_m2: 4230 },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
describe('validateAgainstSource', () => {
|
|
14
|
+
it('validates text with exact source numbers', () => {
|
|
15
|
+
const text = 'On observe 22 306 ventes appartements et 3 337 EUR/m2 moyen.'
|
|
16
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS)
|
|
17
|
+
|
|
18
|
+
expect(result.valid).toBe(true)
|
|
19
|
+
expect(result.hallucinated).toHaveLength(0)
|
|
20
|
+
expect(result.confidence).toBe(1)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('detects hallucinated numbers', () => {
|
|
24
|
+
const text = 'On observe 18 245 ventes en 2023, prix moyen 2 850 EUR.'
|
|
25
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS)
|
|
26
|
+
|
|
27
|
+
expect(result.valid).toBe(false)
|
|
28
|
+
expect(result.hallucinated.length).toBeGreaterThan(0)
|
|
29
|
+
expect(result.confidence).toBeLessThan(1)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('returns hallucinated items with closest source number', () => {
|
|
33
|
+
const text = 'On observe 18 245 ventes.'
|
|
34
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS)
|
|
35
|
+
|
|
36
|
+
expect(result.hallucinated).toHaveLength(1)
|
|
37
|
+
expect(result.hallucinated[0].value).toBe(18245)
|
|
38
|
+
expect(result.hallucinated[0].closest).toBeDefined()
|
|
39
|
+
expect(result.hallucinated[0].distance).toBeDefined()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('accepts rounding within tolerance', () => {
|
|
43
|
+
// 3337 rounded to 3340 is 0.09% — within 1% tolerance
|
|
44
|
+
const text = 'Le prix moyen est de 3 340 EUR.'
|
|
45
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS, { tolerance: 0.01 })
|
|
46
|
+
|
|
47
|
+
expect(result.valid).toBe(true)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('rejects rounding beyond tolerance', () => {
|
|
51
|
+
// 3337 vs 3500 is ~4.9% — beyond 1% tolerance
|
|
52
|
+
const text = 'Le prix moyen est de 3 500 EUR.'
|
|
53
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS, { tolerance: 0.01 })
|
|
54
|
+
|
|
55
|
+
expect(result.valid).toBe(false)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('ignores years by default', () => {
|
|
59
|
+
const text = 'En 2023, on observe 22 306 ventes.'
|
|
60
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS)
|
|
61
|
+
|
|
62
|
+
// 2023 should be ignored, 22306 should be verified
|
|
63
|
+
expect(result.valid).toBe(true)
|
|
64
|
+
expect(result.llmNumbers).toHaveLength(1) // only 22306
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('ignores postal/INSEE codes by default', () => {
|
|
68
|
+
const text = 'Code commune 34172, on observe 22 306 ventes.'
|
|
69
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS)
|
|
70
|
+
|
|
71
|
+
// 34172 should be ignored (5-digit pattern)
|
|
72
|
+
expect(result.valid).toBe(true)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('ignores specified columns', () => {
|
|
76
|
+
const rows = [
|
|
77
|
+
{ code_geo: '34172', ventes: 22306 },
|
|
78
|
+
]
|
|
79
|
+
const text = 'Le code est 34172 avec 22 306 ventes.'
|
|
80
|
+
const result = validateAgainstSource(text, rows, {
|
|
81
|
+
ignoreColumns: ['code_geo'],
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
expect(result.valid).toBe(true)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('handles string-encoded numbers in source', () => {
|
|
88
|
+
const rows = [
|
|
89
|
+
{ prix: '3 337', total: '22306' },
|
|
90
|
+
]
|
|
91
|
+
const text = 'Prix 3 337, total 22 306.'
|
|
92
|
+
const result = validateAgainstSource(text, rows)
|
|
93
|
+
|
|
94
|
+
expect(result.valid).toBe(true)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('returns confidence = 1 for text with no numbers', () => {
|
|
98
|
+
const text = 'Les données sont stables et cohérentes.'
|
|
99
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS)
|
|
100
|
+
|
|
101
|
+
expect(result.valid).toBe(true)
|
|
102
|
+
expect(result.confidence).toBe(1)
|
|
103
|
+
expect(result.llmNumbers).toHaveLength(0)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('handles empty source rows', () => {
|
|
107
|
+
const text = 'On observe 22 306 ventes.'
|
|
108
|
+
const result = validateAgainstSource(text, [])
|
|
109
|
+
|
|
110
|
+
// With empty source, any number in text is hallucinated
|
|
111
|
+
expect(result.valid).toBe(false)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('mixes verified and hallucinated', () => {
|
|
115
|
+
const text = 'On observe 22 306 ventes reelles et 15 000 ventes estimees.'
|
|
116
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS)
|
|
117
|
+
|
|
118
|
+
expect(result.valid).toBe(false)
|
|
119
|
+
expect(result.hallucinated).toHaveLength(1)
|
|
120
|
+
expect(result.hallucinated[0].value).toBe(15000)
|
|
121
|
+
// 1 hallucinated out of 2 = 50% confidence
|
|
122
|
+
expect(result.confidence).toBe(0.5)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('provides context around numbers', () => {
|
|
126
|
+
const text = 'Le prix moyen est de 3 337 EUR/m2 a Montpellier.'
|
|
127
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS)
|
|
128
|
+
|
|
129
|
+
expect(result.llmNumbers[0].context).toContain('3 337')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('handles custom ignorePatterns', () => {
|
|
133
|
+
const text = 'Parcelle 1234 avec 22 306 ventes.'
|
|
134
|
+
const result = validateAgainstSource(text, SAMPLE_ROWS, {
|
|
135
|
+
ignorePatterns: [/^\d{4}$/], // ignore 4-digit numbers
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
expect(result.valid).toBe(true)
|
|
139
|
+
expect(result.llmNumbers).toHaveLength(1) // only 22306
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('returns sourceNumbers as a Set', () => {
|
|
143
|
+
const result = validateAgainstSource('test', SAMPLE_ROWS)
|
|
144
|
+
|
|
145
|
+
expect(result.sourceNumbers).toBeInstanceOf(Set)
|
|
146
|
+
expect(result.sourceNumbers.has(22306)).toBe(true)
|
|
147
|
+
expect(result.sourceNumbers.has(3337)).toBe(true)
|
|
148
|
+
expect(result.sourceNumbers.has(2492)).toBe(true)
|
|
149
|
+
expect(result.sourceNumbers.has(4230)).toBe(true)
|
|
150
|
+
})
|
|
151
|
+
})
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Validator — anti-hallucination for LLM-generated text
|
|
3
|
+
* v3.1.0: Pure regex-based number verification against source data
|
|
4
|
+
*
|
|
5
|
+
* Compares numbers in LLM text to numbers in source data rows.
|
|
6
|
+
* Detects ~90% of numerical hallucinations with zero LLM cost, <1ms latency.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { DataValidation, DataValidationOptions, LLMNumber, HallucinatedNumber } from '../types/chat-bus'
|
|
10
|
+
|
|
11
|
+
const DEFAULT_IGNORE_COLUMNS = new Set(['id', 'code_geo', 'code_parent'])
|
|
12
|
+
const DEFAULT_IGNORE_PATTERNS: RegExp[] = [
|
|
13
|
+
/^20[012]\d$/, // years 2000-2029
|
|
14
|
+
/^\d{5}$/, // postal codes / INSEE codes
|
|
15
|
+
/^\d{1,2}$/, // indices, ranks (1-99)
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extract all numeric values from source data rows.
|
|
20
|
+
* Handles both number types and string-encoded numbers (e.g. "22 306", "3,337").
|
|
21
|
+
*/
|
|
22
|
+
function extractSourceNumbers(
|
|
23
|
+
rows: Record<string, unknown>[],
|
|
24
|
+
ignoreColumns: Set<string>
|
|
25
|
+
): Set<number> {
|
|
26
|
+
const numbers = new Set<number>()
|
|
27
|
+
for (const row of rows) {
|
|
28
|
+
for (const [col, val] of Object.entries(row)) {
|
|
29
|
+
if (ignoreColumns.has(col)) continue
|
|
30
|
+
if (typeof val === 'number' && isFinite(val)) {
|
|
31
|
+
numbers.add(val)
|
|
32
|
+
} else if (typeof val === 'string') {
|
|
33
|
+
const parsed = Number(val.replace(/\s/g, '').replace(',', '.'))
|
|
34
|
+
if (!isNaN(parsed) && isFinite(parsed)) {
|
|
35
|
+
numbers.add(parsed)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return numbers
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extract all numbers from LLM text.
|
|
45
|
+
* Handles French/European formats: "22 306", "3 337", "3,337", "22306".
|
|
46
|
+
*/
|
|
47
|
+
function extractLLMNumbers(
|
|
48
|
+
text: string,
|
|
49
|
+
ignorePatterns: RegExp[]
|
|
50
|
+
): LLMNumber[] {
|
|
51
|
+
const numberRegex = /\d[\d\s,.]*\d|\d+/g
|
|
52
|
+
const results: LLMNumber[] = []
|
|
53
|
+
let match: RegExpExecArray | null
|
|
54
|
+
|
|
55
|
+
while ((match = numberRegex.exec(text)) !== null) {
|
|
56
|
+
const raw = match[0]
|
|
57
|
+
// Normalize: remove spaces/dots (thousand separators), comma → decimal point
|
|
58
|
+
const cleaned = raw.replace(/[\s.]/g, '').replace(',', '.')
|
|
59
|
+
const value = Number(cleaned)
|
|
60
|
+
|
|
61
|
+
if (isNaN(value) || !isFinite(value)) continue
|
|
62
|
+
if (ignorePatterns.some(p => p.test(raw.trim()))) continue
|
|
63
|
+
|
|
64
|
+
results.push({
|
|
65
|
+
value,
|
|
66
|
+
position: match.index,
|
|
67
|
+
context: text.slice(
|
|
68
|
+
Math.max(0, match.index - 10),
|
|
69
|
+
match.index + raw.length + 10
|
|
70
|
+
),
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return results
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validate LLM-generated text against source data rows.
|
|
79
|
+
*
|
|
80
|
+
* Pure function — no IO, no LLM calls, no side effects.
|
|
81
|
+
* Returns which numbers in the text are verified vs hallucinated.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* const rows = [{ type: 'Appartement', ventes: 22306, prix_m2: 3337 }]
|
|
86
|
+
* const result = validateAgainstSource(
|
|
87
|
+
* "On observe 22 306 ventes à 3 337 EUR/m². En 2023, 18 245 ventes.",
|
|
88
|
+
* rows
|
|
89
|
+
* )
|
|
90
|
+
* // result.valid === false
|
|
91
|
+
* // result.hallucinated === [{ value: 18245, ... }]
|
|
92
|
+
* // result.confidence === 0.67
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function validateAgainstSource(
|
|
96
|
+
text: string,
|
|
97
|
+
sourceRows: Record<string, unknown>[],
|
|
98
|
+
options: DataValidationOptions = {}
|
|
99
|
+
): DataValidation {
|
|
100
|
+
const tolerance = options.tolerance ?? 0.01
|
|
101
|
+
const ignoreColumns = new Set(options.ignoreColumns || [...DEFAULT_IGNORE_COLUMNS])
|
|
102
|
+
const ignorePatterns = options.ignorePatterns || DEFAULT_IGNORE_PATTERNS
|
|
103
|
+
|
|
104
|
+
// 1. Extract source numbers
|
|
105
|
+
const sourceNumbers = extractSourceNumbers(sourceRows, ignoreColumns)
|
|
106
|
+
|
|
107
|
+
// 2. Extract LLM numbers
|
|
108
|
+
const llmNumbers = extractLLMNumbers(text, ignorePatterns)
|
|
109
|
+
|
|
110
|
+
// 3. Check each LLM number against source
|
|
111
|
+
const hallucinated: HallucinatedNumber[] = []
|
|
112
|
+
|
|
113
|
+
for (const num of llmNumbers) {
|
|
114
|
+
// Exact match
|
|
115
|
+
if (sourceNumbers.has(num.value)) continue
|
|
116
|
+
|
|
117
|
+
// Tolerance match (rounding)
|
|
118
|
+
let closest: number | undefined
|
|
119
|
+
let minDistance = Infinity
|
|
120
|
+
|
|
121
|
+
for (const src of sourceNumbers) {
|
|
122
|
+
const dist = Math.abs(num.value - src) / Math.max(Math.abs(src), 1)
|
|
123
|
+
if (dist < minDistance) {
|
|
124
|
+
minDistance = dist
|
|
125
|
+
closest = src
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (minDistance <= tolerance) continue // acceptable rounding
|
|
130
|
+
|
|
131
|
+
hallucinated.push({
|
|
132
|
+
...num,
|
|
133
|
+
closest,
|
|
134
|
+
distance: minDistance,
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const confidence = llmNumbers.length > 0
|
|
139
|
+
? 1 - (hallucinated.length / llmNumbers.length)
|
|
140
|
+
: 1
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
valid: hallucinated.length === 0,
|
|
144
|
+
llmNumbers,
|
|
145
|
+
sourceNumbers,
|
|
146
|
+
hallucinated,
|
|
147
|
+
confidence,
|
|
148
|
+
}
|
|
149
|
+
}
|
package/src/services/index.ts
CHANGED
package/src/types/chat-bus.ts
CHANGED
|
@@ -366,7 +366,7 @@ export interface ScratchpadState {
|
|
|
366
366
|
export interface ScratchpadSection {
|
|
367
367
|
id: string
|
|
368
368
|
title: string
|
|
369
|
-
type: 'data' | 'filter' | 'preview' | 'message' | 'action' | 'steps' | 'form' | 'understanding' | 'feedback' | 'prompt' | 'stepper' | 'error' | 'source_card' | 'diff'
|
|
369
|
+
type: 'data' | 'filter' | 'preview' | 'message' | 'action' | 'steps' | 'form' | 'understanding' | 'feedback' | 'prompt' | 'stepper' | 'error' | 'source_card' | 'diff' | 'verified_text' | 'data_preview' | 'map' | 'chart'
|
|
370
370
|
content: unknown
|
|
371
371
|
/** Can the human edit this section? */
|
|
372
372
|
editable: boolean
|
|
@@ -438,3 +438,100 @@ export interface ClarificationEvent {
|
|
|
438
438
|
}>
|
|
439
439
|
original_message?: string
|
|
440
440
|
}
|
|
441
|
+
|
|
442
|
+
// ─── Data Validation (v3.1.0 — anti-hallucination) ──────────
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Result of validating LLM text against source data.
|
|
446
|
+
* Pure regex-based — zero LLM cost, <1ms latency.
|
|
447
|
+
*/
|
|
448
|
+
export interface DataValidation {
|
|
449
|
+
/** Is the text free of hallucinated numbers? */
|
|
450
|
+
valid: boolean
|
|
451
|
+
/** Numbers found in the LLM text */
|
|
452
|
+
llmNumbers: LLMNumber[]
|
|
453
|
+
/** Numbers present in the source data */
|
|
454
|
+
sourceNumbers: Set<number>
|
|
455
|
+
/** Numbers from the LLM NOT found in the source */
|
|
456
|
+
hallucinated: HallucinatedNumber[]
|
|
457
|
+
/** Confidence score 0-1 (1 = all numbers verified) */
|
|
458
|
+
confidence: number
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export interface LLMNumber {
|
|
462
|
+
value: number
|
|
463
|
+
/** Character index in the text */
|
|
464
|
+
position: number
|
|
465
|
+
/** ~20 chars surrounding context */
|
|
466
|
+
context: string
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export interface HallucinatedNumber extends LLMNumber {
|
|
470
|
+
/** Closest number in source data */
|
|
471
|
+
closest?: number
|
|
472
|
+
/** Distance as ratio (0.18 = 18% off) */
|
|
473
|
+
distance?: number
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Options for validateAgainstSource */
|
|
477
|
+
export interface DataValidationOptions {
|
|
478
|
+
/** Tolerance for rounding (default: 0.01 = 1%) */
|
|
479
|
+
tolerance?: number
|
|
480
|
+
/** Columns to ignore (e.g. 'id', 'code_geo') */
|
|
481
|
+
ignoreColumns?: string[]
|
|
482
|
+
/** Number patterns to ignore (e.g. years, postal codes) */
|
|
483
|
+
ignorePatterns?: RegExp[]
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Content for verified_text scratchpad section */
|
|
487
|
+
export interface VerifiedTextContent {
|
|
488
|
+
/** Original LLM text */
|
|
489
|
+
text: string
|
|
490
|
+
/** Validation result from validateAgainstSource */
|
|
491
|
+
validation: DataValidation
|
|
492
|
+
/** Display mode */
|
|
493
|
+
mode?: 'highlight' | 'strip' | 'annotate'
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Column definition for data_preview section */
|
|
497
|
+
export interface DataPreviewColumn {
|
|
498
|
+
key: string
|
|
499
|
+
label: string
|
|
500
|
+
type?: 'number' | 'string' | 'date'
|
|
501
|
+
format?: string
|
|
502
|
+
align?: 'left' | 'right' | 'center'
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** Content for data_preview scratchpad section */
|
|
506
|
+
export interface DataPreviewContent {
|
|
507
|
+
columns: DataPreviewColumn[]
|
|
508
|
+
rows: Record<string, unknown>[]
|
|
509
|
+
/** Total rows (if paginated — e.g. 22306 total, 30 displayed) */
|
|
510
|
+
totalRows?: number
|
|
511
|
+
/** Data source attribution */
|
|
512
|
+
source?: string
|
|
513
|
+
/** Data freshness label */
|
|
514
|
+
freshness?: string
|
|
515
|
+
/** Enable export buttons (CSV/JSON) */
|
|
516
|
+
exportable?: boolean
|
|
517
|
+
/** Rows per page (default: 25) */
|
|
518
|
+
pageSize?: number
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Content for map scratchpad section (v3.1.0) */
|
|
522
|
+
export interface MapSectionContent {
|
|
523
|
+
/** GeoJSON FeatureCollection */
|
|
524
|
+
geojson: unknown
|
|
525
|
+
/** Map center [lat, lng] */
|
|
526
|
+
center?: [number, number]
|
|
527
|
+
/** Zoom level */
|
|
528
|
+
zoom?: number
|
|
529
|
+
/** GeoJSON style (including choropleth) */
|
|
530
|
+
style?: import('./index').MapGeoJSONStyle
|
|
531
|
+
/** Popup config for feature click */
|
|
532
|
+
popup?: import('./index').MapPopupConfig
|
|
533
|
+
/** Named layers */
|
|
534
|
+
layers?: import('./index').MapLayer[]
|
|
535
|
+
/** Map height (CSS, default: '300px') */
|
|
536
|
+
height?: string
|
|
537
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -94,10 +94,14 @@ export interface ChartComponentParams {
|
|
|
94
94
|
labels: string[]
|
|
95
95
|
datasets: Array<{
|
|
96
96
|
label: string
|
|
97
|
-
data: number[]
|
|
97
|
+
data: number[] | Array<{ x: string | number; y: number }>
|
|
98
98
|
backgroundColor?: string | string[]
|
|
99
99
|
borderColor?: string | string[]
|
|
100
100
|
borderWidth?: number
|
|
101
|
+
/** Fill area under line (useful for time-series) */
|
|
102
|
+
fill?: boolean | string
|
|
103
|
+
/** Line tension (0 = straight, 0.4 = smooth) */
|
|
104
|
+
tension?: number
|
|
101
105
|
}>
|
|
102
106
|
}
|
|
103
107
|
options?: {
|
|
@@ -118,6 +122,22 @@ export interface ChartComponentParams {
|
|
|
118
122
|
* Enable PNG export button (v2.2.0)
|
|
119
123
|
*/
|
|
120
124
|
exportable?: boolean
|
|
125
|
+
/**
|
|
126
|
+
* Time-series axis configuration (v3.1.0).
|
|
127
|
+
* When set, x-axis labels are parsed as dates.
|
|
128
|
+
*/
|
|
129
|
+
timeAxis?: {
|
|
130
|
+
/** Date format for parsing labels (Chart.js adapter format, e.g. 'yyyy-MM-dd') */
|
|
131
|
+
parser?: string
|
|
132
|
+
/** Display unit for x-axis ticks */
|
|
133
|
+
unit?: 'day' | 'week' | 'month' | 'quarter' | 'year'
|
|
134
|
+
/** Date format for tooltip display */
|
|
135
|
+
tooltipFormat?: string
|
|
136
|
+
/** Min date (ISO string) */
|
|
137
|
+
min?: string
|
|
138
|
+
/** Max date (ISO string) */
|
|
139
|
+
max?: string
|
|
140
|
+
}
|
|
121
141
|
/**
|
|
122
142
|
* Chart container height as CSS value (v2.2.0, default '250px')
|
|
123
143
|
*/
|
|
@@ -750,9 +770,60 @@ export interface MapClusterOptions {
|
|
|
750
770
|
animateAddingMarkers?: boolean
|
|
751
771
|
}
|
|
752
772
|
|
|
773
|
+
/**
|
|
774
|
+
* GeoJSON feature popup configuration (v3.1.0)
|
|
775
|
+
*/
|
|
776
|
+
export interface MapPopupConfig {
|
|
777
|
+
/** Property key used as popup title */
|
|
778
|
+
titleField?: string
|
|
779
|
+
/** Property keys to display in popup body */
|
|
780
|
+
fields?: string[]
|
|
781
|
+
/** Custom HTML template (use {{property}} placeholders) */
|
|
782
|
+
template?: string
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* GeoJSON style configuration (v3.1.0)
|
|
787
|
+
* Supports static styles and choropleth (data-driven) coloring.
|
|
788
|
+
*/
|
|
789
|
+
export interface MapGeoJSONStyle {
|
|
790
|
+
/** Fill color (CSS color or choropleth config) */
|
|
791
|
+
fillColor?: string
|
|
792
|
+
/** Fill opacity (0-1, default: 0.6) */
|
|
793
|
+
fillOpacity?: number
|
|
794
|
+
/** Stroke color (default: '#333') */
|
|
795
|
+
strokeColor?: string
|
|
796
|
+
/** Stroke width (default: 1) */
|
|
797
|
+
strokeWeight?: number
|
|
798
|
+
/** Stroke opacity (0-1, default: 1) */
|
|
799
|
+
strokeOpacity?: number
|
|
800
|
+
/** Choropleth: property key for data-driven coloring */
|
|
801
|
+
choroplethField?: string
|
|
802
|
+
/** Choropleth: color scale stops [value, color][] sorted ascending */
|
|
803
|
+
choroplethScale?: Array<[number, string]>
|
|
804
|
+
/** Choropleth: color for features with missing/null values */
|
|
805
|
+
choroplethFallback?: string
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Named GeoJSON layer for multi-layer maps (v3.1.0)
|
|
810
|
+
*/
|
|
811
|
+
export interface MapLayer {
|
|
812
|
+
/** Layer name (shown in layer control) */
|
|
813
|
+
name: string
|
|
814
|
+
/** Is this layer visible by default? */
|
|
815
|
+
visible?: boolean
|
|
816
|
+
/** GeoJSON FeatureCollection (inline or from API) */
|
|
817
|
+
geojson: unknown // GeoJSON.FeatureCollection — kept as unknown for zero-dep types
|
|
818
|
+
/** Per-layer style override */
|
|
819
|
+
style?: MapGeoJSONStyle
|
|
820
|
+
/** Per-layer popup config */
|
|
821
|
+
popup?: MapPopupConfig
|
|
822
|
+
}
|
|
823
|
+
|
|
753
824
|
/**
|
|
754
825
|
* Map component parameters (Sprint 6)
|
|
755
|
-
* Updated
|
|
826
|
+
* Updated v3.1.0: GeoJSON, choropleth, popups, layers
|
|
756
827
|
*/
|
|
757
828
|
export interface MapComponentParams {
|
|
758
829
|
/**
|
|
@@ -776,7 +847,7 @@ export interface MapComponentParams {
|
|
|
776
847
|
height?: string
|
|
777
848
|
|
|
778
849
|
/**
|
|
779
|
-
* Auto-fit bounds to show all markers (default: false)
|
|
850
|
+
* Auto-fit bounds to show all markers/features (default: false)
|
|
780
851
|
*/
|
|
781
852
|
fitBounds?: boolean
|
|
782
853
|
|
|
@@ -802,9 +873,6 @@ export interface MapComponentParams {
|
|
|
802
873
|
|
|
803
874
|
/**
|
|
804
875
|
* Enable marker clustering (Sprint Ultimate U.2)
|
|
805
|
-
* - true: Enable with default options
|
|
806
|
-
* - false: Disable clustering
|
|
807
|
-
* - MapClusterOptions: Enable with custom options
|
|
808
876
|
*/
|
|
809
877
|
clustering?: boolean | MapClusterOptions
|
|
810
878
|
|
|
@@ -812,6 +880,77 @@ export interface MapComponentParams {
|
|
|
812
880
|
* Custom CSS class (Sprint 7)
|
|
813
881
|
*/
|
|
814
882
|
className?: string
|
|
883
|
+
|
|
884
|
+
// ─── GeoJSON (v3.1.0) ────────────────────
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* GeoJSON FeatureCollection to render on the map.
|
|
888
|
+
* Use this for polygons, lines, points from structured data.
|
|
889
|
+
*/
|
|
890
|
+
geojson?: unknown // GeoJSON.FeatureCollection
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Style for the GeoJSON layer.
|
|
894
|
+
* Supports static colors and choropleth (data-driven) coloring.
|
|
895
|
+
*/
|
|
896
|
+
geojsonStyle?: MapGeoJSONStyle
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Popup configuration for GeoJSON features.
|
|
900
|
+
* Shown on feature click.
|
|
901
|
+
*/
|
|
902
|
+
popup?: MapPopupConfig
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Named layers for multi-layer maps.
|
|
906
|
+
* Each layer has its own GeoJSON, style, and popup config.
|
|
907
|
+
* A Leaflet layer control is added when layers are present.
|
|
908
|
+
*/
|
|
909
|
+
layers?: MapLayer[]
|
|
910
|
+
|
|
911
|
+
// ─── PMTiles (v3.1.0) ────────────────────
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* PMTiles vector tile source for large datasets (>5000 features).
|
|
915
|
+
* Requires protomaps-leaflet peer dependency.
|
|
916
|
+
* Pipeline: GeoParquet -> Tippecanoe -> PMTiles (static file on S3/CDN).
|
|
917
|
+
*/
|
|
918
|
+
pmtiles?: MapPMTilesConfig
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* PMTiles configuration for large vector tile datasets (v3.1.0)
|
|
923
|
+
*/
|
|
924
|
+
export interface MapPMTilesConfig {
|
|
925
|
+
/** URL to the .pmtiles file (S3, CDN, local) */
|
|
926
|
+
url: string
|
|
927
|
+
/** Attribution text for this tile source */
|
|
928
|
+
attribution?: string
|
|
929
|
+
/** Style rules for vector features */
|
|
930
|
+
paintRules?: Array<{
|
|
931
|
+
/** Layer name in the PMTiles source */
|
|
932
|
+
dataLayer: string
|
|
933
|
+
/** Symbol type */
|
|
934
|
+
symbolizer: 'polygon' | 'line' | 'circle'
|
|
935
|
+
/** Fill/stroke color (CSS color or function name) */
|
|
936
|
+
color?: string
|
|
937
|
+
/** Stroke width */
|
|
938
|
+
width?: number
|
|
939
|
+
/** Fill opacity */
|
|
940
|
+
opacity?: number
|
|
941
|
+
}>
|
|
942
|
+
/** Label rules for text labels */
|
|
943
|
+
labelRules?: Array<{
|
|
944
|
+
dataLayer: string
|
|
945
|
+
/** Property key for label text */
|
|
946
|
+
textField: string
|
|
947
|
+
/** Font size */
|
|
948
|
+
fontSize?: number
|
|
949
|
+
}>
|
|
950
|
+
/** Max zoom level */
|
|
951
|
+
maxZoom?: number
|
|
952
|
+
/** Min zoom level */
|
|
953
|
+
minZoom?: number
|
|
815
954
|
}
|
|
816
955
|
|
|
817
956
|
/**
|