@retrivora-ai/rag-engine 1.6.8 → 1.7.0

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.
@@ -2259,17 +2259,18 @@ var Pipeline = class {
2259
2259
  async initialize() {
2260
2260
  var _a, _b;
2261
2261
  if (this.initialised) return;
2262
- const CHART_MARKER = "<!-- UI_PROTOCOL_V6 -->";
2262
+ const CHART_MARKER = "<!-- UI_PROTOCOL_V7 -->";
2263
2263
  const chartInstruction = `
2264
2264
 
2265
2265
  ${CHART_MARKER}
2266
- ### UNIVERSAL UI PROTOCOL
2267
- When the user asks for a visual representation, comparison, distribution, breakdown, table, list, catalog, products, carousel, stock view, or category summary, you MUST include exactly one \`\`\`ui\`\`\` block.
2266
+ ### UNIVERSAL UI PROTOCOL (MANDATORY)
2267
+ CRITICAL: When providing visual data (carousels, charts, tables), you MUST wrap the JSON payload in a \`\`\`ui\`\`\` code block.
2268
+ NEVER output naked JSON. If you provide a visualization, do not also provide a markdown table of the same data unless explicitly asked.
2268
2269
 
2269
2270
  1. VIEW SELECTION
2270
- - Use "carousel" for browsing specific products, items, or a catalog. This is the DEFAULT for "show me", "what are", or "find me" requests for a specific category (e.g. "beauty").
2271
- - Use "chart" ONLY for numerical aggregations: distributions, percentages, counts, comparisons over time, or trends (e.g. "how many beauty products?").
2272
- - Use "table" for detailed technical specifications, row-heavy comparisons, or when the user explicitly asks for a list/table format.
2271
+ - Use "carousel" for browsing products, items, or catalogs. (e.g., "show me beauty products").
2272
+ - Use "chart" ONLY for numerical aggregations (counts, sums, percentages).
2273
+ - Use "table" for technical specs or row-heavy comparisons.
2273
2274
 
2274
2275
  2. REQUIRED JSON SHAPE
2275
2276
  {
@@ -2277,52 +2278,31 @@ When the user asks for a visual representation, comparison, distribution, breakd
2277
2278
  "title": "Short heading",
2278
2279
  "description": "One sentence describing the view",
2279
2280
  "chartType": "pie" | "bar" | "line", // Required if view is "chart"
2280
- "xAxisKey": "label",
2281
- "dataKeys": ["value"],
2282
- "columns": ["dynamicFieldKey1", "dynamicFieldKey2"],
2283
- "insights": ["Short takeaway 1", "Short takeaway 2"],
2284
- "data": []
2281
+ "data": [] // Array of objects
2285
2282
  }
2286
2283
 
2287
2284
  3. DATA RULES
2288
- - Build the UI payload from retrieved context only. Do not invent products or values.
2289
- - For CAROUSEL: "data" must be a list of product objects (id, name, brand, price, image, link).
2290
- - For CHART: "data" must be aggregated counts or sums. Never put raw product lists in a chart view.
2291
- - If the user asks for "products in beauty", do NOT return a pie chart of categories; return a carousel of the beauty products themselves.
2285
+ - Build the UI payload from retrieved context and metadata.
2286
+ - For CAROUSEL: "data" items MUST have { id, name, brand, price, image, link }.
2287
+ - IMPORTANT: Extract "image" and "price" from Source metadata. Image URLs are often in fields like "images", "thumbnail", or "img".
2292
2288
 
2293
2289
  4. FORMAT RULES
2294
- - Return valid JSON inside the \`\`\`ui\`\`\` block.
2295
- - Put natural language explanation before or after the block, not inside.
2290
+ - Wrap the JSON in \`\`\`ui ... \`\`\` blocks.
2291
+ - Place natural language text OUTSIDE the block.
2296
2292
 
2297
- 5. EXAMPLES
2298
- User: "show me beauty products"
2299
- Assistant:
2293
+ 5. EXAMPLE
2294
+ User: "find me beauty products"
2295
+ Assistant: "I found several beauty products in our catalog:"
2300
2296
  \`\`\`ui
2301
2297
  {
2302
2298
  "view": "carousel",
2303
2299
  "title": "Beauty Products",
2304
- "description": "Browse our latest skincare and makeup collection.",
2300
+ "description": "Latest skincare and makeup items.",
2305
2301
  "data": [
2306
- { "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "...", "link": "..." },
2307
- { "id": "2", "name": "Lipstick", "brand": "Chic", "price": "$12.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" }
2308
2303
  ]
2309
2304
  }
2310
2305
  \`\`\`
2311
-
2312
- User: "breakdown of products by category"
2313
- Assistant:
2314
- \`\`\`ui
2315
- {
2316
- "view": "chart",
2317
- "title": "Category Breakdown",
2318
- "chartType": "pie",
2319
- "xAxisKey": "label",
2320
- "dataKeys": ["value"],
2321
- "data": [
2322
- { "label": "Beauty", "value": 15 },
2323
- { "label": "Fragrances", "value": 8 }
2324
- ]
2325
- }
2326
2306
  \`\`\``;
2327
2307
  if (!((_a = this.config.llm.systemPrompt) == null ? void 0 : _a.includes(CHART_MARKER))) {
2328
2308
  this.config.llm.systemPrompt = (this.config.llm.systemPrompt || "") + chartInstruction;
@@ -2531,7 +2511,8 @@ Assistant:
2531
2511
  sources = sources.slice(0, topK);
2532
2512
  }
2533
2513
  let context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
2534
- ${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.";
2535
2516
  if (graphData && graphData.nodes.length > 0) {
2536
2517
  const graphContext = graphData.nodes.map(
2537
2518
  (n) => `Entity: ${n.label} (${n.id})${n.properties ? " - " + JSON.stringify(n.properties) : ""}`
@@ -3822,17 +3822,18 @@ var Pipeline = class {
3822
3822
  async initialize() {
3823
3823
  var _a, _b;
3824
3824
  if (this.initialised) return;
3825
- const CHART_MARKER = "<!-- UI_PROTOCOL_V6 -->";
3825
+ const CHART_MARKER = "<!-- UI_PROTOCOL_V7 -->";
3826
3826
  const chartInstruction = `
3827
3827
 
3828
3828
  ${CHART_MARKER}
3829
- ### UNIVERSAL UI PROTOCOL
3830
- When the user asks for a visual representation, comparison, distribution, breakdown, table, list, catalog, products, carousel, stock view, or category summary, you MUST include exactly one \`\`\`ui\`\`\` block.
3829
+ ### UNIVERSAL UI PROTOCOL (MANDATORY)
3830
+ CRITICAL: When providing visual data (carousels, charts, tables), you MUST wrap the JSON payload in a \`\`\`ui\`\`\` code block.
3831
+ NEVER output naked JSON. If you provide a visualization, do not also provide a markdown table of the same data unless explicitly asked.
3831
3832
 
3832
3833
  1. VIEW SELECTION
3833
- - Use "carousel" for browsing specific products, items, or a catalog. This is the DEFAULT for "show me", "what are", or "find me" requests for a specific category (e.g. "beauty").
3834
- - Use "chart" ONLY for numerical aggregations: distributions, percentages, counts, comparisons over time, or trends (e.g. "how many beauty products?").
3835
- - Use "table" for detailed technical specifications, row-heavy comparisons, or when the user explicitly asks for a list/table format.
3834
+ - Use "carousel" for browsing products, items, or catalogs. (e.g., "show me beauty products").
3835
+ - Use "chart" ONLY for numerical aggregations (counts, sums, percentages).
3836
+ - Use "table" for technical specs or row-heavy comparisons.
3836
3837
 
3837
3838
  2. REQUIRED JSON SHAPE
3838
3839
  {
@@ -3840,52 +3841,31 @@ When the user asks for a visual representation, comparison, distribution, breakd
3840
3841
  "title": "Short heading",
3841
3842
  "description": "One sentence describing the view",
3842
3843
  "chartType": "pie" | "bar" | "line", // Required if view is "chart"
3843
- "xAxisKey": "label",
3844
- "dataKeys": ["value"],
3845
- "columns": ["dynamicFieldKey1", "dynamicFieldKey2"],
3846
- "insights": ["Short takeaway 1", "Short takeaway 2"],
3847
- "data": []
3844
+ "data": [] // Array of objects
3848
3845
  }
3849
3846
 
3850
3847
  3. DATA RULES
3851
- - Build the UI payload from retrieved context only. Do not invent products or values.
3852
- - For CAROUSEL: "data" must be a list of product objects (id, name, brand, price, image, link).
3853
- - For CHART: "data" must be aggregated counts or sums. Never put raw product lists in a chart view.
3854
- - If the user asks for "products in beauty", do NOT return a pie chart of categories; return a carousel of the beauty products themselves.
3848
+ - Build the UI payload from retrieved context and metadata.
3849
+ - For CAROUSEL: "data" items MUST have { id, name, brand, price, image, link }.
3850
+ - IMPORTANT: Extract "image" and "price" from Source metadata. Image URLs are often in fields like "images", "thumbnail", or "img".
3855
3851
 
3856
3852
  4. FORMAT RULES
3857
- - Return valid JSON inside the \`\`\`ui\`\`\` block.
3858
- - Put natural language explanation before or after the block, not inside.
3853
+ - Wrap the JSON in \`\`\`ui ... \`\`\` blocks.
3854
+ - Place natural language text OUTSIDE the block.
3859
3855
 
3860
- 5. EXAMPLES
3861
- User: "show me beauty products"
3862
- Assistant:
3856
+ 5. EXAMPLE
3857
+ User: "find me beauty products"
3858
+ Assistant: "I found several beauty products in our catalog:"
3863
3859
  \`\`\`ui
3864
3860
  {
3865
3861
  "view": "carousel",
3866
3862
  "title": "Beauty Products",
3867
- "description": "Browse our latest skincare and makeup collection.",
3863
+ "description": "Latest skincare and makeup items.",
3868
3864
  "data": [
3869
- { "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "...", "link": "..." },
3870
- { "id": "2", "name": "Lipstick", "brand": "Chic", "price": "$12.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" }
3871
3866
  ]
3872
3867
  }
3873
3868
  \`\`\`
3874
-
3875
- User: "breakdown of products by category"
3876
- Assistant:
3877
- \`\`\`ui
3878
- {
3879
- "view": "chart",
3880
- "title": "Category Breakdown",
3881
- "chartType": "pie",
3882
- "xAxisKey": "label",
3883
- "dataKeys": ["value"],
3884
- "data": [
3885
- { "label": "Beauty", "value": 15 },
3886
- { "label": "Fragrances", "value": 8 }
3887
- ]
3888
- }
3889
3869
  \`\`\``;
3890
3870
  if (!((_a = this.config.llm.systemPrompt) == null ? void 0 : _a.includes(CHART_MARKER))) {
3891
3871
  this.config.llm.systemPrompt = (this.config.llm.systemPrompt || "") + chartInstruction;
@@ -4094,7 +4074,8 @@ Assistant:
4094
4074
  sources = sources.slice(0, topK);
4095
4075
  }
4096
4076
  let context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
4097
- ${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.";
4098
4079
  if (graphData && graphData.nodes.length > 0) {
4099
4080
  const graphContext = graphData.nodes.map(
4100
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-J7BCIPO2.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
@@ -405,7 +405,7 @@ function sanitizeJson(raw) {
405
405
  return `: ${result}`;
406
406
  }
407
407
  );
408
- s = s.replace(/\/\/[^\n]*/g, "");
408
+ s = s.replace(/(^|[^\:])\/\/[^\n]*/g, "$1");
409
409
  s = s.replace(/([{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/g, '$1"$2":');
410
410
  s = s.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"');
411
411
  s = s.replace(/,\s*([}\]])/g, "$1");
@@ -456,7 +456,7 @@ function extractLikelyJsonBlock(raw) {
456
456
  return trimmed.slice(Math.min(...starts));
457
457
  }
458
458
  function looksLikeStructuredPayload(raw) {
459
- return /"?(view|chartType|type|data|items|rows|products|results)"?\s*:/.test(raw);
459
+ return /"?(view|chartType|type|data|items|rows|products|results|columns|xAxisKey)"?\s*:/.test(raw);
460
460
  }
461
461
  function stripMarkdownTables(raw) {
462
462
  return raw.replace(
@@ -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]
@@ -841,15 +847,16 @@ function MessageBubble({
841
847
  let raw = structuredContent.payload ? structuredContent.text : cleanContent || message.content;
842
848
  const hasStructuredUiBlock = Boolean(structuredContent.payload) || /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
843
849
  raw = raw.replace(/([^\n])\n\|/g, "$1\n\n|").replace(/(\|[^\n]*\|)\n\n+(?=\|)/g, "$1\n");
844
- if (!isStreaming) {
845
- raw = raw.replace(/(?:^|\n)\s*\{[\s\S]*\}\s*(?:\n|$)/g, (match) => {
850
+ if (!isStreaming && !hasStructuredUiBlock) {
851
+ raw = raw.replace(/(?:^|\n)\s*(\{[\s\S]+?\})(?:\s*\n|$)/g, (match, body) => {
846
852
  if (match.includes("```")) return match;
847
- if (!looksLikeStructuredPayload(match)) return match;
848
- const type = match.includes('"view"') || match.includes('"chartType"') || match.includes('"type": "pie"') || match.includes('"type":"pie"') || match.includes('"type": "bar"') || match.includes('"type":"bar"') || match.includes('"type": "line"') || match.includes('"type":"line"') ? "ui" : "json";
853
+ const trimmedBody = body.trim();
854
+ if (!looksLikeStructuredPayload(trimmedBody)) return match;
855
+ const type = trimmedBody.includes('"view"') || trimmedBody.includes('"chartType"') || trimmedBody.includes('"type"') || trimmedBody.includes('"xAxisKey"') ? "ui" : "json";
849
856
  return `
850
857
 
851
858
  \`\`\`${type}
852
- ${match.trim()}
859
+ ${trimmedBody}
853
860
  \`\`\`
854
861
 
855
862
  `;
@@ -949,7 +956,6 @@ ${match.trim()}
949
956
  primaryColor,
950
957
  accentColor,
951
958
  isStreaming,
952
- onAddToCart,
953
959
  viewportSize
954
960
  }
955
961
  );
@@ -964,7 +970,6 @@ ${match.trim()}
964
970
  primaryColor,
965
971
  accentColor,
966
972
  isStreaming,
967
- onAddToCart,
968
973
  viewportSize
969
974
  }
970
975
  );
@@ -980,7 +985,6 @@ ${match.trim()}
980
985
  primaryColor,
981
986
  accentColor,
982
987
  isStreaming,
983
- onAddToCart,
984
988
  viewportSize
985
989
  }
986
990
  );
@@ -992,7 +996,7 @@ ${match.trim()}
992
996
  return /* @__PURE__ */ import_react5.default.createElement("code", __spreadValues({ className }, props), typeof children === "string" || typeof children === "number" ? children : JSON.stringify(children));
993
997
  }
994
998
  }),
995
- [primaryColor, accentColor, isStreaming, onAddToCart, viewportSize]
999
+ [primaryColor, accentColor, isStreaming, viewportSize]
996
1000
  );
997
1001
  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(
998
1002
  "div",
@@ -1014,11 +1018,10 @@ ${match.trim()}
1014
1018
  primaryColor,
1015
1019
  accentColor,
1016
1020
  isStreaming,
1017
- onAddToCart,
1018
1021
  viewportSize
1019
1022
  }
1020
1023
  ), !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" }))
1021
- ), !isUser && !hasStructuredProductBlock && allProducts.length > 0 && /* @__PURE__ */ import_react5.default.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ import_react5.default.createElement(
1024
+ ), !isUser && allProducts.length > 0 && /* @__PURE__ */ import_react5.default.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ import_react5.default.createElement(
1022
1025
  ProductCarousel,
1023
1026
  {
1024
1027
  products: allProducts,
package/dist/index.mjs CHANGED
@@ -368,7 +368,7 @@ function sanitizeJson(raw) {
368
368
  return `: ${result}`;
369
369
  }
370
370
  );
371
- s = s.replace(/\/\/[^\n]*/g, "");
371
+ s = s.replace(/(^|[^\:])\/\/[^\n]*/g, "$1");
372
372
  s = s.replace(/([{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/g, '$1"$2":');
373
373
  s = s.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"');
374
374
  s = s.replace(/,\s*([}\]])/g, "$1");
@@ -419,7 +419,7 @@ function extractLikelyJsonBlock(raw) {
419
419
  return trimmed.slice(Math.min(...starts));
420
420
  }
421
421
  function looksLikeStructuredPayload(raw) {
422
- return /"?(view|chartType|type|data|items|rows|products|results)"?\s*:/.test(raw);
422
+ return /"?(view|chartType|type|data|items|rows|products|results|columns|xAxisKey)"?\s*:/.test(raw);
423
423
  }
424
424
  function stripMarkdownTables(raw) {
425
425
  return raw.replace(
@@ -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]
@@ -804,15 +810,16 @@ function MessageBubble({
804
810
  let raw = structuredContent.payload ? structuredContent.text : cleanContent || message.content;
805
811
  const hasStructuredUiBlock = Boolean(structuredContent.payload) || /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
806
812
  raw = raw.replace(/([^\n])\n\|/g, "$1\n\n|").replace(/(\|[^\n]*\|)\n\n+(?=\|)/g, "$1\n");
807
- if (!isStreaming) {
808
- raw = raw.replace(/(?:^|\n)\s*\{[\s\S]*\}\s*(?:\n|$)/g, (match) => {
813
+ if (!isStreaming && !hasStructuredUiBlock) {
814
+ raw = raw.replace(/(?:^|\n)\s*(\{[\s\S]+?\})(?:\s*\n|$)/g, (match, body) => {
809
815
  if (match.includes("```")) return match;
810
- if (!looksLikeStructuredPayload(match)) return match;
811
- const type = match.includes('"view"') || match.includes('"chartType"') || match.includes('"type": "pie"') || match.includes('"type":"pie"') || match.includes('"type": "bar"') || match.includes('"type":"bar"') || match.includes('"type": "line"') || match.includes('"type":"line"') ? "ui" : "json";
816
+ const trimmedBody = body.trim();
817
+ if (!looksLikeStructuredPayload(trimmedBody)) return match;
818
+ const type = trimmedBody.includes('"view"') || trimmedBody.includes('"chartType"') || trimmedBody.includes('"type"') || trimmedBody.includes('"xAxisKey"') ? "ui" : "json";
812
819
  return `
813
820
 
814
821
  \`\`\`${type}
815
- ${match.trim()}
822
+ ${trimmedBody}
816
823
  \`\`\`
817
824
 
818
825
  `;
@@ -912,7 +919,6 @@ ${match.trim()}
912
919
  primaryColor,
913
920
  accentColor,
914
921
  isStreaming,
915
- onAddToCart,
916
922
  viewportSize
917
923
  }
918
924
  );
@@ -927,7 +933,6 @@ ${match.trim()}
927
933
  primaryColor,
928
934
  accentColor,
929
935
  isStreaming,
930
- onAddToCart,
931
936
  viewportSize
932
937
  }
933
938
  );
@@ -943,7 +948,6 @@ ${match.trim()}
943
948
  primaryColor,
944
949
  accentColor,
945
950
  isStreaming,
946
- onAddToCart,
947
951
  viewportSize
948
952
  }
949
953
  );
@@ -955,7 +959,7 @@ ${match.trim()}
955
959
  return /* @__PURE__ */ React5.createElement("code", __spreadValues({ className }, props), typeof children === "string" || typeof children === "number" ? children : JSON.stringify(children));
956
960
  }
957
961
  }),
958
- [primaryColor, accentColor, isStreaming, onAddToCart, viewportSize]
962
+ [primaryColor, accentColor, isStreaming, viewportSize]
959
963
  );
960
964
  return /* @__PURE__ */ React5.createElement("div", { className: `flex ${isCompact ? "gap-2" : "gap-3"} ${isUser ? "flex-row-reverse" : "flex-row"} items-start` }, /* @__PURE__ */ React5.createElement(
961
965
  "div",
@@ -977,11 +981,10 @@ ${match.trim()}
977
981
  primaryColor,
978
982
  accentColor,
979
983
  isStreaming,
980
- onAddToCart,
981
984
  viewportSize
982
985
  }
983
986
  ), !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" }))
984
- ), !isUser && !hasStructuredProductBlock && allProducts.length > 0 && /* @__PURE__ */ React5.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ React5.createElement(
987
+ ), !isUser && allProducts.length > 0 && /* @__PURE__ */ React5.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ React5.createElement(
985
988
  ProductCarousel,
986
989
  {
987
990
  products: allProducts,
package/dist/server.js CHANGED
@@ -3913,17 +3913,18 @@ var Pipeline = class {
3913
3913
  async initialize() {
3914
3914
  var _a, _b;
3915
3915
  if (this.initialised) return;
3916
- const CHART_MARKER = "<!-- UI_PROTOCOL_V6 -->";
3916
+ const CHART_MARKER = "<!-- UI_PROTOCOL_V7 -->";
3917
3917
  const chartInstruction = `
3918
3918
 
3919
3919
  ${CHART_MARKER}
3920
- ### UNIVERSAL UI PROTOCOL
3921
- When the user asks for a visual representation, comparison, distribution, breakdown, table, list, catalog, products, carousel, stock view, or category summary, you MUST include exactly one \`\`\`ui\`\`\` block.
3920
+ ### UNIVERSAL UI PROTOCOL (MANDATORY)
3921
+ CRITICAL: When providing visual data (carousels, charts, tables), you MUST wrap the JSON payload in a \`\`\`ui\`\`\` code block.
3922
+ NEVER output naked JSON. If you provide a visualization, do not also provide a markdown table of the same data unless explicitly asked.
3922
3923
 
3923
3924
  1. VIEW SELECTION
3924
- - Use "carousel" for browsing specific products, items, or a catalog. This is the DEFAULT for "show me", "what are", or "find me" requests for a specific category (e.g. "beauty").
3925
- - Use "chart" ONLY for numerical aggregations: distributions, percentages, counts, comparisons over time, or trends (e.g. "how many beauty products?").
3926
- - Use "table" for detailed technical specifications, row-heavy comparisons, or when the user explicitly asks for a list/table format.
3925
+ - Use "carousel" for browsing products, items, or catalogs. (e.g., "show me beauty products").
3926
+ - Use "chart" ONLY for numerical aggregations (counts, sums, percentages).
3927
+ - Use "table" for technical specs or row-heavy comparisons.
3927
3928
 
3928
3929
  2. REQUIRED JSON SHAPE
3929
3930
  {
@@ -3931,52 +3932,31 @@ When the user asks for a visual representation, comparison, distribution, breakd
3931
3932
  "title": "Short heading",
3932
3933
  "description": "One sentence describing the view",
3933
3934
  "chartType": "pie" | "bar" | "line", // Required if view is "chart"
3934
- "xAxisKey": "label",
3935
- "dataKeys": ["value"],
3936
- "columns": ["dynamicFieldKey1", "dynamicFieldKey2"],
3937
- "insights": ["Short takeaway 1", "Short takeaway 2"],
3938
- "data": []
3935
+ "data": [] // Array of objects
3939
3936
  }
3940
3937
 
3941
3938
  3. DATA RULES
3942
- - Build the UI payload from retrieved context only. Do not invent products or values.
3943
- - For CAROUSEL: "data" must be a list of product objects (id, name, brand, price, image, link).
3944
- - For CHART: "data" must be aggregated counts or sums. Never put raw product lists in a chart view.
3945
- - If the user asks for "products in beauty", do NOT return a pie chart of categories; return a carousel of the beauty products themselves.
3939
+ - Build the UI payload from retrieved context and metadata.
3940
+ - For CAROUSEL: "data" items MUST have { id, name, brand, price, image, link }.
3941
+ - IMPORTANT: Extract "image" and "price" from Source metadata. Image URLs are often in fields like "images", "thumbnail", or "img".
3946
3942
 
3947
3943
  4. FORMAT RULES
3948
- - Return valid JSON inside the \`\`\`ui\`\`\` block.
3949
- - Put natural language explanation before or after the block, not inside.
3944
+ - Wrap the JSON in \`\`\`ui ... \`\`\` blocks.
3945
+ - Place natural language text OUTSIDE the block.
3950
3946
 
3951
- 5. EXAMPLES
3952
- User: "show me beauty products"
3953
- Assistant:
3947
+ 5. EXAMPLE
3948
+ User: "find me beauty products"
3949
+ Assistant: "I found several beauty products in our catalog:"
3954
3950
  \`\`\`ui
3955
3951
  {
3956
3952
  "view": "carousel",
3957
3953
  "title": "Beauty Products",
3958
- "description": "Browse our latest skincare and makeup collection.",
3954
+ "description": "Latest skincare and makeup items.",
3959
3955
  "data": [
3960
- { "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "...", "link": "..." },
3961
- { "id": "2", "name": "Lipstick", "brand": "Chic", "price": "$12.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" }
3962
3957
  ]
3963
3958
  }
3964
3959
  \`\`\`
3965
-
3966
- User: "breakdown of products by category"
3967
- Assistant:
3968
- \`\`\`ui
3969
- {
3970
- "view": "chart",
3971
- "title": "Category Breakdown",
3972
- "chartType": "pie",
3973
- "xAxisKey": "label",
3974
- "dataKeys": ["value"],
3975
- "data": [
3976
- { "label": "Beauty", "value": 15 },
3977
- { "label": "Fragrances", "value": 8 }
3978
- ]
3979
- }
3980
3960
  \`\`\``;
3981
3961
  if (!((_a = this.config.llm.systemPrompt) == null ? void 0 : _a.includes(CHART_MARKER))) {
3982
3962
  this.config.llm.systemPrompt = (this.config.llm.systemPrompt || "") + chartInstruction;
@@ -4185,7 +4165,8 @@ Assistant:
4185
4165
  sources = sources.slice(0, topK);
4186
4166
  }
4187
4167
  let context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
4188
- ${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.";
4189
4170
  if (graphData && graphData.nodes.length > 0) {
4190
4171
  const graphContext = graphData.nodes.map(
4191
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-J7BCIPO2.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.8",
3
+ "version": "1.7.0",
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",
@@ -32,8 +32,8 @@ function sanitizeJson(raw: string): string {
32
32
  },
33
33
  );
34
34
 
35
- // 2. Strip JS-style single-line comments
36
- s = s.replace(/\/\/[^\n]*/g, '');
35
+ // 2. Strip JS-style single-line comments, but avoid URL protocols like https://
36
+ s = s.replace(/(^|[^\:])\/\/[^\n]*/g, '$1');
37
37
 
38
38
  // 3. Quote bare (unquoted) object keys
39
39
  s = s.replace(/([{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/g, '$1"$2":');
@@ -103,7 +103,7 @@ function extractLikelyJsonBlock(raw: string): string {
103
103
  }
104
104
 
105
105
  function looksLikeStructuredPayload(raw: string): boolean {
106
- return /"?(view|chartType|type|data|items|rows|products|results)"?\s*:/.test(raw);
106
+ return /"?(view|chartType|type|data|items|rows|products|results|columns|xAxisKey)"?\s*:/.test(raw);
107
107
  }
108
108
 
109
109
  function stripMarkdownTables(raw: string): string {
@@ -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],
@@ -718,17 +728,23 @@ export function MessageBubble({
718
728
  .replace(/(\|[^\n]*\|)\n\n+(?=\|)/g, '$1\n'); // Remove blank lines between rows
719
729
 
720
730
  // 2. HEALING: Detect naked JSON charts/products and wrap them if markers are missing
721
- // This handles models (like Llama 3.2) that often omit the ```chart markers.
722
- if (!isStreaming) {
731
+ // This handles models (like Llama 3.2) that often omit the ```ui markers.
732
+ if (!isStreaming && !hasStructuredUiBlock) {
723
733
  // Look for objects that look like chart or product data
724
- raw = raw.replace(/(?:^|\n)\s*\{[\s\S]*\}\s*(?:\n|$)/g, (match) => {
734
+ raw = raw.replace(/(?:^|\n)\s*(\{[\s\S]+?\})(?:\s*\n|$)/g, (match, body) => {
725
735
  // Only wrap if not already in a code block
726
736
  if (match.includes('```')) return match;
727
- if (!looksLikeStructuredPayload(match)) return match;
728
- const type = match.includes('"view"') || match.includes('"chartType"') || match.includes('"type": "pie"') || match.includes('"type":"pie"') || match.includes('"type": "bar"') || match.includes('"type":"bar"') || match.includes('"type": "line"') || match.includes('"type":"line"')
737
+ const trimmedBody = body.trim();
738
+ if (!looksLikeStructuredPayload(trimmedBody)) return match;
739
+
740
+ const type = trimmedBody.includes('"view"') ||
741
+ trimmedBody.includes('"chartType"') ||
742
+ trimmedBody.includes('"type"') ||
743
+ trimmedBody.includes('"xAxisKey"')
729
744
  ? 'ui'
730
745
  : 'json';
731
- return `\n\n\`\`\`${type}\n${match.trim()}\n\`\`\`\n\n`;
746
+
747
+ return `\n\n\`\`\`${type}\n${trimmedBody}\n\`\`\`\n\n`;
732
748
  });
733
749
  }
734
750
 
@@ -824,7 +840,6 @@ export function MessageBubble({
824
840
  primaryColor={primaryColor}
825
841
  accentColor={accentColor}
826
842
  isStreaming={isStreaming}
827
- onAddToCart={onAddToCart}
828
843
  viewportSize={viewportSize}
829
844
  />
830
845
  );
@@ -839,7 +854,6 @@ export function MessageBubble({
839
854
  primaryColor={primaryColor}
840
855
  accentColor={accentColor}
841
856
  isStreaming={isStreaming}
842
- onAddToCart={onAddToCart}
843
857
  viewportSize={viewportSize}
844
858
  />
845
859
  );
@@ -855,7 +869,6 @@ export function MessageBubble({
855
869
  primaryColor={primaryColor}
856
870
  accentColor={accentColor}
857
871
  isStreaming={isStreaming}
858
- onAddToCart={onAddToCart}
859
872
  viewportSize={viewportSize}
860
873
  />
861
874
  );
@@ -882,7 +895,7 @@ export function MessageBubble({
882
895
  );
883
896
  },
884
897
  }),
885
- [primaryColor, accentColor, isStreaming, onAddToCart, viewportSize],
898
+ [primaryColor, accentColor, isStreaming, viewportSize],
886
899
  );
887
900
 
888
901
  // ── Render ─────────────────────────────────────────────────────────────────
@@ -924,7 +937,6 @@ export function MessageBubble({
924
937
  primaryColor={primaryColor}
925
938
  accentColor={accentColor}
926
939
  isStreaming={isStreaming}
927
- onAddToCart={onAddToCart}
928
940
  viewportSize={viewportSize}
929
941
  />
930
942
  )}
@@ -943,7 +955,7 @@ export function MessageBubble({
943
955
  </div>
944
956
 
945
957
  {/* Product Carousel */}
946
- {!isUser && !hasStructuredProductBlock && allProducts.length > 0 && (
958
+ {!isUser && allProducts.length > 0 && (
947
959
  <div className="w-full mt-1">
948
960
  <ProductCarousel
949
961
  products={allProducts}
@@ -101,15 +101,16 @@ export class Pipeline {
101
101
 
102
102
  // Augment system prompt with a Universal Visualization Protocol
103
103
  // We use a unique marker to ensure this is only injected once but is ALWAYS present.
104
- const CHART_MARKER = '<!-- UI_PROTOCOL_V6 -->';
104
+ const CHART_MARKER = '<!-- UI_PROTOCOL_V7 -->';
105
105
  const chartInstruction = `\n\n${CHART_MARKER}
106
- ### UNIVERSAL UI PROTOCOL
107
- When the user asks for a visual representation, comparison, distribution, breakdown, table, list, catalog, products, carousel, stock view, or category summary, you MUST include exactly one \`\`\`ui\`\`\` block.
106
+ ### UNIVERSAL UI PROTOCOL (MANDATORY)
107
+ CRITICAL: When providing visual data (carousels, charts, tables), you MUST wrap the JSON payload in a \`\`\`ui\`\`\` code block.
108
+ NEVER output naked JSON. If you provide a visualization, do not also provide a markdown table of the same data unless explicitly asked.
108
109
 
109
110
  1. VIEW SELECTION
110
- - Use "carousel" for browsing specific products, items, or a catalog. This is the DEFAULT for "show me", "what are", or "find me" requests for a specific category (e.g. "beauty").
111
- - Use "chart" ONLY for numerical aggregations: distributions, percentages, counts, comparisons over time, or trends (e.g. "how many beauty products?").
112
- - Use "table" for detailed technical specifications, row-heavy comparisons, or when the user explicitly asks for a list/table format.
111
+ - Use "carousel" for browsing products, items, or catalogs. (e.g., "show me beauty products").
112
+ - Use "chart" ONLY for numerical aggregations (counts, sums, percentages).
113
+ - Use "table" for technical specs or row-heavy comparisons.
113
114
 
114
115
  2. REQUIRED JSON SHAPE
115
116
  {
@@ -117,52 +118,31 @@ When the user asks for a visual representation, comparison, distribution, breakd
117
118
  "title": "Short heading",
118
119
  "description": "One sentence describing the view",
119
120
  "chartType": "pie" | "bar" | "line", // Required if view is "chart"
120
- "xAxisKey": "label",
121
- "dataKeys": ["value"],
122
- "columns": ["dynamicFieldKey1", "dynamicFieldKey2"],
123
- "insights": ["Short takeaway 1", "Short takeaway 2"],
124
- "data": []
121
+ "data": [] // Array of objects
125
122
  }
126
123
 
127
124
  3. DATA RULES
128
- - Build the UI payload from retrieved context only. Do not invent products or values.
129
- - For CAROUSEL: "data" must be a list of product objects (id, name, brand, price, image, link).
130
- - For CHART: "data" must be aggregated counts or sums. Never put raw product lists in a chart view.
131
- - If the user asks for "products in beauty", do NOT return a pie chart of categories; return a carousel of the beauty products themselves.
125
+ - Build the UI payload from retrieved context and metadata.
126
+ - For CAROUSEL: "data" items MUST have { id, name, brand, price, image, link }.
127
+ - IMPORTANT: Extract "image" and "price" from Source metadata. Image URLs are often in fields like "images", "thumbnail", or "img".
132
128
 
133
129
  4. FORMAT RULES
134
- - Return valid JSON inside the \`\`\`ui\`\`\` block.
135
- - Put natural language explanation before or after the block, not inside.
130
+ - Wrap the JSON in \`\`\`ui ... \`\`\` blocks.
131
+ - Place natural language text OUTSIDE the block.
136
132
 
137
- 5. EXAMPLES
138
- User: "show me beauty products"
139
- Assistant:
133
+ 5. EXAMPLE
134
+ User: "find me beauty products"
135
+ Assistant: "I found several beauty products in our catalog:"
140
136
  \`\`\`ui
141
137
  {
142
138
  "view": "carousel",
143
139
  "title": "Beauty Products",
144
- "description": "Browse our latest skincare and makeup collection.",
140
+ "description": "Latest skincare and makeup items.",
145
141
  "data": [
146
- { "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "...", "link": "..." },
147
- { "id": "2", "name": "Lipstick", "brand": "Chic", "price": "$12.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" }
148
143
  ]
149
144
  }
150
145
  \`\`\`
151
-
152
- User: "breakdown of products by category"
153
- Assistant:
154
- \`\`\`ui
155
- {
156
- "view": "chart",
157
- "title": "Category Breakdown",
158
- "chartType": "pie",
159
- "xAxisKey": "label",
160
- "dataKeys": ["value"],
161
- "data": [
162
- { "label": "Beauty", "value": 15 },
163
- { "label": "Fragrances", "value": 8 }
164
- ]
165
- }
166
146
  \`\`\``;
167
147
 
168
148
  if (!this.config.llm.systemPrompt?.includes(CHART_MARKER)) {
@@ -407,7 +387,7 @@ Assistant:
407
387
 
408
388
  // 4. Context Augmentation
409
389
  let context = sources.length
410
- ? 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')
411
391
  : 'No relevant context found.';
412
392
 
413
393
  if (graphData && graphData.nodes.length > 0) {