@retrivora-ai/rag-engine 1.5.8 → 1.6.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.
@@ -2279,7 +2279,7 @@ When the user asks for a visual representation, comparison, distribution, breakd
2279
2279
  "chartType": "pie" | "bar" | "line",
2280
2280
  "xAxisKey": "label",
2281
2281
  "dataKeys": ["value"],
2282
- "columns": ["name", "category", "price", "stockStatus"],
2282
+ "columns": ["dynamicFieldKey1", "dynamicFieldKey2"],
2283
2283
  "insights": ["Short takeaway 1", "Short takeaway 2"],
2284
2284
  "data": []
2285
2285
  }
@@ -2292,7 +2292,7 @@ When the user asks for a visual representation, comparison, distribution, breakd
2292
2292
  - If stock information exists, preserve it in each row with fields like "inStock", "inStockCount", "outOfStockCount", or "stockStatus".
2293
2293
  - If the user asks to highlight what is in stock, include those stock-related fields in the chart data and mention the pattern in "insights".
2294
2294
  - For carousel rows, include product-friendly fields such as id, name, brand, price, image, link, category, stockStatus.
2295
- - For tables, keep "data" row-oriented and include "columns" when possible.
2295
+ - For tables, keep "data" row-oriented and include "columns" only as dynamic field keys/order hints from the fetched data, never as hardcoded UI labels.
2296
2296
 
2297
2297
  4. FORMAT RULES
2298
2298
  - Never use ASCII art, markdown tables as the primary answer, or text-only fake charts when a UI block is requested.
@@ -3842,7 +3842,7 @@ When the user asks for a visual representation, comparison, distribution, breakd
3842
3842
  "chartType": "pie" | "bar" | "line",
3843
3843
  "xAxisKey": "label",
3844
3844
  "dataKeys": ["value"],
3845
- "columns": ["name", "category", "price", "stockStatus"],
3845
+ "columns": ["dynamicFieldKey1", "dynamicFieldKey2"],
3846
3846
  "insights": ["Short takeaway 1", "Short takeaway 2"],
3847
3847
  "data": []
3848
3848
  }
@@ -3855,7 +3855,7 @@ When the user asks for a visual representation, comparison, distribution, breakd
3855
3855
  - If stock information exists, preserve it in each row with fields like "inStock", "inStockCount", "outOfStockCount", or "stockStatus".
3856
3856
  - If the user asks to highlight what is in stock, include those stock-related fields in the chart data and mention the pattern in "insights".
3857
3857
  - For carousel rows, include product-friendly fields such as id, name, brand, price, image, link, category, stockStatus.
3858
- - For tables, keep "data" row-oriented and include "columns" when possible.
3858
+ - For tables, keep "data" row-oriented and include "columns" only as dynamic field keys/order hints from the fetched data, never as hardcoded UI labels.
3859
3859
 
3860
3860
  4. FORMAT RULES
3861
3861
  - Never use ASCII art, markdown tables as the primary answer, or text-only fake charts when a UI block is requested.
@@ -9,7 +9,7 @@ import {
9
9
  sseFrame,
10
10
  sseMetaFrame,
11
11
  sseTextFrame
12
- } from "../chunk-ID2Q4CF7.mjs";
12
+ } from "../chunk-5K7EU4KW.mjs";
13
13
  import "../chunk-YLTMFW4M.mjs";
14
14
  import "../chunk-X4TOT24V.mjs";
