@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 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 chartHeightClass = isTiny ? "h-48 min-h-[180px]" : isCompact ? "h-56 min-h-[220px]" : isMedium ? "h-64 min-h-[250px]" : "h-72 min-h-[280px]";
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: `w-full min-w-0 ${chartHeightClass} mt-4 mb-2 select-none overflow-hidden` }, /* @__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(
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: `w-full min-w-0 ${chartHeightClass} mt-4 mb-2 select-none overflow-hidden` }, /* @__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(
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\s*([\s\S]*?)\s*```/g;
773
+ const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
709
774
  const products = [];
710
- let content = message.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"}` }, /* @__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" }))
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 chartHeightClass = isTiny ? "h-48 min-h-[180px]" : isCompact ? "h-56 min-h-[220px]" : isMedium ? "h-64 min-h-[250px]" : "h-72 min-h-[280px]";
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: `w-full min-w-0 ${chartHeightClass} mt-4 mb-2 select-none overflow-hidden` }, /* @__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(
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: `w-full min-w-0 ${chartHeightClass} mt-4 mb-2 select-none overflow-hidden` }, /* @__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(
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\s*([\s\S]*?)\s*```/g;
736
+ const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
672
737
  const products = [];
673
- let content = message.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"}` }, /* @__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" }))
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.0",
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 chartHeightClass = isTiny
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={`w-full min-w-0 ${chartHeightClass} mt-4 mb-2 select-none overflow-hidden`}>
236
- <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">
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={`w-full min-w-0 ${chartHeightClass} mt-4 mb-2 select-none overflow-hidden`}>
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\s*([\s\S]*?)\s*```/g;
616
+ const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
533
617
  const products: Product[] = [];
534
- let content = message.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>