@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.
- package/dist/{chunk-BLLMNP77.mjs → chunk-Q4MDH6C4.mjs} +56 -11
- package/dist/handlers/index.js +56 -11
- package/dist/handlers/index.mjs +1 -1
- package/dist/index.js +95 -34
- package/dist/index.mjs +95 -34
- package/dist/server.js +56 -11
- package/dist/server.mjs +1 -1
- package/package.json +1 -1
- package/src/app/api/chat/route.ts +2 -2
- package/src/components/DynamicChart.tsx +81 -14
- package/src/components/MessageBubble.tsx +56 -7
- package/src/core/Pipeline.ts +56 -11
|
@@ -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 = "<!--
|
|
2262
|
+
const CHART_MARKER = "<!-- UI_PROTOCOL_V5 -->";
|
|
2263
2263
|
const chartInstruction = `
|
|
2264
2264
|
|
|
2265
2265
|
${CHART_MARKER}
|
|
2266
|
-
### UNIVERSAL UI PROTOCOL
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
- "
|
|
2271
|
-
- "
|
|
2272
|
-
|
|
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
|
-
"
|
|
2276
|
-
"
|
|
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
|
-
|
|
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
|
}
|
package/dist/handlers/index.js
CHANGED
|
@@ -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 = "<!--
|
|
3825
|
+
const CHART_MARKER = "<!-- UI_PROTOCOL_V5 -->";
|
|
3826
3826
|
const chartInstruction = `
|
|
3827
3827
|
|
|
3828
3828
|
${CHART_MARKER}
|
|
3829
|
-
### UNIVERSAL UI PROTOCOL
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
- "
|
|
3834
|
-
- "
|
|
3835
|
-
|
|
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
|
-
"
|
|
3839
|
-
"
|
|
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
|
-
|
|
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
|
}
|
package/dist/handlers/index.mjs
CHANGED
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
|
-
|
|
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
|
|
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: (
|
|
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(
|
|
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
|
-
|
|
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
|
|
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: (
|
|
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(
|
|
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 = "<!--
|
|
3916
|
+
const CHART_MARKER = "<!-- UI_PROTOCOL_V5 -->";
|
|
3917
3917
|
const chartInstruction = `
|
|
3918
3918
|
|
|
3919
3919
|
${CHART_MARKER}
|
|
3920
|
-
### UNIVERSAL UI PROTOCOL
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
- "
|
|
3925
|
-
- "
|
|
3926
|
-
|
|
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
|
-
"
|
|
3930
|
-
"
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retrivora-ai/rag-engine",
|
|
3
|
-
"version": "1.5.
|
|
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 {
|
|
1
|
+
import { createStreamHandler } from '@/handlers';
|
|
2
2
|
import { getRagConfig } from '@/config/serverConfig';
|
|
3
3
|
|
|
4
|
-
export const POST =
|
|
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
|
-
{
|
|
118
|
-
<Cell key={`cell-${index}`} fill={
|
|
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
|
-
|
|
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
|
+
}
|
package/src/core/Pipeline.ts
CHANGED
|
@@ -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 = '<!--
|
|
104
|
+
const CHART_MARKER = '<!-- UI_PROTOCOL_V5 -->';
|
|
105
105
|
const chartInstruction = `\n\n${CHART_MARKER}
|
|
106
|
-
### UNIVERSAL UI PROTOCOL
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
- "
|
|
111
|
-
- "
|
|
112
|
-
|
|
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
|
-
"
|
|
116
|
-
"
|
|
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
|
-
|
|
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;
|