@retrivora-ai/rag-engine 1.6.1 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js 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,63 @@ function stripStructuredUiLabels(raw) {
501
473
  "\n"
502
474
  );
503
475
  }
476
+ function extractBareJsonObject(raw) {
477
+ const start = raw.indexOf("{");
478
+ if (start === -1) return null;
479
+ let depth = 0;
480
+ let inString = false;
481
+ let escape = false;
482
+ for (let i = start; i < raw.length; i += 1) {
483
+ const ch = raw[i];
484
+ if (escape) {
485
+ escape = false;
486
+ continue;
487
+ }
488
+ if (ch === "\\" && inString) {
489
+ escape = true;
490
+ continue;
491
+ }
492
+ if (ch === '"') {
493
+ inString = !inString;
494
+ continue;
495
+ }
496
+ if (inString) continue;
497
+ if (ch === "{") depth += 1;
498
+ if (ch === "}") {
499
+ depth -= 1;
500
+ if (depth === 0) {
501
+ return raw.slice(start, i + 1);
502
+ }
503
+ }
504
+ }
505
+ return null;
506
+ }
507
+ function extractStructuredPayload(raw) {
508
+ var _a, _b;
509
+ let working = raw;
510
+ const fenceRegex = /```(?:[a-z]+)?\s*([\s\S]*?)```/gi;
511
+ for (const match of raw.matchAll(fenceRegex)) {
512
+ const body = (_b = (_a = match[1]) == null ? void 0 : _a.trim()) != null ? _b : "";
513
+ if (!looksLikeStructuredPayload(body)) continue;
514
+ working = working.replace(match[0], "");
515
+ return {
516
+ payload: body,
517
+ text: stripStructuredUiLabels(working).replace(/\n{3,}/g, "\n\n").trim()
518
+ };
519
+ }
520
+ const bareJson = extractBareJsonObject(raw);
521
+ if (bareJson && looksLikeStructuredPayload(bareJson)) {
522
+ working = working.replace(bareJson, "");
523
+ return {
524
+ payload: bareJson,
525
+ text: stripStructuredUiLabels(working).replace(/\n{3,}/g, "\n\n").trim()
526
+ };
527
+ }
528
+ return {
529
+ payload: null,
530
+ text: stripStructuredUiLabels(raw).replace(/\n{3,}/g, "\n\n").trim()
531
+ };
532
+ }
504
533
  function resolveImage(data) {
505
534
  for (const key of ["image", "img", "thumbnail"]) {
506
535
  const v = data[key];
@@ -713,6 +742,10 @@ function MessageBubble({
713
742
  () => /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|type)"\s*:\s*"(?:carousel|products)"/i.test(message.content),
714
743
  [message.content]
715
744
  );
745
+ const structuredContent = import_react5.default.useMemo(
746
+ () => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
747
+ [isUser, message.content]
748
+ );
716
749
  const productsFromSources = import_react5.default.useMemo(() => {
717
750
  if (isUser || !sources) return [];
718
751
  return sources.filter((s) => {
@@ -737,13 +770,31 @@ function MessageBubble({
737
770
  if (isUser) {
738
771
  return { productsFromContent: [], cleanContent: message.content };
739
772
  }
740
- const jsonRegex = /```json\s*([\s\S]*?)\s*```/g;
773
+ const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
741
774
  const products = [];
742
- 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
+ }
743
794
  if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
744
795
  for (const match of content.matchAll(jsonRegex)) {
745
796
  try {
746
- const data = JSON.parse(match[1]);
797
+ const data = JSON.parse(sanitizeJson(match[1]));
747
798
  if (data.type === "products" && Array.isArray(data.items)) {
748
799
  products.push(
749
800
  ...data.items.map((item) => {
@@ -760,7 +811,7 @@ function MessageBubble({
760
811
  }
761
812
  }
762
813
  return { productsFromContent: products, cleanContent: content.trim() };
763
- }, [message.content, isUser]);
814
+ }, [message.content, isUser, structuredContent]);
764
815
  const allProducts = import_react5.default.useMemo(() => {
765
816
  if (productsFromContent.length > 0) return productsFromContent;
766
817
  const seen = /* @__PURE__ */ new Set();
@@ -778,8 +829,8 @@ function MessageBubble({
778
829
  });
779
830
  }, [productsFromSources, productsFromContent, message.content]);
780
831
  const processedMarkdown = import_react5.default.useMemo(() => {
781
- let raw = cleanContent || message.content;
782
- const hasStructuredUiBlock = /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
832
+ let raw = structuredContent.payload ? structuredContent.text : cleanContent || message.content;
833
+ const hasStructuredUiBlock = Boolean(structuredContent.payload) || /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
783
834
  raw = raw.replace(/([^\n])\n\|/g, "$1\n\n|").replace(/(\|[^\n]*\|)\n\n+(?=\|)/g, "$1\n");
784
835
  if (!isStreaming) {
785
836
  raw = raw.replace(/(?:^|\n)\s*\{[\s\S]*\}\s*(?:\n|$)/g, (match) => {
@@ -810,7 +861,7 @@ ${match.trim()}
810
861
  });
811
862
  }
812
863
  return raw;
813
- }, [cleanContent, message.content, isStreaming]);
864
+ }, [cleanContent, message.content, isStreaming, structuredContent]);
814
865
  const markdownComponents = import_react5.default.useMemo(
815
866
  () => ({
816
867
  // Wrap in not-prose so Tailwind Typography resets don't clobber our styles.
@@ -947,7 +998,17 @@ ${match.trim()}
947
998
  className: `relative ${isCompact ? "px-3 py-2.5 text-[13px]" : "px-4 py-3 text-sm"} rounded-2xl leading-relaxed shadow-sm dark:shadow-lg ${isUser ? "text-white rounded-tr-sm" : "bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-slate-800 dark:text-white/90 rounded-tl-sm"}`,
948
999
  style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
949
1000
  },
950
- isStreaming && !message.content ? /* @__PURE__ */ import_react5.default.createElement("span", { className: "flex items-center gap-1 py-1" }, /* @__PURE__ */ import_react5.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:0ms]" }), /* @__PURE__ */ import_react5.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:150ms]" }), /* @__PURE__ */ import_react5.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:300ms]" })) : /* @__PURE__ */ import_react5.default.createElement("div", { className: `prose ${isCompact ? "prose-xs" : "prose-sm"} max-w-none break-words ${isUser ? "prose-invert" : "dark:prose-invert"}` }, /* @__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" }))
951
1012
  ), !isUser && !hasStructuredProductBlock && allProducts.length > 0 && /* @__PURE__ */ import_react5.default.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ import_react5.default.createElement(
952
1013
  ProductCarousel,
953
1014
  {
package/dist/index.mjs CHANGED
@@ -210,19 +210,7 @@ function DynamicChart({
210
210
  const isTiny = containerWidth > 0 && containerWidth < 300;
211
211
  const isCompact = viewportSize === "compact" || containerWidth > 0 && containerWidth < 420;
212
212
  const isMedium = !isCompact && (viewportSize === "medium" || containerWidth > 0 && containerWidth < 640);
213
- const 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,63 @@ function stripStructuredUiLabels(raw) {
464
436
  "\n"
465
437
  );
466
438
  }
439
+ function extractBareJsonObject(raw) {
440
+ const start = raw.indexOf("{");
441
+ if (start === -1) return null;
442
+ let depth = 0;
443
+ let inString = false;
444
+ let escape = false;
445
+ for (let i = start; i < raw.length; i += 1) {
446
+ const ch = raw[i];
447
+ if (escape) {
448
+ escape = false;
449
+ continue;
450
+ }
451
+ if (ch === "\\" && inString) {
452
+ escape = true;
453
+ continue;
454
+ }
455
+ if (ch === '"') {
456
+ inString = !inString;
457
+ continue;
458
+ }
459
+ if (inString) continue;
460
+ if (ch === "{") depth += 1;
461
+ if (ch === "}") {
462
+ depth -= 1;
463
+ if (depth === 0) {
464
+ return raw.slice(start, i + 1);
465
+ }
466
+ }
467
+ }
468
+ return null;
469
+ }
470
+ function extractStructuredPayload(raw) {
471
+ var _a, _b;
472
+ let working = raw;
473
+ const fenceRegex = /```(?:[a-z]+)?\s*([\s\S]*?)```/gi;
474
+ for (const match of raw.matchAll(fenceRegex)) {
475
+ const body = (_b = (_a = match[1]) == null ? void 0 : _a.trim()) != null ? _b : "";
476
+ if (!looksLikeStructuredPayload(body)) continue;
477
+ working = working.replace(match[0], "");
478
+ return {
479
+ payload: body,
480
+ text: stripStructuredUiLabels(working).replace(/\n{3,}/g, "\n\n").trim()
481
+ };
482
+ }
483
+ const bareJson = extractBareJsonObject(raw);
484
+ if (bareJson && looksLikeStructuredPayload(bareJson)) {
485
+ working = working.replace(bareJson, "");
486
+ return {
487
+ payload: bareJson,
488
+ text: stripStructuredUiLabels(working).replace(/\n{3,}/g, "\n\n").trim()
489
+ };
490
+ }
491
+ return {
492
+ payload: null,
493
+ text: stripStructuredUiLabels(raw).replace(/\n{3,}/g, "\n\n").trim()
494
+ };
495
+ }
467
496
  function resolveImage(data) {
468
497
  for (const key of ["image", "img", "thumbnail"]) {
469
498
  const v = data[key];
@@ -676,6 +705,10 @@ function MessageBubble({
676
705
  () => /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|type)"\s*:\s*"(?:carousel|products)"/i.test(message.content),
677
706
  [message.content]
678
707
  );
708
+ const structuredContent = React5.useMemo(
709
+ () => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
710
+ [isUser, message.content]
711
+ );
679
712
  const productsFromSources = React5.useMemo(() => {
680
713
  if (isUser || !sources) return [];
681
714
  return sources.filter((s) => {
@@ -700,13 +733,31 @@ function MessageBubble({
700
733
  if (isUser) {
701
734
  return { productsFromContent: [], cleanContent: message.content };
702
735
  }
703
- const jsonRegex = /```json\s*([\s\S]*?)\s*```/g;
736
+ const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
704
737
  const products = [];
705
- 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
+ }
706
757
  if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
707
758
  for (const match of content.matchAll(jsonRegex)) {
708
759
  try {
709
- const data = JSON.parse(match[1]);
760
+ const data = JSON.parse(sanitizeJson(match[1]));
710
761
  if (data.type === "products" && Array.isArray(data.items)) {
711
762
  products.push(
712
763
  ...data.items.map((item) => {
@@ -723,7 +774,7 @@ function MessageBubble({
723
774
  }
724
775
  }
725
776
  return { productsFromContent: products, cleanContent: content.trim() };
726
- }, [message.content, isUser]);
777
+ }, [message.content, isUser, structuredContent]);
727
778
  const allProducts = React5.useMemo(() => {
728
779
  if (productsFromContent.length > 0) return productsFromContent;
729
780
  const seen = /* @__PURE__ */ new Set();
@@ -741,8 +792,8 @@ function MessageBubble({
741
792
  });
742
793
  }, [productsFromSources, productsFromContent, message.content]);
743
794
  const processedMarkdown = React5.useMemo(() => {
744
- let raw = cleanContent || message.content;
745
- const hasStructuredUiBlock = /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
795
+ let raw = structuredContent.payload ? structuredContent.text : cleanContent || message.content;
796
+ const hasStructuredUiBlock = Boolean(structuredContent.payload) || /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
746
797
  raw = raw.replace(/([^\n])\n\|/g, "$1\n\n|").replace(/(\|[^\n]*\|)\n\n+(?=\|)/g, "$1\n");
747
798
  if (!isStreaming) {
748
799
  raw = raw.replace(/(?:^|\n)\s*\{[\s\S]*\}\s*(?:\n|$)/g, (match) => {
@@ -773,7 +824,7 @@ ${match.trim()}
773
824
  });
774
825
  }
775
826
  return raw;
776
- }, [cleanContent, message.content, isStreaming]);
827
+ }, [cleanContent, message.content, isStreaming, structuredContent]);
777
828
  const markdownComponents = React5.useMemo(
778
829
  () => ({
779
830
  // Wrap in not-prose so Tailwind Typography resets don't clobber our styles.
@@ -910,7 +961,17 @@ ${match.trim()}
910
961
  className: `relative ${isCompact ? "px-3 py-2.5 text-[13px]" : "px-4 py-3 text-sm"} rounded-2xl leading-relaxed shadow-sm dark:shadow-lg ${isUser ? "text-white rounded-tr-sm" : "bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-slate-800 dark:text-white/90 rounded-tl-sm"}`,
911
962
  style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
912
963
  },
913
- isStreaming && !message.content ? /* @__PURE__ */ React5.createElement("span", { className: "flex items-center gap-1 py-1" }, /* @__PURE__ */ React5.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:0ms]" }), /* @__PURE__ */ React5.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:150ms]" }), /* @__PURE__ */ React5.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:300ms]" })) : /* @__PURE__ */ React5.createElement("div", { className: `prose ${isCompact ? "prose-xs" : "prose-sm"} max-w-none break-words ${isUser ? "prose-invert" : "dark:prose-invert"}` }, /* @__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" }))
914
975
  ), !isUser && !hasStructuredProductBlock && allProducts.length > 0 && /* @__PURE__ */ React5.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ React5.createElement(
915
976
  ProductCarousel,
916
977
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retrivora-ai/rag-engine",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "description": "Retrivora AI is a plug-and-play AI engine for RAG chat experiences — generic vector DB + LLM provider, embeddable or standalone.",
5
5
  "author": "Abhinav Alkuchi",
6
6
  "license": "MIT",
@@ -57,20 +57,7 @@ export function DynamicChart({
57
57
  const isTiny = containerWidth > 0 && containerWidth < 300;
58
58
  const isCompact = viewportSize === 'compact' || (containerWidth > 0 && containerWidth < 420);
59
59
  const isMedium = !isCompact && (viewportSize === 'medium' || (containerWidth > 0 && containerWidth < 640));
60
- const 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,76 @@ function stripStructuredUiLabels(raw: string): string {
123
123
  );
124
124
  }
125
125
 
126
+ function extractBareJsonObject(raw: string): string | null {
127
+ const start = raw.indexOf('{');
128
+ if (start === -1) return null;
129
+
130
+ let depth = 0;
131
+ let inString = false;
132
+ let escape = false;
133
+
134
+ for (let i = start; i < raw.length; i += 1) {
135
+ const ch = raw[i];
136
+
137
+ if (escape) {
138
+ escape = false;
139
+ continue;
140
+ }
141
+
142
+ if (ch === '\\' && inString) {
143
+ escape = true;
144
+ continue;
145
+ }
146
+
147
+ if (ch === '"') {
148
+ inString = !inString;
149
+ continue;
150
+ }
151
+
152
+ if (inString) continue;
153
+
154
+ if (ch === '{') depth += 1;
155
+ if (ch === '}') {
156
+ depth -= 1;
157
+ if (depth === 0) {
158
+ return raw.slice(start, i + 1);
159
+ }
160
+ }
161
+ }
162
+
163
+ return null;
164
+ }
165
+
166
+ function extractStructuredPayload(raw: string): { payload: string | null; text: string } {
167
+ let working = raw;
168
+ const fenceRegex = /```(?:[a-z]+)?\s*([\s\S]*?)```/gi;
169
+
170
+ for (const match of raw.matchAll(fenceRegex)) {
171
+ const body = match[1]?.trim() ?? '';
172
+ if (!looksLikeStructuredPayload(body)) continue;
173
+
174
+ working = working.replace(match[0], '');
175
+ return {
176
+ payload: body,
177
+ text: stripStructuredUiLabels(working).replace(/\n{3,}/g, '\n\n').trim(),
178
+ };
179
+ }
180
+
181
+ const bareJson = extractBareJsonObject(raw);
182
+ if (bareJson && looksLikeStructuredPayload(bareJson)) {
183
+ working = working.replace(bareJson, '');
184
+ return {
185
+ payload: bareJson,
186
+ text: stripStructuredUiLabels(working).replace(/\n{3,}/g, '\n\n').trim(),
187
+ };
188
+ }
189
+
190
+ return {
191
+ payload: null,
192
+ text: stripStructuredUiLabels(raw).replace(/\n{3,}/g, '\n\n').trim(),
193
+ };
194
+ }
195
+
126
196
  // ─── Tiny helpers ─────────────────────────────────────────────────────────────
127
197
 
128
198
  function resolveImage(data: Record<string, unknown>): string | undefined {
@@ -510,6 +580,10 @@ export function MessageBubble({
510
580
  () => /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|type)"\s*:\s*"(?:carousel|products)"/i.test(message.content),
511
581
  [message.content],
512
582
  );
583
+ const structuredContent = React.useMemo(
584
+ () => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
585
+ [isUser, message.content],
586
+ );
513
587
 
514
588
  // ── Products from sources ──────────────────────────────────────────────────
515
589
  const productsFromSources = React.useMemo<Product[]>(() => {
@@ -539,14 +613,40 @@ export function MessageBubble({
539
613
  return { productsFromContent: [] as Product[], cleanContent: message.content };
540
614
  }
541
615
 
542
- const jsonRegex = /```json\s*([\s\S]*?)\s*```/g;
616
+ const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
543
617
  const products: Product[] = [];
544
- 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
+ }
545
645
 
546
646
  if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
547
647
  for (const match of content.matchAll(jsonRegex)) {
548
648
  try {
549
- const data = JSON.parse(match[1]);
649
+ const data = JSON.parse(sanitizeJson(match[1]));
550
650
  if (data.type === 'products' && Array.isArray(data.items)) {
551
651
  products.push(
552
652
  ...data.items.map((item: Record<string, unknown>) => ({
@@ -563,7 +663,7 @@ export function MessageBubble({
563
663
  }
564
664
 
565
665
  return { productsFromContent: products, cleanContent: content.trim() };
566
- }, [message.content, isUser]);
666
+ }, [message.content, isUser, structuredContent]);
567
667
 
568
668
  // ── Merge & deduplicate products ───────────────────────────────────────────
569
669
  const allProducts = React.useMemo<Product[]>(() => {
@@ -584,8 +684,8 @@ export function MessageBubble({
584
684
  }, [productsFromSources, productsFromContent, message.content]);
585
685
 
586
686
  const processedMarkdown = React.useMemo(() => {
587
- let raw = cleanContent || message.content;
588
- const hasStructuredUiBlock = /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
687
+ let raw = structuredContent.payload ? structuredContent.text : (cleanContent || message.content);
688
+ const hasStructuredUiBlock = Boolean(structuredContent.payload) || /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
589
689
 
590
690
  // 1. Structural fixes
591
691
  raw = raw.replace(/([^\n])\n\|/g, '$1\n\n|') // Ensure blank line before table
@@ -622,7 +722,7 @@ export function MessageBubble({
622
722
  }
623
723
 
624
724
  return raw;
625
- }, [cleanContent, message.content, isStreaming]);
725
+ }, [cleanContent, message.content, isStreaming, structuredContent]);
626
726
 
627
727
  // ── Markdown component overrides ───────────────────────────────────────────
628
728
  const markdownComponents = React.useMemo(
@@ -792,6 +892,17 @@ export function MessageBubble({
792
892
  </span>
793
893
  ) : (
794
894
  <div className={`prose ${isCompact ? 'prose-xs' : 'prose-sm'} max-w-none break-words ${isUser ? 'prose-invert' : 'dark:prose-invert'}`}>
895
+ {!isUser && structuredContent.payload && (
896
+ <UIDispatcher
897
+ rawContent={structuredContent.payload}
898
+ primaryColor={primaryColor}
899
+ accentColor={accentColor}
900
+ isStreaming={isStreaming}
901
+ onAddToCart={onAddToCart}
902
+ viewportSize={viewportSize}
903
+ />
904
+ )}
905
+
795
906
  <ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
796
907
  {processedMarkdown}
797
908
  </ReactMarkdown>