@retrivora-ai/rag-engine 1.5.4 → 1.5.5

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,23 +2259,68 @@ var Pipeline = class {
2259
2259
  async initialize() {
2260
2260
  var _a, _b;
2261
2261
  if (this.initialised) return;
2262
- const CHART_MARKER = "<!-- UI_PROTOCOL_V3 -->";
2262
+ const CHART_MARKER = "<!-- UI_PROTOCOL_V5 -->";
2263
2263
  const chartInstruction = `
2264
2264
 
2265
2265
  ${CHART_MARKER}
2266
- ### UNIVERSAL UI PROTOCOL:
2267
- You have a specialized UI system. For any structured data (charts, product lists, or tables), you MUST use a single \`\`\`ui\`\`\` block.
2268
- 1. DECIDE the best "view" for the user's query:
2269
- - "chart": For statistics, distributions, or comparisons. (Requires "chartType": "pie" | "bar" | "line")
2270
- - "carousel": For browsing specific product items.
2271
- - "table": For detailed raw data comparisons.
2272
- 2. OUTPUT a \`\`\`ui\`\`\` block with this schema:
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.
2268
+
2269
+ 1. VIEW SELECTION
2270
+ - Use "chart" for distributions, percentages, counts, comparisons, trends, or "show me in a chart".
2271
+ - Use "carousel" for browseable product recommendations or when the user asks to see products/items/cards.
2272
+ - Use "table" for tabular/raw detail, explicit table/list requests, or when there are many attributes to compare.
2273
+
2274
+ 2. REQUIRED JSON SHAPE
2273
2275
  {
2274
2276
  "view": "chart" | "carousel" | "table",
2275
- "chartType": "pie" | "bar" | "line" (only for view: "chart"),
2276
- "data": [ ... objects ... ]
2277
+ "title": "Short heading",
2278
+ "description": "One sentence describing the view",
2279
+ "chartType": "pie" | "bar" | "line",
2280
+ "xAxisKey": "label",
2281
+ "dataKeys": ["value"],
2282
+ "columns": ["name", "category", "price", "stockStatus"],
2283
+ "insights": ["Short takeaway 1", "Short takeaway 2"],
2284
+ "data": []
2277
2285
  }
2278
- 3. RULES: Valid JSON only. Data must be FLAT. Never draw charts/tables with text. Choose ONLY the most useful view.`;
2286
+
2287
+ 3. DATA RULES
2288
+ - Build the UI payload from retrieved context only. Do not invent products, categories, or stock values.
2289
+ - Aggregate raw product rows when the user asks for a chart.
2290
+ - For charts, every row in "data" must be flat JSON with primitive values only.
2291
+ - Prefer { "label": "...", "value": number } for pie charts.
2292
+ - If stock information exists, preserve it in each row with fields like "inStock", "inStockCount", "outOfStockCount", or "stockStatus".
2293
+ - If the user asks to highlight what is in stock, include those stock-related fields in the chart data and mention the pattern in "insights".
2294
+ - For carousel rows, include product-friendly fields such as id, name, brand, price, image, link, category, stockStatus.
2295
+ - For tables, keep "data" row-oriented and include "columns" when possible.
2296
+
2297
+ 4. FORMAT RULES
2298
+ - Never use ASCII art, markdown tables as the primary answer, or text-only fake charts when a UI block is requested.
2299
+ - Put any short natural-language explanation before or after the \`\`\`ui\`\`\` block, but keep it concise.
2300
+ - Return valid JSON inside the \`\`\`ui\`\`\` block.
2301
+
2302
+ 5. EXAMPLE
2303
+ User: "provide me the distribution of products across categories and highlight which ones are in stock in a pie chart"
2304
+ Assistant:
2305
+ \`\`\`ui
2306
+ {
2307
+ "view": "chart",
2308
+ "title": "Product Distribution Across Categories",
2309
+ "description": "Pie chart showing product count by category with stock signals preserved per slice.",
2310
+ "chartType": "pie",
2311
+ "xAxisKey": "label",
2312
+ "dataKeys": ["value"],
2313
+ "insights": [
2314
+ "Beauty has the highest product count.",
2315
+ "Electronics has the largest number of in-stock products."
2316
+ ],
2317
+ "data": [
2318
+ { "label": "Beauty", "value": 12, "inStockCount": 9, "outOfStockCount": 3 },
2319
+ { "label": "Electronics", "value": 8, "inStockCount": 7, "outOfStockCount": 1 },
2320
+ { "label": "Home", "value": 5, "inStockCount": 2, "outOfStockCount": 3 }
2321
+ ]
2322
+ }
2323
+ \`\`\``;
2279
2324
  if (!((_a = this.config.llm.systemPrompt) == null ? void 0 : _a.includes(CHART_MARKER))) {
2280
2325
  this.config.llm.systemPrompt = (this.config.llm.systemPrompt || "") + chartInstruction;
2281
2326
  }
@@ -3822,23 +3822,68 @@ var Pipeline = class {
3822
3822
  async initialize() {
3823
3823
  var _a, _b;
3824
3824
  if (this.initialised) return;
3825
- const CHART_MARKER = "<!-- UI_PROTOCOL_V3 -->";
3825
+ const CHART_MARKER = "<!-- UI_PROTOCOL_V5 -->";
3826
3826
  const chartInstruction = `
3827
3827
 
3828
3828
  ${CHART_MARKER}
3829
- ### UNIVERSAL UI PROTOCOL:
3830
- You have a specialized UI system. For any structured data (charts, product lists, or tables), you MUST use a single \`\`\`ui\`\`\` block.
3831
- 1. DECIDE the best "view" for the user's query:
3832
- - "chart": For statistics, distributions, or comparisons. (Requires "chartType": "pie" | "bar" | "line")
3833
- - "carousel": For browsing specific product items.
3834
- - "table": For detailed raw data comparisons.
3835
- 2. OUTPUT a \`\`\`ui\`\`\` block with this schema:
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.
3831
+
3832
+ 1. VIEW SELECTION
3833
+ - Use "chart" for distributions, percentages, counts, comparisons, trends, or "show me in a chart".
3834
+ - Use "carousel" for browseable product recommendations or when the user asks to see products/items/cards.
3835
+ - Use "table" for tabular/raw detail, explicit table/list requests, or when there are many attributes to compare.
3836
+
3837
+ 2. REQUIRED JSON SHAPE
3836
3838
  {
3837
3839
  "view": "chart" | "carousel" | "table",
3838
- "chartType": "pie" | "bar" | "line" (only for view: "chart"),
3839
- "data": [ ... objects ... ]
3840
+ "title": "Short heading",
3841
+ "description": "One sentence describing the view",
3842
+ "chartType": "pie" | "bar" | "line",
3843
+ "xAxisKey": "label",
3844
+ "dataKeys": ["value"],
3845
+ "columns": ["name", "category", "price", "stockStatus"],
3846
+ "insights": ["Short takeaway 1", "Short takeaway 2"],
3847
+ "data": []
3840
3848
  }
3841
- 3. RULES: Valid JSON only. Data must be FLAT. Never draw charts/tables with text. Choose ONLY the most useful view.`;
3849
+
3850
+ 3. DATA RULES
3851
+ - Build the UI payload from retrieved context only. Do not invent products, categories, or stock values.
3852
+ - Aggregate raw product rows when the user asks for a chart.
3853
+ - For charts, every row in "data" must be flat JSON with primitive values only.
3854
+ - Prefer { "label": "...", "value": number } for pie charts.
3855
+ - If stock information exists, preserve it in each row with fields like "inStock", "inStockCount", "outOfStockCount", or "stockStatus".
3856
+ - If the user asks to highlight what is in stock, include those stock-related fields in the chart data and mention the pattern in "insights".
3857
+ - For carousel rows, include product-friendly fields such as id, name, brand, price, image, link, category, stockStatus.
3858
+ - For tables, keep "data" row-oriented and include "columns" when possible.
3859
+
3860
+ 4. FORMAT RULES
3861
+ - Never use ASCII art, markdown tables as the primary answer, or text-only fake charts when a UI block is requested.
3862
+ - Put any short natural-language explanation before or after the \`\`\`ui\`\`\` block, but keep it concise.
3863
+ - Return valid JSON inside the \`\`\`ui\`\`\` block.
3864
+
3865
+ 5. EXAMPLE
3866
+ User: "provide me the distribution of products across categories and highlight which ones are in stock in a pie chart"
3867
+ Assistant:
3868
+ \`\`\`ui
3869
+ {
3870
+ "view": "chart",
3871
+ "title": "Product Distribution Across Categories",
3872
+ "description": "Pie chart showing product count by category with stock signals preserved per slice.",
3873
+ "chartType": "pie",
3874
+ "xAxisKey": "label",
3875
+ "dataKeys": ["value"],
3876
+ "insights": [
3877
+ "Beauty has the highest product count.",
3878
+ "Electronics has the largest number of in-stock products."
3879
+ ],
3880
+ "data": [
3881
+ { "label": "Beauty", "value": 12, "inStockCount": 9, "outOfStockCount": 3 },
3882
+ { "label": "Electronics", "value": 8, "inStockCount": 7, "outOfStockCount": 1 },
3883
+ { "label": "Home", "value": 5, "inStockCount": 2, "outOfStockCount": 3 }
3884
+ ]
3885
+ }
3886
+ \`\`\``;
3842
3887
  if (!((_a = this.config.llm.systemPrompt) == null ? void 0 : _a.includes(CHART_MARKER))) {
3843
3888
  this.config.llm.systemPrompt = (this.config.llm.systemPrompt || "") + chartInstruction;
3844
3889
  }
@@ -9,7 +9,7 @@ import {
9
9
  sseFrame,
10
10
  sseMetaFrame,
11
11
  sseTextFrame
12
- } from "../chunk-BLLMNP77.mjs";
12
+ } from "../chunk-Q4MDH6C4.mjs";
13
13
  import "../chunk-YLTMFW4M.mjs";
14
14
  import "../chunk-X4TOT24V.mjs";
15
15
  export {
package/dist/index.js CHANGED
@@ -270,8 +270,58 @@ function DynamicChart({ config, primaryColor = "#6366f1", accentColor = "#8b5cf6
270
270
  }).slice(0, 5);
271
271
  const finalXKey = String(resolvedXKey);
272
272
  const finalDataKeys = resolvedDataKeys.map(String).filter((k) => k !== "undefined");
273
+ const pieDataKey = finalDataKeys[0] || "value";
274
+ const stockAwareColor = (entry, index) => {
275
+ var _a, _b, _c;
276
+ const stockStatus = String(
277
+ (_c = (_b = (_a = entry.stockStatus) != null ? _a : entry.status) != null ? _b : entry.availability) != null ? _c : ""
278
+ ).toLowerCase();
279
+ const inStockCount = typeof entry.inStockCount === "number" ? entry.inStockCount : void 0;
280
+ const outOfStockCount = typeof entry.outOfStockCount === "number" ? entry.outOfStockCount : void 0;
281
+ const inStockFlag = entry.inStock;
282
+ if (stockStatus.includes("in stock")) return "#10b981";
283
+ if (stockStatus.includes("out of stock")) return "#f97316";
284
+ if (typeof inStockFlag === "string" && inStockFlag.toLowerCase() === "true") return "#10b981";
285
+ if (inStockFlag === 1) return "#10b981";
286
+ if (inStockFlag === 0) return "#f97316";
287
+ if (typeof inStockCount === "number" || typeof outOfStockCount === "number") {
288
+ const inCount = inStockCount != null ? inStockCount : 0;
289
+ const outCount = outOfStockCount != null ? outOfStockCount : 0;
290
+ if (inCount > outCount) return "#10b981";
291
+ if (outCount > inCount) return "#f97316";
292
+ }
293
+ return colors[index % colors.length];
294
+ };
295
+ const tooltipLabelMap = {
296
+ value: "Value",
297
+ count: "Count",
298
+ inStock: "In stock",
299
+ inStockCount: "In stock",
300
+ outOfStockCount: "Out of stock",
301
+ stockStatus: "Stock status",
302
+ category: "Category",
303
+ label: "Label",
304
+ name: "Name"
305
+ };
306
+ const renderTooltip = (row, label) => {
307
+ var _a, _b;
308
+ if (!row) return null;
309
+ return /* @__PURE__ */ import_react4.default.createElement("div", { className: "rounded-xl border border-slate-200 bg-white px-3 py-2 text-xs shadow-xl dark:border-white/10 dark:bg-slate-950" }, /* @__PURE__ */ import_react4.default.createElement("p", { className: "mb-2 font-semibold text-slate-800 dark:text-white/90" }, String((_b = (_a = row[finalXKey]) != null ? _a : label) != null ? _b : "Details")), Object.entries(row).map(([key, value]) => {
310
+ var _a2;
311
+ if (value === null || value === void 0 || key === finalXKey) return null;
312
+ if (typeof value === "object") return null;
313
+ return /* @__PURE__ */ import_react4.default.createElement("div", { key, className: "flex items-center justify-between gap-3 text-slate-600 dark:text-white/70" }, /* @__PURE__ */ import_react4.default.createElement("span", null, (_a2 = tooltipLabelMap[key]) != null ? _a2 : key), /* @__PURE__ */ import_react4.default.createElement("span", { className: "font-medium text-slate-900 dark:text-white/90" }, String(value)));
314
+ }));
315
+ };
316
+ const getTooltipRow = (payload) => {
317
+ if (!Array.isArray(payload) || payload.length === 0) return null;
318
+ const first = payload[0];
319
+ if (!first || typeof first !== "object" || !("payload" in first)) return null;
320
+ const row = first.payload;
321
+ if (!row || typeof row !== "object" || Array.isArray(row)) return null;
322
+ return row;
323
+ };
273
324
  if (type === "pie") {
274
- const pieDataKey = finalDataKeys[0] || "value";
275
325
  return /* @__PURE__ */ import_react4.default.createElement("div", { className: "w-full h-72 min-h-[280px] mt-4 mb-2 select-none overflow-hidden" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.ResponsiveContainer, { width: "99%", height: "100%" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.PieChart, null, /* @__PURE__ */ import_react4.default.createElement(
276
326
  import_recharts.Pie,
277
327
  {
@@ -283,21 +333,10 @@ function DynamicChart({ config, primaryColor = "#6366f1", accentColor = "#8b5cf6
283
333
  outerRadius: 80,
284
334
  label: ({ name, percent }) => `${name} ${((percent != null ? percent : 0) * 100).toFixed(0)}%`
285
335
  },
286
- data.map((entry, index) => /* @__PURE__ */ import_react4.default.createElement(import_recharts.Cell, { key: `cell-${index}`, fill: colors[index % colors.length] }))
287
- ), /* @__PURE__ */ import_react4.default.createElement(
288
- import_recharts.Tooltip,
289
- {
290
- contentStyle: { borderRadius: "8px", border: "none", boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)" }
291
- }
292
- ), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Legend, null))));
336
+ sanitizedData.map((entry, index) => /* @__PURE__ */ import_react4.default.createElement(import_recharts.Cell, { key: `cell-${index}`, fill: stockAwareColor(entry, index) }))
337
+ ), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Tooltip, { content: (props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === "string" ? props.label : void 0) }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Legend, null))));
293
338
  }
