@retrivora-ai/rag-engine 1.6.9 → 1.7.1

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.
@@ -2282,9 +2282,9 @@ NEVER output naked JSON. If you provide a visualization, do not also provide a m
2282
2282
  }
2283
2283
 
2284
2284
  3. DATA RULES
2285
- - Build the UI payload from retrieved context only.
2285
+ - Build the UI payload from retrieved context and metadata.
2286
2286
  - For CAROUSEL: "data" items MUST have { id, name, brand, price, image, link }.
2287
- - For CHART: "data" items MUST have { label, value }.
2287
+ - IMPORTANT: Extract "image" and "price" from Source metadata. Image URLs are often in fields like "images", "thumbnail", or "img".
2288
2288
 
2289
2289
  4. FORMAT RULES
2290
2290
  - Wrap the JSON in \`\`\`ui ... \`\`\` blocks.
@@ -2299,7 +2299,7 @@ Assistant: "I found several beauty products in our catalog:"
2299
2299
  "title": "Beauty Products",
2300
2300
  "description": "Latest skincare and makeup items.",
2301
2301
  "data": [
2302
- { "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "...", "link": "..." }
2302
+ { "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "https://placehold.co/600x400?text=Product", "link": "https://example.com/product" }
2303
2303
  ]
2304
2304
  }
2305
2305
  \`\`\`
@@ -2511,7 +2511,8 @@ Assistant: "I found several beauty products in our catalog:"
2511
2511
  sources = sources.slice(0, topK);
2512
2512
  }
2513
2513
  let context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
2514
- ${m.content}`).join("\n\n---\n\n") : "No relevant context found.";
2514
+ Content: ${m.content}
2515
+ Metadata: ${JSON.stringify(m.metadata)}`).join("\n\n---\n\n") : "No relevant context found.";
2515
2516
  if (graphData && graphData.nodes.length > 0) {
2516
2517
  const graphContext = graphData.nodes.map(
2517
2518
  (n) => `Entity: ${n.label} (${n.id})${n.properties ? " - " + JSON.stringify(n.properties) : ""}`
@@ -3845,9 +3845,9 @@ NEVER output naked JSON. If you provide a visualization, do not also provide a m
3845
3845
  }
3846
3846
 
3847
3847
  3. DATA RULES
3848
- - Build the UI payload from retrieved context only.
3848
+ - Build the UI payload from retrieved context and metadata.
3849
3849
  - For CAROUSEL: "data" items MUST have { id, name, brand, price, image, link }.
3850
- - For CHART: "data" items MUST have { label, value }.
3850
+ - IMPORTANT: Extract "image" and "price" from Source metadata. Image URLs are often in fields like "images", "thumbnail", or "img".
3851
3851
 
3852
3852
  4. FORMAT RULES
3853
3853
  - Wrap the JSON in \`\`\`ui ... \`\`\` blocks.
@@ -3862,7 +3862,7 @@ Assistant: "I found several beauty products in our catalog:"
3862
3862
  "title": "Beauty Products",
3863
3863
  "description": "Latest skincare and makeup items.",
3864
3864
  "data": [
3865
- { "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "...", "link": "..." }
3865
+ { "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "https://placehold.co/600x400?text=Product", "link": "https://example.com/product" }
3866
3866
  ]
3867
3867
  }
3868
3868
  \`\`\`
@@ -4074,7 +4074,8 @@ Assistant: "I found several beauty products in our catalog:"
4074
4074
  sources = sources.slice(0, topK);
4075
4075
  }
4076
4076
  let context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
4077
- ${m.content}`).join("\n\n---\n\n") : "No relevant context found.";
4077
+ Content: ${m.content}
4078
+ Metadata: ${JSON.stringify(m.metadata)}`).join("\n\n---\n\n") : "No relevant context found.";
4078
4079
  if (graphData && graphData.nodes.length > 0) {
4079
4080
  const graphContext = graphData.nodes.map(
4080
4081
  (n) => `Entity: ${n.label} (${n.id})${n.properties ? " - " + JSON.stringify(n.properties) : ""}`
@@ -9,7 +9,7 @@ import {
9
9
  sseFrame,
10
10
  sseMetaFrame,
11
11
  sseTextFrame
12
- } from "../chunk-Q7FLITXB.mjs";
12
+ } from "../chunk-OHXRQTQH.mjs";
13
13
  import "../chunk-YLTMFW4M.mjs";
14
14
  import "../chunk-X4TOT24V.mjs";
15
15
  export {
package/dist/index.js CHANGED
@@ -535,12 +535,23 @@ function isLikelyUiOnlyMessage(text) {
535
535
  return trimmed.length === 0 || /^(chart data|table data|visualization|visual representation|product catalog|product recommendations|catalog summary)\s*:?\s*$/i.test(trimmed);
536
536
  }
537
537
  function resolveImage(data) {
538
- for (const key of ["image", "img", "thumbnail"]) {
538
+ const candidates = ["image", "img", "thumbnail", "url", "picture", "link"];
539
+ for (const key of candidates) {
539
540
  const v = data[key];
540
- if (typeof v === "string" && v) return v;
541
+ if (typeof v === "string" && v.startsWith("http")) return v;
542
+ if (v && typeof v === "object" && !Array.isArray(v)) {
543
+ const subVal = v.url || v.link || v.src;
544
+ if (typeof subVal === "string" && subVal.startsWith("http")) return subVal;
545
+ }
541
546
  }
542
- if (Array.isArray(data.images) && typeof data.images[0] === "string") {
543
- return data.images[0];
547
+ if (Array.isArray(data.images)) {
548
+ for (const item of data.images) {
549
+ if (typeof item === "string" && item.startsWith("http")) return item;
550
+ if (item && typeof item === "object") {
551
+ const url = item.url || item.link || item.src;
552
+ if (typeof url === "string" && url.startsWith("http")) return url;
553
+ }
554
+ }
544
555
  }
545
556
  return void 0;
546
557
  }
@@ -643,10 +654,13 @@ function resolveStructuredRows(config) {
643
654
  if (Array.isArray(config.results) && config.results.length > 0) return config.results;
644
655
  return [];
645
656
  }
646
- function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAddToCart, viewportSize = "large" }) {
657
+ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, viewportSize = "large" }) {
647
658
  var _a;
648
659
  const result = import_react5.default.useMemo(() => {
649
- if (isStreaming) return { loading: true };
660
+ if (isStreaming) {
661
+ const looksLikeCarousel = rawContent.includes('"view": "carousel"') || rawContent.includes('"view":"carousel"') || rawContent.includes('"type": "products"') || rawContent.includes('"type":"products"');
662
+ return { loading: true, silent: looksLikeCarousel };
663
+ }
650
664
  try {
651
665
  const clean = rawContent.replace(/^```[a-z]*\s*/i, "").replace(/```\s*$/, "").trim();
652
666
  const parsed = JSON.parse(sanitizeJson(extractLikelyJsonBlock(clean)));
@@ -679,6 +693,7 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
679
693
  }
680
694
  }, [rawContent, isStreaming]);
681
695
  if ("loading" in result) {
696
+ if (result.silent) return null;
682
697
  return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-4 p-8 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex flex-col items-center justify-center gap-3 animate-pulse" }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "w-5 h-5 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" }, "Preparing view..."));
683
698
  }
684
699
  if ("error" in result) {
@@ -713,12 +728,7 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
713
728
  insight
714
729
  ))));
715
730
  case "carousel":
716
- return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-4" }, config.title && /* @__PURE__ */ import_react5.default.createElement("h4", { className: `${isCompact ? "mb-1.5 text-[11px]" : "mb-2 text-xs"} font-semibold text-slate-500` }, config.title), hasDescription && /* @__PURE__ */ import_react5.default.createElement("p", { className: `mb-3 ${isCompact ? "text-xs" : "text-sm"} text-slate-500 dark:text-white/60` }, config.description), /* @__PURE__ */ import_react5.default.createElement(ProductCarousel, { products: config.data.map((item) => {
717
- var _a2;
718
- return __spreadProps(__spreadValues({}, item), {
719
- image: (_a2 = item.image) != null ? _a2 : typeof resolveImage === "function" ? resolveImage(item) : void 0
720
- });
721
- }), primaryColor, onAddToCart }));
731
+ return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-4" }, config.title && /* @__PURE__ */ import_react5.default.createElement("h4", { className: `${isCompact ? "mb-1.5 text-[11px]" : "mb-2 text-xs"} font-semibold text-slate-500` }, config.title), hasDescription && /* @__PURE__ */ import_react5.default.createElement("p", { className: `mb-3 ${isCompact ? "text-xs" : "text-sm"} text-slate-500 dark:text-white/60` }, config.description));
722
732
  case "table":
723
733
  return /* @__PURE__ */ import_react5.default.createElement("div", { className: `${isCompact ? "my-3 p-3 max-h-[320px]" : "my-4 p-4 max-h-[400px]"} bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm overflow-auto` }, config.title && /* @__PURE__ */ import_react5.default.createElement("h4", { className: `${isCompact ? "text-[11px]" : "text-xs"} font-semibold text-slate-500 mb-2` }, config.title), hasDescription && /* @__PURE__ */ import_react5.default.createElement("p", { className: `mb-3 ${isCompact ? "text-xs" : "text-sm"} text-slate-500 dark:text-white/60` }, config.description), /* @__PURE__ */ import_react5.default.createElement(DataTable, { config: {
724
734
  type: "table",
@@ -746,10 +756,6 @@ function MessageBubble({
746
756
  const isCompact = viewportSize === "compact";
747
757
  const isMedium = viewportSize === "medium";
748
758
  const [showSources, setShowSources] = import_react5.default.useState(false);
749
- const hasStructuredProductBlock = import_react5.default.useMemo(
750
- () => /```(?:ui|json|chart)\s*[\s\S]*?"?(?:view|type|data|products)"?\s*:\s*"?(?:carousel|products|table)"?/i.test(message.content) || /\{[\s\S]*?"?(?:view|type|data|products)"?\s*:\s*"?(?:carousel|products)"?[\s\S]*\}/i.test(message.content) && looksLikeStructuredPayload(message.content),
751
- [message.content]
752
- );
753
759
  const structuredContent = import_react5.default.useMemo(
754
760
  () => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
755
761
  [isUser, message.content]
@@ -822,20 +828,26 @@ function MessageBubble({
822
828
  return { productsFromContent: products, cleanContent: content.trim() };
823
829
  }, [message.content, isUser, structuredContent]);
824
830
  const allProducts = import_react5.default.useMemo(() => {
825
- if (productsFromContent.length > 0) return productsFromContent;
826
831
  const seen = /* @__PURE__ */ new Set();
832
+ const merged = [];
833
+ for (const p of productsFromContent) {
834
+ const id = String(p.id || p.name);
835
+ if (!seen.has(id)) {
836
+ seen.add(id);
837
+ merged.push(p);
838
+ }
839
+ }
827
840
  const contentLower = message.content.toLowerCase();
828
- return productsFromSources.filter((p) => {
829
- var _a;
830
- const id = String((_a = p.id) != null ? _a : p.name);
831
- if (seen.has(id)) return false;
841
+ for (const p of productsFromSources) {
842
+ const id = String(p.id || p.name);
843
+ if (seen.has(id)) continue;
832
844
  const mentioned = contentLower.includes(p.name.toLowerCase()) || (p.brand ? contentLower.includes(p.brand.toLowerCase()) : false);
833
845
  if (mentioned) {
834
846
  seen.add(id);
835
- return true;
847
+ merged.push(p);
836
848
  }
837
- return false;
838
- });
849
+ }
850
+ return merged;
839
851
  }, [productsFromSources, productsFromContent, message.content]);
840
852
  const processedMarkdown = import_react5.default.useMemo(() => {
841
853
  let raw = structuredContent.payload ? structuredContent.text : cleanContent || message.content;
@@ -950,7 +962,6 @@ ${match.trim()}
950
962
  primaryColor,
951
963
  accentColor,
952
964
  isStreaming,
953
- onAddToCart,
954
965
  viewportSize
955
966
  }
956
967
  );
@@ -965,7 +976,6 @@ ${match.trim()}
965
976
  primaryColor,
966
977
  accentColor,
967
978
  isStreaming,
968
- onAddToCart,
969
979
  viewportSize
970
980
  }
