@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 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 chartHeight = (() => {
251
- if (type === "pie") {
252
- if (containerWidth <= 0) return isTiny ? 180 : isCompact ? 220 : isMedium ? 250 : 280;
253
- return Math.max(180, Math.min(containerWidth, isTiny ? 180 : isCompact ? 220 : isMedium ? 250 : 280));
254
- }
255
- if (containerWidth <= 0) return isTiny ? 190 : isCompact ? 230 : isMedium ? 260 : 300;
256
- return Math.max(190, Math.min(Math.round(containerWidth * 0.7), isTiny ? 190 : isCompact ? 230 : isMedium ? 260 : 300));
257
- })();
258
- const pieOuterRadius = (() => {
259
- if (containerWidth <= 0) return isTiny ? 44 : isCompact ? 56 : isMedium ? 68 : 80;
260
- const squareSize = Math.min(containerWidth, chartHeight);
261
- return Math.max(36, Math.floor(squareSize * 0.28));
262
- })();
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
- "div",
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
- ref: containerRef,
374
- className: "w-full min-w-0 mt-4 mb-2 select-none overflow-hidden",
375
- style: { height: chartHeight }
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
- /* @__PURE__ */ import_react4.default.createElement("div", { className: "mx-auto h-full aspect-square max-w-full" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.PieChart, null, /* @__PURE__ */ import_react4.default.createElement(
378
- import_recharts.Pie,
379
- {
380
- data: sanitizedData,
381
- dataKey: pieDataKey,
382
- nameKey: finalXKey,
383
- cx: "50%",
384
- cy: "50%",
385
- outerRadius: pieOuterRadius,
386
- label: false,
387
- labelLine: false
388
- },
389
- sanitizedData.map((entry, index) => /* @__PURE__ */ import_react4.default.createElement(import_recharts.Cell, { key: `cell-${index}`, fill: stockAwareColor(entry, index) }))
390
- ), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Tooltip, { content: (props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === "string" ? props.label : void 0) }), showLegend && !isCompact && /* @__PURE__ */ import_react4.default.createElement(import_recharts.Legend, { wrapperStyle: { fontSize: 12 }, verticalAlign: "bottom" }))))
391
- );
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
- "div",
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
- ref: containerRef,
397
- className: "w-full min-w-0 mt-4 mb-2 select-none overflow-hidden",
398
- style: { height: chartHeight }
399
- },
400
- /* @__PURE__ */ import_react4.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: "100%" }, type === "bar" ? /* @__PURE__ */ import_react4.default.createElement(import_recharts.BarChart, { data: sanitizedData, margin: chartMargin }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3", vertical: false, stroke: "#e2e8f0" }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.XAxis, { dataKey: finalXKey, tick: axisTick, tickFormatter: truncateAxisLabel, tickLine: false, axisLine: { stroke: "#cbd5e1" }, interval: 0, minTickGap: isTiny ? 2 : isCompact ? 6 : 12 }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.YAxis, { tick: axisTick, tickLine: false, axisLine: false }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Tooltip, { content: (props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === "string" ? props.label : void 0), cursor: { fill: "#f1f5f9" } }), showLegend && /* @__PURE__ */ import_react4.default.createElement(import_recharts.Legend, { wrapperStyle: { fontSize: isCompact ? 10 : 12 } }), finalDataKeys.map((key, index) => /* @__PURE__ */ import_react4.default.createElement(
401
- import_recharts.Bar,
402
- {
403
- key,
404
- dataKey: key,
405
- fill: colors[index % colors.length],
406
- radius: [4, 4, 0, 0],
407
- barSize: isTiny ? Math.max(10, 28 / data.length) : isCompact ? Math.max(14, 42 / data.length) : Math.max(20, 60 / data.length)
408
- }
409
- ))) : /* @__PURE__ */ import_react4.default.createElement(import_recharts.LineChart, { data: sanitizedData, margin: chartMargin }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3", vertical: false, stroke: "#e2e8f0" }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.XAxis, { dataKey: finalXKey, tick: axisTick, tickFormatter: truncateAxisLabel, tickLine: false, axisLine: { stroke: "#cbd5e1" }, interval: 0, minTickGap: isTiny ? 2 : isCompact ? 6 : 12 }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.YAxis, { tick: axisTick, tickLine: false, axisLine: false }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Tooltip, { content: (props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === "string" ? props.label : void 0) }), showLegend && /* @__PURE__ */ import_react4.default.createElement(import_recharts.Legend, { wrapperStyle: { fontSize: isCompact ? 10 : 12 } }), finalDataKeys.map((key, index) => /* @__PURE__ */ import_react4.default.createElement(
410
- import_recharts.Line,
411
- {
412
- key,
413
- type: "monotone",
414
- dataKey: key,
415
- stroke: colors[index % colors.length],
416
- strokeWidth: isTiny ? 1.75 : isCompact ? 2 : 3,
417
- dot: { r: isTiny ? 2.5 : isCompact ? 3 : 4, strokeWidth: 2 },
418
- activeDot: { r: isTiny ? 4 : isCompact ? 5 : 6 }
419
- }
420
- ))))
421
- );
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\s*([\s\S]*?)\s*```/g;
778
+ const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
741
779
  const products = [];
742
- let content = message.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"}` }, /* @__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" }))
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 chartHeight = (() => {
214
- if (type === "pie") {
215
- if (containerWidth <= 0) return isTiny ? 180 : isCompact ? 220 : isMedium ? 250 : 280;
216
- return Math.max(180, Math.min(containerWidth, isTiny ? 180 : isCompact ? 220 : isMedium ? 250 : 280));
217
- }
218
- if (containerWidth <= 0) return isTiny ? 190 : isCompact ? 230 : isMedium ? 260 : 300;
219
- return Math.max(190, Math.min(Math.round(containerWidth * 0.7), isTiny ? 190 : isCompact ? 230 : isMedium ? 260 : 300));
220
- })();
221
- const pieOuterRadius = (() => {
222
- if (containerWidth <= 0) return isTiny ? 44 : isCompact ? 56 : isMedium ? 68 : 80;
223
- const squareSize = Math.min(containerWidth, chartHeight);
224
- return Math.max(36, Math.floor(squareSize * 0.28));
225
- })();
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
- "div",
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
- ref: containerRef,
337
- className: "w-full min-w-0 mt-4 mb-2 select-none overflow-hidden",
338
- style: { height: chartHeight }
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
- /* @__PURE__ */ React4.createElement("div", { className: "mx-auto h-full aspect-square max-w-full" }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(PieChart, null, /* @__PURE__ */ React4.createElement(
341
- Pie,
342
- {
343
- data: sanitizedData,
344
- dataKey: pieDataKey,
345
- nameKey: finalXKey,
346
- cx: "50%",
347
- cy: "50%",
348
- outerRadius: pieOuterRadius,
349
- label: false,
350
- labelLine: false
351
- },
352
- sanitizedData.map((entry, index) => /* @__PURE__ */ React4.createElement(Cell, { key: `cell-${index}`, fill: stockAwareColor(entry, index) }))
353
- ), /* @__PURE__ */ React4.createElement(Tooltip, { content: (props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === "string" ? props.label : void 0) }), showLegend && !isCompact && /* @__PURE__ */ React4.createElement(Legend, { wrapperStyle: { fontSize: 12 }, verticalAlign: "bottom" }))))
354
- );
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
- "div",
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
- ref: containerRef,
360
- className: "w-full min-w-0 mt-4 mb-2 select-none overflow-hidden",
361
- style: { height: chartHeight }
362
- },
363
- /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, type === "bar" ? /* @__PURE__ */ React4.createElement(BarChart, { data: sanitizedData, margin: chartMargin }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", vertical: false, stroke: "#e2e8f0" }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: finalXKey, tick: axisTick, tickFormatter: truncateAxisLabel, tickLine: false, axisLine: { stroke: "#cbd5e1" }, interval: 0, minTickGap: isTiny ? 2 : isCompact ? 6 : 12 }), /* @__PURE__ */ React4.createElement(YAxis, { tick: axisTick, tickLine: false, axisLine: false }), /* @__PURE__ */ React4.createElement(Tooltip, { content: (props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === "string" ? props.label : void 0), cursor: { fill: "#f1f5f9" } }), showLegend && /* @__PURE__ */ React4.createElement(Legend, { wrapperStyle: { fontSize: isCompact ? 10 : 12 } }), finalDataKeys.map((key, index) => /* @__PURE__ */ React4.createElement(
364
- Bar,
365
- {
366
- key,
367
- dataKey: key,
368
- fill: colors[index % colors.length],
369
- radius: [4, 4, 0, 0],
370
- barSize: isTiny ? Math.max(10, 28 / data.length) : isCompact ? Math.max(14, 42 / data.length) : Math.max(20, 60 / data.length)
371
- }
372
- ))) : /* @__PURE__ */ React4.createElement(LineChart, { data: sanitizedData, margin: chartMargin }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", vertical: false, stroke: "#e2e8f0" }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: finalXKey, tick: axisTick, tickFormatter: truncateAxisLabel, tickLine: false, axisLine: { stroke: "#cbd5e1" }, interval: 0, minTickGap: isTiny ? 2 : isCompact ? 6 : 12 }), /* @__PURE__ */ React4.createElement(YAxis, { tick: axisTick, tickLine: false, axisLine: false }), /* @__PURE__ */ React4.createElement(Tooltip, { content: (props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === "string" ? props.label : void 0) }), showLegend && /* @__PURE__ */ React4.createElement(Legend, { wrapperStyle: { fontSize: isCompact ? 10 : 12 } }), finalDataKeys.map((key, index) => /* @__PURE__ */ React4.createElement(
373
- Line,
374
- {
375
- key,
376
- type: "monotone",
377
- dataKey: key,
378
- stroke: colors[index % colors.length],
379
- strokeWidth: isTiny ? 1.75 : isCompact ? 2 : 3,
380
- dot: { r: isTiny ? 2.5 : isCompact ? 3 : 4, strokeWidth: 2 },
381
- activeDot: { r: isTiny ? 4 : isCompact ? 5 : 6 }
382
- }
383
- ))))
384
- );
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\s*([\s\S]*?)\s*```/g;
741
+ const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
704
742
  const products = [];
705
- let content = message.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"}` }, /* @__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" }))
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.1",
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 chartHeight = (() => {
61
- if (type === 'pie') {
62
- if (containerWidth <= 0) return isTiny ? 180 : isCompact ? 220 : isMedium ? 250 : 280;
63
- return Math.max(180, Math.min(containerWidth, isTiny ? 180 : isCompact ? 220 : isMedium ? 250 : 280));
64
- }
65
-
66
- if (containerWidth <= 0) return isTiny ? 190 : isCompact ? 230 : isMedium ? 260 : 300;
67
- return Math.max(190, Math.min(Math.round(containerWidth * 0.7), isTiny ? 190 : isCompact ? 230 : isMedium ? 260 : 300));
68
- })();
69
- const pieOuterRadius = (() => {
70
- if (containerWidth <= 0) return isTiny ? 44 : isCompact ? 56 : isMedium ? 68 : 80;
71
- const squareSize = Math.min(containerWidth, chartHeight);
72
- return Math.max(36, Math.floor(squareSize * 0.28));
73
- })();
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
- ref={containerRef}
239
- className="w-full min-w-0 mt-4 mb-2 select-none overflow-hidden"
240
- style={{ height: chartHeight }}
241
- >
242
- <div className="mx-auto h-full aspect-square max-w-full">
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
- ref={containerRef}
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\s*([\s\S]*?)\s*```/g;
622
+ const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
543
623
  const products: Product[] = [];
544
- let content = message.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
- <ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
796
- {processedMarkdown}
797
- </ReactMarkdown>
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" />