294
- return /* @__PURE__ */ import_react4.default.createElement("div", { className: "w-full h-72 min-h-[280px] mt-4 mb-2 select-none overflow-hidden" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.ResponsiveContainer, { width: "99%", height: "100%" }, type === "bar" ? /* @__PURE__ */ import_react4.default.createElement(import_recharts.BarChart, { data: sanitizedData, margin: { top: 10, right: 10, left: -20, bottom: 0 } }, /* @__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: { fontSize: 12, fill: "#64748b" }, tickLine: false, axisLine: { stroke: "#cbd5e1" } }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.YAxis, { tick: { fontSize: 12, fill: "#64748b" }, tickLine: false, axisLine: false }), /* @__PURE__ */ import_react4.default.createElement(
295
- import_recharts.Tooltip,
296
- {
297
- contentStyle: { borderRadius: "8px", border: "none", boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)" },
298
- cursor: { fill: "#f1f5f9" }
299
- }
300
- ), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Legend, { wrapperStyle: { fontSize: 12 } }), finalDataKeys.map((key, index) => /* @__PURE__ */ import_react4.default.createElement(
339
+ return /* @__PURE__ */ import_react4.default.createElement("div", { className: "w-full h-72 min-h-[280px] mt-4 mb-2 select-none overflow-hidden" }, /* @__PURE__ */ import_react4.default.createElement(import_recharts.ResponsiveContainer, { width: "99%", height: "100%" }, type === "bar" ? /* @__PURE__ */ import_react4.default.createElement(import_recharts.BarChart, { data: sanitizedData, margin: { top: 10, right: 10, left: -20, bottom: 0 } }, /* @__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: { fontSize: 12, fill: "#64748b" }, tickLine: false, axisLine: { stroke: "#cbd5e1" } }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.YAxis, { tick: { fontSize: 12, fill: "#64748b" }, 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" } }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Legend, { wrapperStyle: { fontSize: 12 } }), finalDataKeys.map((key, index) => /* @__PURE__ */ import_react4.default.createElement(
301
340
  import_recharts.Bar,
302
341
  {
303
342
  key,
@@ -306,12 +345,7 @@ function DynamicChart({ config, primaryColor = "#6366f1", accentColor = "#8b5cf6
306
345
  radius: [4, 4, 0, 0],
307
346
  barSize: Math.max(20, 60 / data.length)
308
347
  }
309
- ))) : /* @__PURE__ */ import_react4.default.createElement(import_recharts.LineChart, { data: sanitizedData, margin: { top: 10, right: 10, left: -20, bottom: 0 } }, /* @__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: { fontSize: 12, fill: "#64748b" }, tickLine: false, axisLine: { stroke: "#cbd5e1" } }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.YAxis, { tick: { fontSize: 12, fill: "#64748b" }, tickLine: false, axisLine: false }), /* @__PURE__ */ import_react4.default.createElement(
310
- import_recharts.Tooltip,
311
- {
312
- contentStyle: { borderRadius: "8px", border: "none", boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)" }
313
- }
314
- ), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Legend, { wrapperStyle: { fontSize: 12 } }), finalDataKeys.map((key, index) => /* @__PURE__ */ import_react4.default.createElement(
348
+ ))) : /* @__PURE__ */ import_react4.default.createElement(import_recharts.LineChart, { data: sanitizedData, margin: { top: 10, right: 10, left: -20, bottom: 0 } }, /* @__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: { fontSize: 12, fill: "#64748b" }, tickLine: false, axisLine: { stroke: "#cbd5e1" } }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.YAxis, { tick: { fontSize: 12, fill: "#64748b" }, 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) }), /* @__PURE__ */ import_react4.default.createElement(import_recharts.Legend, { wrapperStyle: { fontSize: 12 } }), finalDataKeys.map((key, index) => /* @__PURE__ */ import_react4.default.createElement(
315
349
  import_recharts.Line,
316
350
  {
317
351
  key,
@@ -440,7 +474,8 @@ function DataTable({ config }) {
440
474
  ))
441
475
  ))))), /* @__PURE__ */ import_react5.default.createElement("div", { className: "px-4 py-2 bg-slate-50 dark:bg-white/5 border-t border-slate-100 dark:border-white/5" }, /* @__PURE__ */ import_react5.default.createElement("p", { className: "text-[11px] text-slate-400 dark:text-white/30" }, config.data.length, " row", config.data.length !== 1 ? "s" : "")));
442
476
  }
443
- function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming }) {
477
+ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAddToCart }) {
478
+ var _a;
444
479
  const result = import_react5.default.useMemo(() => {
445
480
  if (isStreaming) return { loading: true };
446
481
  try {
@@ -472,28 +507,48 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming }) {
472
507
  return /* @__PURE__ */ import_react5.default.createElement("pre", { className: "p-4 my-2 bg-slate-900 text-slate-100 rounded-lg text-[10px] overflow-auto max-h-40" }, /* @__PURE__ */ import_react5.default.createElement("code", null, rawContent));
473
508
  }
474
509
  const { config } = result;
510
+ const hasInsights = Array.isArray(config.insights) && config.insights.length > 0;
511
+ const hasDescription = typeof config.description === "string" && config.description.trim().length > 0;
475
512
  switch (config.view) {
476
513
  case "chart":
477
- return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-6 p-4 bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-white/10 shadow-sm overflow-hidden" }, config.title && /* @__PURE__ */ import_react5.default.createElement("h4", { className: "text-xs font-semibold text-slate-500 mb-4 px-2" }, config.title), /* @__PURE__ */ import_react5.default.createElement(
514
+ return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-6 p-4 bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-white/10 shadow-sm overflow-hidden" }, config.title && /* @__PURE__ */ import_react5.default.createElement("h4", { className: "text-xs font-semibold text-slate-500 mb-4 px-2" }, config.title), hasDescription && /* @__PURE__ */ import_react5.default.createElement("p", { className: "px-2 text-sm text-slate-500 dark:text-white/60" }, config.description), /* @__PURE__ */ import_react5.default.createElement(
478
515
  DynamicChart,
479
516
  {
480
517
  config: {
481
518
  type: config.chartType || "bar",
482
- data: config.data
519
+ data: config.data,
520
+ xAxisKey: config.xAxisKey,
521
+ dataKeys: config.dataKeys,
522
+ colors: config.colors
483
523
  },
484
524
  primaryColor,
485
525
  accentColor
486
526
  }
487
- ));
527
+ ), hasInsights && /* @__PURE__ */ import_react5.default.createElement("div", { className: "mt-4 flex flex-wrap gap-2 px-2" }, (_a = config.insights) == null ? void 0 : _a.map((insight) => /* @__PURE__ */ import_react5.default.createElement(
528
+ "span",
529
+ {
530
+ key: insight,
531
+ className: "rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-[11px] font-medium text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-300"
532
+ },
533
+ insight
534
+ ))));
488
535
  case "carousel":