15
15
  export {
package/dist/index.js CHANGED
@@ -248,8 +248,11 @@ function DynamicChart({
248
248
  const isCompact = viewportSize === "compact" || containerWidth > 0 && containerWidth < 420;
249
249
  const isMedium = !isCompact && (viewportSize === "medium" || containerWidth > 0 && containerWidth < 640);
250
250
  const chartHeightClass = isTiny ? "h-48 min-h-[180px]" : isCompact ? "h-56 min-h-[220px]" : isMedium ? "h-64 min-h-[250px]" : "h-72 min-h-[280px]";
251
- const pieOuterRadius = isTiny ? 44 : isCompact ? 56 : isMedium ? 68 : 80;
252
- const pieLabel = ({ name, percent }) => isTiny ? "" : isCompact ? `${name}` : `${name} ${((percent != null ? percent : 0) * 100).toFixed(0)}%`;
251
+ const pieOuterRadius = (() => {
252
+ if (containerWidth <= 0) return isTiny ? 44 : isCompact ? 56 : isMedium ? 68 : 80;
253
+ const squareSize = Math.min(containerWidth, isTiny ? 180 : isCompact ? 220 : isMedium ? 250 : 280);
254
+ return Math.max(36, Math.floor(squareSize * 0.28));
255
+ })();
253
256
  const axisTick = { fontSize: isTiny ? 9 : isCompact ? 10 : 12, fill: "#64748b" };
254
257
  const showLegend = !isTiny;
255
258
  const truncateAxisLabel = (value) => {
@@ -357,7 +360,7 @@ function DynamicChart({
357
360
  return row;
358
361
  };
359
362
  if (type === "pie") {
360
- return /* @__PURE__ */ import_react4.default.createElement("div", { ref: containerRef, className: `w-full min-w-0 ${chartHeightClass} mt-4 mb-2 select-none overflow-hidden` }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.PieChart, null, /* @__PURE__ */ import_react4.default.createElement(
363
+ return /* @__PURE__ */ import_react4.default.createElement("div", { ref: containerRef, className: `w-full min-w-0 ${chartHeightClass} mt-4 mb-2 select-none overflow-hidden` }, /* @__PURE__ */ import_react4.default.createElement("div", { className: "mx-auto h-full aspect-square max-w-full" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.PieChart, null, /* @__PURE__ */ import_react4.default.createElement(
361
364
  import_recharts.Pie,
362
365
  {
363
366
  data: sanitizedData,
@@ -366,10 +369,11 @@ function DynamicChart({
366
369
  cx: "50%",
367
370
  cy: "50%",
368
371
  outerRadius: pieOuterRadius,
369
- label: isTiny ? false : pieLabel
372
+ label: false,
373
+ labelLine: false
370
374
  },
371
375
  sanitizedData.map((entry, index) => /* @__PURE__ */ import_react4.default.createElement(import_recharts.Cell, { key: `cell-${index}`, fill: stockAwareColor(entry, index) }))
372
- ), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Tooltip, { content: (props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === "string" ? props.label : void 0) }), showLegend && /* @__PURE__ */ import_react4.default.createElement(import_recharts.Legend, { wrapperStyle: { fontSize: isCompact ? 10 : 12 } }))));
376
+ ), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Tooltip, { content: (props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === "string" ? props.label : void 0) }), showLegend && !isCompact && /* @__PURE__ */ import_react4.default.createElement(import_recharts.Legend, { wrapperStyle: { fontSize: 12 }, verticalAlign: "bottom" })))));
373
377
  }
374
378
  return /* @__PURE__ */ import_react4.default.createElement("div", { ref: containerRef, className: `w-full min-w-0 ${chartHeightClass} mt-4 mb-2 select-none overflow-hidden` }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, type === "bar" ? /* @__PURE__ */ import_react4.default.createElement(import_recharts.BarChart, { data: sanitizedData, margin: chartMargin }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3", vertical: false, stroke: "#e2e8f0" }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.XAxis, { dataKey: finalXKey, tick: axisTick, tickFormatter: truncateAxisLabel, tickLine: false, axisLine: { stroke: "#cbd5e1" }, interval: 0, minTickGap: isTiny ? 2 : isCompact ? 6 : 12 }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.YAxis, { tick: axisTick, tickLine: false, axisLine: false }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Tooltip, { content: (props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === "string" ? props.label : void 0), cursor: { fill: "#f1f5f9" } }), showLegend && /* @__PURE__ */ import_react4.default.createElement(import_recharts.Legend, { wrapperStyle: { fontSize: isCompact ? 10 : 12 } }), finalDataKeys.map((key, index) => /* @__PURE__ */ import_react4.default.createElement(
375
379
  import_recharts.Bar,
@@ -459,6 +463,12 @@ function extractLikelyJsonBlock(raw) {
459
463
  function looksLikeStructuredPayload(raw) {
460
464
  return /"(view|chartType|type|data|items|rows|products|results)"\s*:/.test(raw);
461
465
  }
466
+ function stripMarkdownTables(raw) {
467
+ return raw.replace(
468
+ /(?:^|\n)\|[^\n]+\|\n\|(?:\s*:?-+:?\s*\|)+\n(?:\|[^\n]*\|\n?)*/g,
469
+ "\n\n"
470
+ );
471
+ }
462
472
  function resolveImage(data) {
463
473
  for (const key of ["image", "img", "thumbnail"]) {
464
474
  const v = data[key];
@@ -475,23 +485,45 @@ function normaliseChild(children) {
475
485
  }
476
486
  return children;
477
487
  }
488
+ function normaliseFieldName(value) {
489
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
490
+ }
491
+ function formatColumnLabel(value) {
492
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim().replace(/\b\w/g, (char) => char.toUpperCase());
493
+ }
478
494
  function DataTable({ config, viewportSize = "large" }) {
479
495
  const isCompact = viewportSize === "compact";
480
496
  const isMedium = viewportSize === "medium";
481
- const keys = import_react5.default.useMemo(() => {
497
+ const columns = import_react5.default.useMemo(() => {
498
+ const rowKeys = Array.from(
499
+ new Set(
500
+ config.data.flatMap((row) => Object.keys(row))
501
+ )
502
+ );
503
+ const findAccessor = (label) => {
504
+ const exact = rowKeys.find((key) => key === label);
505
+ if (exact) return exact;
506
+ const normalisedLabel = normaliseFieldName(label);
507
+ const normalisedMatch = rowKeys.find((key) => normaliseFieldName(key) === normalisedLabel);
508
+ if (normalisedMatch) return normalisedMatch;
509
+ return label;
510
+ };
482
511
  if (Array.isArray(config.columns) && config.columns.length) {
483
- return config.columns;
512
+ return config.columns.map((label) => ({
513
+ label: formatColumnLabel(label),
514
+ accessor: findAccessor(label)
515
+ }));
484
516
  }
485
517
  if (Array.isArray(config.dataKeys) && config.dataKeys.length) {
486
- if (config.xAxisKey && !config.dataKeys.includes(config.xAxisKey)) {
487
- return [config.xAxisKey, ...config.dataKeys];
488
- }
489
- return config.dataKeys;
518
+ const keys = config.xAxisKey && !config.dataKeys.includes(config.xAxisKey) ? [config.xAxisKey, ...config.dataKeys] : config.dataKeys;
519
+ return keys.map((key) => ({
520
+ label: formatColumnLabel(key),
521
+ accessor: findAccessor(key)
522
+ }));
490
523
  }
491
- if (config.data.length > 0) return Object.keys(config.data[0]);
492
- return [];
524
+ return rowKeys.map((key) => ({ label: formatColumnLabel(key), accessor: key }));
493
525
  }, [config]);
494
- if (!config.data.length || !keys.length) {
526
+ if (!config.data.length || !columns.length) {
495
527
  return /* @__PURE__ */ import_react5.default.createElement("p", { className: "text-xs text-slate-400 italic my-2" }, "No data to display.");
496
528
  }
497
529
  const formatCell = (val) => {
@@ -499,26 +531,26 @@ function DataTable({ config, viewportSize = "large" }) {
499
531
  if (typeof val === "boolean") return val ? "\u2713" : "\u2717";
500
532
  return String(val);
501
533
  };
502
- return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-4 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm overflow-hidden" }, config.title && /* @__PURE__ */ import_react5.default.createElement("div", { className: "px-4 py-2.5 bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10" }, /* @__PURE__ */ import_react5.default.createElement("p", { className: "text-xs font-semibold text-slate-600 dark:text-white/70 uppercase tracking-wide" }, config.title)), /* @__PURE__ */ import_react5.default.createElement("div", { className: "overflow-x-auto" }, /* @__PURE__ */ import_react5.default.createElement("table", { className: `w-full text-left border-collapse ${isCompact ? "min-w-[240px] text-xs" : isMedium ? "min-w-[280px] text-[13px]" : "min-w-[320px] text-sm"}` }, /* @__PURE__ */ import_react5.default.createElement("thead", { className: "bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10" }, /* @__PURE__ */ import_react5.default.createElement("tr", null, keys.map((k) => /* @__PURE__ */ import_react5.default.createElement(
534
+ return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-4 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm overflow-hidden" }, config.title && /* @__PURE__ */ import_react5.default.createElement("div", { className: "px-4 py-2.5 bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10" }, /* @__PURE__ */ import_react5.default.createElement("p", { className: "text-xs font-semibold text-slate-600 dark:text-white/70 uppercase tracking-wide" }, config.title)), /* @__PURE__ */ import_react5.default.createElement("div", { className: "overflow-x-auto" }, /* @__PURE__ */ import_react5.default.createElement("table", { className: `w-full text-left border-collapse ${isCompact ? "min-w-[240px] text-xs" : isMedium ? "min-w-[280px] text-[13px]" : "min-w-[320px] text-sm"}` }, /* @__PURE__ */ import_react5.default.createElement("thead", { className: "bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10" }, /* @__PURE__ */ import_react5.default.createElement("tr", null, columns.map((column) => /* @__PURE__ */ import_react5.default.createElement(
503
535
  "th",
504
536
  {
505
- key: k,
537
+ key: column.label,
506
538
  className: `${isCompact ? "px-2.5 py-2" : "px-4 py-3"} font-semibold text-slate-700 dark:text-white/90 whitespace-nowrap`
507
539
  },
508
- k
540
+ column.label
509
541
  )))), /* @__PURE__ */ import_react5.default.createElement("tbody", null, config.data.map((row, ri) => /* @__PURE__ */ import_react5.default.createElement(
510
542
  "tr",
511
543
  {
512
544
  key: ri,
513
545
  className: "border-b border-slate-100 dark:border-white/5 last:border-0 hover:bg-slate-50/60 dark:hover:bg-white/5 transition-colors"
514
546
  },
515
- keys.map((k) => /* @__PURE__ */ import_react5.default.createElement(
547
+ columns.map((column) => /* @__PURE__ */ import_react5.default.createElement(
516
548
  "td",
517
549
  {
518
- key: k,
519
- className: `${isCompact ? "px-2.5 py-2" : "px-4 py-3"} text-slate-600 dark:text-white/70 whitespace-nowrap`
550
+ key: column.label,
551
+ className: `${isCompact ? "px-2.5 py-2" : "px-4 py-3"} text-slate-600 dark:text-white/70 ${column.accessor.toLowerCase().includes("description") ? "whitespace-normal break-words" : "whitespace-nowrap"}`
520
552
  },
521
- formatCell(row[k])
553
+ formatCell(row[column.accessor])
522
554
  ))
523
555
  ))))), /* @__PURE__ */ import_react5.default.createElement("div", { className: "px-4 py-2 bg-slate-50 dark:bg-white/5 border-t border-slate-100 dark:border-white/5" }, /* @__PURE__ */ import_react5.default.createElement("p", { className: "text-[11px] text-slate-400 dark:text-white/30" }, config.data.length, " row", config.data.length !== 1 ? "s" : "")));
524
556
  }
@@ -715,6 +747,7 @@ function MessageBubble({
715
747
  }, [productsFromSources, productsFromContent, message.content]);
716
748
  const processedMarkdown = import_react5.default.useMemo(() => {
717
749
  let raw = cleanContent || message.content;
750
+ const hasStructuredUiBlock = /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
718
751
  raw = raw.replace(/([^\n])\n\|/g, "$1\n\n|").replace(/(\|[^\n]*\|)\n\n+(?=\|)/g, "$1\n");
719
752
  if (!isStreaming) {
720
753
  raw = raw.replace(/(?:^|\n)\s*\{[\s\S]*\}\s*(?:\n|$)/g, (match) => {
@@ -730,6 +763,9 @@ ${match.trim()}
730
763
  `;
731
764
  });
732
765
  }
766
+ if (hasStructuredUiBlock) {
767
+ raw = stripMarkdownTables(raw).replace(/\n{3,}/g, "\n\n").trim();
768
+ }
733
769
  if (isStreaming) {
734
770
  raw = raw.replace(/((?:^|\n)\|.*)+/g, (match) => {
735
771
  return `
package/dist/index.mjs CHANGED
@@ -211,8 +211,11 @@ function DynamicChart({
211
211
  const isCompact = viewportSize === "compact" || containerWidth > 0 && containerWidth < 420;
212
212
  const isMedium = !isCompact && (viewportSize === "medium" || containerWidth > 0 && containerWidth < 640);
213
213
  const chartHeightClass = isTiny ? "h-48 min-h-[180px]" : isCompact ? "h-56 min-h-[220px]" : isMedium ? "h-64 min-h-[250px]" : "h-72 min-h-[280px]";
214
- const pieOuterRadius = isTiny ? 44 : isCompact ? 56 : isMedium ? 68 : 80;
215
- const pieLabel = ({ name, percent }) => isTiny ? "" : isCompact ? `${name}` : `${name} ${((percent != null ? percent : 0) * 100).toFixed(0)}%`;
214
+ const pieOuterRadius = (() => {
215
+ if (containerWidth <= 0) return isTiny ? 44 : isCompact ? 56 : isMedium ? 68 : 80;
216
+ const squareSize = Math.min(containerWidth, isTiny ? 180 : isCompact ? 220 : isMedium ? 250 : 280);
217
+ return Math.max(36, Math.floor(squareSize * 0.28));
218
+ })();
216
219
  const axisTick = { fontSize: isTiny ? 9 : isCompact ? 10 : 12, fill: "#64748b" };
217
220
  const showLegend = !isTiny;
218
221
  const truncateAxisLabel = (value) => {
@@ -320,7 +323,7 @@ function DynamicChart({
320
323
  return row;
321
324
  };
322
325
  if (type === "pie") {
323
- return /* @__PURE__ */ React4.createElement("div", { ref: containerRef, className: `w-full min-w-0 ${chartHeightClass} mt-4 mb-2 select-none overflow-hidden` }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(PieChart, null, /* @__PURE__ */ React4.createElement(
326
+ return /* @__PURE__ */ React4.createElement("div", { ref: containerRef, className: `w-full min-w-0 ${chartHeightClass} mt-4 mb-2 select-none overflow-hidden` }, /* @__PURE__ */ React4.createElement("div", { className: "mx-auto h-full aspect-square max-w-full" }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(PieChart, null, /* @__PURE__ */ React4.createElement(
324
327
  Pie,
325
328
  {
326
329
  data: sanitizedData,
@@ -329,10 +332,11 @@ function DynamicChart({
329
332
  cx: "50%",
330
333
  cy: "50%",
331
334
  outerRadius: pieOuterRadius,
332
- label: isTiny ? false : pieLabel
335
+ label: false,
336
+ labelLine: false
333
337
  },
334
338
  sanitizedData.map((entry, index) => /* @__PURE__ */ React4.createElement(Cell, { key: `cell-${index}`, fill: stockAwareColor(entry, index) }))
335
- ), /* @__PURE__ */ React4.createElement(Tooltip, { content: (props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === "string" ? props.label : void 0) }), showLegend && /* @__PURE__ */ React4.createElement(Legend, { wrapperStyle: { fontSize: isCompact ? 10 : 12 } }))));
339
+ ), /* @__PURE__ */ React4.createElement(Tooltip, { content: (props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === "string" ? props.label : void 0) }), showLegend && !isCompact && /* @__PURE__ */ React4.createElement(Legend, { wrapperStyle: { fontSize: 12 }, verticalAlign: "bottom" })))));
336
340
  }
337
341
  return /* @__PURE__ */ React4.createElement("div", { ref: containerRef, className: `w-full min-w-0 ${chartHeightClass} mt-4 mb-2 select-none overflow-hidden` }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, type === "bar" ? /* @__PURE__ */ React4.createElement(BarChart, { data: sanitizedData, margin: chartMargin }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", vertical: false, stroke: "#e2e8f0" }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: finalXKey, tick: axisTick, tickFormatter: truncateAxisLabel, tickLine: false, axisLine: { stroke: "#cbd5e1" }, interval: 0, minTickGap: isTiny ? 2 : isCompact ? 6 : 12 }), /* @__PURE__ */ React4.createElement(YAxis, { tick: axisTick, tickLine: false, axisLine: false }), /* @__PURE__ */ React4.createElement(Tooltip, { content: (props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === "string" ? props.label : void 0), cursor: { fill: "#f1f5f9" } }), showLegend && /* @__PURE__ */ React4.createElement(Legend, { wrapperStyle: { fontSize: isCompact ? 10 : 12 } }), finalDataKeys.map((key, index) => /* @__PURE__ */ React4.createElement(
338
342
  Bar,
@@ -422,6 +426,12 @@ function extractLikelyJsonBlock(raw) {
422
426
  function looksLikeStructuredPayload(raw) {
423
427
  return /"(view|chartType|type|data|items|rows|products|results)"\s*:/.test(raw);
424
428
  }
429
+ function stripMarkdownTables(raw) {
430
+ return raw.replace(
431
+ /(?:^|\n)\|[^\n]+\|\n\|(?:\s*:?-+:?\s*\|)+\n(?:\|[^\n]*\|\n?)*/g,
432
+ "\n\n"
433
+ );
434
+ }
425
435
  function resolveImage(data) {
426
436
  for (const key of ["image", "img", "thumbnail"]) {
427
437
  const v = data[key];
@@ -438,23 +448,45 @@ function normaliseChild(children) {
438
448
  }
439
449
  return children;
440
450
  }
451
+ function normaliseFieldName(value) {
452
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
453
+ }
454
+ function formatColumnLabel(value) {
455
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim().replace(/\b\w/g, (char) => char.toUpperCase());
456
+ }
441
457
  function DataTable({ config, viewportSize = "large" }) {
442
458
  const isCompact = viewportSize === "compact";
443
459
  const isMedium = viewportSize === "medium";
444
- const keys = React5.useMemo(() => {
460
+ const columns = React5.useMemo(() => {
461
+ const rowKeys = Array.from(
462
+ new Set(
463
+ config.data.flatMap((row) => Object.keys(row))
464
+ )
465
+ );
466
+ const findAccessor = (label) => {
467
+ const exact = rowKeys.find((key) => key === label);
468
+ if (exact) return exact;
469
+ const normalisedLabel = normaliseFieldName(label);
470
+ const normalisedMatch = rowKeys.find((key) => normaliseFieldName(key) === normalisedLabel);
471
+ if (normalisedMatch) return normalisedMatch;
472
+ return label;
473
+ };
445
474
  if (Array.isArray(config.columns) && config.columns.length) {
446
- return config.columns;
475
+ return config.columns.map((label) => ({
476
+ label: formatColumnLabel(label),
477
+ accessor: findAccessor(label)
478
+ }));
447
479
  }
448
480
  if (Array.isArray(config.dataKeys) && config.dataKeys.length) {
449
- if (config.xAxisKey && !config.dataKeys.includes(config.xAxisKey)) {
450
- return [config.xAxisKey, ...config.dataKeys];
451
- }
452
- return config.dataKeys;
481
+ const keys = config.xAxisKey && !config.dataKeys.includes(config.xAxisKey) ? [config.xAxisKey, ...config.dataKeys] : config.dataKeys;
482
+ return keys.map((key) => ({
483
+ label: formatColumnLabel(key),
484
+ accessor: findAccessor(key)
485
+ }));
453
486
  }
454
- if (config.data.length > 0) return Object.keys(config.data[0]);
455
- return [];
487
+ return rowKeys.map((key) => ({ label: formatColumnLabel(key), accessor: key }));
456
488
  }, [config]);
457
- if (!config.data.length || !keys.length) {
489
+ if (!config.data.length || !columns.length) {
458
490
  return /* @__PURE__ */ React5.createElement("p", { className: "text-xs text-slate-400 italic my-2" }, "No data to display.");
459
491
  }
460
492
  const formatCell = (val) => {
@@ -462,26 +494,26 @@ function DataTable({ config, viewportSize = "large" }) {
462
494
  if (typeof val === "boolean") return val ? "\u2713" : "\u2717";
463
495
  return String(val);
464
496
  };
465
- return /* @__PURE__ */ React5.createElement("div", { className: "my-4 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm overflow-hidden" }, config.title && /* @__PURE__ */ React5.createElement("div", { className: "px-4 py-2.5 bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10" }, /* @__PURE__ */ React5.createElement("p", { className: "text-xs font-semibold text-slate-600 dark:text-white/70 uppercase tracking-wide" }, config.title)), /* @__PURE__ */ React5.createElement("div", { className: "overflow-x-auto" }, /* @__PURE__ */ React5.createElement("table", { className: `w-full text-left border-collapse ${isCompact ? "min-w-[240px] text-xs" : isMedium ? "min-w-[280px] text-[13px]" : "min-w-[320px] text-sm"}` }, /* @__PURE__ */ React5.createElement("thead", { className: "bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10" }, /* @__PURE__ */ React5.createElement("tr", null, keys.map((k) => /* @__PURE__ */ React5.createElement(
497
+ return /* @__PURE__ */ React5.createElement("div", { className: "my-4 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm overflow-hidden" }, config.title && /* @__PURE__ */ React5.createElement("div", { className: "px-4 py-2.5 bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10" }, /* @__PURE__ */ React5.createElement("p", { className: "text-xs font-semibold text-slate-600 dark:text-white/70 uppercase tracking-wide" }, config.title)), /* @__PURE__ */ React5.createElement("div", { className: "overflow-x-auto" }, /* @__PURE__ */ React5.createElement("table", { className: `w-full text-left border-collapse ${isCompact ? "min-w-[240px] text-xs" : isMedium ? "min-w-[280px] text-[13px]" : "min-w-[320px] text-sm"}` }, /* @__PURE__ */ React5.createElement("thead", { className: "bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10" }, /* @__PURE__ */ React5.createElement("tr", null, columns.map((column) => /* @__PURE__ */ React5.createElement(
466
498
  "th",
467
499
  {
468
- key: k,
500
+ key: column.label,
469
501
  className: `${isCompact ? "px-2.5 py-2" : "px-4 py-3"} font-semibold text-slate-700 dark:text-white/90 whitespace-nowrap`
470
502
  },
471
- k
503
+ column.label
472
504
  )))), /* @__PURE__ */ React5.createElement("tbody", null, config.data.map((row, ri) => /* @__PURE__ */ React5.createElement(
473
505
  "tr",
474
506
  {
475
507
  key: ri,
476
508
  className: "border-b border-slate-100 dark:border-white/5 last:border-0 hover:bg-slate-50/60 dark:hover:bg-white/5 transition-colors"
477
509
  },
478
- keys.map((k) => /* @__PURE__ */ React5.createElement(
510
+ columns.map((column) => /* @__PURE__ */ React5.createElement(
479
511
  "td",
480
512
  {
481
- key: k,
482
- className: `${isCompact ? "px-2.5 py-2" : "px-4 py-3"} text-slate-600 dark:text-white/70 whitespace-nowrap`
513
+ key: column.label,
514
+ className: `${isCompact ? "px-2.5 py-2" : "px-4 py-3"} text-slate-600 dark:text-white/70 ${column.accessor.toLowerCase().includes("description") ? "whitespace-normal break-words" : "whitespace-nowrap"}`
483
515
  },
484
- formatCell(row[k])
516
+ formatCell(row[column.accessor])
485
517
  ))
486
518
  ))))), /* @__PURE__ */ React5.createElement("div", { className: "px-4 py-2 bg-slate-50 dark:bg-white/5 border-t border-slate-100 dark:border-white/5" }, /* @__PURE__ */ React5.createElement("p", { className: "text-[11px] text-slate-400 dark:text-white/30" }, config.data.length, " row", config.data.length !== 1 ? "s" : "")));
487
519
  }
@@ -678,6 +710,7 @@ function MessageBubble({
678
710
  }, [productsFromSources, productsFromContent, message.content]);
679
711
  const processedMarkdown = React5.useMemo(() => {
680
712
  let raw = cleanContent || message.content;
713
+ const hasStructuredUiBlock = /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
681
714
  raw = raw.replace(/([^\n])\n\|/g, "$1\n\n|").replace(/(\|[^\n]*\|)\n\n+(?=\|)/g, "$1\n");
682
715
  if (!isStreaming) {
683
716
  raw = raw.replace(/(?:^|\n)\s*\{[\s\S]*\}\s*(?:\n|$)/g, (match) => {
@@ -693,6 +726,9 @@ ${match.trim()}
693
726
  `;
694
727
  });
695
728
  }
729
+ if (hasStructuredUiBlock) {
730
+ raw = stripMarkdownTables(raw).replace(/\n{3,}/g, "\n\n").trim();
731
+ }
696
732
  if (isStreaming) {
697
733
  raw = raw.replace(/((?:^|\n)\|.*)+/g, (match) => {
698
734
  return `
package/dist/server.js CHANGED
@@ -3933,7 +3933,7 @@ When the user asks for a visual representation, comparison, distribution, breakd
3933
3933
  "chartType": "pie" | "bar" | "line",
3934
3934
  "xAxisKey": "label",
3935
3935
  "dataKeys": ["value"],
3936
- "columns": ["name", "category", "price", "stockStatus"],
3936
+ "columns": ["dynamicFieldKey1", "dynamicFieldKey2"],
3937
3937
  "insights": ["Short takeaway 1", "Short takeaway 2"],
3938
3938
  "data": []
3939
3939
  }
@@ -3946,7 +3946,7 @@ When the user asks for a visual representation, comparison, distribution, breakd
3946
3946
  - If stock information exists, preserve it in each row with fields like "inStock", "inStockCount", "outOfStockCount", or "stockStatus".
3947
3947
  - If the user asks to highlight what is in stock, include those stock-related fields in the chart data and mention the pattern in "insights".
3948
3948
  - For carousel rows, include product-friendly fields such as id, name, brand, price, image, link, category, stockStatus.
3949
- - For tables, keep "data" row-oriented and include "columns" when possible.
3949
+ - For tables, keep "data" row-oriented and include "columns" only as dynamic field keys/order hints from the fetched data, never as hardcoded UI labels.
3950
3950
 
3951
3951
  4. FORMAT RULES
3952
3952
  - Never use ASCII art, markdown tables as the primary answer, or text-only fake charts when a UI block is requested.
package/dist/server.mjs CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  sseFrame,
40
40
  sseMetaFrame,
41
41
  sseTextFrame
42
- } from "./chunk-ID2Q4CF7.mjs";
42
+ } from "./chunk-5K7EU4KW.mjs";
43
43
  import "./chunk-YLTMFW4M.mjs";
44
44
  import {
45
45
  PineconeProvider
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retrivora-ai/rag-engine",
3
- "version": "1.5.8",
3
+ "version": "1.6.0",
4
4
  "description": "Retrivora AI is a plug-and-play AI engine for RAG chat experiences — generic vector DB + LLM provider, embeddable or standalone.",
5
5
  "author": "Abhinav Alkuchi",
6
6
  "license": "MIT",
@@ -64,13 +64,11 @@ export function DynamicChart({
64
64
  : isMedium
65
65
  ? 'h-64 min-h-[250px]'
66
66
  : 'h-72 min-h-[280px]';
67
- const pieOuterRadius = isTiny ? 44 : isCompact ? 56 : isMedium ? 68 : 80;
68
- const pieLabel = ({ name, percent }: { name?: string; percent?: number }) =>
69
- isTiny
70
- ? ''
71
- : isCompact
72
- ? `${name}`
73
- : `${name} ${((percent ?? 0) * 100).toFixed(0)}%`;
67
+ const pieOuterRadius = (() => {
68
+ if (containerWidth <= 0) return isTiny ? 44 : isCompact ? 56 : isMedium ? 68 : 80;
69
+ const squareSize = Math.min(containerWidth, isTiny ? 180 : isCompact ? 220 : isMedium ? 250 : 280);
70
+ return Math.max(36, Math.floor(squareSize * 0.28));
71
+ })();
74
72
  const axisTick = { fontSize: isTiny ? 9 : isCompact ? 10 : 12, fill: '#64748b' };
75
73
  const showLegend = !isTiny;
76
74
  const truncateAxisLabel = (value: string | number) => {
@@ -235,25 +233,28 @@ export function DynamicChart({
235
233
  if (type === 'pie') {
236
234
  return (
237
235
  <div ref={containerRef} className={`w-full min-w-0 ${chartHeightClass} mt-4 mb-2 select-none overflow-hidden`}>
238
- <ResponsiveContainer width="100%" height="100%">
239
- <PieChart>
240
- <Pie
241
- data={sanitizedData}
242
- dataKey={pieDataKey}
243
- nameKey={finalXKey}
244
- cx="50%"
245
- cy="50%"
246
- outerRadius={pieOuterRadius}
247
- label={isTiny ? false : pieLabel}
248
- >
249
- {sanitizedData.map((entry, index) => (
250
- <Cell key={`cell-${index}`} fill={stockAwareColor(entry, index)} />
251
- ))}
252
- </Pie>
253
- <Tooltip content={(props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === 'string' ? props.label : undefined)} />
254
- {showLegend && <Legend wrapperStyle={{ fontSize: isCompact ? 10 : 12 }} />}
255
- </PieChart>
256
- </ResponsiveContainer>
236
+ <div className="mx-auto h-full aspect-square max-w-full">
237
+ <ResponsiveContainer width="100%" height="100%">
238
+ <PieChart>
239
+ <Pie
240
+ data={sanitizedData}
241
+ dataKey={pieDataKey}
242
+ nameKey={finalXKey}
243
+ cx="50%"
244
+ cy="50%"
245
+ outerRadius={pieOuterRadius}
246
+ label={false}
247
+ labelLine={false}
248
+ >
249
+ {sanitizedData.map((entry, index) => (
250
+ <Cell key={`cell-${index}`} fill={stockAwareColor(entry, index)} />
251
+ ))}
252
+ </Pie>
253
+ <Tooltip content={(props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === 'string' ? props.label : undefined)} />
254
+ {showLegend && !isCompact && <Legend wrapperStyle={{ fontSize: 12 }} verticalAlign="bottom" />}
255
+ </PieChart>
256
+ </ResponsiveContainer>
257
+ </div>
257
258
  </div>
258
259
  );
259
260
  }
@@ -106,6 +106,13 @@ function looksLikeStructuredPayload(raw: string): boolean {
106
106
  return /"(view|chartType|type|data|items|rows|products|results)"\s*:/.test(raw);
107
107
  }
108
108
 
109
+ function stripMarkdownTables(raw: string): string {
110
+ return raw.replace(
111
+ /(?:^|\n)\|[^\n]+\|\n\|(?:\s*:?-+:?\s*\|)+\n(?:\|[^\n]*\|\n?)*/g,
112
+ '\n\n',
113
+ );
114
+ }
115
+
109
116
  // ─── Tiny helpers ─────────────────────────────────────────────────────────────
110
117
 
111
118
  function resolveImage(data: Record<string, unknown>): string | undefined {
@@ -132,6 +139,19 @@ function normaliseChild(children: React.ReactNode): React.ReactNode {
132
139
  return children;
133
140
  }
134
141
 
142
+ function normaliseFieldName(value: string): string {
143
+ return value.toLowerCase().replace(/[^a-z0-9]/g, '');
144
+ }
145
+
146
+ function formatColumnLabel(value: string): string {
147
+ return value
148
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
149
+ .replace(/[_-]+/g, ' ')
150
+ .replace(/\s+/g, ' ')
151
+ .trim()
152
+ .replace(/\b\w/g, (char) => char.toUpperCase());
153
+ }
154
+
135
155
  // ─── Inline data table renderer ───────────────────────────────────────────────
136
156
  // Handles chart configs where type === "table". The LLM may emit any of:
137
157
  // columns, dataKeys (+optional xAxisKey), or no key hints (auto-detect from row).
@@ -149,24 +169,46 @@ interface TableConfig {
149
169
  function DataTable({ config, viewportSize = 'large' }: { config: TableConfig; viewportSize?: ChatViewportSize }) {
150
170
  const isCompact = viewportSize === 'compact';
151
171
  const isMedium = viewportSize === 'medium';
152
- const keys = React.useMemo<string[]>(() => {
153
- // Priority 1: explicit columns list
172
+ const columns = React.useMemo<Array<{ label: string; accessor: string }>>(() => {
173
+ const rowKeys = Array.from(
174
+ new Set(
175
+ config.data.flatMap((row) => Object.keys(row)),
176
+ ),
177
+ );
178
+
179
+ const findAccessor = (label: string) => {
180
+ const exact = rowKeys.find((key) => key === label);
181
+ if (exact) return exact;
182
+
183
+ const normalisedLabel = normaliseFieldName(label);
184
+ const normalisedMatch = rowKeys.find((key) => normaliseFieldName(key) === normalisedLabel);
185
+ if (normalisedMatch) return normalisedMatch;
186
+
187
+ return label;
188
+ };
189
+
154
190
  if (Array.isArray(config.columns) && config.columns.length) {
155
- return config.columns;
191
+ return config.columns.map((label) => ({
192
+ label: formatColumnLabel(label),
193
+ accessor: findAccessor(label),
194
+ }));
156
195
  }
157
- // Priority 2: dataKeys, with xAxisKey prepended if not already present
196
+
158
197
  if (Array.isArray(config.dataKeys) && config.dataKeys.length) {
159
- if (config.xAxisKey && !config.dataKeys.includes(config.xAxisKey)) {
160
- return [config.xAxisKey, ...config.dataKeys];
161
- }
162
- return config.dataKeys;
198
+ const keys = config.xAxisKey && !config.dataKeys.includes(config.xAxisKey)
199
+ ? [config.xAxisKey, ...config.dataKeys]
200
+ : config.dataKeys;
201
+
202
+ return keys.map((key) => ({
203
+ label: formatColumnLabel(key),
204
+ accessor: findAccessor(key),
205
+ }));
163
206
  }
164
- // Priority 3: auto-detect from first data row
165
- if (config.data.length > 0) return Object.keys(config.data[0]);
166
- return [];
207
+
208
+ return rowKeys.map((key) => ({ label: formatColumnLabel(key), accessor: key }));
167
209
  }, [config]);
168
210
 
169
- if (!config.data.length || !keys.length) {
211
+ if (!config.data.length || !columns.length) {
170
212
  return <p className="text-xs text-slate-400 italic my-2">No data to display.</p>;
171
213
  }
172
214
 
@@ -190,12 +232,12 @@ function DataTable({ config, viewportSize = 'large' }: { config: TableConfig; vi
190
232
  <table className={`w-full text-left border-collapse ${isCompact ? 'min-w-[240px] text-xs' : isMedium ? 'min-w-[280px] text-[13px]' : 'min-w-[320px] text-sm'}`}>
191
233
  <thead className="bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10">
192
234
  <tr>
193
- {keys.map(k => (
235
+ {columns.map((column) => (
194
236
  <th
195
- key={k}
237
+ key={column.label}
196
238
  className={`${isCompact ? 'px-2.5 py-2' : 'px-4 py-3'} font-semibold text-slate-700 dark:text-white/90 whitespace-nowrap`}
197
239
  >
198
- {k}
240
+ {column.label}
199
241
  </th>
200
242
  ))}
201
243
  </tr>
@@ -206,12 +248,12 @@ function DataTable({ config, viewportSize = 'large' }: { config: TableConfig; vi
206
248
  key={ri}
207
249
  className="border-b border-slate-100 dark:border-white/5 last:border-0 hover:bg-slate-50/60 dark:hover:bg-white/5 transition-colors"
208
250
  >
209
- {keys.map(k => (
251
+ {columns.map((column) => (
210
252
  <td
211
- key={k}
212
- className={`${isCompact ? 'px-2.5 py-2' : 'px-4 py-3'} text-slate-600 dark:text-white/70 whitespace-nowrap`}
253
+ key={column.label}
254
+ className={`${isCompact ? 'px-2.5 py-2' : 'px-4 py-3'} text-slate-600 dark:text-white/70 ${column.accessor.toLowerCase().includes('description') ? 'whitespace-normal break-words' : 'whitespace-nowrap'}`}
213
255
  >
214
- {formatCell(row[k])}
256
+ {formatCell(row[column.accessor])}
215
257
  </td>
216
258
  ))}
217
259
  </tr>
@@ -533,6 +575,7 @@ export function MessageBubble({
533
575
 
534
576
  const processedMarkdown = React.useMemo(() => {
535
577
  let raw = cleanContent || message.content;
578
+ const hasStructuredUiBlock = /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
536
579
 
537
580
  // 1. Structural fixes
538
581
  raw = raw.replace(/([^\n])\n\|/g, '$1\n\n|') // Ensure blank line before table
@@ -553,6 +596,14 @@ export function MessageBubble({
553
596
  });
554
597
  }
555
598
 
599
+ // 2b. If the model already provided a structured UI block, suppress duplicated
600
+ // markdown tables that often follow as a fallback explanation.
601
+ if (hasStructuredUiBlock) {
602
+ raw = stripMarkdownTables(raw)
603
+ .replace(/\n{3,}/g, '\n\n')
604
+ .trim();
605
+ }
606
+
556
607
  // 3. HIJACKING: If streaming, hide table-like content to prevent raw markdown flashing.
557
608
  if (isStreaming) {
558
609
  raw = raw.replace(/((?:^|\n)\|.*)+/g, (match) => {
@@ -119,7 +119,7 @@ When the user asks for a visual representation, comparison, distribution, breakd
119
119
  "chartType": "pie" | "bar" | "line",
120
120
  "xAxisKey": "label",
121
121
  "dataKeys": ["value"],
122
- "columns": ["name", "category", "price", "stockStatus"],
122
+ "columns": ["dynamicFieldKey1", "dynamicFieldKey2"],
123
123
  "insights": ["Short takeaway 1", "Short takeaway 2"],
124
124
  "data": []
125
125
  }
@@ -132,7 +132,7 @@ When the user asks for a visual representation, comparison, distribution, breakd
132
132
  - If stock information exists, preserve it in each row with fields like "inStock", "inStockCount", "outOfStockCount", or "stockStatus".
133
133
  - If the user asks to highlight what is in stock, include those stock-related fields in the chart data and mention the pattern in "insights".
134
134
  - For carousel rows, include product-friendly fields such as id, name, brand, price, image, link, category, stockStatus.
135
- - For tables, keep "data" row-oriented and include "columns" when possible.
135
+ - For tables, keep "data" row-oriented and include "columns" only as dynamic field keys/order hints from the fetched data, never as hardcoded UI labels.
136
136
 
137
137
  4. FORMAT RULES
138
138
  - Never use ASCII art, markdown tables as the primary answer, or text-only fake charts when a UI block is requested.