971
981
  );
@@ -981,7 +991,6 @@ ${match.trim()}
981
991
  primaryColor,
982
992
  accentColor,
983
993
  isStreaming,
984
- onAddToCart,
985
994
  viewportSize
986
995
  }
987
996
  );
@@ -993,7 +1002,7 @@ ${match.trim()}
993
1002
  return /* @__PURE__ */ import_react5.default.createElement("code", __spreadValues({ className }, props), typeof children === "string" || typeof children === "number" ? children : JSON.stringify(children));
994
1003
  }
995
1004
  }),
996
- [primaryColor, accentColor, isStreaming, onAddToCart, viewportSize]
1005
+ [primaryColor, accentColor, isStreaming, viewportSize]
997
1006
  );
998
1007
  return /* @__PURE__ */ import_react5.default.createElement("div", { className: `flex ${isCompact ? "gap-2" : "gap-3"} ${isUser ? "flex-row-reverse" : "flex-row"} items-start` }, /* @__PURE__ */ import_react5.default.createElement(
999
1008
  "div",
@@ -1015,11 +1024,10 @@ ${match.trim()}
1015
1024
  primaryColor,
1016
1025
  accentColor,
1017
1026
  isStreaming,
1018
- onAddToCart,
1019
1027
  viewportSize
1020
1028
  }
1021
1029
  ), !shouldRenderStructuredOnly && /* @__PURE__ */ import_react5.default.createElement(import_react_markdown.default, { remarkPlugins: [import_remark_gfm.default], components: markdownComponents }, processedMarkdown), isStreaming && message.content && /* @__PURE__ */ import_react5.default.createElement("span", { className: "inline-block w-1.5 h-3.5 ml-1 bg-emerald-500 animate-pulse align-middle" }))