489
- return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-4" }, /* @__PURE__ */ import_react5.default.createElement(ProductCarousel, { products: config.data.map((item) => {
490
- var _a;
536
+ return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-4" }, config.title && /* @__PURE__ */ import_react5.default.createElement("h4", { className: "mb-2 text-xs font-semibold text-slate-500" }, config.title), hasDescription && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mb-3 text-sm text-slate-500 dark:text-white/60" }, config.description), /* @__PURE__ */ import_react5.default.createElement(ProductCarousel, { products: config.data.map((item) => {
537
+ var _a2;
491
538
  return __spreadProps(__spreadValues({}, item), {
492
- image: (_a = item.image) != null ? _a : typeof resolveImage === "function" ? resolveImage(item) : void 0
539
+ image: (_a2 = item.image) != null ? _a2 : typeof resolveImage === "function" ? resolveImage(item) : void 0
493
540
  });
494
- }) }));
541
+ }), primaryColor, onAddToCart }));
495
542
  case "table":
496
- return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm overflow-auto max-h-[400px]" }, config.title && /* @__PURE__ */ import_react5.default.createElement("h4", { className: "text-xs font-semibold text-slate-500 mb-2" }, config.title), /* @__PURE__ */ import_react5.default.createElement(DataTable, { config: { type: "table", data: config.data } }));
543
+ return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm overflow-auto max-h-[400px]" }, config.title && /* @__PURE__ */ import_react5.default.createElement("h4", { className: "text-xs font-semibold text-slate-500 mb-2" }, config.title), hasDescription && /* @__PURE__ */ import_react5.default.createElement("p", { className: "mb-3 text-sm text-slate-500 dark:text-white/60" }, config.description), /* @__PURE__ */ import_react5.default.createElement(DataTable, { config: {
544
+ type: "table",
545
+ title: config.title,
546
+ description: config.description,
547
+ columns: config.columns,
548
+ dataKeys: config.dataKeys,
549
+ xAxisKey: config.xAxisKey,
550
+ data: config.data
551
+ } }));
497
552
  default:
498
553
  return null;
499
554
  }
@@ -508,6 +563,10 @@ function MessageBubble({
508
563
  }) {
509
564
  const isUser = message.role === "user";
510
565
  const [showSources, setShowSources] = import_react5.default.useState(false);
566
+ const hasStructuredProductBlock = import_react5.default.useMemo(
567
+ () => /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|type)"\s*:\s*"(?:carousel|products)"/i.test(message.content),
568
+ [message.content]
569
+ );
511
570
  const productsFromSources = import_react5.default.useMemo(() => {
512
571
  if (isUser || !sources) return [];
513
572
  return sources.filter((s) => {
@@ -678,7 +737,8 @@ ${match.trim()}
678
737
  rawContent: String(children != null ? children : "").trim(),
679
738
  primaryColor,
680
739
  accentColor,
681
- isStreaming
740
+ isStreaming,
741
+ onAddToCart
682
742
  }
683
743
  );
684
744
  }
@@ -691,7 +751,8 @@ ${match.trim()}
691
751
  rawContent: content,
692
752
  primaryColor,
693
753
  accentColor,
694
- isStreaming
754
+ isStreaming,
755
+ onAddToCart
695
756
  }
