@retrivora-ai/rag-engine 1.5.9 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-ID2Q4CF7.mjs → chunk-5K7EU4KW.mjs} +2 -2
- package/dist/handlers/index.js +2 -2
- package/dist/handlers/index.mjs +1 -1
- package/dist/index.js +125 -52
- package/dist/index.mjs +125 -52
- package/dist/server.js +2 -2
- package/dist/server.mjs +1 -1
- package/package.json +1 -1
- package/src/components/DynamicChart.tsx +28 -26
- package/src/components/MessageBubble.tsx +81 -20
- package/src/core/Pipeline.ts +2 -2
|
@@ -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": ["
|
|
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"
|
|
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.
|
package/dist/handlers/index.js
CHANGED
|
@@ -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": ["
|
|
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"
|
|
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.
|
package/dist/handlers/index.mjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -247,10 +247,19 @@ function DynamicChart({
|
|
|
247
247
|
const isTiny = containerWidth > 0 && containerWidth < 300;
|
|
248
248
|
const isCompact = viewportSize === "compact" || containerWidth > 0 && containerWidth < 420;
|
|
249
249
|
const isMedium = !isCompact && (viewportSize === "medium" || containerWidth > 0 && containerWidth < 640);
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
const chartHeight = (() => {
|
|
251
|
+
if (type === "pie") {
|
|
252
|
+
if (containerWidth <= 0) return isTiny ? 180 : isCompact ? 220 : isMedium ? 250 : 280;
|
|
253
|
+
return Math.max(180, Math.min(containerWidth, isTiny ? 180 : isCompact ? 220 : isMedium ? 250 : 280));
|
|
254
|
+
}
|
|
255
|
+
if (containerWidth <= 0) return isTiny ? 190 : isCompact ? 230 : isMedium ? 260 : 300;
|
|
256
|
+
return Math.max(190, Math.min(Math.round(containerWidth * 0.7), isTiny ? 190 : isCompact ? 230 : isMedium ? 260 : 300));
|
|
257
|
+
})();
|
|
258
|
+
const pieOuterRadius = (() => {
|
|
259
|
+
if (containerWidth <= 0) return isTiny ? 44 : isCompact ? 56 : isMedium ? 68 : 80;
|
|
260
|
+
const squareSize = Math.min(containerWidth, chartHeight);
|
|
261
|
+
return Math.max(36, Math.floor(squareSize * 0.28));
|
|
262
|
+
})();
|
|
254
263
|
const axisTick = { fontSize: isTiny ? 9 : isCompact ? 10 : 12, fill: "#64748b" };
|
|
255
264
|
const showLegend = !isTiny;
|
|
256
265
|
const truncateAxisLabel = (value) => {
|
|
@@ -358,41 +367,58 @@ function DynamicChart({
|
|
|
358
367
|
return row;
|
|
359
368
|
};
|
|
360
369
|
if (type === "pie") {
|
|
361
|
-
return /* @__PURE__ */ import_react4.default.createElement(
|
|
362
|
-
|
|
370
|
+
return /* @__PURE__ */ import_react4.default.createElement(
|
|
371
|
+
"div",
|
|
363
372
|
{
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
cx: "50%",
|
|
368
|
-
cy: "50%",
|
|
369
|
-
outerRadius: pieOuterRadius,
|
|
370
|
-
label: isTiny ? false : pieLabel
|
|
373
|
+
ref: containerRef,
|
|
374
|
+
className: "w-full min-w-0 mt-4 mb-2 select-none overflow-hidden",
|
|
375
|
+
style: { height: chartHeight }
|
|
371
376
|
},
|
|
372
|
-
|
|
373
|
-
|
|
377
|
+
/* @__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(
|
|
378
|
+
import_recharts.Pie,
|
|
379
|
+
{
|
|
380
|
+
data: sanitizedData,
|
|
381
|
+
dataKey: pieDataKey,
|
|
382
|
+
nameKey: finalXKey,
|
|
383
|
+
cx: "50%",
|
|
384
|
+
cy: "50%",
|
|
385
|
+
outerRadius: pieOuterRadius,
|
|
386
|
+
label: false,
|
|
387
|
+
labelLine: false
|
|
388
|
+
},
|
|
389
|
+
sanitizedData.map((entry, index) => /* @__PURE__ */ import_react4.default.createElement(import_recharts.Cell, { key: `cell-${index}`, fill: stockAwareColor(entry, index) }))
|
|
390
|
+
), /* @__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" }))))
|
|
391
|
+
);
|
|
374
392
|
}
|
|
375
|
-
return /* @__PURE__ */ import_react4.default.createElement(
|
|
376
|
-
|
|
377
|
-
{
|
|
378
|
-
key,
|
|
379
|
-
dataKey: key,
|
|
380
|
-
fill: colors[index % colors.length],
|
|
381
|
-
radius: [4, 4, 0, 0],
|
|
382
|
-
barSize: isTiny ? Math.max(10, 28 / data.length) : isCompact ? Math.max(14, 42 / data.length) : Math.max(20, 60 / data.length)
|
|
383
|
-
}
|
|
384
|
-
))) : /* @__PURE__ */ import_react4.default.createElement(import_recharts.LineChart, { 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) }), showLegend && /* @__PURE__ */ import_react4.default.createElement(import_recharts.Legend, { wrapperStyle: { fontSize: isCompact ? 10 : 12 } }), finalDataKeys.map((key, index) => /* @__PURE__ */ import_react4.default.createElement(
|
|
385
|
-
import_recharts.Line,
|
|
393
|
+
return /* @__PURE__ */ import_react4.default.createElement(
|
|
394
|
+
"div",
|
|
386
395
|
{
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
+
ref: containerRef,
|
|
397
|
+
className: "w-full min-w-0 mt-4 mb-2 select-none overflow-hidden",
|
|
398
|
+
style: { height: chartHeight }
|
|
399
|
+
},
|
|
400
|
+
/* @__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(
|
|
401
|
+
import_recharts.Bar,
|
|
402
|
+
{
|
|
403
|
+
key,
|
|
404
|
+
dataKey: key,
|
|
405
|
+
fill: colors[index % colors.length],
|
|
406
|
+
radius: [4, 4, 0, 0],
|
|
407
|
+
barSize: isTiny ? Math.max(10, 28 / data.length) : isCompact ? Math.max(14, 42 / data.length) : Math.max(20, 60 / data.length)
|
|
408
|
+
}
|
|
409
|
+
))) : /* @__PURE__ */ import_react4.default.createElement(import_recharts.LineChart, { 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) }), showLegend && /* @__PURE__ */ import_react4.default.createElement(import_recharts.Legend, { wrapperStyle: { fontSize: isCompact ? 10 : 12 } }), finalDataKeys.map((key, index) => /* @__PURE__ */ import_react4.default.createElement(
|
|
410
|
+
import_recharts.Line,
|
|
411
|
+
{
|
|
412
|
+
key,
|
|
413
|
+
type: "monotone",
|
|
414
|
+
dataKey: key,
|
|
415
|
+
stroke: colors[index % colors.length],
|
|
416
|
+
strokeWidth: isTiny ? 1.75 : isCompact ? 2 : 3,
|
|
417
|
+
dot: { r: isTiny ? 2.5 : isCompact ? 3 : 4, strokeWidth: 2 },
|
|
418
|
+
activeDot: { r: isTiny ? 4 : isCompact ? 5 : 6 }
|
|
419
|
+
}
|
|
420
|
+
))))
|
|
421
|
+
);
|
|
396
422
|
}
|
|
397
423
|
|
|
398
424
|
// src/components/MessageBubble.tsx
|
|
@@ -466,6 +492,15 @@ function stripMarkdownTables(raw) {
|
|
|
466
492
|
"\n\n"
|
|
467
493
|
);
|
|
468
494
|
}
|
|
495
|
+
function stripStructuredUiLabels(raw) {
|
|
496
|
+
return raw.replace(
|
|
497
|
+
/(?:^|\n)\*\*(?:Chart Data|Table Data|Visualization|Visual Representation)\*\:\s*(?=\n|$)/gi,
|
|
498
|
+
"\n"
|
|
499
|
+
).replace(
|
|
500
|
+
/(?:^|\n)(?:Chart Data|Table Data|Visualization|Visual Representation)\s*\:\s*(?=\n|$)/gi,
|
|
501
|
+
"\n"
|
|
502
|
+
);
|
|
503
|
+
}
|
|
469
504
|
function resolveImage(data) {
|
|
470
505
|
for (const key of ["image", "img", "thumbnail"]) {
|
|
471
506
|
const v = data[key];
|
|
@@ -482,23 +517,45 @@ function normaliseChild(children) {
|
|
|
482
517
|
}
|
|
483
518
|
return children;
|
|
484
519
|
}
|
|
520
|
+
function normaliseFieldName(value) {
|
|
521
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
522
|
+
}
|
|
523
|
+
function formatColumnLabel(value) {
|
|
524
|
+
return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim().replace(/\b\w/g, (char) => char.toUpperCase());
|
|
525
|
+
}
|
|
485
526
|
function DataTable({ config, viewportSize = "large" }) {
|
|
486
527
|
const isCompact = viewportSize === "compact";
|
|
487
528
|
const isMedium = viewportSize === "medium";
|
|
488
|
-
const
|
|
529
|
+
const columns = import_react5.default.useMemo(() => {
|
|
530
|
+
const rowKeys = Array.from(
|
|
531
|
+
new Set(
|
|
532
|
+
config.data.flatMap((row) => Object.keys(row))
|
|
533
|
+
)
|
|
534
|
+
);
|
|
535
|
+
const findAccessor = (label) => {
|
|
536
|
+
const exact = rowKeys.find((key) => key === label);
|
|
537
|
+
if (exact) return exact;
|
|
538
|
+
const normalisedLabel = normaliseFieldName(label);
|
|
539
|
+
const normalisedMatch = rowKeys.find((key) => normaliseFieldName(key) === normalisedLabel);
|
|
540
|
+
if (normalisedMatch) return normalisedMatch;
|
|
541
|
+
return label;
|
|
542
|
+
};
|
|
489
543
|
if (Array.isArray(config.columns) && config.columns.length) {
|
|
490
|
-
return config.columns
|
|
544
|
+
return config.columns.map((label) => ({
|
|
545
|
+
label: formatColumnLabel(label),
|
|
546
|
+
accessor: findAccessor(label)
|
|
547
|
+
}));
|
|
491
548
|
}
|
|
492
549
|
if (Array.isArray(config.dataKeys) && config.dataKeys.length) {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
550
|
+
const keys = config.xAxisKey && !config.dataKeys.includes(config.xAxisKey) ? [config.xAxisKey, ...config.dataKeys] : config.dataKeys;
|
|
551
|
+
return keys.map((key) => ({
|
|
552
|
+
label: formatColumnLabel(key),
|
|
553
|
+
accessor: findAccessor(key)
|
|
554
|
+
}));
|
|
497
555
|
}
|
|
498
|
-
|
|
499
|
-
return [];
|
|
556
|
+
return rowKeys.map((key) => ({ label: formatColumnLabel(key), accessor: key }));
|
|
500
557
|
}, [config]);
|
|
501
|
-
if (!config.data.length || !
|
|
558
|
+
if (!config.data.length || !columns.length) {
|
|
502
559
|
return /* @__PURE__ */ import_react5.default.createElement("p", { className: "text-xs text-slate-400 italic my-2" }, "No data to display.");
|
|
503
560
|
}
|
|
504
561
|
const formatCell = (val) => {
|
|
@@ -506,26 +563,26 @@ function DataTable({ config, viewportSize = "large" }) {
|
|
|
506
563
|
if (typeof val === "boolean") return val ? "\u2713" : "\u2717";
|
|
507
564
|
return String(val);
|
|
508
565
|
};
|
|
509
|
-
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,
|
|
566
|
+
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(
|
|
510
567
|
"th",
|
|
511
568
|
{
|
|
512
|
-
key:
|
|
569
|
+
key: column.label,
|
|
513
570
|
className: `${isCompact ? "px-2.5 py-2" : "px-4 py-3"} font-semibold text-slate-700 dark:text-white/90 whitespace-nowrap`
|
|
514
571
|
},
|
|
515
|
-
|
|
572
|
+
column.label
|
|
516
573
|
)))), /* @__PURE__ */ import_react5.default.createElement("tbody", null, config.data.map((row, ri) => /* @__PURE__ */ import_react5.default.createElement(
|
|
517
574
|
"tr",
|
|
518
575
|
{
|
|
519
576
|
key: ri,
|
|
520
577
|
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"
|
|
521
578
|
},
|
|
522
|
-
|
|
579
|
+
columns.map((column) => /* @__PURE__ */ import_react5.default.createElement(
|
|
523
580
|
"td",
|
|
524
581
|
{
|
|
525
|
-
key:
|
|
526
|
-
className: `${isCompact ? "px-2.5 py-2" : "px-4 py-3"} text-slate-600 dark:text-white/70 whitespace-nowrap`
|
|
582
|
+
key: column.label,
|
|
583
|
+
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"}`
|
|
527
584
|
},
|
|
528
|
-
formatCell(row[
|
|
585
|
+
formatCell(row[column.accessor])
|
|
529
586
|
))
|
|
530
587
|
))))), /* @__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" : "")));
|
|
531
588
|
}
|
|
@@ -739,7 +796,7 @@ ${match.trim()}
|
|
|
739
796
|
});
|
|
740
797
|
}
|
|
741
798
|
if (hasStructuredUiBlock) {
|
|
742
|
-
raw = stripMarkdownTables(raw).replace(/\n{3,}/g, "\n\n").trim();
|
|
799
|
+
raw = stripStructuredUiLabels(stripMarkdownTables(raw)).replace(/\n{3,}/g, "\n\n").trim();
|
|
743
800
|
}
|
|
744
801
|
if (isStreaming) {
|
|
745
802
|
raw = raw.replace(/((?:^|\n)\|.*)+/g, (match) => {
|
|
@@ -853,6 +910,22 @@ ${match.trim()}
|
|
|
853
910
|
);
|
|
854
911
|
}
|
|
855
912
|
}
|
|
913
|
+
if (!inline) {
|
|
914
|
+
const content = String(children != null ? children : "").trim();
|
|
915
|
+
if (looksLikeStructuredPayload(content)) {
|
|
916
|
+
return /* @__PURE__ */ import_react5.default.createElement(
|
|
917
|
+
UIDispatcher,
|
|
918
|
+
{
|
|
919
|
+
rawContent: content,
|
|
920
|
+
primaryColor,
|
|
921
|
+
accentColor,
|
|
922
|
+
isStreaming,
|
|
923
|
+
onAddToCart,
|
|
924
|
+
viewportSize
|
|
925
|
+
}
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
856
929
|
if (!inline && lang === "table-loading") {
|
|
857
930
|
return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-5 p-6 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex items-center justify-center gap-3 select-none" }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "w-4 h-4 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" }), /* @__PURE__ */ import_react5.default.createElement("p", { className: "text-xs text-slate-500 font-medium italic animate-pulse" }, "Generating data table..."));
|
|
858
931
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -210,10 +210,19 @@ function DynamicChart({
|
|
|
210
210
|
const isTiny = containerWidth > 0 && containerWidth < 300;
|
|
211
211
|
const isCompact = viewportSize === "compact" || containerWidth > 0 && containerWidth < 420;
|
|
212
212
|
const isMedium = !isCompact && (viewportSize === "medium" || containerWidth > 0 && containerWidth < 640);
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
213
|
+
const chartHeight = (() => {
|
|
214
|
+
if (type === "pie") {
|
|
215
|
+
if (containerWidth <= 0) return isTiny ? 180 : isCompact ? 220 : isMedium ? 250 : 280;
|
|
216
|
+
return Math.max(180, Math.min(containerWidth, isTiny ? 180 : isCompact ? 220 : isMedium ? 250 : 280));
|
|
217
|
+
}
|
|
218
|
+
if (containerWidth <= 0) return isTiny ? 190 : isCompact ? 230 : isMedium ? 260 : 300;
|
|
219
|
+
return Math.max(190, Math.min(Math.round(containerWidth * 0.7), isTiny ? 190 : isCompact ? 230 : isMedium ? 260 : 300));
|
|
220
|
+
})();
|
|
221
|
+
const pieOuterRadius = (() => {
|
|
222
|
+
if (containerWidth <= 0) return isTiny ? 44 : isCompact ? 56 : isMedium ? 68 : 80;
|
|
223
|
+
const squareSize = Math.min(containerWidth, chartHeight);
|
|
224
|
+
return Math.max(36, Math.floor(squareSize * 0.28));
|
|
225
|
+
})();
|
|
217
226
|
const axisTick = { fontSize: isTiny ? 9 : isCompact ? 10 : 12, fill: "#64748b" };
|
|
218
227
|
const showLegend = !isTiny;
|
|
219
228
|
const truncateAxisLabel = (value) => {
|
|
@@ -321,41 +330,58 @@ function DynamicChart({
|
|
|
321
330
|
return row;
|
|
322
331
|
};
|
|
323
332
|
if (type === "pie") {
|
|
324
|
-
return /* @__PURE__ */ React4.createElement(
|
|
325
|
-
|
|
333
|
+
return /* @__PURE__ */ React4.createElement(
|
|
334
|
+
"div",
|
|
326
335
|
{
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
cx: "50%",
|
|
331
|
-
cy: "50%",
|
|
332
|
-
outerRadius: pieOuterRadius,
|
|
333
|
-
label: isTiny ? false : pieLabel
|
|
336
|
+
ref: containerRef,
|
|
337
|
+
className: "w-full min-w-0 mt-4 mb-2 select-none overflow-hidden",
|
|
338
|
+
style: { height: chartHeight }
|
|
334
339
|
},
|
|
335
|
-
|
|
336
|
-
|
|
340
|
+
/* @__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(
|
|
341
|
+
Pie,
|
|
342
|
+
{
|
|
343
|
+
data: sanitizedData,
|
|
344
|
+
dataKey: pieDataKey,
|
|
345
|
+
nameKey: finalXKey,
|
|
346
|
+
cx: "50%",
|
|
347
|
+
cy: "50%",
|
|
348
|
+
outerRadius: pieOuterRadius,
|
|
349
|
+
label: false,
|
|
350
|
+
labelLine: false
|
|
351
|
+
},
|
|
352
|
+
sanitizedData.map((entry, index) => /* @__PURE__ */ React4.createElement(Cell, { key: `cell-${index}`, fill: stockAwareColor(entry, index) }))
|
|
353
|
+
), /* @__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" }))))
|
|
354
|
+
);
|
|
337
355
|
}
|
|
338
|
-
return /* @__PURE__ */ React4.createElement(
|
|
339
|
-
|
|
340
|
-
{
|
|
341
|
-
key,
|
|
342
|
-
dataKey: key,
|
|
343
|
-
fill: colors[index % colors.length],
|
|
344
|
-
radius: [4, 4, 0, 0],
|
|
345
|
-
barSize: isTiny ? Math.max(10, 28 / data.length) : isCompact ? Math.max(14, 42 / data.length) : Math.max(20, 60 / data.length)
|
|
346
|
-
}
|
|
347
|
-
))) : /* @__PURE__ */ React4.createElement(LineChart, { 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) }), showLegend && /* @__PURE__ */ React4.createElement(Legend, { wrapperStyle: { fontSize: isCompact ? 10 : 12 } }), finalDataKeys.map((key, index) => /* @__PURE__ */ React4.createElement(
|
|
348
|
-
Line,
|
|
356
|
+
return /* @__PURE__ */ React4.createElement(
|
|
357
|
+
"div",
|
|
349
358
|
{
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
+
ref: containerRef,
|
|
360
|
+
className: "w-full min-w-0 mt-4 mb-2 select-none overflow-hidden",
|
|
361
|
+
style: { height: chartHeight }
|
|
362
|
+
},
|
|
363
|
+
/* @__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(
|
|
364
|
+
Bar,
|
|
365
|
+
{
|
|
366
|
+
key,
|
|
367
|
+
dataKey: key,
|
|
368
|
+
fill: colors[index % colors.length],
|
|
369
|
+
radius: [4, 4, 0, 0],
|
|
370
|
+
barSize: isTiny ? Math.max(10, 28 / data.length) : isCompact ? Math.max(14, 42 / data.length) : Math.max(20, 60 / data.length)
|
|
371
|
+
}
|
|
372
|
+
))) : /* @__PURE__ */ React4.createElement(LineChart, { 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) }), showLegend && /* @__PURE__ */ React4.createElement(Legend, { wrapperStyle: { fontSize: isCompact ? 10 : 12 } }), finalDataKeys.map((key, index) => /* @__PURE__ */ React4.createElement(
|
|
373
|
+
Line,
|
|
374
|
+
{
|
|
375
|
+
key,
|
|
376
|
+
type: "monotone",
|
|
377
|
+
dataKey: key,
|
|
378
|
+
stroke: colors[index % colors.length],
|
|
379
|
+
strokeWidth: isTiny ? 1.75 : isCompact ? 2 : 3,
|
|
380
|
+
dot: { r: isTiny ? 2.5 : isCompact ? 3 : 4, strokeWidth: 2 },
|
|
381
|
+
activeDot: { r: isTiny ? 4 : isCompact ? 5 : 6 }
|
|
382
|
+
}
|
|
383
|
+
))))
|
|
384
|
+
);
|
|
359
385
|
}
|
|
360
386
|
|
|
361
387
|
// src/components/MessageBubble.tsx
|
|
@@ -429,6 +455,15 @@ function stripMarkdownTables(raw) {
|
|
|
429
455
|
"\n\n"
|
|
430
456
|
);
|
|
431
457
|
}
|
|
458
|
+
function stripStructuredUiLabels(raw) {
|
|
459
|
+
return raw.replace(
|
|
460
|
+
/(?:^|\n)\*\*(?:Chart Data|Table Data|Visualization|Visual Representation)\*\:\s*(?=\n|$)/gi,
|
|
461
|
+
"\n"
|
|
462
|
+
).replace(
|
|
463
|
+
/(?:^|\n)(?:Chart Data|Table Data|Visualization|Visual Representation)\s*\:\s*(?=\n|$)/gi,
|
|
464
|
+
"\n"
|
|
465
|
+
);
|
|
466
|
+
}
|
|
432
467
|
function resolveImage(data) {
|
|
433
468
|
for (const key of ["image", "img", "thumbnail"]) {
|
|
434
469
|
const v = data[key];
|
|
@@ -445,23 +480,45 @@ function normaliseChild(children) {
|
|
|
445
480
|
}
|
|
446
481
|
return children;
|
|
447
482
|
}
|
|
483
|
+
function normaliseFieldName(value) {
|
|
484
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
485
|
+
}
|
|
486
|
+
function formatColumnLabel(value) {
|
|
487
|
+
return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim().replace(/\b\w/g, (char) => char.toUpperCase());
|
|
488
|
+
}
|
|
448
489
|
function DataTable({ config, viewportSize = "large" }) {
|
|
449
490
|
const isCompact = viewportSize === "compact";
|
|
450
491
|
const isMedium = viewportSize === "medium";
|
|
451
|
-
const
|
|
492
|
+
const columns = React5.useMemo(() => {
|
|
493
|
+
const rowKeys = Array.from(
|
|
494
|
+
new Set(
|
|
495
|
+
config.data.flatMap((row) => Object.keys(row))
|
|
496
|
+
)
|
|
497
|
+
);
|
|
498
|
+
const findAccessor = (label) => {
|
|
499
|
+
const exact = rowKeys.find((key) => key === label);
|
|
500
|
+
if (exact) return exact;
|
|
501
|
+
const normalisedLabel = normaliseFieldName(label);
|
|
502
|
+
const normalisedMatch = rowKeys.find((key) => normaliseFieldName(key) === normalisedLabel);
|
|
503
|
+
if (normalisedMatch) return normalisedMatch;
|
|
504
|
+
return label;
|
|
505
|
+
};
|
|
452
506
|
if (Array.isArray(config.columns) && config.columns.length) {
|
|
453
|
-
return config.columns
|
|
507
|
+
return config.columns.map((label) => ({
|
|
508
|
+
label: formatColumnLabel(label),
|
|
509
|
+
accessor: findAccessor(label)
|
|
510
|
+
}));
|
|
454
511
|
}
|
|
455
512
|
if (Array.isArray(config.dataKeys) && config.dataKeys.length) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
513
|
+
const keys = config.xAxisKey && !config.dataKeys.includes(config.xAxisKey) ? [config.xAxisKey, ...config.dataKeys] : config.dataKeys;
|
|
514
|
+
return keys.map((key) => ({
|
|
515
|
+
label: formatColumnLabel(key),
|
|
516
|
+
accessor: findAccessor(key)
|
|
517
|
+
}));
|
|
460
518
|
}
|
|
461
|
-
|
|
462
|
-
return [];
|
|
519
|
+
return rowKeys.map((key) => ({ label: formatColumnLabel(key), accessor: key }));
|
|
463
520
|
}, [config]);
|
|
464
|
-
if (!config.data.length || !
|
|
521
|
+
if (!config.data.length || !columns.length) {
|
|
465
522
|
return /* @__PURE__ */ React5.createElement("p", { className: "text-xs text-slate-400 italic my-2" }, "No data to display.");
|
|
466
523
|
}
|
|
467
524
|
const formatCell = (val) => {
|
|
@@ -469,26 +526,26 @@ function DataTable({ config, viewportSize = "large" }) {
|
|
|
469
526
|
if (typeof val === "boolean") return val ? "\u2713" : "\u2717";
|
|
470
527
|
return String(val);
|
|
471
528
|
};
|
|
472
|
-
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,
|
|
529
|
+
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(
|
|
473
530
|
"th",
|
|
474
531
|
{
|
|
475
|
-
key:
|
|
532
|
+
key: column.label,
|
|
476
533
|
className: `${isCompact ? "px-2.5 py-2" : "px-4 py-3"} font-semibold text-slate-700 dark:text-white/90 whitespace-nowrap`
|
|
477
534
|
},
|
|
478
|
-
|
|
535
|
+
column.label
|
|
479
536
|
)))), /* @__PURE__ */ React5.createElement("tbody", null, config.data.map((row, ri) => /* @__PURE__ */ React5.createElement(
|
|
480
537
|
"tr",
|
|
481
538
|
{
|
|
482
539
|
key: ri,
|
|
483
540
|
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"
|
|
484
541
|
},
|
|
485
|
-
|
|
542
|
+
columns.map((column) => /* @__PURE__ */ React5.createElement(
|
|
486
543
|
"td",
|
|
487
544
|
{
|
|
488
|
-
key:
|
|
489
|
-
className: `${isCompact ? "px-2.5 py-2" : "px-4 py-3"} text-slate-600 dark:text-white/70 whitespace-nowrap`
|
|
545
|
+
key: column.label,
|
|
546
|
+
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"}`
|
|
490
547
|
},
|
|
491
|
-
formatCell(row[
|
|
548
|
+
formatCell(row[column.accessor])
|
|
492
549
|
))
|
|
493
550
|
))))), /* @__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" : "")));
|
|
494
551
|
}
|
|
@@ -702,7 +759,7 @@ ${match.trim()}
|
|
|
702
759
|
});
|
|
703
760
|
}
|
|
704
761
|
if (hasStructuredUiBlock) {
|
|
705
|
-
raw = stripMarkdownTables(raw).replace(/\n{3,}/g, "\n\n").trim();
|
|
762
|
+
raw = stripStructuredUiLabels(stripMarkdownTables(raw)).replace(/\n{3,}/g, "\n\n").trim();
|
|
706
763
|
}
|
|
707
764
|
if (isStreaming) {
|
|
708
765
|
raw = raw.replace(/((?:^|\n)\|.*)+/g, (match) => {
|
|
@@ -816,6 +873,22 @@ ${match.trim()}
|
|
|
816
873
|
);
|
|
817
874
|
}
|
|
818
875
|
}
|
|
876
|
+
if (!inline) {
|
|
877
|
+
const content = String(children != null ? children : "").trim();
|
|
878
|
+
if (looksLikeStructuredPayload(content)) {
|
|
879
|
+
return /* @__PURE__ */ React5.createElement(
|
|
880
|
+
UIDispatcher,
|
|
881
|
+
{
|
|
882
|
+
rawContent: content,
|
|
883
|
+
primaryColor,
|
|
884
|
+
accentColor,
|
|
885
|
+
isStreaming,
|
|
886
|
+
onAddToCart,
|
|
887
|
+
viewportSize
|
|
888
|
+
}
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
819
892
|
if (!inline && lang === "table-loading") {
|
|
820
893
|
return /* @__PURE__ */ React5.createElement("div", { className: "my-5 p-6 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex items-center justify-center gap-3 select-none" }, /* @__PURE__ */ React5.createElement("div", { className: "w-4 h-4 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" }), /* @__PURE__ */ React5.createElement("p", { className: "text-xs text-slate-500 font-medium italic animate-pulse" }, "Generating data table..."));
|
|
821
894
|
}
|
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": ["
|
|
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"
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retrivora-ai/rag-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
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",
|
|
@@ -57,27 +57,20 @@ export function DynamicChart({
|
|
|
57
57
|
const isTiny = containerWidth > 0 && containerWidth < 300;
|
|
58
58
|
const isCompact = viewportSize === 'compact' || (containerWidth > 0 && containerWidth < 420);
|
|
59
59
|
const isMedium = !isCompact && (viewportSize === 'medium' || (containerWidth > 0 && containerWidth < 640));
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
?
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const pieOuterRadius = isTiny ? 44 : isCompact ? 56 : isMedium ? 68 : 80;
|
|
75
|
-
const pieLabel = ({ name, percent }: { name?: string; percent?: number }) =>
|
|
76
|
-
isTiny
|
|
77
|
-
? ''
|
|
78
|
-
: isCompact
|
|
79
|
-
? `${name}`
|
|
80
|
-
: `${name} ${((percent ?? 0) * 100).toFixed(0)}%`;
|
|
60
|
+
const chartHeight = (() => {
|
|
61
|
+
if (type === 'pie') {
|
|
62
|
+
if (containerWidth <= 0) return isTiny ? 180 : isCompact ? 220 : isMedium ? 250 : 280;
|
|
63
|
+
return Math.max(180, Math.min(containerWidth, isTiny ? 180 : isCompact ? 220 : isMedium ? 250 : 280));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (containerWidth <= 0) return isTiny ? 190 : isCompact ? 230 : isMedium ? 260 : 300;
|
|
67
|
+
return Math.max(190, Math.min(Math.round(containerWidth * 0.7), isTiny ? 190 : isCompact ? 230 : isMedium ? 260 : 300));
|
|
68
|
+
})();
|
|
69
|
+
const pieOuterRadius = (() => {
|
|
70
|
+
if (containerWidth <= 0) return isTiny ? 44 : isCompact ? 56 : isMedium ? 68 : 80;
|
|
71
|
+
const squareSize = Math.min(containerWidth, chartHeight);
|
|
72
|
+
return Math.max(36, Math.floor(squareSize * 0.28));
|
|
73
|
+
})();
|
|
81
74
|
const axisTick = { fontSize: isTiny ? 9 : isCompact ? 10 : 12, fill: '#64748b' };
|
|
82
75
|
const showLegend = !isTiny;
|
|
83
76
|
const truncateAxisLabel = (value: string | number) => {
|
|
@@ -241,8 +234,12 @@ export function DynamicChart({
|
|
|
241
234
|
// Handle Pie Chart separately as it has a different structure
|
|
242
235
|
if (type === 'pie') {
|
|
243
236
|
return (
|
|
244
|
-
<div
|
|
245
|
-
|
|
237
|
+
<div
|
|
238
|
+
ref={containerRef}
|
|
239
|
+
className="w-full min-w-0 mt-4 mb-2 select-none overflow-hidden"
|
|
240
|
+
style={{ height: chartHeight }}
|
|
241
|
+
>
|
|
242
|
+
<div className="mx-auto h-full aspect-square max-w-full">
|
|
246
243
|
<ResponsiveContainer width="100%" height="100%">
|
|
247
244
|
<PieChart>
|
|
248
245
|
<Pie
|
|
@@ -252,14 +249,15 @@ export function DynamicChart({
|
|
|
252
249
|
cx="50%"
|
|
253
250
|
cy="50%"
|
|
254
251
|
outerRadius={pieOuterRadius}
|
|
255
|
-
label={
|
|
252
|
+
label={false}
|
|
253
|
+
labelLine={false}
|
|
256
254
|
>
|
|
257
255
|
{sanitizedData.map((entry, index) => (
|
|
258
256
|
<Cell key={`cell-${index}`} fill={stockAwareColor(entry, index)} />
|
|
259
257
|
))}
|
|
260
258
|
</Pie>
|
|
261
259
|
<Tooltip content={(props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === 'string' ? props.label : undefined)} />
|
|
262
|
-
{showLegend && <Legend wrapperStyle={{ fontSize:
|
|
260
|
+
{showLegend && !isCompact && <Legend wrapperStyle={{ fontSize: 12 }} verticalAlign="bottom" />}
|
|
263
261
|
</PieChart>
|
|
264
262
|
</ResponsiveContainer>
|
|
265
263
|
</div>
|
|
@@ -269,7 +267,11 @@ export function DynamicChart({
|
|
|
269
267
|
|
|
270
268
|
// Bar and Line charts
|
|
271
269
|
return (
|
|
272
|
-
<div
|
|
270
|
+
<div
|
|
271
|
+
ref={containerRef}
|
|
272
|
+
className="w-full min-w-0 mt-4 mb-2 select-none overflow-hidden"
|
|
273
|
+
style={{ height: chartHeight }}
|
|
274
|
+
>
|
|
273
275
|
<ResponsiveContainer width="100%" height="100%">
|
|
274
276
|
{type === 'bar' ? (
|
|
275
277
|
<BarChart data={sanitizedData} margin={chartMargin}>
|
|
@@ -113,6 +113,16 @@ function stripMarkdownTables(raw: string): string {
|
|
|
113
113
|
);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
function stripStructuredUiLabels(raw: string): string {
|
|
117
|
+
return raw.replace(
|
|
118
|
+
/(?:^|\n)\*\*(?:Chart Data|Table Data|Visualization|Visual Representation)\*\:\s*(?=\n|$)/gi,
|
|
119
|
+
'\n',
|
|
120
|
+
).replace(
|
|
121
|
+
/(?:^|\n)(?:Chart Data|Table Data|Visualization|Visual Representation)\s*\:\s*(?=\n|$)/gi,
|
|
122
|
+
'\n',
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
116
126
|
// ─── Tiny helpers ─────────────────────────────────────────────────────────────
|
|
117
127
|
|
|
118
128
|
function resolveImage(data: Record<string, unknown>): string | undefined {
|
|
@@ -139,6 +149,19 @@ function normaliseChild(children: React.ReactNode): React.ReactNode {
|
|
|
139
149
|
return children;
|
|
140
150
|
}
|
|
141
151
|
|
|
152
|
+
function normaliseFieldName(value: string): string {
|
|
153
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function formatColumnLabel(value: string): string {
|
|
157
|
+
return value
|
|
158
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
159
|
+
.replace(/[_-]+/g, ' ')
|
|
160
|
+
.replace(/\s+/g, ' ')
|
|
161
|
+
.trim()
|
|
162
|
+
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
163
|
+
}
|
|
164
|
+
|
|
142
165
|
// ─── Inline data table renderer ───────────────────────────────────────────────
|
|
143
166
|
// Handles chart configs where type === "table". The LLM may emit any of:
|
|
144
167
|
// columns, dataKeys (+optional xAxisKey), or no key hints (auto-detect from row).
|
|
@@ -156,24 +179,46 @@ interface TableConfig {
|
|
|
156
179
|
function DataTable({ config, viewportSize = 'large' }: { config: TableConfig; viewportSize?: ChatViewportSize }) {
|
|
157
180
|
const isCompact = viewportSize === 'compact';
|
|
158
181
|
const isMedium = viewportSize === 'medium';
|
|
159
|
-
const
|
|
160
|
-
|
|
182
|
+
const columns = React.useMemo<Array<{ label: string; accessor: string }>>(() => {
|
|
183
|
+
const rowKeys = Array.from(
|
|
184
|
+
new Set(
|
|
185
|
+
config.data.flatMap((row) => Object.keys(row)),
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const findAccessor = (label: string) => {
|
|
190
|
+
const exact = rowKeys.find((key) => key === label);
|
|
191
|
+
if (exact) return exact;
|
|
192
|
+
|
|
193
|
+
const normalisedLabel = normaliseFieldName(label);
|
|
194
|
+
const normalisedMatch = rowKeys.find((key) => normaliseFieldName(key) === normalisedLabel);
|
|
195
|
+
if (normalisedMatch) return normalisedMatch;
|
|
196
|
+
|
|
197
|
+
return label;
|
|
198
|
+
};
|
|
199
|
+
|
|
161
200
|
if (Array.isArray(config.columns) && config.columns.length) {
|
|
162
|
-
return config.columns
|
|
201
|
+
return config.columns.map((label) => ({
|
|
202
|
+
label: formatColumnLabel(label),
|
|
203
|
+
accessor: findAccessor(label),
|
|
204
|
+
}));
|
|
163
205
|
}
|
|
164
|
-
|
|
206
|
+
|
|
165
207
|
if (Array.isArray(config.dataKeys) && config.dataKeys.length) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
208
|
+
const keys = config.xAxisKey && !config.dataKeys.includes(config.xAxisKey)
|
|
209
|
+
? [config.xAxisKey, ...config.dataKeys]
|
|
210
|
+
: config.dataKeys;
|
|
211
|
+
|
|
212
|
+
return keys.map((key) => ({
|
|
213
|
+
label: formatColumnLabel(key),
|
|
214
|
+
accessor: findAccessor(key),
|
|
215
|
+
}));
|
|
170
216
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return [];
|
|
217
|
+
|
|
218
|
+
return rowKeys.map((key) => ({ label: formatColumnLabel(key), accessor: key }));
|
|
174
219
|
}, [config]);
|
|
175
220
|
|
|
176
|
-
if (!config.data.length || !
|
|
221
|
+
if (!config.data.length || !columns.length) {
|
|
177
222
|
return <p className="text-xs text-slate-400 italic my-2">No data to display.</p>;
|
|
178
223
|
}
|
|
179
224
|
|
|
@@ -197,12 +242,12 @@ function DataTable({ config, viewportSize = 'large' }: { config: TableConfig; vi
|
|
|
197
242
|
<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'}`}>
|
|
198
243
|
<thead className="bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10">
|
|
199
244
|
<tr>
|
|
200
|
-
{
|
|
245
|
+
{columns.map((column) => (
|
|
201
246
|
<th
|
|
202
|
-
key={
|
|
247
|
+
key={column.label}
|
|
203
248
|
className={`${isCompact ? 'px-2.5 py-2' : 'px-4 py-3'} font-semibold text-slate-700 dark:text-white/90 whitespace-nowrap`}
|
|
204
249
|
>
|
|
205
|
-
{
|
|
250
|
+
{column.label}
|
|
206
251
|
</th>
|
|
207
252
|
))}
|
|
208
253
|
</tr>
|
|
@@ -213,12 +258,12 @@ function DataTable({ config, viewportSize = 'large' }: { config: TableConfig; vi
|
|
|
213
258
|
key={ri}
|
|
214
259
|
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"
|
|
215
260
|
>
|
|
216
|
-
{
|
|
261
|
+
{columns.map((column) => (
|
|
217
262
|
<td
|
|
218
|
-
key={
|
|
219
|
-
className={`${isCompact ? 'px-2.5 py-2' : 'px-4 py-3'} text-slate-600 dark:text-white/70 whitespace-nowrap`}
|
|
263
|
+
key={column.label}
|
|
264
|
+
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'}`}
|
|
220
265
|
>
|
|
221
|
-
{formatCell(row[
|
|
266
|
+
{formatCell(row[column.accessor])}
|
|
222
267
|
</td>
|
|
223
268
|
))}
|
|
224
269
|
</tr>
|
|
@@ -564,7 +609,7 @@ export function MessageBubble({
|
|
|
564
609
|
// 2b. If the model already provided a structured UI block, suppress duplicated
|
|
565
610
|
// markdown tables that often follow as a fallback explanation.
|
|
566
611
|
if (hasStructuredUiBlock) {
|
|
567
|
-
raw = stripMarkdownTables(raw)
|
|
612
|
+
raw = stripStructuredUiLabels(stripMarkdownTables(raw))
|
|
568
613
|
.replace(/\n{3,}/g, '\n\n')
|
|
569
614
|
.trim();
|
|
570
615
|
}
|
|
@@ -675,6 +720,22 @@ export function MessageBubble({
|
|
|
675
720
|
}
|
|
676
721
|
}
|
|
677
722
|
|
|
723
|
+
if (!inline) {
|
|
724
|
+
const content = String(children ?? '').trim();
|
|
725
|
+
if (looksLikeStructuredPayload(content)) {
|
|
726
|
+
return (
|
|
727
|
+
<UIDispatcher
|
|
728
|
+
rawContent={content}
|
|
729
|
+
primaryColor={primaryColor}
|
|
730
|
+
accentColor={accentColor}
|
|
731
|
+
isStreaming={isStreaming}
|
|
732
|
+
onAddToCart={onAddToCart}
|
|
733
|
+
viewportSize={viewportSize}
|
|
734
|
+
/>
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
678
739
|
if (!inline && lang === 'table-loading') {
|
|
679
740
|
return (
|
|
680
741
|
<div className="my-5 p-6 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex items-center justify-center gap-3 select-none">
|
package/src/core/Pipeline.ts
CHANGED
|
@@ -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": ["
|
|
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"
|
|
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.
|