1022
- ), !isUser && !hasStructuredProductBlock && allProducts.length > 0 && /* @__PURE__ */ import_react5.default.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ import_react5.default.createElement(
1030
+ ), !isUser && allProducts.length > 0 && /* @__PURE__ */ import_react5.default.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ import_react5.default.createElement(
1023
1031
  ProductCarousel,
1024
1032
  {
1025
1033
  products: allProducts,
package/dist/index.mjs CHANGED
@@ -498,12 +498,23 @@ function isLikelyUiOnlyMessage(text) {
498
498
  return trimmed.length === 0 || /^(chart data|table data|visualization|visual representation|product catalog|product recommendations|catalog summary)\s*:?\s*$/i.test(trimmed);
499
499
  }
500
500
  function resolveImage(data) {
501
- for (const key of ["image", "img", "thumbnail"]) {
501
+ const candidates = ["image", "img", "thumbnail", "url", "picture", "link"];
502
+ for (const key of candidates) {
502
503
  const v = data[key];
503
- if (typeof v === "string" && v) return v;
504
+ if (typeof v === "string" && v.startsWith("http")) return v;
505
+ if (v && typeof v === "object" && !Array.isArray(v)) {
506
+ const subVal = v.url || v.link || v.src;
507
+ if (typeof subVal === "string" && subVal.startsWith("http")) return subVal;
508
+ }
504
509
  }
505
- if (Array.isArray(data.images) && typeof data.images[0] === "string") {
506
- return data.images[0];
510
+ if (Array.isArray(data.images)) {
511
+ for (const item of data.images) {
512
+ if (typeof item === "string" && item.startsWith("http")) return item;
513
+ if (item && typeof item === "object") {
514
+ const url = item.url || item.link || item.src;
515
+ if (typeof url === "string" && url.startsWith("http")) return url;
516
+ }
517
+ }
507
518
  }
508
519
  return void 0;
509
520
  }
@@ -606,10 +617,13 @@ function resolveStructuredRows(config) {
606
617
  if (Array.isArray(config.results) && config.results.length > 0) return config.results;
607
618
  return [];
608
619
  }
609
- function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAddToCart, viewportSize = "large" }) {
620
+ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, viewportSize = "large" }) {
610
621
  var _a;
611
622
  const result = React5.useMemo(() => {
612
- if (isStreaming) return { loading: true };
623
+ if (isStreaming) {
624
+ const looksLikeCarousel = rawContent.includes('"view": "carousel"') || rawContent.includes('"view":"carousel"') || rawContent.includes('"type": "products"') || rawContent.includes('"type":"products"');
625
+ return { loading: true, silent: looksLikeCarousel };
626
+ }
613
627
  try {
614
628
  const clean = rawContent.replace(/^```[a-z]*\s*/i, "").replace(/```\s*$/, "").trim();
615
629
  const parsed = JSON.parse(sanitizeJson(extractLikelyJsonBlock(clean)));
@@ -642,6 +656,7 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
642
656
  }
643
657
  }, [rawContent, isStreaming]);
644
658
  if ("loading" in result) {
659
+ if (result.silent) return null;
645
660
  return /* @__PURE__ */ React5.createElement("div", { className: "my-4 p-8 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex flex-col items-center justify-center gap-3 animate-pulse" }, /* @__PURE__ */ React5.createElement("div", { className: "w-5 h-5 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" }, "Preparing view..."));
646
661
  }
647
662
  if ("error" in result) {
@@ -676,12 +691,7 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
676
691
  insight
677
692
  ))));
