@retrivora-ai/rag-engine 1.8.1 → 1.8.2

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.
Files changed (55) hide show
  1. package/dist/{ILLMProvider-BfRgI1Xh.d.mts → ILLMProvider-BOJFz3Na.d.mts} +47 -1
  2. package/dist/{ILLMProvider-BfRgI1Xh.d.ts → ILLMProvider-BOJFz3Na.d.ts} +47 -1
  3. package/dist/{MultiTablePostgresProvider-YY7LPNJK.mjs → MultiTablePostgresProvider-ZLGSKTJR.mjs} +1 -1
  4. package/dist/chunk-ICKRMZQK.mjs +76 -0
  5. package/dist/{chunk-BFYLQYQU.mjs → chunk-LZVVLSDN.mjs} +192 -100
  6. package/dist/{chunk-R3RGUMHE.mjs → chunk-OZFBG4BA.mjs} +121 -48
  7. package/dist/handlers/index.d.mts +2 -2
  8. package/dist/handlers/index.d.ts +2 -2
  9. package/dist/handlers/index.js +368 -147
  10. package/dist/handlers/index.mjs +4 -1
  11. package/dist/{index-BV0z5mb6.d.mts → index-BwpcaziY.d.ts} +4 -2
  12. package/dist/{index-1Z4GuYBi.d.ts → index-D3V9Et2M.d.mts} +4 -2
  13. package/dist/index.d.mts +23 -3
  14. package/dist/index.d.ts +23 -3
  15. package/dist/index.js +1143 -790
  16. package/dist/index.mjs +1065 -770
  17. package/dist/server.d.mts +15 -25
  18. package/dist/server.d.ts +15 -25
  19. package/dist/server.js +366 -147
  20. package/dist/server.mjs +3 -2
  21. package/package.json +4 -2
  22. package/src/app/api/upload/route.ts +4 -0
  23. package/src/app/constants.tsx +2 -2
  24. package/src/app/page.tsx +12 -321
  25. package/src/components/AmbientBackground.tsx +29 -0
  26. package/src/components/ArchitectureCard.tsx +17 -0
  27. package/src/components/ArchitectureCardsSection.tsx +15 -0
  28. package/src/components/ChatWindow.tsx +32 -0
  29. package/src/components/CodeViewer.tsx +51 -0
  30. package/src/components/ConfigProvider.tsx +1 -0
  31. package/src/components/DocViewer.tsx +37 -0
  32. package/src/components/DocumentUpload.tsx +44 -1
  33. package/src/components/Documentation.tsx +58 -0
  34. package/src/components/DynamicChart.tsx +27 -2
  35. package/src/components/Hero.tsx +59 -0
  36. package/src/components/HourglassLoader.tsx +87 -0
  37. package/src/components/Lifecycle.tsx +37 -0
  38. package/src/components/MarkdownComponents.tsx +140 -0
  39. package/src/components/MessageBubble.tsx +88 -1010
  40. package/src/components/Navbar.tsx +55 -0
  41. package/src/components/ObservabilityPanel.tsx +374 -0
  42. package/src/components/ProductCard.tsx +3 -1
  43. package/src/components/UIDispatcher.tsx +344 -0
  44. package/src/components/VisualizationRenderer.tsx +48 -26
  45. package/src/core/Pipeline.ts +186 -76
  46. package/src/handlers/index.ts +72 -12
  47. package/src/hooks/useRagChat.ts +19 -9
  48. package/src/index.ts +9 -1
  49. package/src/providers/vectordb/MultiTablePostgresProvider.ts +150 -64
  50. package/src/types/chat.ts +2 -0
  51. package/src/types/index.ts +52 -0
  52. package/src/types/props.ts +9 -1
  53. package/src/utils/ProductExtractor.ts +347 -0
  54. package/src/utils/UITransformer.ts +4 -53
  55. package/src/utils/synonyms.ts +78 -0
