@retrivora-ai/rag-engine 1.5.3 → 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.
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,37 +437,84 @@ 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 ChartBlock({ 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
- if (!rawContent || rawContent === "undefined") return { error: "Empty visualization config." };
410
444
  try {
411
445
  const clean = rawContent.replace(/^```[a-z]*\s*/i, "").replace(/```\s*$/, "").trim();
412
- const config2 = JSON.parse(sanitizeJson(clean));
446
+ const parsed = JSON.parse(sanitizeJson(clean));
447
+ const config2 = __spreadValues({}, parsed);
448
+ if (!config2.view) {
449
+ if (config2.type === "products" || Array.isArray(config2.items)) {
450
+ config2.view = "carousel";
451
+ config2.data = config2.data || config2.items || [];
452
+ } else if (["pie", "bar", "line"].includes(config2.type) || ["pie", "bar", "line"].includes(config2.chartType)) {
453
+ config2.view = "chart";
454
+ config2.chartType = config2.chartType || config2.type || "bar";
455
+ config2.data = config2.data || [];
456
+ } else {
457
+ config2.view = "table";
458
+ config2.data = Array.isArray(config2.data) ? config2.data : Array.isArray(config2) ? config2 : [];
459
+ }
460
+ }
413
461
  return { config: config2 };
414
462
  } catch (err) {
415
- console.error("[ChartBlock] Parsing failed.\nError:", err);
416
463
  return { error: String(err) };
417
464
  }
418
465
  }, [rawContent, isStreaming]);
419
466
  if ("loading" in result) {
420
- 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 select-none" }, /* @__PURE__ */ React5.createElement("div", { className: "w-6 h-6 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" }), /* @__PURE__ */ React5.createElement("p", { className: "text-xs text-slate-500 font-medium animate-pulse" }, "Preparing data visualization\u2026"));
467
+ 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..."));
421
468
  }
422
469
  if ("error" in result) {
423
- return /* @__PURE__ */ React5.createElement("div", { className: "p-4 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg text-sm border border-red-100 dark:border-red-900/30" }, /* @__PURE__ */ React5.createElement("p", { className: "font-medium mb-1" }, "Failed to render visualization"), /* @__PURE__ */ React5.createElement("p", { className: "opacity-70 text-xs" }, "The generated configuration is invalid. Check the console for details."));
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));
424
471
  }
425
472
  const { config } = result;
426
- if (config.type === "table") {
427
- return /* @__PURE__ */ React5.createElement(DataTable, { config });
473
+ const hasInsights = Array.isArray(config.insights) && config.insights.length > 0;
474
+ const hasDescription = typeof config.description === "string" && config.description.trim().length > 0;
475
+ switch (config.view) {
476
+ case "chart":
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(
478
+ DynamicChart,
479
+ {
480
+ config: {
481
+ type: config.chartType || "bar",
482
+ data: config.data,
483
+ xAxisKey: config.xAxisKey,
484
+ dataKeys: config.dataKeys,
485
+ colors: config.colors
486
+ },
487
+ primaryColor,
488
+ accentColor
489
+ }
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
+ ))));
498
+ case "carousel":
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;
501
+ return __spreadProps(__spreadValues({}, item), {
502
+ image: (_a2 = item.image) != null ? _a2 : typeof resolveImage === "function" ? resolveImage(item) : void 0
503
+ });
504
+ }), primaryColor, onAddToCart }));
505
+ case "table":
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
+ } }));
515
+ default:
516
+ return null;
428
517
  }
429
- 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" }, /* @__PURE__ */ React5.createElement(
430
- DynamicChart,
431
- {
432
- config,
433
- primaryColor,
434
- accentColor
435
- }
436
- ));
437
518
  }