678
693
  case "carousel":
679
- return /* @__PURE__ */ React5.createElement("div", { className: "my-4" }, config.title && /* @__PURE__ */ React5.createElement("h4", { className: `${isCompact ? "mb-1.5 text-[11px]" : "mb-2 text-xs"} font-semibold text-slate-500` }, config.title), hasDescription && /* @__PURE__ */ React5.createElement("p", { className: `mb-3 ${isCompact ? "text-xs" : "text-sm"} text-slate-500 dark:text-white/60` }, config.description), /* @__PURE__ */ React5.createElement(ProductCarousel, { products: config.data.map((item) => {
680
- var _a2;
681
- return __spreadProps(__spreadValues({}, item), {
682
- image: (_a2 = item.image) != null ? _a2 : typeof resolveImage === "function" ? resolveImage(item) : void 0
683
- });
684
- }), primaryColor, onAddToCart }));
694
+ return /* @__PURE__ */ React5.createElement("div", { className: "my-4" }, config.title && /* @__PURE__ */ React5.createElement("h4", { className: `${isCompact ? "mb-1.5 text-[11px]" : "mb-2 text-xs"} font-semibold text-slate-500` }, config.title), hasDescription && /* @__PURE__ */ React5.createElement("p", { className: `mb-3 ${isCompact ? "text-xs" : "text-sm"} text-slate-500 dark:text-white/60` }, config.description));
685
695
  case "table":