696
757
  );
697
758
  }
@@ -702,7 +763,7 @@ ${match.trim()}
702
763
  return /* @__PURE__ */ import_react5.default.createElement("code", __spreadValues({ className }, props), typeof children === "string" || typeof children === "number" ? children : JSON.stringify(children));
703
764
  }
704
765
  }),
705
- [primaryColor, accentColor, isStreaming]
766
+ [primaryColor, accentColor, isStreaming, onAddToCart]
706
767
  );
707
768
  return /* @__PURE__ */ import_react5.default.createElement("div", { className: `flex gap-3 ${isUser ? "flex-row-reverse" : "flex-row"} items-start` }, /* @__PURE__ */ import_react5.default.createElement(
708
769
  "div",
@@ -718,7 +779,7 @@ ${match.trim()}
718
779
  style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
719
780
  },
720
781
  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 prose-sm max-w-none ${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" }))
721
- ), !isUser && allProducts.length > 0 && /* @__PURE__ */ import_react5.default.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ import_react5.default.createElement(
782
+ ), !isUser && !hasStructuredProductBlock && allProducts.length > 0 && /* @__PURE__ */ import_react5.default.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ import_react5.default.createElement(
722
783
  ProductCarousel,
723
784
  {
724
785
  products: allProducts,
package/dist/index.mjs CHANGED
@@ -233,8 +233,58 @@ function DynamicChart({ config, primaryColor = "#6366f1", accentColor = "#8b5cf6
233
233
  }).slice(0, 5);
234
234
  const finalXKey = String(resolvedXKey);
235
235
  const finalDataKeys = resolvedDataKeys.map(String).filter((k) => k !== "undefined");
236
+ const pieDataKey = finalDataKeys[0] || "value";
237
+ const stockAwareColor = (entry, index) => {
238
+ var _a, _b, _c;
239
+ const stockStatus = String(
240
+ (_c = (_b = (_a = entry.stockStatus) != null ? _a : entry.status) != null ? _b : entry.availability) != null ? _c : ""
241
+ ).toLowerCase();
242
+ const inStockCount = typeof entry.inStockCount === "number" ? entry.inStockCount : void 0;
243
+ const outOfStockCount = typeof entry.outOfStockCount === "number" ? entry.outOfStockCount : void 0;
244
+ const inStockFlag = entry.inStock;
245
+ if (stockStatus.includes("in stock")) return "#10b981";
246
+ if (stockStatus.includes("out of stock")) return "#f97316";
247
+ if (typeof inStockFlag === "string" && inStockFlag.toLowerCase() === "true") return "#10b981";
248
+ if (inStockFlag === 1) return "#10b981";
249
+ if (inStockFlag === 0) return "#f97316";
250
+ if (typeof inStockCount === "number" || typeof outOfStockCount === "number") {
251
+ const inCount = inStockCount != null ? inStockCount : 0;
252
+ const outCount = outOfStockCount != null ? outOfStockCount : 0;
253
+ if (inCount > outCount) return "#10b981";
254
+ if (outCount > inCount) return "#f97316";
255
+ }
256
+ return colors[index % colors.length];
257
+ };
258
+ const tooltipLabelMap = {
259
+ value: "Value",
260
+ count: "Count",
261
+ inStock: "In stock",
262
+ inStockCount: "In stock",
263
+ outOfStockCount: "Out of stock",
264
+ stockStatus: "Stock status",
265
+ category: "Category",
266
+ label: "Label",
267
+ name: "Name"
268
+ };
269
+ const renderTooltip = (row, label) => {
270
+ var _a, _b;
271
+ if (!row) return null;
272
+ return /* @__PURE__ */ React4.createElement("div", { className: "rounded-xl border border-slate-200 bg-white px-3 py-2 text-xs shadow-xl dark:border-white/10 dark:bg-slate-950" }, /* @__PURE__ */ React4.createElement("p", { className: "mb-2 font-semibold text-slate-800 dark:text-white/90" }, String((_b = (_a = row[finalXKey]) != null ? _a : label) != null ? _b : "Details")), Object.entries(row).map(([key, value]) => {
273
+ var _a2;
274
+ if (value === null || value === void 0 || key === finalXKey) return null;
275
+ if (typeof value === "object") return null;
276
+ return /* @__PURE__ */ React4.createElement("div", { key, className: "flex items-center justify-between gap-3 text-slate-600 dark:text-white/70" }, /* @__PURE__ */ React4.createElement("span", null, (_a2 = tooltipLabelMap[key]) != null ? _a2 : key), /* @__PURE__ */ React4.createElement("span", { className: "font-medium text-slate-900 dark:text-white/90" }, String(value)));
277
+ }));
278
+ };
279
+ const getTooltipRow = (payload) => {
280
+ if (!Array.isArray(payload) || payload.length === 0) return null;
281
+ const first = payload[0];
282
+ if (!first || typeof first !== "object" || !("payload" in first)) return null;
283
+ const row = first.payload;
284
+ if (!row || typeof row !== "object" || Array.isArray(row)) return null;
285
+ return row;
286
+ };
236
287
  if (type === "pie") {
237
- const pieDataKey = finalDataKeys[0] || "value";
238
288
  return /* @__PURE__ */ React4.createElement("div", { className: "w-full h-72 min-h-[280px] mt-4 mb-2 select-none overflow-hidden" }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "99%", height: "100%" }, /* @__PURE__ */ React4.createElement(PieChart, null, /* @__PURE__ */ React4.createElement(
239
289
  Pie,
240
290
  {
@@ -246,21 +296,10 @@ function DynamicChart({ config, primaryColor = "#6366f1", accentColor = "#8b5cf6
246
296
  outerRadius: 80,
247
297
  label: ({ name, percent }) => `${name} ${((percent != null ? percent : 0) * 100).toFixed(0)}%`
248
298
  },
249
- data.map((entry, index) => /* @__PURE__ */ React4.createElement(Cell, { key: `cell-${index}`, fill: colors[index % colors.length] }))
250
- ), /* @__PURE__ */ React4.createElement(
251
- Tooltip,
252
- {
253
- contentStyle: { borderRadius: "8px", border: "none", boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)" }
254
- }
255
- ), /* @__PURE__ */ React4.createElement(Legend, null))));
299
+ sanitizedData.map((entry, index) => /* @__PURE__ */ React4.createElement(Cell, { key: `cell-${index}`, fill: stockAwareColor(entry, index) }))
300
+ ), /* @__PURE__ */ React4.createElement(Tooltip, { content: (props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === "string" ? props.label : void 0) }), /* @__PURE__ */ React4.createElement(Legend, null))));
256
301
  }
