@retrivora-ai/rag-engine 1.6.1 → 1.6.3
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 +135 -69
- package/dist/index.mjs +135 -69
- package/package.json +1 -1
- package/src/components/DynamicChart.tsx +6 -25
- package/src/components/MessageBubble.tsx +129 -10
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,67 @@ 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
|
+
}
|
|
533
|
+
function isLikelyUiOnlyMessage(text) {
|
|
534
|
+
const trimmed = text.trim();
|
|
535
|
+
return trimmed.length === 0 || /^(chart data|table data|visualization|visual representation)\s*:?\s*$/i.test(trimmed);
|
|
536
|
+
}
|
|
504
537
|
function resolveImage(data) {
|
|
505
538
|
for (const key of ["image", "img", "thumbnail"]) {
|
|
506
539
|
const v = data[key];
|
|
@@ -713,6 +746,11 @@ function MessageBubble({
|
|
|
713
746
|
() => /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|type)"\s*:\s*"(?:carousel|products)"/i.test(message.content),
|
|
714
747
|
[message.content]
|
|
715
748
|
);
|
|
749
|
+
const structuredContent = import_react5.default.useMemo(
|
|
750
|
+
() => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
|
|
751
|
+
[isUser, message.content]
|
|
752
|
+
);
|
|
753
|
+
const shouldRenderStructuredOnly = !isUser && Boolean(structuredContent.payload) && isLikelyUiOnlyMessage(structuredContent.text);
|
|
716
754
|
const productsFromSources = import_react5.default.useMemo(() => {
|
|
717
755
|
if (isUser || !sources) return [];
|
|
718
756
|
return sources.filter((s) => {
|
|
@@ -737,13 +775,31 @@ function MessageBubble({
|
|
|
737
775
|
if (isUser) {
|
|
738
776
|
return { productsFromContent: [], cleanContent: message.content };
|
|
739
777
|
}
|
|
740
|
-
const jsonRegex = /```json
|
|
778
|
+
const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
|
|
741
779
|
const products = [];
|
|
742
|
-
let content =
|
|
780
|
+
let content = structuredContent.text;
|
|
781
|
+
const payloadCandidates = [structuredContent.payload, ...content.matchAll(jsonRegex).map((match) => match[1])].filter(Boolean);
|
|
782
|
+
for (const candidate of payloadCandidates) {
|
|
783
|
+
try {
|
|
784
|
+
const data = JSON.parse(sanitizeJson(candidate));
|
|
785
|
+
const itemSet = Array.isArray(data.items) ? data.items : Array.isArray(data.products) ? data.products : null;
|
|
786
|
+
if ((data.type === "products" || data.view === "carousel" || data.view === "table" || Array.isArray(data.products)) && itemSet) {
|
|
787
|
+
products.push(
|
|
788
|
+
...itemSet.map((item) => {
|
|
789
|
+
var _a;
|
|
790
|
+
return __spreadProps(__spreadValues({}, item), {
|
|
791
|
+
image: (_a = item.image) != null ? _a : resolveImage(item)
|
|
792
|
+
});
|
|
793
|
+
})
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
} catch (e) {
|
|
797
|
+
}
|
|
798
|
+
}
|
|
743
799
|
if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
|
|
744
800
|
for (const match of content.matchAll(jsonRegex)) {
|
|
745
801
|
try {
|
|
746
|
-
const data = JSON.parse(match[1]);
|
|
802
|
+
const data = JSON.parse(sanitizeJson(match[1]));
|
|
747
803
|
if (data.type === "products" && Array.isArray(data.items)) {
|
|
748
804
|
products.push(
|
|
749
805
|
...data.items.map((item) => {
|
|
@@ -760,7 +816,7 @@ function MessageBubble({
|
|
|
760
816
|
}
|
|
761
817
|
}
|
|
762
818
|
return { productsFromContent: products, cleanContent: content.trim() };
|
|
763
|
-
}, [message.content, isUser]);
|
|
819
|
+
}, [message.content, isUser, structuredContent]);
|
|
764
820
|
const allProducts = import_react5.default.useMemo(() => {
|
|
765
821
|
if (productsFromContent.length > 0) return productsFromContent;
|
|
766
822
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -778,8 +834,8 @@ function MessageBubble({
|
|
|
778
834
|
});
|
|
779
835
|
}, [productsFromSources, productsFromContent, message.content]);
|
|
780
836
|
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);
|
|
837
|
+
let raw = structuredContent.payload ? structuredContent.text : cleanContent || message.content;
|
|
838
|
+
const hasStructuredUiBlock = Boolean(structuredContent.payload) || /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
|
|
783
839
|
raw = raw.replace(/([^\n])\n\|/g, "$1\n\n|").replace(/(\|[^\n]*\|)\n\n+(?=\|)/g, "$1\n");
|
|
784
840
|
if (!isStreaming) {
|
|
785
841
|
raw = raw.replace(/(?:^|\n)\s*\{[\s\S]*\}\s*(?:\n|$)/g, (match) => {
|
|
@@ -810,7 +866,7 @@ ${match.trim()}
|
|
|
810
866
|
});
|
|
811
867
|
}
|
|
812
868
|
return raw;
|
|
813
|
-
}, [cleanContent, message.content, isStreaming]);
|
|
869
|
+
}, [cleanContent, message.content, isStreaming, structuredContent]);
|
|
814
870
|
const markdownComponents = import_react5.default.useMemo(
|
|
815
871
|
() => ({
|
|
816
872
|
// Wrap in not-prose so Tailwind Typography resets don't clobber our styles.
|
|
@@ -947,7 +1003,17 @@ ${match.trim()}
|
|
|
947
1003
|
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
1004
|
style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
|
|
949
1005
|
},
|
|
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"}` },
|
|
1006
|
+
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(
|
|
1007
|
+
UIDispatcher,
|
|
1008
|
+
{
|
|
1009
|
+
rawContent: structuredContent.payload,
|
|
1010
|
+
primaryColor,
|
|
1011
|
+
accentColor,
|
|
1012
|
+
isStreaming,
|
|
1013
|
+
onAddToCart,
|
|
1014
|
+
viewportSize
|
|
1015
|
+
}
|
|
1016
|
+
), !shouldRenderStructuredOnly && /* @__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
1017
|
), !isUser && !hasStructuredProductBlock && allProducts.length > 0 && /* @__PURE__ */ import_react5.default.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ import_react5.default.createElement(
|
|
952
1018
|
ProductCarousel,
|
|
953
1019
|
{
|
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,67 @@ 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
|
+
}
|
|
496
|
+
function isLikelyUiOnlyMessage(text) {
|
|
497
|
+
const trimmed = text.trim();
|
|
498
|
+
return trimmed.length === 0 || /^(chart data|table data|visualization|visual representation)\s*:?\s*$/i.test(trimmed);
|
|
499
|
+
}
|
|
467
500
|
function resolveImage(data) {
|
|
468
501
|
for (const key of ["image", "img", "thumbnail"]) {
|
|
469
502
|
const v = data[key];
|
|
@@ -676,6 +709,11 @@ function MessageBubble({
|
|
|
676
709
|
() => /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|type)"\s*:\s*"(?:carousel|products)"/i.test(message.content),
|
|
677
710
|
[message.content]
|
|
678
711
|
);
|
|
712
|
+
const structuredContent = React5.useMemo(
|
|
713
|
+
() => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
|
|
714
|
+
[isUser, message.content]
|
|
715
|
+
);
|
|
716
|
+
const shouldRenderStructuredOnly = !isUser && Boolean(structuredContent.payload) && isLikelyUiOnlyMessage(structuredContent.text);
|
|
679
717
|
const productsFromSources = React5.useMemo(() => {
|
|
680
718
|
if (isUser || !sources) return [];
|
|
681
719
|
return sources.filter((s) => {
|
|
@@ -700,13 +738,31 @@ function MessageBubble({
|
|
|
700
738
|
if (isUser) {
|
|
701
739
|
return { productsFromContent: [], cleanContent: message.content };
|
|
702
740
|
}
|
|
703
|
-
const jsonRegex = /```json
|
|
741
|
+
const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
|
|
704
742
|
const products = [];
|
|
705
|
-
let content =
|
|
743
|
+
let content = structuredContent.text;
|
|
744
|
+
const payloadCandidates = [structuredContent.payload, ...content.matchAll(jsonRegex).map((match) => match[1])].filter(Boolean);
|
|
745
|
+
for (const candidate of payloadCandidates) {
|
|
746
|
+
try {
|
|
747
|
+
const data = JSON.parse(sanitizeJson(candidate));
|
|
748
|
+
const itemSet = Array.isArray(data.items) ? data.items : Array.isArray(data.products) ? data.products : null;
|
|
749
|
+
if ((data.type === "products" || data.view === "carousel" || data.view === "table" || Array.isArray(data.products)) && itemSet) {
|
|
750
|
+
products.push(
|
|
751
|
+
...itemSet.map((item) => {
|
|
752
|
+
var _a;
|
|
753
|
+
return __spreadProps(__spreadValues({}, item), {
|
|
754
|
+
image: (_a = item.image) != null ? _a : resolveImage(item)
|
|
755
|
+
});
|
|
756
|
+
})
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
} catch (e) {
|
|
760
|
+
}
|
|
761
|
+
}
|
|
706
762
|
if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
|
|
707
763
|
for (const match of content.matchAll(jsonRegex)) {
|
|
708
764
|
try {
|
|
709
|
-
const data = JSON.parse(match[1]);
|
|
765
|
+
const data = JSON.parse(sanitizeJson(match[1]));
|
|
710
766
|
if (data.type === "products" && Array.isArray(data.items)) {
|
|
711
767
|
products.push(
|
|
712
768
|
...data.items.map((item) => {
|
|
@@ -723,7 +779,7 @@ function MessageBubble({
|
|
|
723
779
|
}
|
|
724
780
|
}
|
|
725
781
|
return { productsFromContent: products, cleanContent: content.trim() };
|
|
726
|
-
}, [message.content, isUser]);
|
|
782
|
+
}, [message.content, isUser, structuredContent]);
|
|
727
783
|
const allProducts = React5.useMemo(() => {
|
|
728
784
|
if (productsFromContent.length > 0) return productsFromContent;
|
|
729
785
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -741,8 +797,8 @@ function MessageBubble({
|
|
|
741
797
|
});
|
|
742
798
|
}, [productsFromSources, productsFromContent, message.content]);
|
|
743
799
|
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);
|
|
800
|
+
let raw = structuredContent.payload ? structuredContent.text : cleanContent || message.content;
|
|
801
|
+
const hasStructuredUiBlock = Boolean(structuredContent.payload) || /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
|
|
746
802
|
raw = raw.replace(/([^\n])\n\|/g, "$1\n\n|").replace(/(\|[^\n]*\|)\n\n+(?=\|)/g, "$1\n");
|
|
747
803
|
if (!isStreaming) {
|
|
748
804
|
raw = raw.replace(/(?:^|\n)\s*\{[\s\S]*\}\s*(?:\n|$)/g, (match) => {
|
|
@@ -773,7 +829,7 @@ ${match.trim()}
|
|
|
773
829
|
});
|
|
774
830
|
}
|
|
775
831
|
return raw;
|
|
776
|
-
}, [cleanContent, message.content, isStreaming]);
|
|
832
|
+
}, [cleanContent, message.content, isStreaming, structuredContent]);
|
|
777
833
|
const markdownComponents = React5.useMemo(
|
|
778
834
|
() => ({
|
|
779
835
|
// Wrap in not-prose so Tailwind Typography resets don't clobber our styles.
|
|
@@ -910,7 +966,17 @@ ${match.trim()}
|
|
|
910
966
|
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
967
|
style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
|
|
912
968
|
},
|
|
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"}` },
|
|
969
|
+
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(
|
|
970
|
+
UIDispatcher,
|
|
971
|
+
{
|
|
972
|
+
rawContent: structuredContent.payload,
|
|
973
|
+
primaryColor,
|
|
974
|
+
accentColor,
|
|
975
|
+
isStreaming,
|
|
976
|
+
onAddToCart,
|
|
977
|
+
viewportSize
|
|
978
|
+
}
|
|
979
|
+
), !shouldRenderStructuredOnly && /* @__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
980
|
), !isUser && !hasStructuredProductBlock && allProducts.length > 0 && /* @__PURE__ */ React5.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ React5.createElement(
|
|
915
981
|
ProductCarousel,
|
|
916
982
|
{
|
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.3",
|
|
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,81 @@ 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
|
+
|
|
196
|
+
function isLikelyUiOnlyMessage(text: string): boolean {
|
|
197
|
+
const trimmed = text.trim();
|
|
198
|
+
return trimmed.length === 0 || /^(chart data|table data|visualization|visual representation)\s*:?\s*$/i.test(trimmed);
|
|
199
|
+
}
|
|
200
|
+
|
|
126
201
|
// ─── Tiny helpers ─────────────────────────────────────────────────────────────
|
|
127
202
|
|
|
128
203
|
function resolveImage(data: Record<string, unknown>): string | undefined {
|
|
@@ -510,6 +585,11 @@ export function MessageBubble({
|
|
|
510
585
|
() => /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|type)"\s*:\s*"(?:carousel|products)"/i.test(message.content),
|
|
511
586
|
[message.content],
|
|
512
587
|
);
|
|
588
|
+
const structuredContent = React.useMemo(
|
|
589
|
+
() => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
|
|
590
|
+
[isUser, message.content],
|
|
591
|
+
);
|
|
592
|
+
const shouldRenderStructuredOnly = !isUser && Boolean(structuredContent.payload) && isLikelyUiOnlyMessage(structuredContent.text);
|
|
513
593
|
|
|
514
594
|
// ── Products from sources ──────────────────────────────────────────────────
|
|
515
595
|
const productsFromSources = React.useMemo<Product[]>(() => {
|
|
@@ -539,14 +619,40 @@ export function MessageBubble({
|
|
|
539
619
|
return { productsFromContent: [] as Product[], cleanContent: message.content };
|
|
540
620
|
}
|
|
541
621
|
|
|
542
|
-
const jsonRegex = /```json
|
|
622
|
+
const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
|
|
543
623
|
const products: Product[] = [];
|
|
544
|
-
let content =
|
|
624
|
+
let content = structuredContent.text;
|
|
625
|
+
|
|
626
|
+
const payloadCandidates = [structuredContent.payload, ...content.matchAll(jsonRegex).map((match) => match[1])].filter(Boolean) as string[];
|
|
627
|
+
|
|
628
|
+
for (const candidate of payloadCandidates) {
|
|
629
|
+
try {
|
|
630
|
+
const data = JSON.parse(sanitizeJson(candidate));
|
|
631
|
+
const itemSet =
|
|
632
|
+
Array.isArray(data.items) ? data.items
|
|
633
|
+
: Array.isArray(data.products) ? data.products
|
|
634
|
+
: null;
|
|
635
|
+
|
|
636
|
+
if (
|
|
637
|
+
(data.type === 'products' || data.view === 'carousel' || data.view === 'table' || Array.isArray(data.products)) &&
|
|
638
|
+
itemSet
|
|
639
|
+
) {
|
|
640
|
+
products.push(
|
|
641
|
+
...itemSet.map((item: Record<string, unknown>) => ({
|
|
642
|
+
...item,
|
|
643
|
+
image: (item.image as string) ?? resolveImage(item),
|
|
644
|
+
})) as Product[],
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
} catch {
|
|
648
|
+
// Ignore malformed product payloads
|
|
649
|
+
}
|
|
650
|
+
}
|
|
545
651
|
|
|
546
652
|
if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
|
|
547
653
|
for (const match of content.matchAll(jsonRegex)) {
|
|
548
654
|
try {
|
|
549
|
-
const data = JSON.parse(match[1]);
|
|
655
|
+
const data = JSON.parse(sanitizeJson(match[1]));
|
|
550
656
|
if (data.type === 'products' && Array.isArray(data.items)) {
|
|
551
657
|
products.push(
|
|
552
658
|
...data.items.map((item: Record<string, unknown>) => ({
|
|
@@ -563,7 +669,7 @@ export function MessageBubble({
|
|
|
563
669
|
}
|
|
564
670
|
|
|
565
671
|
return { productsFromContent: products, cleanContent: content.trim() };
|
|
566
|
-
}, [message.content, isUser]);
|
|
672
|
+
}, [message.content, isUser, structuredContent]);
|
|
567
673
|
|
|
568
674
|
// ── Merge & deduplicate products ───────────────────────────────────────────
|
|
569
675
|
const allProducts = React.useMemo<Product[]>(() => {
|
|
@@ -584,8 +690,8 @@ export function MessageBubble({
|
|
|
584
690
|
}, [productsFromSources, productsFromContent, message.content]);
|
|
585
691
|
|
|
586
692
|
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);
|
|
693
|
+
let raw = structuredContent.payload ? structuredContent.text : (cleanContent || message.content);
|
|
694
|
+
const hasStructuredUiBlock = Boolean(structuredContent.payload) || /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
|
|
589
695
|
|
|
590
696
|
// 1. Structural fixes
|
|
591
697
|
raw = raw.replace(/([^\n])\n\|/g, '$1\n\n|') // Ensure blank line before table
|
|
@@ -622,7 +728,7 @@ export function MessageBubble({
|
|
|
622
728
|
}
|
|
623
729
|
|
|
624
730
|
return raw;
|
|
625
|
-
}, [cleanContent, message.content, isStreaming]);
|
|
731
|
+
}, [cleanContent, message.content, isStreaming, structuredContent]);
|
|
626
732
|
|
|
627
733
|
// ── Markdown component overrides ───────────────────────────────────────────
|
|
628
734
|
const markdownComponents = React.useMemo(
|
|
@@ -792,9 +898,22 @@ export function MessageBubble({
|
|
|
792
898
|
</span>
|
|
793
899
|
) : (
|
|
794
900
|
<div className={`prose ${isCompact ? 'prose-xs' : 'prose-sm'} max-w-none break-words ${isUser ? 'prose-invert' : 'dark:prose-invert'}`}>
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
901
|
+
{!isUser && structuredContent.payload && (
|
|
902
|
+
<UIDispatcher
|
|
903
|
+
rawContent={structuredContent.payload}
|
|
904
|
+
primaryColor={primaryColor}
|
|
905
|
+
accentColor={accentColor}
|
|
906
|
+
isStreaming={isStreaming}
|
|
907
|
+
onAddToCart={onAddToCart}
|
|
908
|
+
viewportSize={viewportSize}
|
|
909
|
+
/>
|
|
910
|
+
)}
|
|
911
|
+
|
|
912
|
+
{!shouldRenderStructuredOnly && (
|
|
913
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
|
914
|
+
{processedMarkdown}
|
|
915
|
+
</ReactMarkdown>
|
|
916
|
+
)}
|
|
798
917
|
|
|
799
918
|
{isStreaming && message.content && (
|
|
800
919
|
<span className="inline-block w-1.5 h-3.5 ml-1 bg-emerald-500 animate-pulse align-middle" />
|