686
696
  return /* @__PURE__ */ React5.createElement("div", { className: `${isCompact ? "my-3 p-3 max-h-[320px]" : "my-4 p-4 max-h-[400px]"} bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm overflow-auto` }, config.title && /* @__PURE__ */ React5.createElement("h4", { className: `${isCompact ? "text-[11px]" : "text-xs"} font-semibold text-slate-500 mb-2` }, config.title), hasDescription && /* @__PURE__ */ React5.createElement("p", { className: `mb-3 ${isCompact ? "text-xs" : "text-sm"} text-slate-500 dark:text-white/60` }, config.description), /* @__PURE__ */ React5.createElement(DataTable, { config: {
687
697
  type: "table",
@@ -709,10 +719,6 @@ function MessageBubble({
709
719
  const isCompact = viewportSize === "compact";
710
720
  const isMedium = viewportSize === "medium";
711
721
  const [showSources, setShowSources] = React5.useState(false);
712
- const hasStructuredProductBlock = React5.useMemo(
713
- () => /```(?:ui|json|chart)\s*[\s\S]*?"?(?:view|type|data|products)"?\s*:\s*"?(?:carousel|products|table)"?/i.test(message.content) || /\{[\s\S]*?"?(?:view|type|data|products)"?\s*:\s*"?(?:carousel|products)"?[\s\S]*\}/i.test(message.content) && looksLikeStructuredPayload(message.content),
714
- [message.content]
715
- );
716
722
  const structuredContent = React5.useMemo(
717
723
  () => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
718
724
  [isUser, message.content]
@@ -785,20 +791,26 @@ function MessageBubble({
785
791
  return { productsFromContent: products, cleanContent: content.trim() };
786
792
  }, [message.content, isUser, structuredContent]);
787
793
  const allProducts = React5.useMemo(() => {
788
- if (productsFromContent.length > 0) return productsFromContent;
789
794
  const seen = /* @__PURE__ */ new Set();
795
+ const merged = [];
796
+ for (const p of productsFromContent) {
797
+ const id = String(p.id || p.name);
798
+ if (!seen.has(id)) {
799
+ seen.add(id);
800
+ merged.push(p);
801
+ }
802
+ }
790
803
  const contentLower = message.content.toLowerCase();
791
- return productsFromSources.filter((p) => {
792
- var _a;
793
- const id = String((_a = p.id) != null ? _a : p.name);
794
- if (seen.has(id)) return false;
804
+ for (const p of productsFromSources) {
805
+ const id = String(p.id || p.name);
806
+ if (seen.has(id)) continue;
795
807
  const mentioned = contentLower.includes(p.name.toLowerCase()) || (p.brand ? contentLower.includes(p.brand.toLowerCase()) : false);
796
808
  if (mentioned) {
797
809
  seen.add(id);
798
- return true;
810
+ merged.push(p);
799
811
  }
800
- return false;
801
- });
812
+ }
813
+ return merged;
802
814
  }, [productsFromSources, productsFromContent, message.content]);
803
815
  const processedMarkdown = React5.useMemo(() => {
804
816
  let raw = structuredContent.payload ? structuredContent.text : cleanContent || message.content;
@@ -913,7 +925,6 @@ ${match.trim()}
913
925
  primaryColor,
914
926
  accentColor,
915
927
  isStreaming,
916
- onAddToCart,
917
928
  viewportSize
918
929
  }
919
930
  );
@@ -928,7 +939,6 @@ ${match.trim()}
928
939
  primaryColor,
929
940
  accentColor,
930
941
  isStreaming,
931
- onAddToCart,
932
942
  viewportSize
933
943
  }
934
944
  );
@@ -944,7 +954,6 @@ ${match.trim()}
944
954
  primaryColor,
945
955
  accentColor,
946
956
  isStreaming,
947
- onAddToCart,
948
957
  viewportSize
949
958
  }
950
959
  );
@@ -956,7 +965,7 @@ ${match.trim()}
956
965
  return /* @__PURE__ */ React5.createElement("code", __spreadValues({ className }, props), typeof children === "string" || typeof children === "number" ? children : JSON.stringify(children));
957
966
  }
958
967
  }),
959
- [primaryColor, accentColor, isStreaming, onAddToCart, viewportSize]
968
+ [primaryColor, accentColor, isStreaming, viewportSize]
960
969
  );
961
970
  return /* @__PURE__ */ React5.createElement("div", { className: `flex ${isCompact ? "gap-2" : "gap-3"} ${isUser ? "flex-row-reverse" : "flex-row"} items-start` }, /* @__PURE__ */ React5.createElement(
962
971
  "div",
@@ -978,11 +987,10 @@ ${match.trim()}
978
987
  primaryColor,
979
988
  accentColor,
980
989
  isStreaming,
981
- onAddToCart,
982
990
  viewportSize
983
991
  }
984
992
  ), !shouldRenderStructuredOnly && /* @__PURE__ */ React5.createElement(ReactMarkdown, { remarkPlugins: [remarkGfm], components: markdownComponents }, processedMarkdown), isStreaming && message.content && /* @__PURE__ */ React5.createElement("span", { className: "inline-block w-1.5 h-3.5 ml-1 bg-emerald-500 animate-pulse align-middle" }))
985
- ), !isUser && !hasStructuredProductBlock && allProducts.length > 0 && /* @__PURE__ */ React5.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ React5.createElement(
993
+ ), !isUser && allProducts.length > 0 && /* @__PURE__ */ React5.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ React5.createElement(
986
994
  ProductCarousel,
987
995
  {
988
996
  products: allProducts,
package/dist/server.js CHANGED
@@ -3936,9 +3936,9 @@ NEVER output naked JSON. If you provide a visualization, do not also provide a m
3936
3936
  }
3937
3937
 
3938
3938
  3. DATA RULES
3939
- - Build the UI payload from retrieved context only.
3939
+ - Build the UI payload from retrieved context and metadata.
3940
3940
  - For CAROUSEL: "data" items MUST have { id, name, brand, price, image, link }.
3941
- - For CHART: "data" items MUST have { label, value }.
3941
+ - IMPORTANT: Extract "image" and "price" from Source metadata. Image URLs are often in fields like "images", "thumbnail", or "img".
3942
3942
 
3943
3943
  4. FORMAT RULES
3944
3944
  - Wrap the JSON in \`\`\`ui ... \`\`\` blocks.
@@ -3953,7 +3953,7 @@ Assistant: "I found several beauty products in our catalog:"
3953
3953
  "title": "Beauty Products",
3954
3954
  "description": "Latest skincare and makeup items.",
3955
3955
  "data": [
3956
- { "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "...", "link": "..." }
3956
+ { "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "https://placehold.co/600x400?text=Product", "link": "https://example.com/product" }
3957
3957
  ]
3958
3958
  }
3959
3959
  \`\`\`
@@ -4165,7 +4165,8 @@ Assistant: "I found several beauty products in our catalog:"
4165
4165
  sources = sources.slice(0, topK);
4166
4166
  }
4167
4167
  let context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
4168
- ${m.content}`).join("\n\n---\n\n") : "No relevant context found.";
4168
+ Content: ${m.content}
4169
+ Metadata: ${JSON.stringify(m.metadata)}`).join("\n\n---\n\n") : "No relevant context found.";
4169
4170
  if (graphData && graphData.nodes.length > 0) {
4170
4171
  const graphContext = graphData.nodes.map(
4171
4172
  (n) => `Entity: ${n.label} (${n.id})${n.properties ? " - " + JSON.stringify(n.properties) : ""}`
package/dist/server.mjs CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  sseFrame,
40
40
  sseMetaFrame,
41
41
  sseTextFrame
42
- } from "./chunk-Q7FLITXB.mjs";
42
+ } from "./chunk-OHXRQTQH.mjs";
43
43
  import "./chunk-YLTMFW4M.mjs";
44
44
  import {
45
45
  PineconeProvider
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retrivora-ai/rag-engine",
3
- "version": "1.6.9",
3
+ "version": "1.7.1",
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",
@@ -204,12 +204,23 @@ function isLikelyUiOnlyMessage(text: string): boolean {
204
204
  // ─── Tiny helpers ─────────────────────────────────────────────────────────────
205
205
 
206
206
  function resolveImage(data: Record<string, unknown>): string | undefined {
207
- for (const key of ['image', 'img', 'thumbnail']) {
207
+ const candidates = ['image', 'img', 'thumbnail', 'url', 'picture', 'link'];
208
+ for (const key of candidates) {
208
209
  const v = data[key];
209
- if (typeof v === 'string' && v) return v;
210
+ if (typeof v === 'string' && v.startsWith('http')) return v;
211
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
212
+ const subVal = (v as Record<string, unknown>).url || (v as Record<string, unknown>).link || (v as Record<string, unknown>).src;
213
+ if (typeof subVal === 'string' && subVal.startsWith('http')) return subVal;
214
+ }
210
215
  }
211
- if (Array.isArray(data.images) && typeof data.images[0] === 'string') {
212
- return data.images[0];
216
+ if (Array.isArray(data.images)) {
217
+ for (const item of data.images) {
218
+ if (typeof item === 'string' && item.startsWith('http')) return item;
219
+ if (item && typeof item === 'object') {
220
+ const url = item.url || item.link || item.src;
221
+ if (typeof url === 'string' && url.startsWith('http')) return url;
222
+ }
223
+ }
213
224
  }
214
225
  return undefined;
215
226
  }
@@ -425,16 +436,21 @@ function resolveStructuredRows(config: Partial<RawUIConfig> & Record<string, unk
425
436
  return [];
426
437
  }
427
438
 
428
- function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAddToCart, viewportSize = 'large' }: {
439
+ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, viewportSize = 'large' }: {
429
440
  rawContent: string;
430
441
  primaryColor?: string;
431
442
  accentColor?: string;
432
443
  isStreaming?: boolean;
433
- onAddToCart?: (product: Product) => void;
434
444
  viewportSize?: ChatViewportSize;
435
445
  }) {
436
- const result = React.useMemo<{ config: UIConfig } | { error: string } | { loading: true }>(() => {
437
- if (isStreaming) return { loading: true };
446
+ const result = React.useMemo<{ config: UIConfig } | { error: string } | { loading: true; silent?: boolean }>(() => {
447
+ if (isStreaming) {
448
+ const looksLikeCarousel = rawContent.includes('"view": "carousel"') ||
449
+ rawContent.includes('"view":"carousel"') ||
450
+ rawContent.includes('"type": "products"') ||
451
+ rawContent.includes('"type":"products"');
452
+ return { loading: true, silent: looksLikeCarousel };
453
+ }
438
454
  try {
439
455
  const clean = rawContent
440
456
  .replace(/^```[a-z]*\s*/i, '')
@@ -495,6 +511,7 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
495
511
  }, [rawContent, isStreaming]);
496
512
 
497
513
  if ('loading' in result) {
514
+ if (result.silent) return null;
498
515
  return (
499
516
  <div className="my-4 p-8 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex flex-col items-center justify-center gap-3 animate-pulse">
500
517
  <div className="w-5 h-5 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" />
@@ -547,16 +564,14 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
547
564
  </div>
548
565
  );
549
566
  case 'carousel':
567
+ // We only render title/description here. The actual carousel is rendered separately
568
+ // at the bottom of the MessageBubble to avoid being trapped in the streaming loading box.
550
569
  return (
551
570
  <div className="my-4">
552
571
  {config.title && <h4 className={`${isCompact ? 'mb-1.5 text-[11px]' : 'mb-2 text-xs'} font-semibold text-slate-500`}>{config.title}</h4>}
553
572
  {hasDescription && (
554
573
  <p className={`mb-3 ${isCompact ? 'text-xs' : 'text-sm'} text-slate-500 dark:text-white/60`}>{config.description}</p>
555
574
  )}
556
- <ProductCarousel products={(config.data as unknown as Product[]).map(item => ({
557
- ...item,
558
- image: item.image ?? (typeof resolveImage === 'function' ? resolveImage(item as unknown as Record<string, unknown>) : undefined)
559
- }))} primaryColor={primaryColor} onAddToCart={onAddToCart} />
560
575
  </div>
561
576
  );
562
577
  case 'table':
@@ -599,11 +614,6 @@ export function MessageBubble({
599
614
  const isCompact = viewportSize === 'compact';
600
615
  const isMedium = viewportSize === 'medium';
601
616
  const [showSources, setShowSources] = React.useState(false);
602
- const hasStructuredProductBlock = React.useMemo(
603
- () => /```(?:ui|json|chart)\s*[\s\S]*?"?(?:view|type|data|products)"?\s*:\s*"?(?:carousel|products|table)"?/i.test(message.content) ||
604
- (/\{[\s\S]*?"?(?:view|type|data|products)"?\s*:\s*"?(?:carousel|products)"?[\s\S]*\}/i.test(message.content) && looksLikeStructuredPayload(message.content)),
605
- [message.content],
606
- );
607
617
  const structuredContent = React.useMemo(
608
618
  () => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
609
619
  [isUser, message.content],
@@ -693,20 +703,34 @@ export function MessageBubble({
693
703
 
694
704
  // ── Merge & deduplicate products ───────────────────────────────────────────
695
705
  const allProducts = React.useMemo<Product[]>(() => {
696
- if (productsFromContent.length > 0) return productsFromContent;
697
-
698
706
  const seen = new Set<string>();
707
+ const merged: Product[] = [];
708
+
709
+ // 1. Add products extracted from the LLM content (highest priority)
710
+ for (const p of productsFromContent) {
711
+ const id = String(p.id || p.name);
712
+ if (!seen.has(id)) {
713
+ seen.add(id);
714
+ merged.push(p);
715
+ }
716
+ }
717
+
718
+ // 2. Add products from sources that were mentioned in the text
699
719
  const contentLower = message.content.toLowerCase();
720
+ for (const p of productsFromSources) {
721
+ const id = String(p.id || p.name);
722
+ if (seen.has(id)) continue;
700
723
 
701
- return productsFromSources.filter(p => {
702
- const id = String(p.id ?? p.name);
703
- if (seen.has(id)) return false;
704
- const mentioned =
705
- contentLower.includes(p.name.toLowerCase()) ||
706
- (p.brand ? contentLower.includes(p.brand.toLowerCase()) : false);
707
- if (mentioned) { seen.add(id); return true; }
708
- return false;
709
- });
724
+ const mentioned = contentLower.includes(p.name.toLowerCase()) ||
725
+ (p.brand ? contentLower.includes(p.brand.toLowerCase()) : false);
726
+
727
+ if (mentioned) {
728
+ seen.add(id);
729
+ merged.push(p);
730
+ }
731
+ }
732
+
733
+ return merged;
710
734
  }, [productsFromSources, productsFromContent, message.content]);
711
735
 
712
736
  const processedMarkdown = React.useMemo(() => {
@@ -830,7 +854,6 @@ export function MessageBubble({
830
854
  primaryColor={primaryColor}
831
855
  accentColor={accentColor}
832
856
  isStreaming={isStreaming}
833
- onAddToCart={onAddToCart}
834
857
  viewportSize={viewportSize}
835
858
  />
836
859
  );
@@ -845,7 +868,6 @@ export function MessageBubble({
845
868
  primaryColor={primaryColor}
846
869
  accentColor={accentColor}
847
870
  isStreaming={isStreaming}
848
- onAddToCart={onAddToCart}
849
871
  viewportSize={viewportSize}
850
872
  />
851
873
  );
@@ -861,7 +883,6 @@ export function MessageBubble({
861
883
  primaryColor={primaryColor}
862
884
  accentColor={accentColor}
863
885
  isStreaming={isStreaming}
864
- onAddToCart={onAddToCart}
865
886
  viewportSize={viewportSize}
866
887
  />
867
888
  );
@@ -888,7 +909,7 @@ export function MessageBubble({
888
909
  );
889
910
  },
890
911
  }),
891
- [primaryColor, accentColor, isStreaming, onAddToCart, viewportSize],
912
+ [primaryColor, accentColor, isStreaming, viewportSize],
892
913
  );
893
914
 
894
915
  // ── Render ─────────────────────────────────────────────────────────────────
@@ -930,7 +951,6 @@ export function MessageBubble({
930
951
  primaryColor={primaryColor}
931
952
  accentColor={accentColor}
932
953
  isStreaming={isStreaming}
933
- onAddToCart={onAddToCart}
934
954
  viewportSize={viewportSize}
935
955
  />
936
956
  )}
@@ -949,7 +969,7 @@ export function MessageBubble({
949
969
  </div>
950
970
 
951
971
  {/* Product Carousel */}
952
- {!isUser && !hasStructuredProductBlock && allProducts.length > 0 && (
972
+ {!isUser && allProducts.length > 0 && (
953
973
  <div className="w-full mt-1">
954
974
  <ProductCarousel
955
975
  products={allProducts}
@@ -122,9 +122,9 @@ NEVER output naked JSON. If you provide a visualization, do not also provide a m
122
122
  }
123
123
 
124
124
  3. DATA RULES
125
- - Build the UI payload from retrieved context only.
125
+ - Build the UI payload from retrieved context and metadata.
126
126
  - For CAROUSEL: "data" items MUST have { id, name, brand, price, image, link }.
127
- - For CHART: "data" items MUST have { label, value }.
127
+ - IMPORTANT: Extract "image" and "price" from Source metadata. Image URLs are often in fields like "images", "thumbnail", or "img".
128
128
 
129
129
  4. FORMAT RULES
130
130
  - Wrap the JSON in \`\`\`ui ... \`\`\` blocks.
@@ -139,7 +139,7 @@ Assistant: "I found several beauty products in our catalog:"
139
139
  "title": "Beauty Products",
140
140
  "description": "Latest skincare and makeup items.",
141
141
  "data": [
142
- { "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "...", "link": "..." }
142
+ { "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "https://placehold.co/600x400?text=Product", "link": "https://example.com/product" }
143
143
  ]
144
144
  }
145
145
  \`\`\`
@@ -387,7 +387,7 @@ Assistant: "I found several beauty products in our catalog:"
387
387
 
388
388
  // 4. Context Augmentation
389
389
  let context = sources.length
390
- ? sources.map((m, i) => `[Source ${i + 1}]\n${m.content}`).join('\n\n---\n\n')
390
+ ? sources.map((m, i) => `[Source ${i + 1}]\nContent: ${m.content}\nMetadata: ${JSON.stringify(m.metadata)}`).join('\n\n---\n\n')
391
391
  : 'No relevant context found.';
392
392
 
393
393
  if (graphData && graphData.nodes.length > 0) {