257
- return /* @__PURE__ */ React4.createElement("div", { className: "w-full h-72 min-h-[280px] mt-4 mb-2 select-none overflow-hidden" }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "99%", height: "100%" }, type === "bar" ? /* @__PURE__ */ React4.createElement(BarChart, { data: sanitizedData, margin: { top: 10, right: 10, left: -20, bottom: 0 } }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", vertical: false, stroke: "#e2e8f0" }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: finalXKey, tick: { fontSize: 12, fill: "#64748b" }, tickLine: false, axisLine: { stroke: "#cbd5e1" } }), /* @__PURE__ */ React4.createElement(YAxis, { tick: { fontSize: 12, fill: "#64748b" }, tickLine: false, axisLine: false }), /* @__PURE__ */ React4.createElement(
258
- Tooltip,
259
- {
260
- contentStyle: { borderRadius: "8px", border: "none", boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)" },
261
- cursor: { fill: "#f1f5f9" }
262
- }
263
- ), /* @__PURE__ */ React4.createElement(Legend, { wrapperStyle: { fontSize: 12 } }), finalDataKeys.map((key, index) => /* @__PURE__ */ React4.createElement(
302
+ return /* @__PURE__ */ React4.createElement("div", { className: "w-full h-72 min-h-[280px] mt-4 mb-2 select-none overflow-hidden" }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "99%", height: "100%" }, type === "bar" ? /* @__PURE__ */ React4.createElement(BarChart, { data: sanitizedData, margin: { top: 10, right: 10, left: -20, bottom: 0 } }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", vertical: false, stroke: "#e2e8f0" }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: finalXKey, tick: { fontSize: 12, fill: "#64748b" }, tickLine: false, axisLine: { stroke: "#cbd5e1" } }), /* @__PURE__ */ React4.createElement(YAxis, { tick: { fontSize: 12, fill: "#64748b" }, 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" } }), /* @__PURE__ */ React4.createElement(Legend, { wrapperStyle: { fontSize: 12 } }), finalDataKeys.map((key, index) => /* @__PURE__ */ React4.createElement(
264
303
  Bar,
265
304
  {
266
305
  key,
@@ -269,12 +308,7 @@ function DynamicChart({ config, primaryColor = "#6366f1", accentColor = "#8b5cf6
269
308
  radius: [4, 4, 0, 0],
270
309
  barSize: Math.max(20, 60 / data.length)
271
310
  }
272
- ))) : /* @__PURE__ */ React4.createElement(LineChart, { data: sanitizedData, margin: { top: 10, right: 10, left: -20, bottom: 0 } }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", vertical: false, stroke: "#e2e8f0" }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: finalXKey, tick: { fontSize: 12, fill: "#64748b" }, tickLine: false, axisLine: { stroke: "#cbd5e1" } }), /* @__PURE__ */ React4.createElement(YAxis, { tick: { fontSize: 12, fill: "#64748b" }, tickLine: false, axisLine: false }), /* @__PURE__ */ React4.createElement(
273
- Tooltip,
274
- {
275
- contentStyle: { borderRadius: "8px", border: "none", boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)" }
276
- }
277
- ), /* @__PURE__ */ React4.createElement(Legend, { wrapperStyle: { fontSize: 12 } }), finalDataKeys.map((key, index) => /* @__PURE__ */ React4.createElement(
311
+ ))) : /* @__PURE__ */ React4.createElement(LineChart, { data: sanitizedData, margin: { top: 10, right: 10, left: -20, bottom: 0 } }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", vertical: false, stroke: "#e2e8f0" }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: finalXKey, tick: { fontSize: 12, fill: "#64748b" }, tickLine: false, axisLine: { stroke: "#cbd5e1" } }), /* @__PURE__ */ React4.createElement(YAxis, { tick: { fontSize: 12, fill: "#64748b" }, tickLine: false, axisLine: false }), /* @__PURE__ */ React4.createElement(Tooltip, { content: (props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === "string" ? props.label : void 0) }), /* @__PURE__ */ React4.createElement(Legend, { wrapperStyle: { fontSize: 12 } }), finalDataKeys.map((key, index) => /* @__PURE__ */ React4.createElement(
278
312
  Line,
279
313
  {
280
314
  key,
@@ -403,7 +437,8 @@ function DataTable({ config }) {
403
437
  ))
404
438
  ))))), /* @__PURE__ */ React5.createElement("div", { className: "px-4 py-2 bg-slate-50 dark:bg-white/5 border-t border-slate-100 dark:border-white/5" }, /* @__PURE__ */ React5.createElement("p", { className: "text-[11px] text-slate-400 dark:text-white/30" }, config.data.length, " row", config.data.length !== 1 ? "s" : "")));
405
439
  }
406
- function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming }) {
440
+ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAddToCart }) {
441
+ var _a;
407
442
  const result = React5.useMemo(() => {
408
443
  if (isStreaming) return { loading: true };
409
444
  try {
@@ -435,28 +470,48 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming }) {
435
470
  return /* @__PURE__ */ React5.createElement("pre", { className: "p-4 my-2 bg-slate-900 text-slate-100 rounded-lg text-[10px] overflow-auto max-h-40" }, /* @__PURE__ */ React5.createElement("code", null, rawContent));
436
471
  }
437
472
  const { config } = result;
473
+ const hasInsights = Array.isArray(config.insights) && config.insights.length > 0;
474
+ const hasDescription = typeof config.description === "string" && config.description.trim().length > 0;
438
475
  switch (config.view) {
439
476
  case "chart":
440
- return /* @__PURE__ */ React5.createElement("div", { className: "my-6 p-4 bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-white/10 shadow-sm overflow-hidden" }, config.title && /* @__PURE__ */ React5.createElement("h4", { className: "text-xs font-semibold text-slate-500 mb-4 px-2" }, config.title), /* @__PURE__ */ React5.createElement(
477
+ return /* @__PURE__ */ React5.createElement("div", { className: "my-6 p-4 bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-white/10 shadow-sm overflow-hidden" }, config.title && /* @__PURE__ */ React5.createElement("h4", { className: "text-xs font-semibold text-slate-500 mb-4 px-2" }, config.title), hasDescription && /* @__PURE__ */ React5.createElement("p", { className: "px-2 text-sm text-slate-500 dark:text-white/60" }, config.description), /* @__PURE__ */ React5.createElement(
441
478
  DynamicChart,
442
479
  {
443
480
  config: {
444
481
  type: config.chartType || "bar",
445
- data: config.data
482
+ data: config.data,
483
+ xAxisKey: config.xAxisKey,
484
+ dataKeys: config.dataKeys,
485
+ colors: config.colors
446
486
  },
447
487
  primaryColor,
448
488
  accentColor
449
489
  }
450
- ));
490
+ ), hasInsights && /* @__PURE__ */ React5.createElement("div", { className: "mt-4 flex flex-wrap gap-2 px-2" }, (_a = config.insights) == null ? void 0 : _a.map((insight) => /* @__PURE__ */ React5.createElement(
491
+ "span",
492
+ {
493
+ key: insight,
494
+ className: "rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-[11px] font-medium text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-300"
495
+ },
496
+ insight
497
+ ))));
451
498
  case "carousel":
452
- return /* @__PURE__ */ React5.createElement("div", { className: "my-4" }, /* @__PURE__ */ React5.createElement(ProductCarousel, { products: config.data.map((item) => {
453
- var _a;
499
+ return /* @__PURE__ */ React5.createElement("div", { className: "my-4" }, config.title && /* @__PURE__ */ React5.createElement("h4", { className: "mb-2 text-xs font-semibold text-slate-500" }, config.title), hasDescription && /* @__PURE__ */ React5.createElement("p", { className: "mb-3 text-sm text-slate-500 dark:text-white/60" }, config.description), /* @__PURE__ */ React5.createElement(ProductCarousel, { products: config.data.map((item) => {
500
+ var _a2;
454
501
  return __spreadProps(__spreadValues({}, item), {
455
- image: (_a = item.image) != null ? _a : typeof resolveImage === "function" ? resolveImage(item) : void 0
502
+ image: (_a2 = item.image) != null ? _a2 : typeof resolveImage === "function" ? resolveImage(item) : void 0
456
503
  });
457
- }) }));
504
+ }), primaryColor, onAddToCart }));
458
505
  case "table":
459
- return /* @__PURE__ */ React5.createElement("div", { className: "my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm overflow-auto max-h-[400px]" }, config.title && /* @__PURE__ */ React5.createElement("h4", { className: "text-xs font-semibold text-slate-500 mb-2" }, config.title), /* @__PURE__ */ React5.createElement(DataTable, { config: { type: "table", data: config.data } }));
506
+ return /* @__PURE__ */ React5.createElement("div", { className: "my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm overflow-auto max-h-[400px]" }, config.title && /* @__PURE__ */ React5.createElement("h4", { className: "text-xs font-semibold text-slate-500 mb-2" }, config.title), hasDescription && /* @__PURE__ */ React5.createElement("p", { className: "mb-3 text-sm text-slate-500 dark:text-white/60" }, config.description), /* @__PURE__ */ React5.createElement(DataTable, { config: {
507
+ type: "table",
508
+ title: config.title,
509
+ description: config.description,
510
+ columns: config.columns,
511
+ dataKeys: config.dataKeys,
512
+ xAxisKey: config.xAxisKey,
513
+ data: config.data
514
+ } }));
460
515
  default:
461
516
  return null;
462
517
  }
@@ -471,6 +526,10 @@ function MessageBubble({
471
526
  }) {
472
527
  const isUser = message.role === "user";
473
528
  const [showSources, setShowSources] = React5.useState(false);
529
+ const hasStructuredProductBlock = React5.useMemo(
530
+ () => /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|type)"\s*:\s*"(?:carousel|products)"/i.test(message.content),
531
+ [message.content]
532
+ );
474
533
  const productsFromSources = React5.useMemo(() => {
475
534
  if (isUser || !sources) return [];
476
535
  return sources.filter((s) => {
@@ -641,7 +700,8 @@ ${match.trim()}
641
700
  rawContent: String(children != null ? children : "").trim(),
642
701
  primaryColor,
643
702
  accentColor,
644
- isStreaming
703
+ isStreaming,
704
+ onAddToCart
645
705
  }
646
706
  );
647
707
  }
@@ -654,7 +714,8 @@ ${match.trim()}
654
714
  rawContent: content,
655
715
  primaryColor,
656
716
  accentColor,
657
- isStreaming
717
+ isStreaming,
718
+ onAddToCart
658
719
  }
659
720
  );
660
721
  }
