@retrivora-ai/rag-engine 1.6.0 → 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 +127 -18
- package/dist/index.mjs +127 -18
- package/package.json +1 -1
- package/src/components/DynamicChart.tsx +6 -15
- package/src/components/MessageBubble.tsx +145 -8
package/dist/index.js
CHANGED
|
@@ -247,12 +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
|
-
const pieOuterRadius = (() => {
|
|
252
|
-
if (containerWidth <= 0) return isTiny ? 44 : isCompact ? 56 : isMedium ? 68 : 80;
|
|
253
|
-
const squareSize = Math.min(containerWidth, isTiny ? 180 : isCompact ? 220 : isMedium ? 250 : 280);
|
|
254
|
-
return Math.max(36, Math.floor(squareSize * 0.28));
|
|
255
|
-
})();
|
|
250
|
+
const pieOuterRadius = isTiny ? "54%" : isCompact ? "60%" : isMedium ? "66%" : "72%";
|
|
256
251
|
const axisTick = { fontSize: isTiny ? 9 : isCompact ? 10 : 12, fill: "#64748b" };
|
|
257
252
|
const showLegend = !isTiny;
|
|
258
253
|
const truncateAxisLabel = (value) => {
|
|
@@ -360,7 +355,7 @@ function DynamicChart({
|
|
|
360
355
|
return row;
|
|
361
356
|
};
|
|
362
357
|
if (type === "pie") {
|
|
363
|
-
return /* @__PURE__ */ import_react4.default.createElement("div", { ref: containerRef, className:
|
|
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(
|
|
364
359
|
import_recharts.Pie,
|
|
365
360
|
{
|
|
366
361
|
data: sanitizedData,
|
|
@@ -375,7 +370,7 @@ function DynamicChart({
|
|
|
375
370
|
sanitizedData.map((entry, index) => /* @__PURE__ */ import_react4.default.createElement(import_recharts.Cell, { key: `cell-${index}`, fill: stockAwareColor(entry, index) }))
|
|
376
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" })))));
|
|
377
372
|
}
|
|
378
|
-
return /* @__PURE__ */ import_react4.default.createElement("div", { ref: containerRef, className:
|
|
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(
|
|
379
374
|
import_recharts.Bar,
|
|
380
375
|
{
|
|
381
376
|
key,
|
|
@@ -395,7 +390,7 @@ function DynamicChart({
|
|
|
395
390
|
dot: { r: isTiny ? 2.5 : isCompact ? 3 : 4, strokeWidth: 2 },
|
|
396
391
|
activeDot: { r: isTiny ? 4 : isCompact ? 5 : 6 }
|
|
397
392
|
}
|
|
398
|
-
)))));
|
|
393
|
+
))))));
|
|
399
394
|
}
|
|
400
395
|
|
|
401
396
|
// src/components/MessageBubble.tsx
|
|
@@ -469,6 +464,72 @@ function stripMarkdownTables(raw) {
|
|
|
469
464
|
"\n\n"
|
|
470
465
|
);
|
|
471
466
|
}
|
|
467
|
+
function stripStructuredUiLabels(raw) {
|
|
468
|
+
return raw.replace(
|
|
469
|
+
/(?:^|\n)\*\*(?:Chart Data|Table Data|Visualization|Visual Representation)\*\:\s*(?=\n|$)/gi,
|
|
470
|
+
"\n"
|
|
471
|
+
).replace(
|
|
472
|
+
/(?:^|\n)(?:Chart Data|Table Data|Visualization|Visual Representation)\s*\:\s*(?=\n|$)/gi,
|
|
473
|
+
"\n"
|
|
474
|
+
);
|
|
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
|
+
}
|
|
472
533
|
function resolveImage(data) {
|
|
473
534
|
for (const key of ["image", "img", "thumbnail"]) {
|
|
474
535
|
const v = data[key];
|
|
@@ -681,6 +742,10 @@ function MessageBubble({
|
|
|
681
742
|
() => /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|type)"\s*:\s*"(?:carousel|products)"/i.test(message.content),
|
|
682
743
|
[message.content]
|
|
683
744
|
);
|
|
745
|
+
const structuredContent = import_react5.default.useMemo(
|
|
746
|
+
() => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
|
|
747
|
+
[isUser, message.content]
|
|
748
|
+
);
|
|
684
749
|
const productsFromSources = import_react5.default.useMemo(() => {
|
|
685
750
|
if (isUser || !sources) return [];
|
|
686
751
|
return sources.filter((s) => {
|
|
@@ -705,13 +770,31 @@ function MessageBubble({
|
|
|
705
770
|
if (isUser) {
|
|
706
771
|
return { productsFromContent: [], cleanContent: message.content };
|
|
707
772
|
}
|
|
708
|
-
const jsonRegex = /```json
|
|
773
|
+
const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
|
|
709
774
|
const products = [];
|
|
710
|
-
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
|
+
}
|
|
711
794
|
if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
|
|
712
795
|
for (const match of content.matchAll(jsonRegex)) {
|
|
713
796
|
try {
|
|
714
|
-
const data = JSON.parse(match[1]);
|
|
797
|
+
const data = JSON.parse(sanitizeJson(match[1]));
|
|
715
798
|
if (data.type === "products" && Array.isArray(data.items)) {
|
|
716
799
|
products.push(
|
|
717
800
|
...data.items.map((item) => {
|
|
@@ -728,7 +811,7 @@ function MessageBubble({
|
|
|
728
811
|
}
|
|
729
812
|
}
|
|
730
813
|
return { productsFromContent: products, cleanContent: content.trim() };
|
|
731
|
-
}, [message.content, isUser]);
|
|
814
|
+
}, [message.content, isUser, structuredContent]);
|
|
732
815
|
const allProducts = import_react5.default.useMemo(() => {
|
|
733
816
|
if (productsFromContent.length > 0) return productsFromContent;
|
|
734
817
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -746,8 +829,8 @@ function MessageBubble({
|
|
|
746
829
|
});
|
|
747
830
|
}, [productsFromSources, productsFromContent, message.content]);
|
|
748
831
|
const processedMarkdown = import_react5.default.useMemo(() => {
|
|
749
|
-
let raw = cleanContent || message.content;
|
|
750
|
-
const hasStructuredUiBlock = /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
|
|
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);
|
|
751
834
|
raw = raw.replace(/([^\n])\n\|/g, "$1\n\n|").replace(/(\|[^\n]*\|)\n\n+(?=\|)/g, "$1\n");
|
|
752
835
|
if (!isStreaming) {
|
|
753
836
|
raw = raw.replace(/(?:^|\n)\s*\{[\s\S]*\}\s*(?:\n|$)/g, (match) => {
|
|
@@ -764,7 +847,7 @@ ${match.trim()}
|
|
|
764
847
|
});
|
|
765
848
|
}
|
|
766
849
|
if (hasStructuredUiBlock) {
|
|
767
|
-
raw = stripMarkdownTables(raw).replace(/\n{3,}/g, "\n\n").trim();
|
|
850
|
+
raw = stripStructuredUiLabels(stripMarkdownTables(raw)).replace(/\n{3,}/g, "\n\n").trim();
|
|
768
851
|
}
|
|
769
852
|
if (isStreaming) {
|
|
770
853
|
raw = raw.replace(/((?:^|\n)\|.*)+/g, (match) => {
|
|
@@ -778,7 +861,7 @@ ${match.trim()}
|
|
|
778
861
|
});
|
|
779
862
|
}
|
|
780
863
|
return raw;
|
|
781
|
-
}, [cleanContent, message.content, isStreaming]);
|
|
864
|
+
}, [cleanContent, message.content, isStreaming, structuredContent]);
|
|
782
865
|
const markdownComponents = import_react5.default.useMemo(
|
|
783
866
|
() => ({
|
|
784
867
|
// Wrap in not-prose so Tailwind Typography resets don't clobber our styles.
|
|
@@ -878,6 +961,22 @@ ${match.trim()}
|
|
|
878
961
|
);
|
|
879
962
|
}
|
|
880
963
|
}
|
|
964
|
+
if (!inline) {
|
|
965
|
+
const content = String(children != null ? children : "").trim();
|
|
966
|
+
if (looksLikeStructuredPayload(content)) {
|
|
967
|
+
return /* @__PURE__ */ import_react5.default.createElement(
|
|
968
|
+
UIDispatcher,
|
|
969
|
+
{
|
|
970
|
+
rawContent: content,
|
|
971
|
+
primaryColor,
|
|
972
|
+
accentColor,
|
|
973
|
+
isStreaming,
|
|
974
|
+
onAddToCart,
|
|
975
|
+
viewportSize
|
|
976
|
+
}
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
881
980
|
if (!inline && lang === "table-loading") {
|
|
882
981
|
return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-5 p-6 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex items-center justify-center gap-3 select-none" }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "w-4 h-4 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" }), /* @__PURE__ */ import_react5.default.createElement("p", { className: "text-xs text-slate-500 font-medium italic animate-pulse" }, "Generating data table..."));
|
|
883
982
|
}
|
|
@@ -899,7 +998,17 @@ ${match.trim()}
|
|
|
899
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"}`,
|
|
900
999
|
style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
|
|
901
1000
|
},
|
|
902
|
-
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" }))
|
|
903
1012
|
), !isUser && !hasStructuredProductBlock && allProducts.length > 0 && /* @__PURE__ */ import_react5.default.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ import_react5.default.createElement(
|
|
904
1013
|
ProductCarousel,
|
|
905
1014
|
{
|
package/dist/index.mjs
CHANGED
|
@@ -210,12 +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
|
-
const pieOuterRadius = (() => {
|
|
215
|
-
if (containerWidth <= 0) return isTiny ? 44 : isCompact ? 56 : isMedium ? 68 : 80;
|
|
216
|
-
const squareSize = Math.min(containerWidth, isTiny ? 180 : isCompact ? 220 : isMedium ? 250 : 280);
|
|
217
|
-
return Math.max(36, Math.floor(squareSize * 0.28));
|
|
218
|
-
})();
|
|
213
|
+
const pieOuterRadius = isTiny ? "54%" : isCompact ? "60%" : isMedium ? "66%" : "72%";
|
|
219
214
|
const axisTick = { fontSize: isTiny ? 9 : isCompact ? 10 : 12, fill: "#64748b" };
|
|
220
215
|
const showLegend = !isTiny;
|
|
221
216
|
const truncateAxisLabel = (value) => {
|
|
@@ -323,7 +318,7 @@ function DynamicChart({
|
|
|
323
318
|
return row;
|
|
324
319
|
};
|
|
325
320
|
if (type === "pie") {
|
|
326
|
-
return /* @__PURE__ */ React4.createElement("div", { ref: containerRef, className:
|
|
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(
|
|
327
322
|
Pie,
|
|
328
323
|
{
|
|
329
324
|
data: sanitizedData,
|
|
@@ -338,7 +333,7 @@ function DynamicChart({
|
|
|
338
333
|
sanitizedData.map((entry, index) => /* @__PURE__ */ React4.createElement(Cell, { key: `cell-${index}`, fill: stockAwareColor(entry, index) }))
|
|
339
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" })))));
|
|
340
335
|
}
|
|
341
|
-
return /* @__PURE__ */ React4.createElement("div", { ref: containerRef, className:
|
|
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(
|
|
342
337
|
Bar,
|
|
343
338
|
{
|
|
344
339
|
key,
|
|
@@ -358,7 +353,7 @@ function DynamicChart({
|
|
|
358
353
|
dot: { r: isTiny ? 2.5 : isCompact ? 3 : 4, strokeWidth: 2 },
|
|
359
354
|
activeDot: { r: isTiny ? 4 : isCompact ? 5 : 6 }
|
|
360
355
|
}
|
|
361
|
-
)))));
|
|
356
|
+
))))));
|
|
362
357
|
}
|
|
363
358
|
|
|
364
359
|
// src/components/MessageBubble.tsx
|
|
@@ -432,6 +427,72 @@ function stripMarkdownTables(raw) {
|
|
|
432
427
|
"\n\n"
|
|
433
428
|
);
|
|
434
429
|
}
|
|
430
|
+
function stripStructuredUiLabels(raw) {
|
|
431
|
+
return raw.replace(
|
|
432
|
+
/(?:^|\n)\*\*(?:Chart Data|Table Data|Visualization|Visual Representation)\*\:\s*(?=\n|$)/gi,
|
|
433
|
+
"\n"
|
|
434
|
+
).replace(
|
|
435
|
+
/(?:^|\n)(?:Chart Data|Table Data|Visualization|Visual Representation)\s*\:\s*(?=\n|$)/gi,
|
|
436
|
+
"\n"
|
|
437
|
+
);
|
|
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
|
+
}
|
|
435
496
|
function resolveImage(data) {
|
|
436
497
|
for (const key of ["image", "img", "thumbnail"]) {
|
|
437
498
|
const v = data[key];
|
|
@@ -644,6 +705,10 @@ function MessageBubble({
|
|
|
644
705
|
() => /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|type)"\s*:\s*"(?:carousel|products)"/i.test(message.content),
|
|
645
706
|
[message.content]
|
|
646
707
|
);
|
|
708
|
+
const structuredContent = React5.useMemo(
|
|
709
|
+
() => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
|
|
710
|
+
[isUser, message.content]
|
|
711
|
+
);
|
|
647
712
|
const productsFromSources = React5.useMemo(() => {
|
|
648
713
|
if (isUser || !sources) return [];
|
|
649
714
|
return sources.filter((s) => {
|
|
@@ -668,13 +733,31 @@ function MessageBubble({
|
|
|
668
733
|
if (isUser) {
|
|
669
734
|
return { productsFromContent: [], cleanContent: message.content };
|
|
670
735
|
}
|
|
671
|
-
const jsonRegex = /```json
|
|
736
|
+
const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
|
|
672
737
|
const products = [];
|
|
673
|
-
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
|
+
}
|
|
674
757
|
if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
|
|
675
758
|
for (const match of content.matchAll(jsonRegex)) {
|
|
676
759
|
try {
|
|
677
|
-
const data = JSON.parse(match[1]);
|
|
760
|
+
const data = JSON.parse(sanitizeJson(match[1]));
|
|
678
761
|
if (data.type === "products" && Array.isArray(data.items)) {
|
|
679
762
|
products.push(
|
|
680
763
|
...data.items.map((item) => {
|
|
@@ -691,7 +774,7 @@ function MessageBubble({
|
|
|
691
774
|
}
|
|
692
775
|
}
|
|
693
776
|
return { productsFromContent: products, cleanContent: content.trim() };
|
|
694
|
-
}, [message.content, isUser]);
|
|
777
|
+
}, [message.content, isUser, structuredContent]);
|
|
695
778
|
const allProducts = React5.useMemo(() => {
|
|
696
779
|
if (productsFromContent.length > 0) return productsFromContent;
|
|
697
780
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -709,8 +792,8 @@ function MessageBubble({
|
|
|
709
792
|
});
|
|
710
793
|
}, [productsFromSources, productsFromContent, message.content]);
|
|
711
794
|
const processedMarkdown = React5.useMemo(() => {
|
|
712
|
-
let raw = cleanContent || message.content;
|
|
713
|
-
const hasStructuredUiBlock = /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
|
|
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);
|
|
714
797
|
raw = raw.replace(/([^\n])\n\|/g, "$1\n\n|").replace(/(\|[^\n]*\|)\n\n+(?=\|)/g, "$1\n");
|
|
715
798
|
if (!isStreaming) {
|
|
716
799
|
raw = raw.replace(/(?:^|\n)\s*\{[\s\S]*\}\s*(?:\n|$)/g, (match) => {
|
|
@@ -727,7 +810,7 @@ ${match.trim()}
|
|
|
727
810
|
});
|
|
728
811
|
}
|
|
729
812
|
if (hasStructuredUiBlock) {
|
|
730
|
-
raw = stripMarkdownTables(raw).replace(/\n{3,}/g, "\n\n").trim();
|
|
813
|
+
raw = stripStructuredUiLabels(stripMarkdownTables(raw)).replace(/\n{3,}/g, "\n\n").trim();
|
|
731
814
|
}
|
|
732
815
|
if (isStreaming) {
|
|
733
816
|
raw = raw.replace(/((?:^|\n)\|.*)+/g, (match) => {
|
|
@@ -741,7 +824,7 @@ ${match.trim()}
|
|
|
741
824
|
});
|
|
742
825
|
}
|
|
743
826
|
return raw;
|
|
744
|
-
}, [cleanContent, message.content, isStreaming]);
|
|
827
|
+
}, [cleanContent, message.content, isStreaming, structuredContent]);
|
|
745
828
|
const markdownComponents = React5.useMemo(
|
|
746
829
|
() => ({
|
|
747
830
|
// Wrap in not-prose so Tailwind Typography resets don't clobber our styles.
|
|
@@ -841,6 +924,22 @@ ${match.trim()}
|
|
|
841
924
|
);
|
|
842
925
|
}
|
|
843
926
|
}
|
|
927
|
+
if (!inline) {
|
|
928
|
+
const content = String(children != null ? children : "").trim();
|
|
929
|
+
if (looksLikeStructuredPayload(content)) {
|
|
930
|
+
return /* @__PURE__ */ React5.createElement(
|
|
931
|
+
UIDispatcher,
|
|
932
|
+
{
|
|
933
|
+
rawContent: content,
|
|
934
|
+
primaryColor,
|
|
935
|
+
accentColor,
|
|
936
|
+
isStreaming,
|
|
937
|
+
onAddToCart,
|
|
938
|
+
viewportSize
|
|
939
|
+
}
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
844
943
|
if (!inline && lang === "table-loading") {
|
|
845
944
|
return /* @__PURE__ */ React5.createElement("div", { className: "my-5 p-6 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex items-center justify-center gap-3 select-none" }, /* @__PURE__ */ React5.createElement("div", { className: "w-4 h-4 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" }), /* @__PURE__ */ React5.createElement("p", { className: "text-xs text-slate-500 font-medium italic animate-pulse" }, "Generating data table..."));
|
|
846
945
|
}
|
|
@@ -862,7 +961,17 @@ ${match.trim()}
|
|
|
862
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"}`,
|
|
863
962
|
style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
|
|
864
963
|
},
|
|
865
|
-
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" }))
|
|
866
975
|
), !isUser && !hasStructuredProductBlock && allProducts.length > 0 && /* @__PURE__ */ React5.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ React5.createElement(
|
|
867
976
|
ProductCarousel,
|
|
868
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,18 +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
|
-
? 'h-48 min-h-[180px]'
|
|
62
|
-
: isCompact
|
|
63
|
-
? 'h-56 min-h-[220px]'
|
|
64
|
-
: isMedium
|
|
65
|
-
? 'h-64 min-h-[250px]'
|
|
66
|
-
: 'h-72 min-h-[280px]';
|
|
67
|
-
const pieOuterRadius = (() => {
|
|
68
|
-
if (containerWidth <= 0) return isTiny ? 44 : isCompact ? 56 : isMedium ? 68 : 80;
|
|
69
|
-
const squareSize = Math.min(containerWidth, isTiny ? 180 : isCompact ? 220 : isMedium ? 250 : 280);
|
|
70
|
-
return Math.max(36, Math.floor(squareSize * 0.28));
|
|
71
|
-
})();
|
|
60
|
+
const pieOuterRadius = isTiny ? '54%' : isCompact ? '60%' : isMedium ? '66%' : '72%';
|
|
72
61
|
const axisTick = { fontSize: isTiny ? 9 : isCompact ? 10 : 12, fill: '#64748b' };
|
|
73
62
|
const showLegend = !isTiny;
|
|
74
63
|
const truncateAxisLabel = (value: string | number) => {
|
|
@@ -232,8 +221,8 @@ export function DynamicChart({
|
|
|
232
221
|
// Handle Pie Chart separately as it has a different structure
|
|
233
222
|
if (type === 'pie') {
|
|
234
223
|
return (
|
|
235
|
-
<div ref={containerRef} className=
|
|
236
|
-
<div className="mx-auto
|
|
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">
|
|
237
226
|
<ResponsiveContainer width="100%" height="100%">
|
|
238
227
|
<PieChart>
|
|
239
228
|
<Pie
|
|
@@ -261,7 +250,8 @@ export function DynamicChart({
|
|
|
261
250
|
|
|
262
251
|
// Bar and Line charts
|
|
263
252
|
return (
|
|
264
|
-
<div ref={containerRef} className=
|
|
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]">
|
|
265
255
|
<ResponsiveContainer width="100%" height="100%">
|
|
266
256
|
{type === 'bar' ? (
|
|
267
257
|
<BarChart data={sanitizedData} margin={chartMargin}>
|
|
@@ -301,6 +291,7 @@ export function DynamicChart({
|
|
|
301
291
|
</LineChart>
|
|
302
292
|
)}
|
|
303
293
|
</ResponsiveContainer>
|
|
294
|
+
</div>
|
|
304
295
|
</div>
|
|
305
296
|
);
|
|
306
297
|
}
|
|
@@ -113,6 +113,86 @@ function stripMarkdownTables(raw: string): string {
|
|
|
113
113
|
);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
function stripStructuredUiLabels(raw: string): string {
|
|
117
|
+
return raw.replace(
|
|
118
|
+
/(?:^|\n)\*\*(?:Chart Data|Table Data|Visualization|Visual Representation)\*\:\s*(?=\n|$)/gi,
|
|
119
|
+
'\n',
|
|
120
|
+
).replace(
|
|
121
|
+
/(?:^|\n)(?:Chart Data|Table Data|Visualization|Visual Representation)\s*\:\s*(?=\n|$)/gi,
|
|
122
|
+
'\n',
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
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
|
+
|
|
116
196
|
// ─── Tiny helpers ─────────────────────────────────────────────────────────────
|
|
117
197
|
|
|
118
198
|
function resolveImage(data: Record<string, unknown>): string | undefined {
|
|
@@ -500,6 +580,10 @@ export function MessageBubble({
|
|
|
500
580
|
() => /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|type)"\s*:\s*"(?:carousel|products)"/i.test(message.content),
|
|
501
581
|
[message.content],
|
|
502
582
|
);
|
|
583
|
+
const structuredContent = React.useMemo(
|
|
584
|
+
() => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
|
|
585
|
+
[isUser, message.content],
|
|
586
|
+
);
|
|
503
587
|
|
|
504
588
|
// ── Products from sources ──────────────────────────────────────────────────
|
|
505
589
|
const productsFromSources = React.useMemo<Product[]>(() => {
|
|
@@ -529,14 +613,40 @@ export function MessageBubble({
|
|
|
529
613
|
return { productsFromContent: [] as Product[], cleanContent: message.content };
|
|
530
614
|
}
|
|
531
615
|
|
|
532
|
-
const jsonRegex = /```json
|
|
616
|
+
const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
|
|
533
617
|
const products: Product[] = [];
|
|
534
|
-
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
|
+
}
|
|
535
645
|
|
|
536
646
|
if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
|
|
537
647
|
for (const match of content.matchAll(jsonRegex)) {
|
|
538
648
|
try {
|
|
539
|
-
const data = JSON.parse(match[1]);
|
|
649
|
+
const data = JSON.parse(sanitizeJson(match[1]));
|
|
540
650
|
if (data.type === 'products' && Array.isArray(data.items)) {
|
|
541
651
|
products.push(
|
|
542
652
|
...data.items.map((item: Record<string, unknown>) => ({
|
|
@@ -553,7 +663,7 @@ export function MessageBubble({
|
|
|
553
663
|
}
|
|
554
664
|
|
|
555
665
|
return { productsFromContent: products, cleanContent: content.trim() };
|
|
556
|
-
}, [message.content, isUser]);
|
|
666
|
+
}, [message.content, isUser, structuredContent]);
|
|
557
667
|
|
|
558
668
|
// ── Merge & deduplicate products ───────────────────────────────────────────
|
|
559
669
|
const allProducts = React.useMemo<Product[]>(() => {
|
|
@@ -574,8 +684,8 @@ export function MessageBubble({
|
|
|
574
684
|
}, [productsFromSources, productsFromContent, message.content]);
|
|
575
685
|
|
|
576
686
|
const processedMarkdown = React.useMemo(() => {
|
|
577
|
-
let raw = cleanContent || message.content;
|
|
578
|
-
const hasStructuredUiBlock = /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
|
|
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);
|
|
579
689
|
|
|
580
690
|
// 1. Structural fixes
|
|
581
691
|
raw = raw.replace(/([^\n])\n\|/g, '$1\n\n|') // Ensure blank line before table
|
|
@@ -599,7 +709,7 @@ export function MessageBubble({
|
|
|
599
709
|
// 2b. If the model already provided a structured UI block, suppress duplicated
|
|
600
710
|
// markdown tables that often follow as a fallback explanation.
|
|
601
711
|
if (hasStructuredUiBlock) {
|
|
602
|
-
raw = stripMarkdownTables(raw)
|
|
712
|
+
raw = stripStructuredUiLabels(stripMarkdownTables(raw))
|
|
603
713
|
.replace(/\n{3,}/g, '\n\n')
|
|
604
714
|
.trim();
|
|
605
715
|
}
|
|
@@ -612,7 +722,7 @@ export function MessageBubble({
|
|
|
612
722
|
}
|
|
613
723
|
|
|
614
724
|
return raw;
|
|
615
|
-
}, [cleanContent, message.content, isStreaming]);
|
|
725
|
+
}, [cleanContent, message.content, isStreaming, structuredContent]);
|
|
616
726
|
|
|
617
727
|
// ── Markdown component overrides ───────────────────────────────────────────
|
|
618
728
|
const markdownComponents = React.useMemo(
|
|
@@ -710,6 +820,22 @@ export function MessageBubble({
|
|
|
710
820
|
}
|
|
711
821
|
}
|
|
712
822
|
|
|
823
|
+
if (!inline) {
|
|
824
|
+
const content = String(children ?? '').trim();
|
|
825
|
+
if (looksLikeStructuredPayload(content)) {
|
|
826
|
+
return (
|
|
827
|
+
<UIDispatcher
|
|
828
|
+
rawContent={content}
|
|
829
|
+
primaryColor={primaryColor}
|
|
830
|
+
accentColor={accentColor}
|
|
831
|
+
isStreaming={isStreaming}
|
|
832
|
+
onAddToCart={onAddToCart}
|
|
833
|
+
viewportSize={viewportSize}
|
|
834
|
+
/>
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
713
839
|
if (!inline && lang === 'table-loading') {
|
|
714
840
|
return (
|
|
715
841
|
<div className="my-5 p-6 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex items-center justify-center gap-3 select-none">
|
|
@@ -766,6 +892,17 @@ export function MessageBubble({
|
|
|
766
892
|
</span>
|
|
767
893
|
) : (
|
|
768
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
|
+
|
|
769
906
|
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
|
770
907
|
{processedMarkdown}
|
|
771
908
|
</ReactMarkdown>
|