@@ -0,0 +1,347 @@
1
+ import { Product, VectorMatch } from '../types';
2
+ import { resolveMetadataValue } from './synonyms';
3
+
4
+ // ─── JSON sanitization ────────────────────────────────────────────────────────
5
+
6
+ export function sanitizeJson(raw: string): string {
7
+ let s = raw.trim();
8
+
9
+ // 1. Evaluate simple arithmetic expressions in values e.g. "v": 495 / 1000
10
+ s = s.replace(
11
+ /:\s*(-?\d+(?:\.\d+)?)\s*([+\-*/])\s*(-?\d+(?:\.\d+)?)/g,
12
+ (_, a, op, b) => {
13
+ const n1 = parseFloat(a);
14
+ const n2 = parseFloat(b);
15
+ const result =
16
+ op === '+' ? n1 + n2
17
+ : op === '-' ? n1 - n2
18
+ : op === '*' ? n1 * n2
19
+ : n2 !== 0 ? n1 / n2
20
+ : n1;
21
+ return `: ${result}`;
22
+ },
23
+ );
24
+
25
+ // 2. Strip JS-style comments while preserving strings (e.g. URLs)
26
+ s = s.replace(/("(?:\\.|[^\\"])*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (m, g1) => g1 || '');
27
+
28
+ // 3. Quote bare (unquoted) object keys
29
+ s = s.replace(/([{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/g, '$1"$2":');
30
+
31
+ // 4. Replace single-quoted strings with double-quoted ones
32
+ s = s.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"');
33
+
34
+ // 5. Remove trailing commas before ] or }
35
+ s = s.replace(/,\s*([}\]])/g, '$1');
36
+
37
+ // 6. Remove completely empty key-value pairs left by prior cleanup
38
+ s = s.replace(/([{,])\s*""\s*(?=[,}])/g, '$1');
39
+
40
+ // 7. Stack-based structure balancer.
41
+ const stack: ('{' | '[')[] = [];
42
+ let inString = false;
43
+ let escape = false;
44
+
45
+ for (const ch of s) {
46
+ if (escape) { escape = false; continue; }
47
+ if (ch === '\\' && inString) { escape = true; continue; }
48
+ if (ch === '"') { inString = !inString; continue; }
49
+ if (inString) continue;
50
+
51
+ if (ch === '{' || ch === '[') {
52
+ stack.push(ch);
53
+ } else if (ch === '}') {
54
+ if (stack[stack.length - 1] === '{') stack.pop();
55
+ } else if (ch === ']') {
56
+ if (stack[stack.length - 1] === '[') stack.pop();
57
+ }
58
+ }
59
+
60
+ if (inString) s += '"';
61
+ for (let i = stack.length - 1; i >= 0; i--) {
62
+ s += stack[i] === '{' ? '}' : ']';
63
+ }
64
+
65
+ const lastClose = Math.max(s.lastIndexOf('}'), s.lastIndexOf(']'));
66
+ if (lastClose !== -1 && lastClose < s.length - 1 && /\S/.test(s.slice(lastClose + 1))) {
67
+ s = s.substring(0, lastClose + 1);
68
+ }
69
+
70
+ return s;
71
+ }
72
+
73
+ export function extractLikelyJsonBlock(raw: string): string {
74
+ const trimmed = raw.trim();
75
+ const objectStart = trimmed.indexOf('{');
76
+ const arrayStart = trimmed.indexOf('[');
77
+
78
+ const starts = [objectStart, arrayStart].filter((index) => index >= 0);
79
+ if (starts.length === 0) return trimmed;
80
+
81
+ return trimmed.slice(Math.min(...starts));
82
+ }
83
+
84
+ export function looksLikeStructuredPayload(raw: string): boolean {
85
+ return /"?(view|chartType|type|data|items|rows|products|results)"?\s*:/.test(raw);
86
+ }
87
+
88
+ export function stripMarkdownTables(raw: string): string {
89
+ return raw.replace(
90
+ /(?:^|\n)\|[^\n]+\|\n\|(?:\s*:?-+:?\s*\|)+\n(?:\|[^\n]*\|\n?)*/g,
91
+ '\n\n',
92
+ );
93
+ }
94
+
95
+ export function stripStructuredUiLabels(raw: string): string {
96
+ return raw.replace(
97
+ /(?:^|\n)\*\*(?:Chart Data|Table Data|Visualization|Visual Representation)\*\:\s*(?=\n|$)/gi,
98
+ '\n',
99
+ ).replace(
100
+ /(?:^|\n)(?:Chart Data|Table Data|Visualization|Visual Representation)\s*\:\s*(?=\n|$)/gi,
101
+ '\n',
102
+ );
103
+ }
104
+
105
+ function extractBareJsonObject(raw: string): string | null {
106
+ let start = raw.indexOf('{');
107
+ if (start === -1) return null;
108
+
109
+ let bestJson: string | null = null;
110
+ let maxLen = 0;
111
+
112
+ while (start !== -1) {
113
+ let depth = 0;
114
+ let inString = false;
115
+ let escape = false;
116
+
117
+ for (let i = start; i < raw.length; i += 1) {
118
+ const ch = raw[i];
119
+ if (escape) { escape = false; continue; }
120
+ if (ch === '\\' && inString) { escape = true; continue; }
121
+ if (ch === '"') { inString = !inString; continue; }
122
+ if (inString) continue;
123
+
124
+ if (ch === '{') depth += 1;
125
+ else if (ch === '}') {
126
+ depth -= 1;
127
+ if (depth === 0) {
128
+ const candidate = raw.slice(start, i + 1);
129
+ if (candidate.length > maxLen) {
130
+ maxLen = candidate.length;
131
+ bestJson = candidate;
132
+ }
133
+ break;
134
+ }
135
+ }
136
+ }
137
+ start = raw.indexOf('{', start + 1);
138
+ }
139
+
140
+ return bestJson;
141
+ }
142
+
143
+ export function extractStructuredPayload(raw: string): { payload: string | null; text: string } {
144
+ let working = raw;
145
+ const fenceRegex = /```(?:[a-z]+)?\s*([\s\S]*?)```/gi;
146
+
147
+ for (const match of Array.from(raw.matchAll(fenceRegex))) {
148
+ const body = match[1]?.trim() ?? '';
149
+ if (!looksLikeStructuredPayload(body)) continue;
150
+
151
+ working = working.replace(match[0], '');
152
+ return {
153
+ payload: body,
154
+ text: stripStructuredUiLabels(working).replace(/\n{3,}/g, '\n\n').trim(),
155
+ };
156
+ }
157
+
158
+ const bareJson = extractBareJsonObject(raw);
159
+ if (bareJson && looksLikeStructuredPayload(bareJson)) {
160
+ working = working.replace(bareJson, '');
161
+ return {
162
+ payload: bareJson,
163
+ text: stripStructuredUiLabels(working).replace(/\n{3,}/g, '\n\n').trim(),
164
+ };
165
+ }
166
+
167
+ return {
168
+ payload: null,
169
+ text: stripStructuredUiLabels(raw).replace(/\n{3,}/g, '\n\n').trim(),
170
+ };
171
+ }
172
+
173
+ export function resolveImage(data: Record<string, unknown>): string | undefined {
174
+ const isPlaceholder = (val: unknown) =>
175
+ typeof val === 'string' && (val === '...' || val === '' || val.toLowerCase() === 'null' || val.toLowerCase() === 'undefined');
176
+
177
+ for (const key of ['image', 'img', 'thumbnail', 'images', 'product_image', 'Image', 'Thumbnail']) {
178
+ const v = data[key];
179
+ if (typeof v === 'string' && v && !isPlaceholder(v)) return v;
180
+ if (Array.isArray(v) && typeof v[0] === 'string' && v[0] && !isPlaceholder(v[0])) return v[0];
181
+ }
182
+ return undefined;
183
+ }
184
+
185
+ export function isLikelyUiOnlyMessage(text: string): boolean {
186
+ const trimmed = text.trim();
187
+ return (
188
+ trimmed.length === 0 ||
189
+ trimmed.length < 30 ||
190
+ /^(chart data|table data|visualization|visual representation|product catalog|product recommendations|catalog summary|availability|inventory status|details)\s*:?\s*$/i.test(trimmed)
191
+ );
192
+ }
193
+
194
+ // ─── Extractors ───────────────────────────────────────────────────────────────
195
+
196
+ export function extractProductsFromSources(sources: VectorMatch[] | undefined, isUser: boolean): Product[] {
197
+ if (isUser || !sources) return [];
198
+ return sources
199
+ .filter(s => {
200
+ const m = s.metadata ?? {};
201
+ const keys = Object.keys(m).map(k => k.toLowerCase());
202
+ const hasProductKey = keys.some(k =>
203
+ ['price', 'image', 'img', 'thumbnail', 'images', 'brand', 'product', 'sku', 'category', 'model', 'cost'].includes(k)
204
+ );
205
+ const hasPricePattern = /\$\s*\d+/.test(s.content);
206
+ return hasProductKey || hasPricePattern || m.type === 'product';
207
+ })
208
+ .map(s => {
209
+ const m = (s.metadata ?? {}) as Record<string, unknown>;
210
+ const name = resolveMetadataValue(m, 'name') as string;
211
+ const brand = resolveMetadataValue(m, 'brand') as string;
212
+ const price = resolveMetadataValue(m, 'price') as string | number;
213
+ const description = resolveMetadataValue(m, 'description') as string;
214
+ console.log("🚀 ~ extractProductsFromSources ~ description:", s)
215
+
216
+ return {
217
+ id: s.id,
218
+ name: name ?? s.content.split('\n')[0] ?? 'Unknown Product',
219
+ brand: brand,
220
+ price: price,
221
+ image: resolveImage(m),
222
+ link: resolveMetadataValue(m, 'link') as string,
223
+ description: description ? description : '' //description ?? s.content,
224
+ };
225
+ });
226
+ }
227
+
228
+ export function extractProductsFromContent(
229
+ content: string,
230
+ payloadText: string,
231
+ payloadCandidates: string[],
232
+ productsFromSources: Product[]
233
+ ): { productsFromContent: Product[]; cleanContent: string } {
234
+ const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
235
+ const products: Product[] = [];
236
+ let cleanContent = payloadText;
237
+
238
+ for (const candidate of payloadCandidates) {
239
+ try {
240
+ const data = JSON.parse(sanitizeJson(candidate));
241
+ const itemSet =
242
+ Array.isArray(data.data) ? data.data
243
+ : Array.isArray(data.items) ? data.items
244
+ : Array.isArray(data.products) ? data.products
245
+ : null;
246
+
247
+ if (
248
+ (data.type === 'products' || data.view === 'carousel' || data.view === 'table' || Array.isArray(data.products)) &&
249
+ itemSet
250
+ ) {
251
+ products.push(
252
+ ...itemSet.map((item: Record<string, unknown>) => ({
253
+ ...item,
254
+ image: (item.image as string) ?? resolveImage(item),
255
+ })) as Product[],
256
+ );
257
+ }
258
+ } catch {
259
+ // Ignore malformed product payloads
260
+ }
261
+ }
262
+
263
+ if (cleanContent.includes('"type": "products"') || cleanContent.includes('"type":"products"')) {
264
+ for (const match of Array.from(cleanContent.matchAll(jsonRegex))) {
265
+ try {
266
+ const data = JSON.parse(sanitizeJson(match[1]));
267
+ if (data.type === 'products' && Array.isArray(data.items)) {
268
+ products.push(
269
+ ...data.items.map((item: Record<string, unknown>) => ({
270
+ ...item,
271
+ image: (item.image as string) ?? resolveImage(item),
272
+ })) as Product[],
273
+ );
274
+ cleanContent = cleanContent.replace(match[0], '');
275
+ }
276
+ } catch {
277
+ // Malformed product block — skip silently
278
+ }
279
+ }
280
+ }
281
+
282
+ // Fallback: Scan for plain text bulleted products
283
+ // Requires a bullet or number at the start of the line to prevent matching plain text paragraphs.
284
+ const bulletRegex = /^[ \t]*(?:[•*-]|\d+\.)\s+(?:\*\*)?([^\n\-\$–—\(]+?)(?:\*\*)?(?:\s*\(?Price\s*[:\-–—]?\s*)?(?:\s*[:\-–—]\s*|\s+)\$?([\d,.]+)(?:\s*USD)?/gim;
285
+ const matches = Array.from(cleanContent.matchAll(bulletRegex));
286
+
287
+ if (matches.length >= 2) {
288
+ const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
289
+
290
+ for (const match of matches) {
291
+ let name = match[1]?.trim() || '';
292
+ name = name.replace(/\s*\(?Price\s*$/i, '').replace(/[:\-–—]\s*$/g, '').trim();
293
+ name = name.replace(/^\*\*|\*\*$/g, '').replace(/^\*|\*$/g, '').trim();
294
+
295
+ let price = match[2]?.trim() || '';
296
+ price = price.replace(/[,\s]+$/, '').trim();
297
+
298
+ if (name && price) {
299
+ const newProduct: Product = {
300
+ id: `text-prod-${name}-${price}`,
301
+ name,
302
+ price: `$${price}`,
303
+ };
304
+
305
+ const normName = normalize(name);
306
+ const sourceMatch = productsFromSources.find(s => {
307
+ const sn = normalize(s.name);
308
+ return sn.includes(normName) || normName.includes(sn);
309
+ });
310
+
311
+ if (sourceMatch) {
312
+ newProduct.image = sourceMatch.image;
313
+ newProduct.brand = sourceMatch.brand;
314
+ newProduct.link = sourceMatch.link;
315
+ newProduct.description = sourceMatch.description;
316
+ }
317
+
318
+ products.push(newProduct);
319
+ cleanContent = cleanContent.replace(match[0], `\n**${name}** — $${price}`);
320
+ }
321
+ }
322
+ cleanContent = cleanContent.replace(/\n{3,}/g, '\n\n').trim();
323
+ }
324
+
325
+ return { productsFromContent: products, cleanContent: cleanContent.trim() };
326
+ }
327
+
328
+ export function deduplicateProducts(
329
+ productsFromSources: Product[],
330
+ productsFromContent: Product[],
331
+ messageContent: string
332
+ ): Product[] {
333
+ if (productsFromContent.length > 0) return productsFromContent;
334
+
335
+ const seen = new Set<string>();
336
+ const contentLower = messageContent.toLowerCase();
337
+
338
+ return productsFromSources.filter(p => {
339
+ const id = String(p.id ?? p.name);
340
+ if (seen.has(id)) return false;
341
+ const mentioned =
342
+ contentLower.includes(p.name.toLowerCase()) ||
343
+ (p.brand ? contentLower.includes(p.brand.toLowerCase()) : false);
344
+ if (mentioned) { seen.add(id); return true; }
345
+ return false;
346
+ });
347
+ }
@@ -1,5 +1,6 @@
1
- import { VectorMatch, UITransformationResponse, PieChartData, BarChartData, LineChartDataPoint, TableData, CarouselProduct, VisualizationType } from '../types';
1
+ import { VectorMatch, UITransformationResponse, PieChartData, LineChartDataPoint, TableData, CarouselProduct, VisualizationType } from '../types';
2
2
  import { ILLMProvider } from '../llm/ILLMProvider';
3
+ import { resolveMetadataValue } from './synonyms';
3
4
 
4
5
  /**
5
6
  * UITransformer — Converts vector database results into structured UI representations
@@ -8,18 +9,6 @@ import { ILLMProvider } from '../llm/ILLMProvider';
8
9
  * format based on the content and structure of the data.
9
10
  */
10
11
  export class UITransformer {
11
- /**
12
- * Central dictionary of common synonyms for UI properties.
13
- * This allows the system to be schema-agnostic by guessing field names.
14
- */
15
- private static readonly SYNONYMS: Record<string, string[]> = {
16
- name: ['product', 'item', 'title', 'label', 'heading', 'subject'],
17
- price: ['cost', 'amount', 'msrp', 'price', 'rate', 'value', 'price_usd'],
18
- brand: ['manufacturer', 'vendor', 'make', 'company', 'brand_name', 'supplier'],
19
- image: ['imageUrl', 'thumbnail', 'img', 'url', 'photo', 'picture', 'media', 'image_url', 'main_image', 'product_image', 'thumb'],
20
- stock: ['inventory', 'quantity', 'count', 'availability', 'stock_level', 'inStock', 'is_available'],
21
- description: ['summary', 'content', 'body', 'text', 'info', 'details'],
22
- };
23
12
 
24
13
  /**
25
14
  * Main transformation method
@@ -121,33 +110,6 @@ export class UITransformer {
121
110
  };
122
111
  }
123
112
 
124
- /**
125
- * Transform data to bar chart format
126
- */
127
- private static transformToBarChart(
128
- data: VectorMatch[]
129
- ): UITransformationResponse {
130
- const categories = this.detectCategories(data);
131
- const categoryData = this.aggregateByCategory(data, categories);
132
-
133
- const barData: BarChartData[] = Object.entries(categoryData).map(([category, value]) => {
134
- const { inStockCount, outOfStockCount } = this.calculateStockCounts(category, data);
135
- return {
136
- category,
137
- value: value as number,
138
- inStockCount,
139
- outOfStockCount,
140
- };
141
- });
142
-
143
- return {
144
- type: 'bar_chart',
145
- title: 'Comparison by Category',
146
- description: `Comparing ${categories.length} categories`,
147
- data: barData,
148
- };
149
- }
150
-
151
113
  /**
152
114
  * Transform data to line chart format
153
115
  */
@@ -488,18 +450,7 @@ export class UITransformer {
488
450
  if (trainedKey && meta[trainedKey as string] !== undefined) return meta[trainedKey as string];
489
451
  }
490
452
 
491
- const synonyms = this.SYNONYMS[uiKey] || [];
492
- const searchKeys = [uiKey, ...synonyms].map((k) => k.toLowerCase());
493
-
494
- const foundDirectKey = Object.keys(meta).find((k) => searchKeys.includes(k.toLowerCase()));
495
- if (foundDirectKey) return meta[foundDirectKey];
496
-
497
- const foundPartialKey = Object.keys(meta).find((k) => {
498
- const keyLow = k.toLowerCase();
499
- return searchKeys.some((sk) => keyLow.includes(sk) || sk.includes(keyLow));
500
- });
501
-
502
- return foundPartialKey ? meta[foundPartialKey] : undefined;
453
+ return resolveMetadataValue(meta, uiKey);
503
454
  }
504
455
 
505
456
  private static extractProductInfo(
@@ -605,7 +556,7 @@ export class UITransformer {
605
556
  const meta = item.metadata || {};
606
557
  const itemCategory = (meta.category || meta.type || 'Other') as string;
607
558
 
608
- if (result.hasOwnProperty(itemCategory)) {
559
+ if (Object.prototype.hasOwnProperty.call(result, itemCategory)) {
609
560
  result[itemCategory]++;
610
561
  } else {
611
562
  result['Other'] = (result['Other'] || 0) + 1;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Shared synonym map used by UITransformer and MessageBubble to resolve
3
+ * schema-agnostic field names from vector metadata.
4
+ */
5
+ export const FIELD_SYNONYMS: Record<string, string[]> = {
6
+ name: ['product', 'item', 'title', 'label', 'heading', 'subject', 'product name', 'item name'],
7
+ price: ['cost', 'amount', 'msrp', 'price', 'rate', 'value', 'price_usd'],
8
+ brand: ['manufacturer', 'vendor', 'make', 'company', 'brand_name', 'supplier'],
9
+ image: ['imageUrl', 'thumbnail', 'img', 'url', 'photo', 'picture', 'media', 'image_url',
10
+ 'main_image', 'product_image', 'thumb'],
11
+ stock: ['inventory', 'quantity', 'count', 'availability', 'stock_level', 'inStock',
12
+ 'is_available', 'in stock', 'status'],
13
+ description: ['summary', 'content', 'body', 'text', 'info', 'details'],
14
+ link: ['url', 'href', 'product_url', 'page_url', 'link'],
15
+ };
16
+
17
+ /**
18
+ * Dynamically registers new synonyms into the global FIELD_SYNONYMS dictionary.
19
+ * You can call this during your app initialization to support custom schema fields.
20
+ */
21
+ export function addSynonyms(customSynonyms: Record<string, string[]>) {
22
+ for (const [key, aliases] of Object.entries(customSynonyms)) {
23
+ if (!FIELD_SYNONYMS[key]) {
24
+ FIELD_SYNONYMS[key] = [];
25
+ }
26
+ const existing = new Set(FIELD_SYNONYMS[key].map(s => s.toLowerCase()));
27
+
28
+ for (const alias of aliases) {
29
+ if (!existing.has(alias.toLowerCase())) {
30
+ FIELD_SYNONYMS[key].push(alias);
31
+ existing.add(alias.toLowerCase());
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Robustly extract a value from metadata by checking exact, case-insensitive,
39
+ * and synonym-based key matches.
40
+ */
41
+ export function resolveMetadataValue(
42
+ meta: Record<string, unknown>,
43
+ uiKey: string,
44
+ ): unknown {
45
+ const synonyms = FIELD_SYNONYMS[uiKey] ?? [];
46
+
47
+ const keys = Object.keys(meta);
48
+ const systemKeys = ['namespace', 'filename', 'filesize', 'filetype', 'docid', 'uploadedat', 'dimension', 'chunkindex'];
49
+
50
+ // 1. Exact case-insensitive match for primary key
51
+ let match = keys.find((k) => k.toLowerCase() === uiKey.toLowerCase());
52
+ if (match !== undefined) return meta[match];
53
+
54
+ // 2. Exact case-insensitive match for synonyms
55
+ match = keys.find((k) => synonyms.map(s => s.toLowerCase()).includes(k.toLowerCase()));
56
+ if (match !== undefined) return meta[match];
57
+
58
+ const isBlacklisted = (kl: string) => {
59
+ return systemKeys.includes(kl) || kl.startsWith('option1') || kl.startsWith('option2') || kl.startsWith('option3');
60
+ };
61
+
62
+ // 3. Partial match (substring) strictly for primary key
63
+ match = keys.find((k) => {
64
+ const kl = k.toLowerCase();
65
+ if (isBlacklisted(kl)) return false;
66
+ return kl.includes(uiKey.toLowerCase());
67
+ });
68
+ if (match !== undefined) return meta[match];
69
+
70
+ // 4. Partial match (substring) for synonyms
71
+ match = keys.find((k) => {
72
+ const kl = k.toLowerCase();
73
+ if (isBlacklisted(kl)) return false;
74
+ return synonyms.some((sk) => kl.includes(sk.toLowerCase()) || sk.toLowerCase().includes(kl));
75
+ });
76
+
77
+ return match !== undefined ? meta[match] : undefined;
78
+ }