@@ -665,7 +726,7 @@ ${match.trim()}
665
726
  return /* @__PURE__ */ React5.createElement("code", __spreadValues({ className }, props), typeof children === "string" || typeof children === "number" ? children : JSON.stringify(children));
666
727
  }
667
728
  }),
668
- [primaryColor, accentColor, isStreaming]
729
+ [primaryColor, accentColor, isStreaming, onAddToCart]
669
730
  );
670
731
  return /* @__PURE__ */ React5.createElement("div", { className: `flex gap-3 ${isUser ? "flex-row-reverse" : "flex-row"} items-start` }, /* @__PURE__ */ React5.createElement(
671
732
  "div",
@@ -681,7 +742,7 @@ ${match.trim()}
681
742
  style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
682
743
  },
683
744
  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 prose-sm max-w-none ${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" }))
684
- ), !isUser && allProducts.length > 0 && /* @__PURE__ */ React5.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ React5.createElement(
745
+ ), !isUser && !hasStructuredProductBlock && allProducts.length > 0 && /* @__PURE__ */ React5.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ React5.createElement(
685
746
  ProductCarousel,
686
747
  {
687
748
  products: allProducts,
package/dist/server.js CHANGED
@@ -3913,23 +3913,68 @@ var Pipeline = class {
3913
3913
  async initialize() {
3914
3914
  var _a, _b;
3915
3915
  if (this.initialised) return;
3916
- const CHART_MARKER = "<!-- UI_PROTOCOL_V3 -->";
3916
+ const CHART_MARKER = "<!-- UI_PROTOCOL_V5 -->";
3917
3917
  const chartInstruction = `
3918
3918
 
3919
3919
  ${CHART_MARKER}
3920
- ### UNIVERSAL UI PROTOCOL:
3921
- You have a specialized UI system. For any structured data (charts, product lists, or tables), you MUST use a single \`\`\`ui\`\`\` block.
3922
- 1. DECIDE the best "view" for the user's query:
3923
- - "chart": For statistics, distributions, or comparisons. (Requires "chartType": "pie" | "bar" | "line")
3924
- - "carousel": For browsing specific product items.
3925
- - "table": For detailed raw data comparisons.
3926
- 2. OUTPUT a \`\`\`ui\`\`\` block with this schema:
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.
3922
+
3923
+ 1. VIEW SELECTION
3924
+ - Use "chart" for distributions, percentages, counts, comparisons, trends, or "show me in a chart".
3925
+ - Use "carousel" for browseable product recommendations or when the user asks to see products/items/cards.
3926
+ - Use "table" for tabular/raw detail, explicit table/list requests, or when there are many attributes to compare.
3927
+
3928
+ 2. REQUIRED JSON SHAPE
3927
3929
  {
3928
3930
  "view": "chart" | "carousel" | "table",
3929
- "chartType": "pie" | "bar" | "line" (only for view: "chart"),
3930
- "data": [ ... objects ... ]
3931
+ "title": "Short heading",
3932
+ "description": "One sentence describing the view",
3933
+ "chartType": "pie" | "bar" | "line",
3934
+ "xAxisKey": "label",
3935
+ "dataKeys": ["value"],
3936
+ "columns": ["name", "category", "price", "stockStatus"],
3937
+ "insights": ["Short takeaway 1", "Short takeaway 2"],
3938
+ "data": []
3931
3939
  }
3932
- 3. RULES: Valid JSON only. Data must be FLAT. Never draw charts/tables with text. Choose ONLY the most useful view.`;
3940
+
3941
+ 3. DATA RULES
3942
+ - Build the UI payload from retrieved context only. Do not invent products, categories, or stock values.
3943
+ - Aggregate raw product rows when the user asks for a chart.
3944
+ - For charts, every row in "data" must be flat JSON with primitive values only.
3945
+ - Prefer { "label": "...", "value": number } for pie charts.
3946
+ - If stock information exists, preserve it in each row with fields like "inStock", "inStockCount", "outOfStockCount", or "stockStatus".
3947
+ - If the user asks to highlight what is in stock, include those stock-related fields in the chart data and mention the pattern in "insights".
3948
+ - For carousel rows, include product-friendly fields such as id, name, brand, price, image, link, category, stockStatus.
3949
+ - For tables, keep "data" row-oriented and include "columns" when possible.
3950
+
3951
+ 4. FORMAT RULES
3952
+ - Never use ASCII art, markdown tables as the primary answer, or text-only fake charts when a UI block is requested.
3953
+ - Put any short natural-language explanation before or after the \`\`\`ui\`\`\` block, but keep it concise.
3954
+ - Return valid JSON inside the \`\`\`ui\`\`\` block.
3955
+
3956
+ 5. EXAMPLE
3957
+ User: "provide me the distribution of products across categories and highlight which ones are in stock in a pie chart"
3958
+ Assistant:
3959
+ \`\`\`ui
3960
+ {
3961
+ "view": "chart",
3962
+ "title": "Product Distribution Across Categories",
3963
+ "description": "Pie chart showing product count by category with stock signals preserved per slice.",
3964
+ "chartType": "pie",
3965
+ "xAxisKey": "label",
3966
+ "dataKeys": ["value"],
3967
+ "insights": [
3968
+ "Beauty has the highest product count.",
3969
+ "Electronics has the largest number of in-stock products."
3970
+ ],
3971
+ "data": [
3972
+ { "label": "Beauty", "value": 12, "inStockCount": 9, "outOfStockCount": 3 },
3973
+ { "label": "Electronics", "value": 8, "inStockCount": 7, "outOfStockCount": 1 },
3974
+ { "label": "Home", "value": 5, "inStockCount": 2, "outOfStockCount": 3 }
3975
+ ]
3976
+ }
3977
+ \`\`\``;
3933
3978
  if (!((_a = this.config.llm.systemPrompt) == null ? void 0 : _a.includes(CHART_MARKER))) {
3934
3979
  this.config.llm.systemPrompt = (this.config.llm.systemPrompt || "") + chartInstruction;
3935
3980
  }
package/dist/server.mjs CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  sseFrame,
40
40
  sseMetaFrame,
41
41
  sseTextFrame
42
- } from "./chunk-BLLMNP77.mjs";
42
+ } from "./chunk-Q4MDH6C4.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.5.4",
3
+ "version": "1.5.5",
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",
@@ -1,4 +1,4 @@
1
- import { createChatHandler } from '@/handlers';
1
+ import { createStreamHandler } from '@/handlers';
2
2
  import { getRagConfig } from '@/config/serverConfig';
3
3
 
4
- export const POST = createChatHandler(getRagConfig());
4
+ export const POST = createStreamHandler(getRagConfig());
@@ -96,11 +96,85 @@ export function DynamicChart({ config, primaryColor = '#6366f1', accentColor = '
96
96
  // Final sanity check: ensure all keys are strings
97
97
  const finalXKey = String(resolvedXKey);
98
98
  const finalDataKeys = resolvedDataKeys.map(String).filter(k => k !== 'undefined');
99
+ const pieDataKey = finalDataKeys[0] || 'value';
100
+
101
+ const stockAwareColor = (entry: Record<string, string | number | null>, index: number) => {
102
+ const stockStatus = String(
103
+ entry.stockStatus ??
104
+ entry.status ??
105
+ entry.availability ??
106
+ '',
107
+ ).toLowerCase();
108
+ const inStockCount = typeof entry.inStockCount === 'number' ? entry.inStockCount : undefined;
109
+ const outOfStockCount = typeof entry.outOfStockCount === 'number' ? entry.outOfStockCount : undefined;
110
+ const inStockFlag = entry.inStock;
111
+
112
+ if (stockStatus.includes('in stock')) return '#10b981';
113
+ if (stockStatus.includes('out of stock')) return '#f97316';
114
+ if (typeof inStockFlag === 'string' && inStockFlag.toLowerCase() === 'true') return '#10b981';
115
+ if (inStockFlag === 1) return '#10b981';
116
+ if (inStockFlag === 0) return '#f97316';
117
+ if (typeof inStockCount === 'number' || typeof outOfStockCount === 'number') {
118
+ const inCount = inStockCount ?? 0;
119
+ const outCount = outOfStockCount ?? 0;
120
+ if (inCount > outCount) return '#10b981';
121
+ if (outCount > inCount) return '#f97316';
122
+ }
123
+
124
+ return colors[index % colors.length];
125
+ };
126
+
127
+ const tooltipLabelMap: Record<string, string> = {
128
+ value: 'Value',
129
+ count: 'Count',
130
+ inStock: 'In stock',
131
+ inStockCount: 'In stock',
132
+ outOfStockCount: 'Out of stock',
133
+ stockStatus: 'Stock status',
134
+ category: 'Category',
135
+ label: 'Label',
136
+ name: 'Name',
137
+ };
138
+
139
+ const renderTooltip = (
140
+ row: Record<string, string | number | null> | null,
141
+ label?: string,
142
+ ) => {
143
+ if (!row) return null;
144
+
145
+ return (
146
+ <div className="rounded-xl border border-slate-200 bg-white px-3 py-2 text-xs shadow-xl dark:border-white/10 dark:bg-slate-950">
147
+ <p className="mb-2 font-semibold text-slate-800 dark:text-white/90">
148
+ {String(row[finalXKey] ?? label ?? 'Details')}
149
+ </p>
150
+ {Object.entries(row).map(([key, value]) => {
151
+ if (value === null || value === undefined || key === finalXKey) return null;
152
+ if (typeof value === 'object') return null;
153
+
154
+ return (
155
+ <div key={key} className="flex items-center justify-between gap-3 text-slate-600 dark:text-white/70">
156
+ <span>{tooltipLabelMap[key] ?? key}</span>
157
+ <span className="font-medium text-slate-900 dark:text-white/90">{String(value)}</span>
158
+ </div>
159
+ );
160
+ })}
161
+ </div>
162
+ );
163
+ };
164
+
165
+ const getTooltipRow = (payload: unknown): Record<string, string | number | null> | null => {
166
+ if (!Array.isArray(payload) || payload.length === 0) return null;
167
+ const first = payload[0];
168
+ if (!first || typeof first !== 'object' || !('payload' in first)) return null;
169
+
170
+ const row = (first as { payload?: unknown }).payload;
171
+ if (!row || typeof row !== 'object' || Array.isArray(row)) return null;
172
+
173
+ return row as Record<string, string | number | null>;
174
+ };
99
175
 
100
176
  // Handle Pie Chart separately as it has a different structure
101
177
  if (type === 'pie') {
102
- const pieDataKey = finalDataKeys[0] || 'value';
103
-
104
178
  return (
105
179
  <div className="w-full h-72 min-h-[280px] mt-4 mb-2 select-none overflow-hidden">
106
180
  <ResponsiveContainer width="99%" height="100%">
@@ -114,13 +188,11 @@ export function DynamicChart({ config, primaryColor = '#6366f1', accentColor = '
114
188
  outerRadius={80}
115
189
  label={({ name, percent }) => `${name} ${((percent ?? 0) * 100).toFixed(0)}%`}
116
190
  >
117
- {data.map((entry, index) => (
118
- <Cell key={`cell-${index}`} fill={colors[index % colors.length]} />
191
+ {sanitizedData.map((entry, index) => (
192
+ <Cell key={`cell-${index}`} fill={stockAwareColor(entry, index)} />
119
193
  ))}
120
194
  </Pie>
121
- <Tooltip
122
- contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }}
123
- />
195
+ <Tooltip content={(props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === 'string' ? props.label : undefined)} />
124
196
  <Legend />
125
197
  </PieChart>
126
198
  </ResponsiveContainer>
@@ -137,10 +209,7 @@ export function DynamicChart({ config, primaryColor = '#6366f1', accentColor = '
137
209
  <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e2e8f0" />
138
210
  <XAxis dataKey={finalXKey} tick={{ fontSize: 12, fill: '#64748b' }} tickLine={false} axisLine={{ stroke: '#cbd5e1' }} />
139
211
  <YAxis tick={{ fontSize: 12, fill: '#64748b' }} tickLine={false} axisLine={false} />
140
- <Tooltip
141
- contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }}
142
- cursor={{ fill: '#f1f5f9' }}
143
- />
212
+ <Tooltip content={(props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === 'string' ? props.label : undefined)} cursor={{ fill: '#f1f5f9' }} />
144
213
  <Legend wrapperStyle={{ fontSize: 12 }} />
145
214
  {finalDataKeys.map((key, index) => (
146
215
  <Bar
@@ -157,9 +226,7 @@ export function DynamicChart({ config, primaryColor = '#6366f1', accentColor = '
157
226
  <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e2e8f0" />
158
227
  <XAxis dataKey={finalXKey} tick={{ fontSize: 12, fill: '#64748b' }} tickLine={false} axisLine={{ stroke: '#cbd5e1' }} />
159
228
  <YAxis tick={{ fontSize: 12, fill: '#64748b' }} tickLine={false} axisLine={false} />
160
- <Tooltip
161
- contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }}
162
- />
229
+ <Tooltip content={(props) => renderTooltip(getTooltipRow(props.payload), typeof props.label === 'string' ? props.label : undefined)} />
163
230
  <Legend wrapperStyle={{ fontSize: 12 }} />
164
231
  {finalDataKeys.map((key, index) => (
165
232
  <Line
@@ -123,6 +123,7 @@ function normaliseChild(children: React.ReactNode): React.ReactNode {
123
123
  interface TableConfig {
124
124
  type: 'table';
125
125
  title?: string;
126
+ description?: string;
126
127
  columns?: string[]; // preferred: explicit ordered column labels
127
128
  dataKeys?: string[]; // fallback column keys (may omit xAxisKey)
128
129
  xAxisKey?: string; // row-label key to prepend when using dataKeys
@@ -216,17 +217,24 @@ function DataTable({ config }: { config: TableConfig }) {
216
217
  interface UIConfig {
217
218
  view: 'chart' | 'carousel' | 'table';
218
219
  chartType?: 'pie' | 'bar' | 'line';
220
+ xAxisKey?: string;
221
+ dataKeys?: string[];
222
+ columns?: string[];
223
+ colors?: string[];
219
224
  data: Record<string, unknown>[];
220
225
  title?: string;
226
+ description?: string;
227
+ insights?: string[];
221
228
  type?: string;
222
229
  items?: Record<string, unknown>[];
223
230
  }
224
231
 
225
- function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming }: {
232
+ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAddToCart }: {
226
233
  rawContent: string;
227
234
  primaryColor?: string;
228
235
  accentColor?: string;
229
236
  isStreaming?: boolean;
237
+ onAddToCart?: (product: Product) => void;
230
238
  }) {
231
239
  const result = React.useMemo<{ config: UIConfig } | { error: string } | { loading: true }>(() => {
232
240
  if (isStreaming) return { loading: true };
@@ -273,36 +281,71 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming }: {
273
281
  }
274
282
 
275
283
  const { config } = result;
284
+ const hasInsights = Array.isArray(config.insights) && config.insights.length > 0;
285
+ const hasDescription = typeof config.description === 'string' && config.description.trim().length > 0;
276
286
 
277
287
  switch (config.view) {
278
288
  case 'chart':
279
289
  return (
280
290
  <div className="my-6 p-4 bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-white/10 shadow-sm overflow-hidden">
281
291
  {config.title && <h4 className="text-xs font-semibold text-slate-500 mb-4 px-2">{config.title}</h4>}
292
+ {hasDescription && (
293
+ <p className="px-2 text-sm text-slate-500 dark:text-white/60">{config.description}</p>
294
+ )}
282
295
  <DynamicChart
283
296
  config={{
284
297
  type: (config.chartType || 'bar') as 'bar' | 'line' | 'pie',
285
- data: config.data as unknown as Record<string, string | number>[]
298
+ data: config.data as unknown as Record<string, string | number>[],
299
+ xAxisKey: config.xAxisKey,
300
+ dataKeys: config.dataKeys,
301
+ colors: config.colors,
286
302
  }}
287
303
  primaryColor={primaryColor}
288
304
  accentColor={accentColor}
289
305
  />
306
+ {hasInsights && (
307
+ <div className="mt-4 flex flex-wrap gap-2 px-2">
308
+ {config.insights?.map((insight) => (
309
+ <span
310
+ key={insight}
311
+ className="rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-[11px] font-medium text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-300"
312
+ >
313
+ {insight}
314
+ </span>
315
+ ))}
316
+ </div>
317
+ )}
290
318
  </div>
291
319
  );
292
320
  case 'carousel':
293
321
  return (
294
322
  <div className="my-4">
323
+ {config.title && <h4 className="mb-2 text-xs font-semibold text-slate-500">{config.title}</h4>}
324
+ {hasDescription && (
325
+ <p className="mb-3 text-sm text-slate-500 dark:text-white/60">{config.description}</p>
326
+ )}
295
327
  <ProductCarousel products={(config.data as unknown as Product[]).map(item => ({
296
328
  ...item,
297
329
  image: item.image ?? (typeof resolveImage === 'function' ? resolveImage(item as unknown as Record<string, unknown>) : undefined)
298
- }))} />
330
+ }))} primaryColor={primaryColor} onAddToCart={onAddToCart} />
299
331
  </div>
300
332
  );
301
333
  case 'table':
302
334
  return (
303
335
  <div className="my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm overflow-auto max-h-[400px]">
304
336
  {config.title && <h4 className="text-xs font-semibold text-slate-500 mb-2">{config.title}</h4>}
305
- <DataTable config={{ type: 'table', data: config.data } as TableConfig} />
337
+ {hasDescription && (
338
+ <p className="mb-3 text-sm text-slate-500 dark:text-white/60">{config.description}</p>
339
+ )}
340
+ <DataTable config={{
341
+ type: 'table',
342
+ title: config.title,
343
+ description: config.description,
344
+ columns: config.columns,
345
+ dataKeys: config.dataKeys,
346
+ xAxisKey: config.xAxisKey,
347
+ data: config.data,
348
+ }} />
306
349
  </div>
307
350
  );
308
351
  default:
@@ -324,6 +367,10 @@ export function MessageBubble({
324
367
  }: MessageBubbleProps) {
325
368
  const isUser = message.role === 'user';
326
369
  const [showSources, setShowSources] = React.useState(false);
370
+ const hasStructuredProductBlock = React.useMemo(
371
+ () => /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|type)"\s*:\s*"(?:carousel|products)"/i.test(message.content),
372
+ [message.content],
373
+ );
327
374
 
328
375
  // ── Products from sources ──────────────────────────────────────────────────
329
376
  const productsFromSources = React.useMemo<Product[]>(() => {
@@ -500,6 +547,7 @@ export function MessageBubble({
500
547
  primaryColor={primaryColor}
501
548
  accentColor={accentColor}
502
549
  isStreaming={isStreaming}
550
+ onAddToCart={onAddToCart}
503
551
  />
504
552
  );
505
553
  }
@@ -513,6 +561,7 @@ export function MessageBubble({
513
561
  primaryColor={primaryColor}
514
562
  accentColor={accentColor}
515
563
  isStreaming={isStreaming}
564
+ onAddToCart={onAddToCart}
516
565
  />
517
566
  );
518
567
  }
@@ -538,7 +587,7 @@ export function MessageBubble({
538
587
  );
539
588
  },
540
589
  }),
541
- [primaryColor, accentColor, isStreaming],
590
+ [primaryColor, accentColor, isStreaming, onAddToCart],
542
591
  );
543
592
 
544
593
  // ── Render ─────────────────────────────────────────────────────────────────
@@ -586,7 +635,7 @@ export function MessageBubble({
586
635
  </div>
587
636
 
588
637
  {/* Product Carousel */}
589
- {!isUser && allProducts.length > 0 && (
638
+ {!isUser && !hasStructuredProductBlock && allProducts.length > 0 && (
590
639
  <div className="w-full mt-1">
591
640
  <ProductCarousel
592
641
  products={allProducts}
@@ -619,4 +668,4 @@ export function MessageBubble({
619
668
  </div>
620
669
  </div>
621
670
  );
622
- }
671
+ }
@@ -101,21 +101,66 @@ 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_V3 -->';
104
+ const CHART_MARKER = '<!-- UI_PROTOCOL_V5 -->';
105
105
  const chartInstruction = `\n\n${CHART_MARKER}
106
- ### UNIVERSAL UI PROTOCOL:
107
- You have a specialized UI system. For any structured data (charts, product lists, or tables), you MUST use a single \`\`\`ui\`\`\` block.
108
- 1. DECIDE the best "view" for the user's query:
109
- - "chart": For statistics, distributions, or comparisons. (Requires "chartType": "pie" | "bar" | "line")
110
- - "carousel": For browsing specific product items.
111
- - "table": For detailed raw data comparisons.
112
- 2. OUTPUT a \`\`\`ui\`\`\` block with this schema:
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.
108
+
109
+ 1. VIEW SELECTION
110
+ - Use "chart" for distributions, percentages, counts, comparisons, trends, or "show me in a chart".
111
+ - Use "carousel" for browseable product recommendations or when the user asks to see products/items/cards.
112
+ - Use "table" for tabular/raw detail, explicit table/list requests, or when there are many attributes to compare.
113
+
114
+ 2. REQUIRED JSON SHAPE
113
115
  {
114
116
  "view": "chart" | "carousel" | "table",
115
- "chartType": "pie" | "bar" | "line" (only for view: "chart"),
116
- "data": [ ... objects ... ]
117
+ "title": "Short heading",
118
+ "description": "One sentence describing the view",
119
+ "chartType": "pie" | "bar" | "line",
120
+ "xAxisKey": "label",
121
+ "dataKeys": ["value"],
122
+ "columns": ["name", "category", "price", "stockStatus"],
123
+ "insights": ["Short takeaway 1", "Short takeaway 2"],
124
+ "data": []
117
125
  }
118
- 3. RULES: Valid JSON only. Data must be FLAT. Never draw charts/tables with text. Choose ONLY the most useful view.`;
126
+
127
+ 3. DATA RULES
128
+ - Build the UI payload from retrieved context only. Do not invent products, categories, or stock values.
129
+ - Aggregate raw product rows when the user asks for a chart.
130
+ - For charts, every row in "data" must be flat JSON with primitive values only.
131
+ - Prefer { "label": "...", "value": number } for pie charts.
132
+ - If stock information exists, preserve it in each row with fields like "inStock", "inStockCount", "outOfStockCount", or "stockStatus".
133
+ - If the user asks to highlight what is in stock, include those stock-related fields in the chart data and mention the pattern in "insights".
134
+ - For carousel rows, include product-friendly fields such as id, name, brand, price, image, link, category, stockStatus.
135
+ - For tables, keep "data" row-oriented and include "columns" when possible.
136
+
137
+ 4. FORMAT RULES
138
+ - Never use ASCII art, markdown tables as the primary answer, or text-only fake charts when a UI block is requested.
139
+ - Put any short natural-language explanation before or after the \`\`\`ui\`\`\` block, but keep it concise.
140
+ - Return valid JSON inside the \`\`\`ui\`\`\` block.
141
+
142
+ 5. EXAMPLE
143
+ User: "provide me the distribution of products across categories and highlight which ones are in stock in a pie chart"
144
+ Assistant:
145
+ \`\`\`ui
146
+ {
147
+ "view": "chart",
148
+ "title": "Product Distribution Across Categories",
149
+ "description": "Pie chart showing product count by category with stock signals preserved per slice.",
150
+ "chartType": "pie",
151
+ "xAxisKey": "label",
152
+ "dataKeys": ["value"],
153
+ "insights": [
154
+ "Beauty has the highest product count.",
155
+ "Electronics has the largest number of in-stock products."
156
+ ],
157
+ "data": [
158
+ { "label": "Beauty", "value": 12, "inStockCount": 9, "outOfStockCount": 3 },
159
+ { "label": "Electronics", "value": 8, "inStockCount": 7, "outOfStockCount": 1 },
160
+ { "label": "Home", "value": 5, "inStockCount": 2, "outOfStockCount": 3 }
161
+ ]
162
+ }
163
+ \`\`\``;
119
164
 
120
165
  if (!this.config.llm.systemPrompt?.includes(CHART_MARKER)) {
121
166
  this.config.llm.systemPrompt = (this.config.llm.systemPrompt || '') + chartInstruction;