@retrivora-ai/rag-engine 1.6.1 → 1.6.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.
- package/dist/index.js +130 -69
- package/dist/index.mjs +130 -69
- package/package.json +1 -1
- package/src/components/DynamicChart.tsx +6 -25
- package/src/components/MessageBubble.tsx +118 -7
package/dist/index.js
CHANGED
|
@@ -247,19 +247,7 @@ 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
|
-
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
|
-
})();
|
|
250
|
+
const pieOuterRadius = isTiny ? "54%" : isCompact ? "60%" : isMedium ? "66%" : "72%";
|
|
263
251
|
const axisTick = { fontSize: isTiny ? 9 : isCompact ? 10 : 12, fill: "#64748b" };
|
|
264
252
|
const showLegend = !isTiny;
|
|
265
253
|
const truncateAxisLabel = (value) => {
|
|
@@ -367,58 +355,42 @@ function DynamicChart({
|
|
|
367
355
|
return row;
|
|
368
356
|
};
|
|
369
357
|
if (type === "pie") {
|
|
370
|
-
return /* @__PURE__ */ import_react4.default.createElement(
|
|
371
|
-
|
|
358
|
+
return /* @__PURE__ */ import_react4.default.createElement("div", { ref: containerRef, className: "w-full min-w-0 mt-4 mb-2 select-none overflow-hidden" }, /* @__PURE__ */ import_react4.default.createElement("div", { className: "mx-auto w-full max-w-full aspect-square" }, /* @__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(
|
|
359
|
+
import_recharts.Pie,
|
|
372
360
|
{
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
361
|
+
data: sanitizedData,
|
|
362
|
+
dataKey: pieDataKey,
|
|
363
|
+
nameKey: finalXKey,
|
|
364
|
+
cx: "50%",
|
|
365
|
+
cy: "50%",
|
|
366
|
+
outerRadius: pieOuterRadius,
|
|
367
|
+
label: false,
|
|
368
|
+
labelLine: false
|
|
376
369
|
},
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
);
|
|
370
|
+
sanitizedData.map((entry, index) => /* @__PURE__ */ import_react4.default.createElement(import_recharts.Cell, { key: `cell-${index}`, fill: stockAwareColor(entry, index) }))
|
|
371
|
+
), /* @__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" })))));
|
|
392
372
|
}
|
|
393
|
-
return /* @__PURE__ */ import_react4.default.createElement(
|
|
394
|
-
|
|
373
|
+
return /* @__PURE__ */ import_react4.default.createElement("div", { ref: containerRef, className: "w-full min-w-0 mt-4 mb-2 select-none overflow-hidden" }, /* @__PURE__ */ import_react4.default.createElement("div", { className: "w-full aspect-[4/3]" }, /* @__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(
|
|
374
|
+
import_recharts.Bar,
|
|
395
375
|
{
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
{
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
);
|
|
376
|
+
key,
|
|
377
|
+
dataKey: key,
|
|
378
|
+
fill: colors[index % colors.length],
|
|
379
|
+
radius: [4, 4, 0, 0],
|
|
380
|
+
barSize: isTiny ? Math.max(10, 28 / data.length) : isCompact ? Math.max(14, 42 / data.length) : Math.max(20, 60 / data.length)
|
|
381
|
+
}
|
|
382
|
+
))) : /* @__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(
|
|
383
|
+
import_recharts.Line,
|
|
384
|
+
{
|
|
385
|
+
key,
|
|
386
|
+
type: "monotone",
|
|
387
|
+
dataKey: key,
|
|
388
|
+
stroke: colors[index % colors.length],
|
|
389
|
+
strokeWidth: isTiny ? 1.75 : isCompact ? 2 : 3,
|
|
390
|
+
dot: { r: isTiny ? 2.5 : isCompact ? 3 : 4, strokeWidth: 2 },
|
|
391
|
+
activeDot: { r: isTiny ? 4 : isCompact ? 5 : 6 }
|
|
392
|
+
}
|
|
393
|
+
))))));
|
|
422
394
|
}
|
|
423
395
|
|
|
424
396
|
// src/components/MessageBubble.tsx
|
|
@@ -501,6 +473,63 @@ function stripStructuredUiLabels(raw) {
|
|
|
501
473
|
"\n"
|
|
502
474
|
);
|
|
503
475
|
}
|
|
476
|
+
function extractBareJsonObject(raw) {
|
|
477
|
+
const start = raw.indexOf("{");
|
|
478
|
+
if (start === -1) return null;
|
|
479
|
+
let depth = 0;
|
|
480
|
+
let inString = false;
|
|
481
|
+
let escape = false;
|
|
482
|
+
for (let i = start; i < raw.length; i += 1) {
|
|
483
|
+
const ch = raw[i];
|
|
484
|
+
if (escape) {
|
|
485
|
+
escape = false;
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (ch === "\\" && inString) {
|
|
489
|
+
escape = true;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (ch === '"') {
|
|
493
|
+
inString = !inString;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (inString) continue;
|
|
497
|
+
if (ch === "{") depth += 1;
|
|
498
|
+
if (ch === "}") {
|
|
499
|
+
depth -= 1;
|
|
500
|
+
if (depth === 0) {
|
|
501
|
+
return raw.slice(start, i + 1);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
function extractStructuredPayload(raw) {
|
|
508
|
+
var _a, _b;
|
|
509
|
+
let working = raw;
|
|
510
|
+
const fenceRegex = /```(?:[a-z]+)?\s*([\s\S]*?)```/gi;
|
|
511
|
+
for (const match of raw.matchAll(fenceRegex)) {
|
|
512
|
+
const body = (_b = (_a = match[1]) == null ? void 0 : _a.trim()) != null ? _b : "";
|
|
513
|
+
if (!looksLikeStructuredPayload(body)) continue;
|
|
514
|
+
working = working.replace(match[0], "");
|
|
515
|
+
return {
|
|
516
|
+
payload: body,
|
|
517
|
+
text: stripStructuredUiLabels(working).replace(/\n{3,}/g, "\n\n").trim()
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
const bareJson = extractBareJsonObject(raw);
|
|
521
|
+
if (bareJson && looksLikeStructuredPayload(bareJson)) {
|
|
522
|
+
working = working.replace(bareJson, "");
|
|
523
|
+
return {
|
|
524
|
+
payload: bareJson,
|
|
525
|
+
text: stripStructuredUiLabels(working).replace(/\n{3,}/g, "\n\n").trim()
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
payload: null,
|
|
530
|
+
text: stripStructuredUiLabels(raw).replace(/\n{3,}/g, "\n\n").trim()
|
|
531
|
+
};
|
|
532
|
+
}
|
|
504
533
|
function resolveImage(data) {
|
|
505
534
|
for (const key of ["image", "img", "thumbnail"]) {
|
|
506
535
|
const v = data[key];
|
|
@@ -713,6 +742,10 @@ function MessageBubble({
|
|
|
713
742
|
() => /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|type)"\s*:\s*"(?:carousel|products)"/i.test(message.content),
|
|
714
743
|
[message.content]
|
|
715
744
|
);
|
|
745
|
+
const structuredContent = import_react5.default.useMemo(
|
|
746
|
+
() => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
|
|
747
|
+
[isUser, message.content]
|
|
748
|
+
);
|
|
716
749
|
const productsFromSources = import_react5.default.useMemo(() => {
|
|
717
750
|
if (isUser || !sources) return [];
|
|
718
751
|
return sources.filter((s) => {
|
|
@@ -737,13 +770,31 @@ function MessageBubble({
|
|
|
737
770
|
if (isUser) {
|
|
738
771
|
return { productsFromContent: [], cleanContent: message.content };
|
|
739
772
|
}
|
|
740
|
-
const jsonRegex = /```json
|
|
773
|
+
const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
|
|
741
774
|
const products = [];
|
|
742
|
-
let content =
|
|
775
|
+
let content = structuredContent.text;
|
|
776
|
+
const payloadCandidates = [structuredContent.payload, ...content.matchAll(jsonRegex).map((match) => match[1])].filter(Boolean);
|
|
777
|
+
for (const candidate of payloadCandidates) {
|
|
778
|
+
try {
|
|
779
|
+
const data = JSON.parse(sanitizeJson(candidate));
|
|
780
|
+
const itemSet = Array.isArray(data.items) ? data.items : Array.isArray(data.products) ? data.products : null;
|
|
781
|
+
if ((data.type === "products" || data.view === "carousel" || data.view === "table" || Array.isArray(data.products)) && itemSet) {
|
|
782
|
+
products.push(
|
|
783
|
+
...itemSet.map((item) => {
|
|
784
|
+
var _a;
|
|
785
|
+
return __spreadProps(__spreadValues({}, item), {
|
|
786
|
+
image: (_a = item.image) != null ? _a : resolveImage(item)
|
|
787
|
+
});
|
|
788
|
+
})
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
} catch (e) {
|
|
792
|
+
}
|
|
793
|
+
}
|
|
743
794
|
if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
|
|
744
795
|
for (const match of content.matchAll(jsonRegex)) {
|
|
745
796
|
try {
|
|
746
|
-
const data = JSON.parse(match[1]);
|
|
797
|
+
const data = JSON.parse(sanitizeJson(match[1]));
|
|
747
798
|
if (data.type === "products" && Array.isArray(data.items)) {
|
|
748
799
|
products.push(
|
|
749
800
|
...data.items.map((item) => {
|
|
@@ -760,7 +811,7 @@ function MessageBubble({
|
|
|
760
811
|
}
|
|
761
812
|
}
|
|
762
813
|
return { productsFromContent: products, cleanContent: content.trim() };
|
|
763
|
-
}, [message.content, isUser]);
|
|
814
|
+
}, [message.content, isUser, structuredContent]);
|
|
764
815
|
const allProducts = import_react5.default.useMemo(() => {
|
|
765
816
|
if (productsFromContent.length > 0) return productsFromContent;
|
|
766
817
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -778,8 +829,8 @@ function MessageBubble({
|
|
|
778
829
|
});
|
|
779
830
|
}, [productsFromSources, productsFromContent, message.content]);
|
|
780
831
|
const processedMarkdown = import_react5.default.useMemo(() => {
|
|
781
|
-
let raw = cleanContent || message.content;
|
|
782
|
-
const hasStructuredUiBlock = /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
|
|
832
|
+
let raw = structuredContent.payload ? structuredContent.text : cleanContent || message.content;
|
|
833
|
+
const hasStructuredUiBlock = Boolean(structuredContent.payload) || /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
|
|
783
834
|
raw = raw.replace(/([^\n])\n\|/g, "$1\n\n|").replace(/(\|[^\n]*\|)\n\n+(?=\|)/g, "$1\n");
|
|
784
835
|
if (!isStreaming) {
|
|
785
836
|
raw = raw.replace(/(?:^|\n)\s*\{[\s\S]*\}\s*(?:\n|$)/g, (match) => {
|
|
@@ -810,7 +861,7 @@ ${match.trim()}
|
|
|
810
861
|
});
|
|
811
862
|
}
|
|
812
863
|
return raw;
|
|
813
|
-
}, [cleanContent, message.content, isStreaming]);
|
|
864
|
+
}, [cleanContent, message.content, isStreaming, structuredContent]);
|
|
814
865
|
const markdownComponents = import_react5.default.useMemo(
|
|
815
866
|
() => ({
|
|
816
867
|
// Wrap in not-prose so Tailwind Typography resets don't clobber our styles.
|
|
@@ -947,7 +998,17 @@ ${match.trim()}
|
|
|
947
998
|
className: `relative ${isCompact ? "px-3 py-2.5 text-[13px]" : "px-4 py-3 text-sm"} rounded-2xl leading-relaxed shadow-sm dark:shadow-lg ${isUser ? "text-white rounded-tr-sm" : "bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-slate-800 dark:text-white/90 rounded-tl-sm"}`,
|
|
948
999
|
style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
|
|
949
1000
|
},
|
|
950
|
-
isStreaming && !message.content ? /* @__PURE__ */ import_react5.default.createElement("span", { className: "flex items-center gap-1 py-1" }, /* @__PURE__ */ import_react5.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:0ms]" }), /* @__PURE__ */ import_react5.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:150ms]" }), /* @__PURE__ */ import_react5.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:300ms]" })) : /* @__PURE__ */ import_react5.default.createElement("div", { className: `prose ${isCompact ? "prose-xs" : "prose-sm"} max-w-none break-words ${isUser ? "prose-invert" : "dark:prose-invert"}` },
|
|
1001
|
+
isStreaming && !message.content ? /* @__PURE__ */ import_react5.default.createElement("span", { className: "flex items-center gap-1 py-1" }, /* @__PURE__ */ import_react5.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:0ms]" }), /* @__PURE__ */ import_react5.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:150ms]" }), /* @__PURE__ */ import_react5.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:300ms]" })) : /* @__PURE__ */ import_react5.default.createElement("div", { className: `prose ${isCompact ? "prose-xs" : "prose-sm"} max-w-none break-words ${isUser ? "prose-invert" : "dark:prose-invert"}` }, !isUser && structuredContent.payload && /* @__PURE__ */ import_react5.default.createElement(
|
|
1002
|
+
UIDispatcher,
|
|
1003
|
+
{
|
|
1004
|
+
rawContent: structuredContent.payload,
|
|
1005
|
+
primaryColor,
|
|
1006
|
+
accentColor,
|
|
1007
|
+
isStreaming,
|
|
1008
|
+
onAddToCart,
|
|
1009
|
+
viewportSize
|
|
1010
|
+
}
|
|
1011
|
+
), /* @__PURE__ */ import_react5.default.createElement(import_react_markdown.default, { remarkPlugins: [import_remark_gfm.default], components: markdownComponents }, processedMarkdown), isStreaming && message.content && /* @__PURE__ */ import_react5.default.createElement("span", { className: "inline-block w-1.5 h-3.5 ml-1 bg-emerald-500 animate-pulse align-middle" }))
|
|
951
1012
|
), !isUser && !hasStructuredProductBlock && allProducts.length > 0 && /* @__PURE__ */ import_react5.default.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ import_react5.default.createElement(
|
|
952
1013
|
ProductCarousel,
|
|
953
1014
|
{
|
package/dist/index.mjs
CHANGED
|
@@ -210,19 +210,7 @@ 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
|
-
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
|
-
})();
|
|
213
|
+
const pieOuterRadius = isTiny ? "54%" : isCompact ? "60%" : isMedium ? "66%" : "72%";
|
|
226
214
|
const axisTick = { fontSize: isTiny ? 9 : isCompact ? 10 : 12, fill: "#64748b" };
|
|
227
215
|
const showLegend = !isTiny;
|
|
228
216
|
const truncateAxisLabel = (value) => {
|
|
@@ -330,58 +318,42 @@ function DynamicChart({
|
|
|
330
318
|
return row;
|
|
331
319
|
};
|
|
332
320
|
if (type === "pie") {
|
|
333
|
-
return /* @__PURE__ */ React4.createElement(
|
|
334
|
-
|
|
321
|
+
return /* @__PURE__ */ React4.createElement("div", { ref: containerRef, className: "w-full min-w-0 mt-4 mb-2 select-none overflow-hidden" }, /* @__PURE__ */ React4.createElement("div", { className: "mx-auto w-full max-w-full aspect-square" }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(PieChart, null, /* @__PURE__ */ React4.createElement(
|
|
322
|
+
Pie,
|
|
335
323
|
{
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
324
|
+
data: sanitizedData,
|
|
325
|
+
dataKey: pieDataKey,
|
|
326
|
+
nameKey: finalXKey,
|
|
327
|
+
cx: "50%",
|
|
328
|
+
cy: "50%",
|
|
329
|
+
outerRadius: pieOuterRadius,
|
|
330
|
+
label: false,
|
|
331
|
+
labelLine: false
|
|
339
332
|
},
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
);
|
|
333
|
+
sanitizedData.map((entry, index) => /* @__PURE__ */ React4.createElement(Cell, { key: `cell-${index}`, fill: stockAwareColor(entry, index) }))
|
|
334
|
+
), /* @__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" })))));
|
|
355
335
|
}
|
|
356
|
-
return /* @__PURE__ */ React4.createElement(
|
|
357
|
-
|
|
336
|
+
return /* @__PURE__ */ React4.createElement("div", { ref: containerRef, className: "w-full min-w-0 mt-4 mb-2 select-none overflow-hidden" }, /* @__PURE__ */ React4.createElement("div", { className: "w-full aspect-[4/3]" }, /* @__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(
|
|
337
|
+
Bar,
|
|
358
338
|
{
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
{
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
);
|
|
339
|
+
key,
|
|
340
|
+
dataKey: key,
|
|
341
|
+
fill: colors[index % colors.length],
|
|
342
|
+
radius: [4, 4, 0, 0],
|
|
343
|
+
barSize: isTiny ? Math.max(10, 28 / data.length) : isCompact ? Math.max(14, 42 / data.length) : Math.max(20, 60 / data.length)
|
|
344
|
+
}
|
|
345
|
+
))) : /* @__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(
|
|
346
|
+
Line,
|
|
347
|
+
{
|
|
348
|
+
key,
|
|
349
|
+
type: "monotone",
|
|
350
|
+
dataKey: key,
|
|
351
|
+
stroke: colors[index % colors.length],
|
|
352
|
+
strokeWidth: isTiny ? 1.75 : isCompact ? 2 : 3,
|
|
353
|
+
dot: { r: isTiny ? 2.5 : isCompact ? 3 : 4, strokeWidth: 2 },
|
|
354
|
+
activeDot: { r: isTiny ? 4 : isCompact ? 5 : 6 }
|
|
355
|
+
}
|
|
356
|
+
))))));
|
|
385
357
|
}
|
|
386
358
|
|
|
387
359
|
// src/components/MessageBubble.tsx
|
|
@@ -464,6 +436,63 @@ function stripStructuredUiLabels(raw) {
|
|
|
464
436
|
"\n"
|
|
465
437
|
);
|
|
466
438
|
}
|
|
439
|
+
function extractBareJsonObject(raw) {
|
|
440
|
+
const start = raw.indexOf("{");
|
|
441
|
+
if (start === -1) return null;
|
|
442
|
+
let depth = 0;
|
|
443
|
+
let inString = false;
|
|
444
|
+
let escape = false;
|
|
445
|
+
for (let i = start; i < raw.length; i += 1) {
|
|
446
|
+
const ch = raw[i];
|
|
447
|
+
if (escape) {
|
|
448
|
+
escape = false;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (ch === "\\" && inString) {
|
|
452
|
+
escape = true;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (ch === '"') {
|
|
456
|
+
inString = !inString;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (inString) continue;
|
|
460
|
+
if (ch === "{") depth += 1;
|
|
461
|
+
if (ch === "}") {
|
|
462
|
+
depth -= 1;
|
|
463
|
+
if (depth === 0) {
|
|
464
|
+
return raw.slice(start, i + 1);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
function extractStructuredPayload(raw) {
|
|
471
|
+
var _a, _b;
|
|
472
|
+
let working = raw;
|
|
473
|
+
const fenceRegex = /```(?:[a-z]+)?\s*([\s\S]*?)```/gi;
|
|
474
|
+
for (const match of raw.matchAll(fenceRegex)) {
|
|
475
|
+
const body = (_b = (_a = match[1]) == null ? void 0 : _a.trim()) != null ? _b : "";
|
|
476
|
+
if (!looksLikeStructuredPayload(body)) continue;
|
|
477
|
+
working = working.replace(match[0], "");
|
|
478
|
+
return {
|
|
479
|
+
payload: body,
|
|
480
|
+
text: stripStructuredUiLabels(working).replace(/\n{3,}/g, "\n\n").trim()
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
const bareJson = extractBareJsonObject(raw);
|
|
484
|
+
if (bareJson && looksLikeStructuredPayload(bareJson)) {
|
|
485
|
+
working = working.replace(bareJson, "");
|
|
486
|
+
return {
|
|
487
|
+
payload: bareJson,
|
|
488
|
+
text: stripStructuredUiLabels(working).replace(/\n{3,}/g, "\n\n").trim()
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
payload: null,
|
|
493
|
+
text: stripStructuredUiLabels(raw).replace(/\n{3,}/g, "\n\n").trim()
|
|
494
|
+
};
|
|
495
|
+
}
|
|
467
496
|
function resolveImage(data) {
|
|
468
497
|
for (const key of ["image", "img", "thumbnail"]) {
|
|
469
498
|
const v = data[key];
|
|
@@ -676,6 +705,10 @@ function MessageBubble({
|
|
|
676
705
|
() => /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|type)"\s*:\s*"(?:carousel|products)"/i.test(message.content),
|
|
677
706
|
[message.content]
|
|
678
707
|
);
|
|
708
|
+
const structuredContent = React5.useMemo(
|
|
709
|
+
() => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
|
|
710
|
+
[isUser, message.content]
|
|
711
|
+
);
|
|
679
712
|
const productsFromSources = React5.useMemo(() => {
|
|
680
713
|
if (isUser || !sources) return [];
|
|
681
714
|
return sources.filter((s) => {
|
|
@@ -700,13 +733,31 @@ function MessageBubble({
|
|
|
700
733
|
if (isUser) {
|
|
701
734
|
return { productsFromContent: [], cleanContent: message.content };
|
|
702
735
|
}
|
|
703
|
-
const jsonRegex = /```json
|
|
736
|
+
const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
|
|
704
737
|
const products = [];
|
|
705
|
-
let content =
|
|
738
|
+
let content = structuredContent.text;
|
|
739
|
+
const payloadCandidates = [structuredContent.payload, ...content.matchAll(jsonRegex).map((match) => match[1])].filter(Boolean);
|
|
740
|
+
for (const candidate of payloadCandidates) {
|
|
741
|
+
try {
|
|
742
|
+
const data = JSON.parse(sanitizeJson(candidate));
|
|
743
|
+
const itemSet = Array.isArray(data.items) ? data.items : Array.isArray(data.products) ? data.products : null;
|
|
744
|
+
if ((data.type === "products" || data.view === "carousel" || data.view === "table" || Array.isArray(data.products)) && itemSet) {
|
|
745
|
+
products.push(
|
|
746
|
+
...itemSet.map((item) => {
|
|
747
|
+
var _a;
|
|
748
|
+
return __spreadProps(__spreadValues({}, item), {
|
|
749
|
+
image: (_a = item.image) != null ? _a : resolveImage(item)
|
|
750
|
+
});
|
|
751
|
+
})
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
} catch (e) {
|
|
755
|
+
}
|
|
756
|
+
}
|
|
706
757
|
if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
|
|
707
758
|
for (const match of content.matchAll(jsonRegex)) {
|
|
708
759
|
try {
|
|
709
|
-
const data = JSON.parse(match[1]);
|
|
760
|
+
const data = JSON.parse(sanitizeJson(match[1]));
|
|
710
761
|
if (data.type === "products" && Array.isArray(data.items)) {
|
|
711
762
|
products.push(
|
|
712
763
|
...data.items.map((item) => {
|
|
@@ -723,7 +774,7 @@ function MessageBubble({
|
|
|
723
774
|
}
|
|
724
775
|
}
|
|
725
776
|
return { productsFromContent: products, cleanContent: content.trim() };
|
|
726
|
-
}, [message.content, isUser]);
|
|
777
|
+
}, [message.content, isUser, structuredContent]);
|
|
727
778
|
const allProducts = React5.useMemo(() => {
|
|
728
779
|
if (productsFromContent.length > 0) return productsFromContent;
|
|
729
780
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -741,8 +792,8 @@ function MessageBubble({
|
|
|
741
792
|
});
|
|
742
793
|
}, [productsFromSources, productsFromContent, message.content]);
|
|
743
794
|
const processedMarkdown = React5.useMemo(() => {
|
|
744
|
-
let raw = cleanContent || message.content;
|
|
745
|
-
const hasStructuredUiBlock = /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
|
|
795
|
+
let raw = structuredContent.payload ? structuredContent.text : cleanContent || message.content;
|
|
796
|
+
const hasStructuredUiBlock = Boolean(structuredContent.payload) || /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
|
|
746
797
|
raw = raw.replace(/([^\n])\n\|/g, "$1\n\n|").replace(/(\|[^\n]*\|)\n\n+(?=\|)/g, "$1\n");
|
|
747
798
|
if (!isStreaming) {
|
|
748
799
|
raw = raw.replace(/(?:^|\n)\s*\{[\s\S]*\}\s*(?:\n|$)/g, (match) => {
|
|
@@ -773,7 +824,7 @@ ${match.trim()}
|
|
|
773
824
|
});
|
|
774
825
|
}
|
|
775
826
|
return raw;
|
|
776
|
-
}, [cleanContent, message.content, isStreaming]);
|
|
827
|
+
}, [cleanContent, message.content, isStreaming, structuredContent]);
|
|
777
828
|
const markdownComponents = React5.useMemo(
|
|
778
829
|
() => ({
|
|
779
830
|
// Wrap in not-prose so Tailwind Typography resets don't clobber our styles.
|
|
@@ -910,7 +961,17 @@ ${match.trim()}
|
|
|
910
961
|
className: `relative ${isCompact ? "px-3 py-2.5 text-[13px]" : "px-4 py-3 text-sm"} rounded-2xl leading-relaxed shadow-sm dark:shadow-lg ${isUser ? "text-white rounded-tr-sm" : "bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-slate-800 dark:text-white/90 rounded-tl-sm"}`,
|
|
911
962
|
style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
|
|
912
963
|
},
|
|
913
|
-
isStreaming && !message.content ? /* @__PURE__ */ React5.createElement("span", { className: "flex items-center gap-1 py-1" }, /* @__PURE__ */ React5.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:0ms]" }), /* @__PURE__ */ React5.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:150ms]" }), /* @__PURE__ */ React5.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:300ms]" })) : /* @__PURE__ */ React5.createElement("div", { className: `prose ${isCompact ? "prose-xs" : "prose-sm"} max-w-none break-words ${isUser ? "prose-invert" : "dark:prose-invert"}` },
|
|
964
|
+
isStreaming && !message.content ? /* @__PURE__ */ React5.createElement("span", { className: "flex items-center gap-1 py-1" }, /* @__PURE__ */ React5.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:0ms]" }), /* @__PURE__ */ React5.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:150ms]" }), /* @__PURE__ */ React5.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:300ms]" })) : /* @__PURE__ */ React5.createElement("div", { className: `prose ${isCompact ? "prose-xs" : "prose-sm"} max-w-none break-words ${isUser ? "prose-invert" : "dark:prose-invert"}` }, !isUser && structuredContent.payload && /* @__PURE__ */ React5.createElement(
|
|
965
|
+
UIDispatcher,
|
|
966
|
+
{
|
|
967
|
+
rawContent: structuredContent.payload,
|
|
968
|
+
primaryColor,
|
|
969
|
+
accentColor,
|
|
970
|
+
isStreaming,
|
|
971
|
+
onAddToCart,
|
|
972
|
+
viewportSize
|
|
973
|
+
}
|
|
974
|
+
), /* @__PURE__ */ React5.createElement(ReactMarkdown, { remarkPlugins: [remarkGfm], components: markdownComponents }, processedMarkdown), isStreaming && message.content && /* @__PURE__ */ React5.createElement("span", { className: "inline-block w-1.5 h-3.5 ml-1 bg-emerald-500 animate-pulse align-middle" }))
|
|
914
975
|
), !isUser && !hasStructuredProductBlock && allProducts.length > 0 && /* @__PURE__ */ React5.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ React5.createElement(
|
|
915
976
|
ProductCarousel,
|
|
916
977
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retrivora-ai/rag-engine",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
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,20 +57,7 @@ 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
|
-
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
|
-
})();
|
|
60
|
+
const pieOuterRadius = isTiny ? '54%' : isCompact ? '60%' : isMedium ? '66%' : '72%';
|
|
74
61
|
const axisTick = { fontSize: isTiny ? 9 : isCompact ? 10 : 12, fill: '#64748b' };
|
|
75
62
|
const showLegend = !isTiny;
|
|
76
63
|
const truncateAxisLabel = (value: string | number) => {
|
|
@@ -234,12 +221,8 @@ export function DynamicChart({
|
|
|
234
221
|
// Handle Pie Chart separately as it has a different structure
|
|
235
222
|
if (type === 'pie') {
|
|
236
223
|
return (
|
|
237
|
-
<div
|
|
238
|
-
|
|
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">
|
|
224
|
+
<div ref={containerRef} className="w-full min-w-0 mt-4 mb-2 select-none overflow-hidden">
|
|
225
|
+
<div className="mx-auto w-full max-w-full aspect-square">
|
|
243
226
|
<ResponsiveContainer width="100%" height="100%">
|
|
244
227
|
<PieChart>
|
|
245
228
|
<Pie
|
|
@@ -267,11 +250,8 @@ export function DynamicChart({
|
|
|
267
250
|
|
|
268
251
|
// Bar and Line charts
|
|
269
252
|
return (
|
|
270
|
-
<div
|
|
271
|
-
|
|
272
|
-
className="w-full min-w-0 mt-4 mb-2 select-none overflow-hidden"
|
|
273
|
-
style={{ height: chartHeight }}
|
|
274
|
-
>
|
|
253
|
+
<div ref={containerRef} className="w-full min-w-0 mt-4 mb-2 select-none overflow-hidden">
|
|
254
|
+
<div className="w-full aspect-[4/3]">
|
|
275
255
|
<ResponsiveContainer width="100%" height="100%">
|
|
276
256
|
{type === 'bar' ? (
|
|
277
257
|
<BarChart data={sanitizedData} margin={chartMargin}>
|
|
@@ -311,6 +291,7 @@ export function DynamicChart({
|
|
|
311
291
|
</LineChart>
|
|
312
292
|
)}
|
|
313
293
|
</ResponsiveContainer>
|
|
294
|
+
</div>
|
|
314
295
|
</div>
|
|
315
296
|
);
|
|
316
297
|
}
|
|
@@ -123,6 +123,76 @@ function stripStructuredUiLabels(raw: string): string {
|
|
|
123
123
|
);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
function extractBareJsonObject(raw: string): string | null {
|
|
127
|
+
const start = raw.indexOf('{');
|
|
128
|
+
if (start === -1) return null;
|
|
129
|
+
|
|
130
|
+
let depth = 0;
|
|
131
|
+
let inString = false;
|
|
132
|
+
let escape = false;
|
|
133
|
+
|
|
134
|
+
for (let i = start; i < raw.length; i += 1) {
|
|
135
|
+
const ch = raw[i];
|
|
136
|
+
|
|
137
|
+
if (escape) {
|
|
138
|
+
escape = false;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (ch === '\\' && inString) {
|
|
143
|
+
escape = true;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (ch === '"') {
|
|
148
|
+
inString = !inString;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (inString) continue;
|
|
153
|
+
|
|
154
|
+
if (ch === '{') depth += 1;
|
|
155
|
+
if (ch === '}') {
|
|
156
|
+
depth -= 1;
|
|
157
|
+
if (depth === 0) {
|
|
158
|
+
return raw.slice(start, i + 1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function extractStructuredPayload(raw: string): { payload: string | null; text: string } {
|
|
167
|
+
let working = raw;
|
|
168
|
+
const fenceRegex = /```(?:[a-z]+)?\s*([\s\S]*?)```/gi;
|
|
169
|
+
|
|
170
|
+
for (const match of raw.matchAll(fenceRegex)) {
|
|
171
|
+
const body = match[1]?.trim() ?? '';
|
|
172
|
+
if (!looksLikeStructuredPayload(body)) continue;
|
|
173
|
+
|
|
174
|
+
working = working.replace(match[0], '');
|
|
175
|
+
return {
|
|
176
|
+
payload: body,
|
|
177
|
+
text: stripStructuredUiLabels(working).replace(/\n{3,}/g, '\n\n').trim(),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const bareJson = extractBareJsonObject(raw);
|
|
182
|
+
if (bareJson && looksLikeStructuredPayload(bareJson)) {
|
|
183
|
+
working = working.replace(bareJson, '');
|
|
184
|
+
return {
|
|
185
|
+
payload: bareJson,
|
|
186
|
+
text: stripStructuredUiLabels(working).replace(/\n{3,}/g, '\n\n').trim(),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
payload: null,
|
|
192
|
+
text: stripStructuredUiLabels(raw).replace(/\n{3,}/g, '\n\n').trim(),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
126
196
|
// ─── Tiny helpers ─────────────────────────────────────────────────────────────
|
|
127
197
|
|
|
128
198
|
function resolveImage(data: Record<string, unknown>): string | undefined {
|
|
@@ -510,6 +580,10 @@ export function MessageBubble({
|
|
|
510
580
|
() => /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|type)"\s*:\s*"(?:carousel|products)"/i.test(message.content),
|
|
511
581
|
[message.content],
|
|
512
582
|
);
|
|
583
|
+
const structuredContent = React.useMemo(
|
|
584
|
+
() => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
|
|
585
|
+
[isUser, message.content],
|
|
586
|
+
);
|
|
513
587
|
|
|
514
588
|
// ── Products from sources ──────────────────────────────────────────────────
|
|
515
589
|
const productsFromSources = React.useMemo<Product[]>(() => {
|
|
@@ -539,14 +613,40 @@ export function MessageBubble({
|
|
|
539
613
|
return { productsFromContent: [] as Product[], cleanContent: message.content };
|
|
540
614
|
}
|
|
541
615
|
|
|
542
|
-
const jsonRegex = /```json
|
|
616
|
+
const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
|
|
543
617
|
const products: Product[] = [];
|
|
544
|
-
let content =
|
|
618
|
+
let content = structuredContent.text;
|
|
619
|
+
|
|
620
|
+
const payloadCandidates = [structuredContent.payload, ...content.matchAll(jsonRegex).map((match) => match[1])].filter(Boolean) as string[];
|
|
621
|
+
|
|
622
|
+
for (const candidate of payloadCandidates) {
|
|
623
|
+
try {
|
|
624
|
+
const data = JSON.parse(sanitizeJson(candidate));
|
|
625
|
+
const itemSet =
|
|
626
|
+
Array.isArray(data.items) ? data.items
|
|
627
|
+
: Array.isArray(data.products) ? data.products
|
|
628
|
+
: null;
|
|
629
|
+
|
|
630
|
+
if (
|
|
631
|
+
(data.type === 'products' || data.view === 'carousel' || data.view === 'table' || Array.isArray(data.products)) &&
|
|
632
|
+
itemSet
|
|
633
|
+
) {
|
|
634
|
+
products.push(
|
|
635
|
+
...itemSet.map((item: Record<string, unknown>) => ({
|
|
636
|
+
...item,
|
|
637
|
+
image: (item.image as string) ?? resolveImage(item),
|
|
638
|
+
})) as Product[],
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
} catch {
|
|
642
|
+
// Ignore malformed product payloads
|
|
643
|
+
}
|
|
644
|
+
}
|
|
545
645
|
|
|
546
646
|
if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
|
|
547
647
|
for (const match of content.matchAll(jsonRegex)) {
|
|
548
648
|
try {
|
|
549
|
-
const data = JSON.parse(match[1]);
|
|
649
|
+
const data = JSON.parse(sanitizeJson(match[1]));
|
|
550
650
|
if (data.type === 'products' && Array.isArray(data.items)) {
|
|
551
651
|
products.push(
|
|
552
652
|
...data.items.map((item: Record<string, unknown>) => ({
|
|
@@ -563,7 +663,7 @@ export function MessageBubble({
|
|
|
563
663
|
}
|
|
564
664
|
|
|
565
665
|
return { productsFromContent: products, cleanContent: content.trim() };
|
|
566
|
-
}, [message.content, isUser]);
|
|
666
|
+
}, [message.content, isUser, structuredContent]);
|
|
567
667
|
|
|
568
668
|
// ── Merge & deduplicate products ───────────────────────────────────────────
|
|
569
669
|
const allProducts = React.useMemo<Product[]>(() => {
|
|
@@ -584,8 +684,8 @@ export function MessageBubble({
|
|
|
584
684
|
}, [productsFromSources, productsFromContent, message.content]);
|
|
585
685
|
|
|
586
686
|
const processedMarkdown = React.useMemo(() => {
|
|
587
|
-
let raw = cleanContent || message.content;
|
|
588
|
-
const hasStructuredUiBlock = /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
|
|
687
|
+
let raw = structuredContent.payload ? structuredContent.text : (cleanContent || message.content);
|
|
688
|
+
const hasStructuredUiBlock = Boolean(structuredContent.payload) || /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
|
|
589
689
|
|
|
590
690
|
// 1. Structural fixes
|
|
591
691
|
raw = raw.replace(/([^\n])\n\|/g, '$1\n\n|') // Ensure blank line before table
|
|
@@ -622,7 +722,7 @@ export function MessageBubble({
|
|
|
622
722
|
}
|
|
623
723
|
|
|
624
724
|
return raw;
|
|
625
|
-
}, [cleanContent, message.content, isStreaming]);
|
|
725
|
+
}, [cleanContent, message.content, isStreaming, structuredContent]);
|
|
626
726
|
|
|
627
727
|
// ── Markdown component overrides ───────────────────────────────────────────
|
|
628
728
|
const markdownComponents = React.useMemo(
|
|
@@ -792,6 +892,17 @@ export function MessageBubble({
|
|
|
792
892
|
</span>
|
|
793
893
|
) : (
|
|
794
894
|
<div className={`prose ${isCompact ? 'prose-xs' : 'prose-sm'} max-w-none break-words ${isUser ? 'prose-invert' : 'dark:prose-invert'}`}>
|
|
895
|
+
{!isUser && structuredContent.payload && (
|
|
896
|
+
<UIDispatcher
|
|
897
|
+
rawContent={structuredContent.payload}
|
|
898
|
+
primaryColor={primaryColor}
|
|
899
|
+
accentColor={accentColor}
|
|
900
|
+
isStreaming={isStreaming}
|
|
901
|
+
onAddToCart={onAddToCart}
|
|
902
|
+
viewportSize={viewportSize}
|
|
903
|
+
/>
|
|
904
|
+
)}
|
|
905
|
+
|
|
795
906
|
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
|
796
907
|
{processedMarkdown}
|
|
797
908
|
</ReactMarkdown>
|