438
519
  function MessageBubble({
439
520
  message,
@@ -445,6 +526,10 @@ function MessageBubble({
445
526
  }) {
446
527
  const isUser = message.role === "user";
447
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
+ );
448
533
  const productsFromSources = React5.useMemo(() => {
449
534
  if (isUser || !sources) return [];
450
535
  return sources.filter((s) => {
@@ -608,24 +693,40 @@ ${match.trim()}
608
693
  ]);
609
694
  var _a;
610
695
  const lang = (_a = /language-(\w+)/.exec(className != null ? className : "")) == null ? void 0 : _a[1];
611
- if (!inline && lang === "chart") {
696
+ if (!inline && (lang === "ui" || lang === "chart")) {
612
697
  return /* @__PURE__ */ React5.createElement(
613
- ChartBlock,
698
+ UIDispatcher,
614
699
  {
615
700
  rawContent: String(children != null ? children : "").trim(),
616
701
  primaryColor,
617
702
  accentColor,
618
- isStreaming
703
+ isStreaming,
704
+ onAddToCart
619
705
  }
620
706
  );
621
707
  }
708
+ if (!inline && lang === "json") {
709
+ const content = String(children != null ? children : "").trim();
710
+ if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
711
+ return /* @__PURE__ */ React5.createElement(
712
+ UIDispatcher,
713
+ {
714
+ rawContent: content,
715
+ primaryColor,
716
+ accentColor,
717
+ isStreaming,
718
+ onAddToCart
719
+ }
720
+ );
721
+ }
722
+ }
622
723
  if (!inline && lang === "table-loading") {
623
724
  return /* @__PURE__ */ React5.createElement("div", { className: "my-5 p-6 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex items-center justify-center gap-3 select-none" }, /* @__PURE__ */ React5.createElement("div", { className: "w-4 h-4 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" }), /* @__PURE__ */ React5.createElement("p", { className: "text-xs text-slate-500 font-medium italic animate-pulse" }, "Generating data table..."));
624
725
  }
625
726
  return /* @__PURE__ */ React5.createElement("code", __spreadValues({ className }, props), typeof children === "string" || typeof children === "number" ? children : JSON.stringify(children));
626
727
  }
627
728
  }),
628
- [primaryColor, accentColor, isStreaming]
729
+ [primaryColor, accentColor, isStreaming, onAddToCart]
629
730
  );
630
731
  return /* @__PURE__ */ React5.createElement("div", { className: `flex gap-3 ${isUser ? "flex-row-reverse" : "flex-row"} items-start` }, /* @__PURE__ */ React5.createElement(
631
732
  "div",
@@ -641,7 +742,7 @@ ${match.trim()}
641
742
  style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
642
743
  },
643
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" }))
644
- ), !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(
645
746
  ProductCarousel,
646
747
  {
647
748
  products: allProducts,
package/dist/server.js CHANGED
@@ -3913,17 +3913,68 @@ var Pipeline = class {
3913
3913
  async initialize() {
3914
3914
  var _a, _b;
3915
3915
  if (this.initialised) return;
3916
- const CHART_MARKER = "<!-- VIZ_PROTOCOL_V2 -->";
3916
+ const CHART_MARKER = "<!-- UI_PROTOCOL_V5 -->";
3917
3917
  const chartInstruction = `
3918
3918
 
3919
3919
  ${CHART_MARKER}
3920
- ### MANDATORY VISUALIZATION RULES:
3921
- - If asked for statistics, distribution, or comparisons: You MUST use the \`\`\`chart block.
3922
- - FORMAT: \`\`\`chart { "type": "pie" | "bar" | "line", "data": [ { "label": "X", "value": 10 } ] } \`\`\`
3923
- - RULES:
3924
- 1. ALWAYS include the \`\`\`chart and \`\`\` markers.
3925
- 2. NEVER use text, tables, or ASCII art for charts.
3926
- 3. Output ONLY the chart block. No Python, no text descriptions.`;
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
3929
+ {
3930
+ "view": "chart" | "carousel" | "table",
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": []
3939
+ }
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
+ \`\`\``;
3927
3978
  if (!((_a = this.config.llm.systemPrompt) == null ? void 0 : _a.includes(CHART_MARKER))) {
3928
3979
  this.config.llm.systemPrompt = (this.config.llm.systemPrompt || "") + chartInstruction;
3929
3980
  }
package/dist/server.mjs CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  sseFrame,
40
40
  sseMetaFrame,
41
41
  sseTextFrame
42
- } from "./chunk-C4UPQX3E.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.3",
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