@retrivora-ai/rag-engine 1.6.8 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-J7BCIPO2.mjs → chunk-OHXRQTQH.mjs} +20 -39
- package/dist/handlers/index.js +20 -39
- package/dist/handlers/index.mjs +1 -1
- package/dist/index.js +32 -29
- package/dist/index.mjs +32 -29
- package/dist/server.js +20 -39
- package/dist/server.mjs +1 -1
- package/package.json +1 -1
- package/src/components/MessageBubble.tsx +44 -32
- package/src/core/Pipeline.ts +19 -39
|
@@ -2259,17 +2259,18 @@ var Pipeline = class {
|
|
|
2259
2259
|
async initialize() {
|
|
2260
2260
|
var _a, _b;
|
|
2261
2261
|
if (this.initialised) return;
|
|
2262
|
-
const CHART_MARKER = "<!--
|
|
2262
|
+
const CHART_MARKER = "<!-- UI_PROTOCOL_V7 -->";
|
|
2263
2263
|
const chartInstruction = `
|
|
2264
2264
|
|
|
2265
2265
|
${CHART_MARKER}
|
|
2266
|
-
### UNIVERSAL UI PROTOCOL
|
|
2267
|
-
When
|
|
2266
|
+
### UNIVERSAL UI PROTOCOL (MANDATORY)
|
|
2267
|
+
CRITICAL: When providing visual data (carousels, charts, tables), you MUST wrap the JSON payload in a \`\`\`ui\`\`\` code block.
|
|
2268
|
+
NEVER output naked JSON. If you provide a visualization, do not also provide a markdown table of the same data unless explicitly asked.
|
|
2268
2269
|
|
|
2269
2270
|
1. VIEW SELECTION
|
|
2270
|
-
- Use "carousel" for browsing
|
|
2271
|
-
- Use "chart" ONLY for numerical aggregations
|
|
2272
|
-
- Use "table" for
|
|
2271
|
+
- Use "carousel" for browsing products, items, or catalogs. (e.g., "show me beauty products").
|
|
2272
|
+
- Use "chart" ONLY for numerical aggregations (counts, sums, percentages).
|
|
2273
|
+
- Use "table" for technical specs or row-heavy comparisons.
|
|
2273
2274
|
|
|
2274
2275
|
2. REQUIRED JSON SHAPE
|
|
2275
2276
|
{
|
|
@@ -2277,52 +2278,31 @@ When the user asks for a visual representation, comparison, distribution, breakd
|
|
|
2277
2278
|
"title": "Short heading",
|
|
2278
2279
|
"description": "One sentence describing the view",
|
|
2279
2280
|
"chartType": "pie" | "bar" | "line", // Required if view is "chart"
|
|
2280
|
-
"
|
|
2281
|
-
"dataKeys": ["value"],
|
|
2282
|
-
"columns": ["dynamicFieldKey1", "dynamicFieldKey2"],
|
|
2283
|
-
"insights": ["Short takeaway 1", "Short takeaway 2"],
|
|
2284
|
-
"data": []
|
|
2281
|
+
"data": [] // Array of objects
|
|
2285
2282
|
}
|
|
2286
2283
|
|
|
2287
2284
|
3. DATA RULES
|
|
2288
|
-
- Build the UI payload from retrieved context
|
|
2289
|
-
- For CAROUSEL: "data"
|
|
2290
|
-
-
|
|
2291
|
-
- If the user asks for "products in beauty", do NOT return a pie chart of categories; return a carousel of the beauty products themselves.
|
|
2285
|
+
- Build the UI payload from retrieved context and metadata.
|
|
2286
|
+
- For CAROUSEL: "data" items MUST have { id, name, brand, price, image, link }.
|
|
2287
|
+
- IMPORTANT: Extract "image" and "price" from Source metadata. Image URLs are often in fields like "images", "thumbnail", or "img".
|
|
2292
2288
|
|
|
2293
2289
|
4. FORMAT RULES
|
|
2294
|
-
-
|
|
2295
|
-
-
|
|
2290
|
+
- Wrap the JSON in \`\`\`ui ... \`\`\` blocks.
|
|
2291
|
+
- Place natural language text OUTSIDE the block.
|
|
2296
2292
|
|
|
2297
|
-
5.
|
|
2298
|
-
User: "
|
|
2299
|
-
Assistant:
|
|
2293
|
+
5. EXAMPLE
|
|
2294
|
+
User: "find me beauty products"
|
|
2295
|
+
Assistant: "I found several beauty products in our catalog:"
|
|
2300
2296
|
\`\`\`ui
|
|
2301
2297
|
{
|
|
2302
2298
|
"view": "carousel",
|
|
2303
2299
|
"title": "Beauty Products",
|
|
2304
|
-
"description": "
|
|
2300
|
+
"description": "Latest skincare and makeup items.",
|
|
2305
2301
|
"data": [
|
|
2306
|
-
{ "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "
|
|
2307
|
-
{ "id": "2", "name": "Lipstick", "brand": "Chic", "price": "$12.99", "image": "...", "link": "..." }
|
|
2302
|
+
{ "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "https://placehold.co/600x400?text=Product", "link": "https://example.com/product" }
|
|
2308
2303
|
]
|
|
2309
2304
|
}
|
|
2310
2305
|
\`\`\`
|
|
2311
|
-
|
|
2312
|
-
User: "breakdown of products by category"
|
|
2313
|
-
Assistant:
|
|
2314
|
-
\`\`\`ui
|
|
2315
|
-
{
|
|
2316
|
-
"view": "chart",
|
|
2317
|
-
"title": "Category Breakdown",
|
|
2318
|
-
"chartType": "pie",
|
|
2319
|
-
"xAxisKey": "label",
|
|
2320
|
-
"dataKeys": ["value"],
|
|
2321
|
-
"data": [
|
|
2322
|
-
{ "label": "Beauty", "value": 15 },
|
|
2323
|
-
{ "label": "Fragrances", "value": 8 }
|
|
2324
|
-
]
|
|
2325
|
-
}
|
|
2326
2306
|
\`\`\``;
|
|
2327
2307
|
if (!((_a = this.config.llm.systemPrompt) == null ? void 0 : _a.includes(CHART_MARKER))) {
|
|
2328
2308
|
this.config.llm.systemPrompt = (this.config.llm.systemPrompt || "") + chartInstruction;
|
|
@@ -2531,7 +2511,8 @@ Assistant:
|
|
|
2531
2511
|
sources = sources.slice(0, topK);
|
|
2532
2512
|
}
|
|
2533
2513
|
let context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
|
|
2534
|
-
${m.content}
|
|
2514
|
+
Content: ${m.content}
|
|
2515
|
+
Metadata: ${JSON.stringify(m.metadata)}`).join("\n\n---\n\n") : "No relevant context found.";
|
|
2535
2516
|
if (graphData && graphData.nodes.length > 0) {
|
|
2536
2517
|
const graphContext = graphData.nodes.map(
|
|
2537
2518
|
(n) => `Entity: ${n.label} (${n.id})${n.properties ? " - " + JSON.stringify(n.properties) : ""}`
|
package/dist/handlers/index.js
CHANGED
|
@@ -3822,17 +3822,18 @@ var Pipeline = class {
|
|
|
3822
3822
|
async initialize() {
|
|
3823
3823
|
var _a, _b;
|
|
3824
3824
|
if (this.initialised) return;
|
|
3825
|
-
const CHART_MARKER = "<!--
|
|
3825
|
+
const CHART_MARKER = "<!-- UI_PROTOCOL_V7 -->";
|
|
3826
3826
|
const chartInstruction = `
|
|
3827
3827
|
|
|
3828
3828
|
${CHART_MARKER}
|
|
3829
|
-
### UNIVERSAL UI PROTOCOL
|
|
3830
|
-
When
|
|
3829
|
+
### UNIVERSAL UI PROTOCOL (MANDATORY)
|
|
3830
|
+
CRITICAL: When providing visual data (carousels, charts, tables), you MUST wrap the JSON payload in a \`\`\`ui\`\`\` code block.
|
|
3831
|
+
NEVER output naked JSON. If you provide a visualization, do not also provide a markdown table of the same data unless explicitly asked.
|
|
3831
3832
|
|
|
3832
3833
|
1. VIEW SELECTION
|
|
3833
|
-
- Use "carousel" for browsing
|
|
3834
|
-
- Use "chart" ONLY for numerical aggregations
|
|
3835
|
-
- Use "table" for
|
|
3834
|
+
- Use "carousel" for browsing products, items, or catalogs. (e.g., "show me beauty products").
|
|
3835
|
+
- Use "chart" ONLY for numerical aggregations (counts, sums, percentages).
|
|
3836
|
+
- Use "table" for technical specs or row-heavy comparisons.
|
|
3836
3837
|
|
|
3837
3838
|
2. REQUIRED JSON SHAPE
|
|
3838
3839
|
{
|
|
@@ -3840,52 +3841,31 @@ When the user asks for a visual representation, comparison, distribution, breakd
|
|
|
3840
3841
|
"title": "Short heading",
|
|
3841
3842
|
"description": "One sentence describing the view",
|
|
3842
3843
|
"chartType": "pie" | "bar" | "line", // Required if view is "chart"
|
|
3843
|
-
"
|
|
3844
|
-
"dataKeys": ["value"],
|
|
3845
|
-
"columns": ["dynamicFieldKey1", "dynamicFieldKey2"],
|
|
3846
|
-
"insights": ["Short takeaway 1", "Short takeaway 2"],
|
|
3847
|
-
"data": []
|
|
3844
|
+
"data": [] // Array of objects
|
|
3848
3845
|
}
|
|
3849
3846
|
|
|
3850
3847
|
3. DATA RULES
|
|
3851
|
-
- Build the UI payload from retrieved context
|
|
3852
|
-
- For CAROUSEL: "data"
|
|
3853
|
-
-
|
|
3854
|
-
- If the user asks for "products in beauty", do NOT return a pie chart of categories; return a carousel of the beauty products themselves.
|
|
3848
|
+
- Build the UI payload from retrieved context and metadata.
|
|
3849
|
+
- For CAROUSEL: "data" items MUST have { id, name, brand, price, image, link }.
|
|
3850
|
+
- IMPORTANT: Extract "image" and "price" from Source metadata. Image URLs are often in fields like "images", "thumbnail", or "img".
|
|
3855
3851
|
|
|
3856
3852
|
4. FORMAT RULES
|
|
3857
|
-
-
|
|
3858
|
-
-
|
|
3853
|
+
- Wrap the JSON in \`\`\`ui ... \`\`\` blocks.
|
|
3854
|
+
- Place natural language text OUTSIDE the block.
|
|
3859
3855
|
|
|
3860
|
-
5.
|
|
3861
|
-
User: "
|
|
3862
|
-
Assistant:
|
|
3856
|
+
5. EXAMPLE
|
|
3857
|
+
User: "find me beauty products"
|
|
3858
|
+
Assistant: "I found several beauty products in our catalog:"
|
|
3863
3859
|
\`\`\`ui
|
|
3864
3860
|
{
|
|
3865
3861
|
"view": "carousel",
|
|
3866
3862
|
"title": "Beauty Products",
|
|
3867
|
-
"description": "
|
|
3863
|
+
"description": "Latest skincare and makeup items.",
|
|
3868
3864
|
"data": [
|
|
3869
|
-
{ "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "
|
|
3870
|
-
{ "id": "2", "name": "Lipstick", "brand": "Chic", "price": "$12.99", "image": "...", "link": "..." }
|
|
3865
|
+
{ "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "https://placehold.co/600x400?text=Product", "link": "https://example.com/product" }
|
|
3871
3866
|
]
|
|
3872
3867
|
}
|
|
3873
3868
|
\`\`\`
|
|
3874
|
-
|
|
3875
|
-
User: "breakdown of products by category"
|
|
3876
|
-
Assistant:
|
|
3877
|
-
\`\`\`ui
|
|
3878
|
-
{
|
|
3879
|
-
"view": "chart",
|
|
3880
|
-
"title": "Category Breakdown",
|
|
3881
|
-
"chartType": "pie",
|
|
3882
|
-
"xAxisKey": "label",
|
|
3883
|
-
"dataKeys": ["value"],
|
|
3884
|
-
"data": [
|
|
3885
|
-
{ "label": "Beauty", "value": 15 },
|
|
3886
|
-
{ "label": "Fragrances", "value": 8 }
|
|
3887
|
-
]
|
|
3888
|
-
}
|
|
3889
3869
|
\`\`\``;
|
|
3890
3870
|
if (!((_a = this.config.llm.systemPrompt) == null ? void 0 : _a.includes(CHART_MARKER))) {
|
|
3891
3871
|
this.config.llm.systemPrompt = (this.config.llm.systemPrompt || "") + chartInstruction;
|
|
@@ -4094,7 +4074,8 @@ Assistant:
|
|
|
4094
4074
|
sources = sources.slice(0, topK);
|
|
4095
4075
|
}
|
|
4096
4076
|
let context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
|
|
4097
|
-
${m.content}
|
|
4077
|
+
Content: ${m.content}
|
|
4078
|
+
Metadata: ${JSON.stringify(m.metadata)}`).join("\n\n---\n\n") : "No relevant context found.";
|
|
4098
4079
|
if (graphData && graphData.nodes.length > 0) {
|
|
4099
4080
|
const graphContext = graphData.nodes.map(
|
|
4100
4081
|
(n) => `Entity: ${n.label} (${n.id})${n.properties ? " - " + JSON.stringify(n.properties) : ""}`
|
package/dist/handlers/index.mjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -405,7 +405,7 @@ function sanitizeJson(raw) {
|
|
|
405
405
|
return `: ${result}`;
|
|
406
406
|
}
|
|
407
407
|
);
|
|
408
|
-
s = s.replace(
|
|
408
|
+
s = s.replace(/(^|[^\:])\/\/[^\n]*/g, "$1");
|
|
409
409
|
s = s.replace(/([{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/g, '$1"$2":');
|
|
410
410
|
s = s.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"');
|
|
411
411
|
s = s.replace(/,\s*([}\]])/g, "$1");
|
|
@@ -456,7 +456,7 @@ function extractLikelyJsonBlock(raw) {
|
|
|
456
456
|
return trimmed.slice(Math.min(...starts));
|
|
457
457
|
}
|
|
458
458
|
function looksLikeStructuredPayload(raw) {
|
|
459
|
-
return /"?(view|chartType|type|data|items|rows|products|results)"?\s*:/.test(raw);
|
|
459
|
+
return /"?(view|chartType|type|data|items|rows|products|results|columns|xAxisKey)"?\s*:/.test(raw);
|
|
460
460
|
}
|
|
461
461
|
function stripMarkdownTables(raw) {
|
|
462
462
|
return raw.replace(
|
|
@@ -535,12 +535,23 @@ function isLikelyUiOnlyMessage(text) {
|
|
|
535
535
|
return trimmed.length === 0 || /^(chart data|table data|visualization|visual representation|product catalog|product recommendations|catalog summary)\s*:?\s*$/i.test(trimmed);
|
|
536
536
|
}
|
|
537
537
|
function resolveImage(data) {
|
|
538
|
-
|
|
538
|
+
const candidates = ["image", "img", "thumbnail", "url", "picture", "link"];
|
|
539
|
+
for (const key of candidates) {
|
|
539
540
|
const v = data[key];
|
|
540
|
-
if (typeof v === "string" && v) return v;
|
|
541
|
+
if (typeof v === "string" && v.startsWith("http")) return v;
|
|
542
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
543
|
+
const subVal = v.url || v.link || v.src;
|
|
544
|
+
if (typeof subVal === "string" && subVal.startsWith("http")) return subVal;
|
|
545
|
+
}
|
|
541
546
|
}
|
|
542
|
-
if (Array.isArray(data.images)
|
|
543
|
-
|
|
547
|
+
if (Array.isArray(data.images)) {
|
|
548
|
+
for (const item of data.images) {
|
|
549
|
+
if (typeof item === "string" && item.startsWith("http")) return item;
|
|
550
|
+
if (item && typeof item === "object") {
|
|
551
|
+
const url = item.url || item.link || item.src;
|
|
552
|
+
if (typeof url === "string" && url.startsWith("http")) return url;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
544
555
|
}
|
|
545
556
|
return void 0;
|
|
546
557
|
}
|
|
@@ -643,10 +654,13 @@ function resolveStructuredRows(config) {
|
|
|
643
654
|
if (Array.isArray(config.results) && config.results.length > 0) return config.results;
|
|
644
655
|
return [];
|
|
645
656
|
}
|
|
646
|
-
function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming,
|
|
657
|
+
function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, viewportSize = "large" }) {
|
|
647
658
|
var _a;
|
|
648
659
|
const result = import_react5.default.useMemo(() => {
|
|
649
|
-
if (isStreaming)
|
|
660
|
+
if (isStreaming) {
|
|
661
|
+
const looksLikeCarousel = rawContent.includes('"view": "carousel"') || rawContent.includes('"view":"carousel"') || rawContent.includes('"type": "products"') || rawContent.includes('"type":"products"');
|
|
662
|
+
return { loading: true, silent: looksLikeCarousel };
|
|
663
|
+
}
|
|
650
664
|
try {
|
|
651
665
|
const clean = rawContent.replace(/^```[a-z]*\s*/i, "").replace(/```\s*$/, "").trim();
|
|
652
666
|
const parsed = JSON.parse(sanitizeJson(extractLikelyJsonBlock(clean)));
|
|
@@ -679,6 +693,7 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
|
|
|
679
693
|
}
|
|
680
694
|
}, [rawContent, isStreaming]);
|
|
681
695
|
if ("loading" in result) {
|
|
696
|
+
if (result.silent) return null;
|
|
682
697
|
return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-4 p-8 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex flex-col items-center justify-center gap-3 animate-pulse" }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "w-5 h-5 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" }), /* @__PURE__ */ import_react5.default.createElement("p", { className: "text-xs text-slate-500 font-medium italic" }, "Preparing view..."));
|
|
683
698
|
}
|
|
684
699
|
if ("error" in result) {
|
|
@@ -713,12 +728,7 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
|
|
|
713
728
|
insight
|
|
714
729
|
))));
|
|
715
730
|
case "carousel":
|
|
716
|
-
return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-4" }, config.title && /* @__PURE__ */ import_react5.default.createElement("h4", { className: `${isCompact ? "mb-1.5 text-[11px]" : "mb-2 text-xs"} font-semibold text-slate-500` }, config.title), hasDescription && /* @__PURE__ */ import_react5.default.createElement("p", { className: `mb-3 ${isCompact ? "text-xs" : "text-sm"} text-slate-500 dark:text-white/60` }, config.description)
|
|
717
|
-
var _a2;
|
|
718
|
-
return __spreadProps(__spreadValues({}, item), {
|
|
719
|
-
image: (_a2 = item.image) != null ? _a2 : typeof resolveImage === "function" ? resolveImage(item) : void 0
|
|
720
|
-
});
|
|
721
|
-
}), primaryColor, onAddToCart }));
|
|
731
|
+
return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-4" }, config.title && /* @__PURE__ */ import_react5.default.createElement("h4", { className: `${isCompact ? "mb-1.5 text-[11px]" : "mb-2 text-xs"} font-semibold text-slate-500` }, config.title), hasDescription && /* @__PURE__ */ import_react5.default.createElement("p", { className: `mb-3 ${isCompact ? "text-xs" : "text-sm"} text-slate-500 dark:text-white/60` }, config.description));
|
|
722
732
|
case "table":
|
|
723
733
|
return /* @__PURE__ */ import_react5.default.createElement("div", { className: `${isCompact ? "my-3 p-3 max-h-[320px]" : "my-4 p-4 max-h-[400px]"} bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm overflow-auto` }, config.title && /* @__PURE__ */ import_react5.default.createElement("h4", { className: `${isCompact ? "text-[11px]" : "text-xs"} font-semibold text-slate-500 mb-2` }, config.title), hasDescription && /* @__PURE__ */ import_react5.default.createElement("p", { className: `mb-3 ${isCompact ? "text-xs" : "text-sm"} text-slate-500 dark:text-white/60` }, config.description), /* @__PURE__ */ import_react5.default.createElement(DataTable, { config: {
|
|
724
734
|
type: "table",
|
|
@@ -746,10 +756,6 @@ function MessageBubble({
|
|
|
746
756
|
const isCompact = viewportSize === "compact";
|
|
747
757
|
const isMedium = viewportSize === "medium";
|
|
748
758
|
const [showSources, setShowSources] = import_react5.default.useState(false);
|
|
749
|
-
const hasStructuredProductBlock = import_react5.default.useMemo(
|
|
750
|
-
() => /```(?:ui|json|chart)\s*[\s\S]*?"?(?:view|type|data|products)"?\s*:\s*"?(?:carousel|products|table)"?/i.test(message.content) || /\{[\s\S]*?"?(?:view|type|data|products)"?\s*:\s*"?(?:carousel|products)"?[\s\S]*\}/i.test(message.content) && looksLikeStructuredPayload(message.content),
|
|
751
|
-
[message.content]
|
|
752
|
-
);
|
|
753
759
|
const structuredContent = import_react5.default.useMemo(
|
|
754
760
|
() => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
|
|
755
761
|
[isUser, message.content]
|
|
@@ -841,15 +847,16 @@ function MessageBubble({
|
|
|
841
847
|
let raw = structuredContent.payload ? structuredContent.text : cleanContent || message.content;
|
|
842
848
|
const hasStructuredUiBlock = Boolean(structuredContent.payload) || /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
|
|
843
849
|
raw = raw.replace(/([^\n])\n\|/g, "$1\n\n|").replace(/(\|[^\n]*\|)\n\n+(?=\|)/g, "$1\n");
|
|
844
|
-
if (!isStreaming) {
|
|
845
|
-
raw = raw.replace(/(?:^|\n)\s
|
|
850
|
+
if (!isStreaming && !hasStructuredUiBlock) {
|
|
851
|
+
raw = raw.replace(/(?:^|\n)\s*(\{[\s\S]+?\})(?:\s*\n|$)/g, (match, body) => {
|
|
846
852
|
if (match.includes("```")) return match;
|
|
847
|
-
|
|
848
|
-
|
|
853
|
+
const trimmedBody = body.trim();
|
|
854
|
+
if (!looksLikeStructuredPayload(trimmedBody)) return match;
|
|
855
|
+
const type = trimmedBody.includes('"view"') || trimmedBody.includes('"chartType"') || trimmedBody.includes('"type"') || trimmedBody.includes('"xAxisKey"') ? "ui" : "json";
|
|
849
856
|
return `
|
|
850
857
|
|
|
851
858
|
\`\`\`${type}
|
|
852
|
-
${
|
|
859
|
+
${trimmedBody}
|
|
853
860
|
\`\`\`
|
|
854
861
|
|
|
855
862
|
`;
|
|
@@ -949,7 +956,6 @@ ${match.trim()}
|
|
|
949
956
|
primaryColor,
|
|
950
957
|
accentColor,
|
|
951
958
|
isStreaming,
|
|
952
|
-
onAddToCart,
|
|
953
959
|
viewportSize
|
|
954
960
|
}
|
|
955
961
|
);
|
|
@@ -964,7 +970,6 @@ ${match.trim()}
|
|
|
964
970
|
primaryColor,
|
|
965
971
|
accentColor,
|
|
966
972
|
isStreaming,
|
|
967
|
-
onAddToCart,
|
|
968
973
|
viewportSize
|
|
969
974
|
}
|
|
970
975
|
);
|
|
@@ -980,7 +985,6 @@ ${match.trim()}
|
|
|
980
985
|
primaryColor,
|
|
981
986
|
accentColor,
|
|
982
987
|
isStreaming,
|
|
983
|
-
onAddToCart,
|
|
984
988
|
viewportSize
|
|
985
989
|
}
|
|
986
990
|
);
|
|
@@ -992,7 +996,7 @@ ${match.trim()}
|
|
|
992
996
|
return /* @__PURE__ */ import_react5.default.createElement("code", __spreadValues({ className }, props), typeof children === "string" || typeof children === "number" ? children : JSON.stringify(children));
|
|
993
997
|
}
|
|
994
998
|
}),
|
|
995
|
-
[primaryColor, accentColor, isStreaming,
|
|
999
|
+
[primaryColor, accentColor, isStreaming, viewportSize]
|
|
996
1000
|
);
|
|
997
1001
|
return /* @__PURE__ */ import_react5.default.createElement("div", { className: `flex ${isCompact ? "gap-2" : "gap-3"} ${isUser ? "flex-row-reverse" : "flex-row"} items-start` }, /* @__PURE__ */ import_react5.default.createElement(
|
|
998
1002
|
"div",
|
|
@@ -1014,11 +1018,10 @@ ${match.trim()}
|
|
|
1014
1018
|
primaryColor,
|
|
1015
1019
|
accentColor,
|
|
1016
1020
|
isStreaming,
|
|
1017
|
-
onAddToCart,
|
|
1018
1021
|
viewportSize
|
|
1019
1022
|
}
|
|
1020
1023
|
), !shouldRenderStructuredOnly && /* @__PURE__ */ import_react5.default.createElement(import_react_markdown.default, { remarkPlugins: [import_remark_gfm.default], components: markdownComponents }, processedMarkdown), isStreaming && message.content && /* @__PURE__ */ import_react5.default.createElement("span", { className: "inline-block w-1.5 h-3.5 ml-1 bg-emerald-500 animate-pulse align-middle" }))
|
|
1021
|
-
), !isUser &&
|
|
1024
|
+
), !isUser && allProducts.length > 0 && /* @__PURE__ */ import_react5.default.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ import_react5.default.createElement(
|
|
1022
1025
|
ProductCarousel,
|
|
1023
1026
|
{
|
|
1024
1027
|
products: allProducts,
|
package/dist/index.mjs
CHANGED
|
@@ -368,7 +368,7 @@ function sanitizeJson(raw) {
|
|
|
368
368
|
return `: ${result}`;
|
|
369
369
|
}
|
|
370
370
|
);
|
|
371
|
-
s = s.replace(
|
|
371
|
+
s = s.replace(/(^|[^\:])\/\/[^\n]*/g, "$1");
|
|
372
372
|
s = s.replace(/([{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/g, '$1"$2":');
|
|
373
373
|
s = s.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"');
|
|
374
374
|
s = s.replace(/,\s*([}\]])/g, "$1");
|
|
@@ -419,7 +419,7 @@ function extractLikelyJsonBlock(raw) {
|
|
|
419
419
|
return trimmed.slice(Math.min(...starts));
|
|
420
420
|
}
|
|
421
421
|
function looksLikeStructuredPayload(raw) {
|
|
422
|
-
return /"?(view|chartType|type|data|items|rows|products|results)"?\s*:/.test(raw);
|
|
422
|
+
return /"?(view|chartType|type|data|items|rows|products|results|columns|xAxisKey)"?\s*:/.test(raw);
|
|
423
423
|
}
|
|
424
424
|
function stripMarkdownTables(raw) {
|
|
425
425
|
return raw.replace(
|
|
@@ -498,12 +498,23 @@ function isLikelyUiOnlyMessage(text) {
|
|
|
498
498
|
return trimmed.length === 0 || /^(chart data|table data|visualization|visual representation|product catalog|product recommendations|catalog summary)\s*:?\s*$/i.test(trimmed);
|
|
499
499
|
}
|
|
500
500
|
function resolveImage(data) {
|
|
501
|
-
|
|
501
|
+
const candidates = ["image", "img", "thumbnail", "url", "picture", "link"];
|
|
502
|
+
for (const key of candidates) {
|
|
502
503
|
const v = data[key];
|
|
503
|
-
if (typeof v === "string" && v) return v;
|
|
504
|
+
if (typeof v === "string" && v.startsWith("http")) return v;
|
|
505
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
506
|
+
const subVal = v.url || v.link || v.src;
|
|
507
|
+
if (typeof subVal === "string" && subVal.startsWith("http")) return subVal;
|
|
508
|
+
}
|
|
504
509
|
}
|
|
505
|
-
if (Array.isArray(data.images)
|
|
506
|
-
|
|
510
|
+
if (Array.isArray(data.images)) {
|
|
511
|
+
for (const item of data.images) {
|
|
512
|
+
if (typeof item === "string" && item.startsWith("http")) return item;
|
|
513
|
+
if (item && typeof item === "object") {
|
|
514
|
+
const url = item.url || item.link || item.src;
|
|
515
|
+
if (typeof url === "string" && url.startsWith("http")) return url;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
507
518
|
}
|
|
508
519
|
return void 0;
|
|
509
520
|
}
|
|
@@ -606,10 +617,13 @@ function resolveStructuredRows(config) {
|
|
|
606
617
|
if (Array.isArray(config.results) && config.results.length > 0) return config.results;
|
|
607
618
|
return [];
|
|
608
619
|
}
|
|
609
|
-
function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming,
|
|
620
|
+
function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, viewportSize = "large" }) {
|
|
610
621
|
var _a;
|
|
611
622
|
const result = React5.useMemo(() => {
|
|
612
|
-
if (isStreaming)
|
|
623
|
+
if (isStreaming) {
|
|
624
|
+
const looksLikeCarousel = rawContent.includes('"view": "carousel"') || rawContent.includes('"view":"carousel"') || rawContent.includes('"type": "products"') || rawContent.includes('"type":"products"');
|
|
625
|
+
return { loading: true, silent: looksLikeCarousel };
|
|
626
|
+
}
|
|
613
627
|
try {
|
|
614
628
|
const clean = rawContent.replace(/^```[a-z]*\s*/i, "").replace(/```\s*$/, "").trim();
|
|
615
629
|
const parsed = JSON.parse(sanitizeJson(extractLikelyJsonBlock(clean)));
|
|
@@ -642,6 +656,7 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
|
|
|
642
656
|
}
|
|
643
657
|
}, [rawContent, isStreaming]);
|
|
644
658
|
if ("loading" in result) {
|
|
659
|
+
if (result.silent) return null;
|
|
645
660
|
return /* @__PURE__ */ React5.createElement("div", { className: "my-4 p-8 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex flex-col items-center justify-center gap-3 animate-pulse" }, /* @__PURE__ */ React5.createElement("div", { className: "w-5 h-5 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" }), /* @__PURE__ */ React5.createElement("p", { className: "text-xs text-slate-500 font-medium italic" }, "Preparing view..."));
|
|
646
661
|
}
|
|
647
662
|
if ("error" in result) {
|
|
@@ -676,12 +691,7 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
|
|
|
676
691
|
insight
|
|
677
692
|
))));
|
|
678
693
|
case "carousel":
|
|
679
|
-
return /* @__PURE__ */ React5.createElement("div", { className: "my-4" }, config.title && /* @__PURE__ */ React5.createElement("h4", { className: `${isCompact ? "mb-1.5 text-[11px]" : "mb-2 text-xs"} font-semibold text-slate-500` }, config.title), hasDescription && /* @__PURE__ */ React5.createElement("p", { className: `mb-3 ${isCompact ? "text-xs" : "text-sm"} text-slate-500 dark:text-white/60` }, config.description)
|
|
680
|
-
var _a2;
|
|
681
|
-
return __spreadProps(__spreadValues({}, item), {
|
|
682
|
-
image: (_a2 = item.image) != null ? _a2 : typeof resolveImage === "function" ? resolveImage(item) : void 0
|
|
683
|
-
});
|
|
684
|
-
}), primaryColor, onAddToCart }));
|
|
694
|
+
return /* @__PURE__ */ React5.createElement("div", { className: "my-4" }, config.title && /* @__PURE__ */ React5.createElement("h4", { className: `${isCompact ? "mb-1.5 text-[11px]" : "mb-2 text-xs"} font-semibold text-slate-500` }, config.title), hasDescription && /* @__PURE__ */ React5.createElement("p", { className: `mb-3 ${isCompact ? "text-xs" : "text-sm"} text-slate-500 dark:text-white/60` }, config.description));
|
|
685
695
|
case "table":
|
|
686
696
|
return /* @__PURE__ */ React5.createElement("div", { className: `${isCompact ? "my-3 p-3 max-h-[320px]" : "my-4 p-4 max-h-[400px]"} bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm overflow-auto` }, config.title && /* @__PURE__ */ React5.createElement("h4", { className: `${isCompact ? "text-[11px]" : "text-xs"} font-semibold text-slate-500 mb-2` }, config.title), hasDescription && /* @__PURE__ */ React5.createElement("p", { className: `mb-3 ${isCompact ? "text-xs" : "text-sm"} text-slate-500 dark:text-white/60` }, config.description), /* @__PURE__ */ React5.createElement(DataTable, { config: {
|
|
687
697
|
type: "table",
|
|
@@ -709,10 +719,6 @@ function MessageBubble({
|
|
|
709
719
|
const isCompact = viewportSize === "compact";
|
|
710
720
|
const isMedium = viewportSize === "medium";
|
|
711
721
|
const [showSources, setShowSources] = React5.useState(false);
|
|
712
|
-
const hasStructuredProductBlock = React5.useMemo(
|
|
713
|
-
() => /```(?:ui|json|chart)\s*[\s\S]*?"?(?:view|type|data|products)"?\s*:\s*"?(?:carousel|products|table)"?/i.test(message.content) || /\{[\s\S]*?"?(?:view|type|data|products)"?\s*:\s*"?(?:carousel|products)"?[\s\S]*\}/i.test(message.content) && looksLikeStructuredPayload(message.content),
|
|
714
|
-
[message.content]
|
|
715
|
-
);
|
|
716
722
|
const structuredContent = React5.useMemo(
|
|
717
723
|
() => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
|
|
718
724
|
[isUser, message.content]
|
|
@@ -804,15 +810,16 @@ function MessageBubble({
|
|
|
804
810
|
let raw = structuredContent.payload ? structuredContent.text : cleanContent || message.content;
|
|
805
811
|
const hasStructuredUiBlock = Boolean(structuredContent.payload) || /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
|
|
806
812
|
raw = raw.replace(/([^\n])\n\|/g, "$1\n\n|").replace(/(\|[^\n]*\|)\n\n+(?=\|)/g, "$1\n");
|
|
807
|
-
if (!isStreaming) {
|
|
808
|
-
raw = raw.replace(/(?:^|\n)\s
|
|
813
|
+
if (!isStreaming && !hasStructuredUiBlock) {
|
|
814
|
+
raw = raw.replace(/(?:^|\n)\s*(\{[\s\S]+?\})(?:\s*\n|$)/g, (match, body) => {
|
|
809
815
|
if (match.includes("```")) return match;
|
|
810
|
-
|
|
811
|
-
|
|
816
|
+
const trimmedBody = body.trim();
|
|
817
|
+
if (!looksLikeStructuredPayload(trimmedBody)) return match;
|
|
818
|
+
const type = trimmedBody.includes('"view"') || trimmedBody.includes('"chartType"') || trimmedBody.includes('"type"') || trimmedBody.includes('"xAxisKey"') ? "ui" : "json";
|
|
812
819
|
return `
|
|
813
820
|
|
|
814
821
|
\`\`\`${type}
|
|
815
|
-
${
|
|
822
|
+
${trimmedBody}
|
|
816
823
|
\`\`\`
|
|
817
824
|
|
|
818
825
|
`;
|
|
@@ -912,7 +919,6 @@ ${match.trim()}
|
|
|
912
919
|
primaryColor,
|
|
913
920
|
accentColor,
|
|
914
921
|
isStreaming,
|
|
915
|
-
onAddToCart,
|
|
916
922
|
viewportSize
|
|
917
923
|
}
|
|
918
924
|
);
|
|
@@ -927,7 +933,6 @@ ${match.trim()}
|
|
|
927
933
|
primaryColor,
|
|
928
934
|
accentColor,
|
|
929
935
|
isStreaming,
|
|
930
|
-
onAddToCart,
|
|
931
936
|
viewportSize
|
|
932
937
|
}
|
|
933
938
|
);
|
|
@@ -943,7 +948,6 @@ ${match.trim()}
|
|
|
943
948
|
primaryColor,
|
|
944
949
|
accentColor,
|
|
945
950
|
isStreaming,
|
|
946
|
-
onAddToCart,
|
|
947
951
|
viewportSize
|
|
948
952
|
}
|
|
949
953
|
);
|
|
@@ -955,7 +959,7 @@ ${match.trim()}
|
|
|
955
959
|
return /* @__PURE__ */ React5.createElement("code", __spreadValues({ className }, props), typeof children === "string" || typeof children === "number" ? children : JSON.stringify(children));
|
|
956
960
|
}
|
|
957
961
|
}),
|
|
958
|
-
[primaryColor, accentColor, isStreaming,
|
|
962
|
+
[primaryColor, accentColor, isStreaming, viewportSize]
|
|
959
963
|
);
|
|
960
964
|
return /* @__PURE__ */ React5.createElement("div", { className: `flex ${isCompact ? "gap-2" : "gap-3"} ${isUser ? "flex-row-reverse" : "flex-row"} items-start` }, /* @__PURE__ */ React5.createElement(
|
|
961
965
|
"div",
|
|
@@ -977,11 +981,10 @@ ${match.trim()}
|
|
|
977
981
|
primaryColor,
|
|
978
982
|
accentColor,
|
|
979
983
|
isStreaming,
|
|
980
|
-
onAddToCart,
|
|
981
984
|
viewportSize
|
|
982
985
|
}
|
|
983
986
|
), !shouldRenderStructuredOnly && /* @__PURE__ */ React5.createElement(ReactMarkdown, { remarkPlugins: [remarkGfm], components: markdownComponents }, processedMarkdown), isStreaming && message.content && /* @__PURE__ */ React5.createElement("span", { className: "inline-block w-1.5 h-3.5 ml-1 bg-emerald-500 animate-pulse align-middle" }))
|
|
984
|
-
), !isUser &&
|
|
987
|
+
), !isUser && allProducts.length > 0 && /* @__PURE__ */ React5.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ React5.createElement(
|
|
985
988
|
ProductCarousel,
|
|
986
989
|
{
|
|
987
990
|
products: allProducts,
|
package/dist/server.js
CHANGED
|
@@ -3913,17 +3913,18 @@ var Pipeline = class {
|
|
|
3913
3913
|
async initialize() {
|
|
3914
3914
|
var _a, _b;
|
|
3915
3915
|
if (this.initialised) return;
|
|
3916
|
-
const CHART_MARKER = "<!--
|
|
3916
|
+
const CHART_MARKER = "<!-- UI_PROTOCOL_V7 -->";
|
|
3917
3917
|
const chartInstruction = `
|
|
3918
3918
|
|
|
3919
3919
|
${CHART_MARKER}
|
|
3920
|
-
### UNIVERSAL UI PROTOCOL
|
|
3921
|
-
When
|
|
3920
|
+
### UNIVERSAL UI PROTOCOL (MANDATORY)
|
|
3921
|
+
CRITICAL: When providing visual data (carousels, charts, tables), you MUST wrap the JSON payload in a \`\`\`ui\`\`\` code block.
|
|
3922
|
+
NEVER output naked JSON. If you provide a visualization, do not also provide a markdown table of the same data unless explicitly asked.
|
|
3922
3923
|
|
|
3923
3924
|
1. VIEW SELECTION
|
|
3924
|
-
- Use "carousel" for browsing
|
|
3925
|
-
- Use "chart" ONLY for numerical aggregations
|
|
3926
|
-
- Use "table" for
|
|
3925
|
+
- Use "carousel" for browsing products, items, or catalogs. (e.g., "show me beauty products").
|
|
3926
|
+
- Use "chart" ONLY for numerical aggregations (counts, sums, percentages).
|
|
3927
|
+
- Use "table" for technical specs or row-heavy comparisons.
|
|
3927
3928
|
|
|
3928
3929
|
2. REQUIRED JSON SHAPE
|
|
3929
3930
|
{
|
|
@@ -3931,52 +3932,31 @@ When the user asks for a visual representation, comparison, distribution, breakd
|
|
|
3931
3932
|
"title": "Short heading",
|
|
3932
3933
|
"description": "One sentence describing the view",
|
|
3933
3934
|
"chartType": "pie" | "bar" | "line", // Required if view is "chart"
|
|
3934
|
-
"
|
|
3935
|
-
"dataKeys": ["value"],
|
|
3936
|
-
"columns": ["dynamicFieldKey1", "dynamicFieldKey2"],
|
|
3937
|
-
"insights": ["Short takeaway 1", "Short takeaway 2"],
|
|
3938
|
-
"data": []
|
|
3935
|
+
"data": [] // Array of objects
|
|
3939
3936
|
}
|
|
3940
3937
|
|
|
3941
3938
|
3. DATA RULES
|
|
3942
|
-
- Build the UI payload from retrieved context
|
|
3943
|
-
- For CAROUSEL: "data"
|
|
3944
|
-
-
|
|
3945
|
-
- If the user asks for "products in beauty", do NOT return a pie chart of categories; return a carousel of the beauty products themselves.
|
|
3939
|
+
- Build the UI payload from retrieved context and metadata.
|
|
3940
|
+
- For CAROUSEL: "data" items MUST have { id, name, brand, price, image, link }.
|
|
3941
|
+
- IMPORTANT: Extract "image" and "price" from Source metadata. Image URLs are often in fields like "images", "thumbnail", or "img".
|
|
3946
3942
|
|
|
3947
3943
|
4. FORMAT RULES
|
|
3948
|
-
-
|
|
3949
|
-
-
|
|
3944
|
+
- Wrap the JSON in \`\`\`ui ... \`\`\` blocks.
|
|
3945
|
+
- Place natural language text OUTSIDE the block.
|
|
3950
3946
|
|
|
3951
|
-
5.
|
|
3952
|
-
User: "
|
|
3953
|
-
Assistant:
|
|
3947
|
+
5. EXAMPLE
|
|
3948
|
+
User: "find me beauty products"
|
|
3949
|
+
Assistant: "I found several beauty products in our catalog:"
|
|
3954
3950
|
\`\`\`ui
|
|
3955
3951
|
{
|
|
3956
3952
|
"view": "carousel",
|
|
3957
3953
|
"title": "Beauty Products",
|
|
3958
|
-
"description": "
|
|
3954
|
+
"description": "Latest skincare and makeup items.",
|
|
3959
3955
|
"data": [
|
|
3960
|
-
{ "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "
|
|
3961
|
-
{ "id": "2", "name": "Lipstick", "brand": "Chic", "price": "$12.99", "image": "...", "link": "..." }
|
|
3956
|
+
{ "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "https://placehold.co/600x400?text=Product", "link": "https://example.com/product" }
|
|
3962
3957
|
]
|
|
3963
3958
|
}
|
|
3964
3959
|
\`\`\`
|
|
3965
|
-
|
|
3966
|
-
User: "breakdown of products by category"
|
|
3967
|
-
Assistant:
|
|
3968
|
-
\`\`\`ui
|
|
3969
|
-
{
|
|
3970
|
-
"view": "chart",
|
|
3971
|
-
"title": "Category Breakdown",
|
|
3972
|
-
"chartType": "pie",
|
|
3973
|
-
"xAxisKey": "label",
|
|
3974
|
-
"dataKeys": ["value"],
|
|
3975
|
-
"data": [
|
|
3976
|
-
{ "label": "Beauty", "value": 15 },
|
|
3977
|
-
{ "label": "Fragrances", "value": 8 }
|
|
3978
|
-
]
|
|
3979
|
-
}
|
|
3980
3960
|
\`\`\``;
|
|
3981
3961
|
if (!((_a = this.config.llm.systemPrompt) == null ? void 0 : _a.includes(CHART_MARKER))) {
|
|
3982
3962
|
this.config.llm.systemPrompt = (this.config.llm.systemPrompt || "") + chartInstruction;
|
|
@@ -4185,7 +4165,8 @@ Assistant:
|
|
|
4185
4165
|
sources = sources.slice(0, topK);
|
|
4186
4166
|
}
|
|
4187
4167
|
let context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
|
|
4188
|
-
${m.content}
|
|
4168
|
+
Content: ${m.content}
|
|
4169
|
+
Metadata: ${JSON.stringify(m.metadata)}`).join("\n\n---\n\n") : "No relevant context found.";
|
|
4189
4170
|
if (graphData && graphData.nodes.length > 0) {
|
|
4190
4171
|
const graphContext = graphData.nodes.map(
|
|
4191
4172
|
(n) => `Entity: ${n.label} (${n.id})${n.properties ? " - " + JSON.stringify(n.properties) : ""}`
|
package/dist/server.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retrivora-ai/rag-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Retrivora AI is a plug-and-play AI engine for RAG chat experiences — generic vector DB + LLM provider, embeddable or standalone.",
|
|
5
5
|
"author": "Abhinav Alkuchi",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,8 +32,8 @@ function sanitizeJson(raw: string): string {
|
|
|
32
32
|
},
|
|
33
33
|
);
|
|
34
34
|
|
|
35
|
-
// 2. Strip JS-style single-line comments
|
|
36
|
-
s = s.replace(
|
|
35
|
+
// 2. Strip JS-style single-line comments, but avoid URL protocols like https://
|
|
36
|
+
s = s.replace(/(^|[^\:])\/\/[^\n]*/g, '$1');
|
|
37
37
|
|
|
38
38
|
// 3. Quote bare (unquoted) object keys
|
|
39
39
|
s = s.replace(/([{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/g, '$1"$2":');
|
|
@@ -103,7 +103,7 @@ function extractLikelyJsonBlock(raw: string): string {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
function looksLikeStructuredPayload(raw: string): boolean {
|
|
106
|
-
return /"?(view|chartType|type|data|items|rows|products|results)"?\s*:/.test(raw);
|
|
106
|
+
return /"?(view|chartType|type|data|items|rows|products|results|columns|xAxisKey)"?\s*:/.test(raw);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
function stripMarkdownTables(raw: string): string {
|
|
@@ -204,12 +204,23 @@ function isLikelyUiOnlyMessage(text: string): boolean {
|
|
|
204
204
|
// ─── Tiny helpers ─────────────────────────────────────────────────────────────
|
|
205
205
|
|
|
206
206
|
function resolveImage(data: Record<string, unknown>): string | undefined {
|
|
207
|
-
|
|
207
|
+
const candidates = ['image', 'img', 'thumbnail', 'url', 'picture', 'link'];
|
|
208
|
+
for (const key of candidates) {
|
|
208
209
|
const v = data[key];
|
|
209
|
-
if (typeof v === 'string' && v) return v;
|
|
210
|
+
if (typeof v === 'string' && v.startsWith('http')) return v;
|
|
211
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
212
|
+
const subVal = (v as Record<string, unknown>).url || (v as Record<string, unknown>).link || (v as Record<string, unknown>).src;
|
|
213
|
+
if (typeof subVal === 'string' && subVal.startsWith('http')) return subVal;
|
|
214
|
+
}
|
|
210
215
|
}
|
|
211
|
-
if (Array.isArray(data.images)
|
|
212
|
-
|
|
216
|
+
if (Array.isArray(data.images)) {
|
|
217
|
+
for (const item of data.images) {
|
|
218
|
+
if (typeof item === 'string' && item.startsWith('http')) return item;
|
|
219
|
+
if (item && typeof item === 'object') {
|
|
220
|
+
const url = item.url || item.link || item.src;
|
|
221
|
+
if (typeof url === 'string' && url.startsWith('http')) return url;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
213
224
|
}
|
|
214
225
|
return undefined;
|
|
215
226
|
}
|
|
@@ -425,16 +436,21 @@ function resolveStructuredRows(config: Partial<RawUIConfig> & Record<string, unk
|
|
|
425
436
|
return [];
|
|
426
437
|
}
|
|
427
438
|
|
|
428
|
-
function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming,
|
|
439
|
+
function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, viewportSize = 'large' }: {
|
|
429
440
|
rawContent: string;
|
|
430
441
|
primaryColor?: string;
|
|
431
442
|
accentColor?: string;
|
|
432
443
|
isStreaming?: boolean;
|
|
433
|
-
onAddToCart?: (product: Product) => void;
|
|
434
444
|
viewportSize?: ChatViewportSize;
|
|
435
445
|
}) {
|
|
436
|
-
const result = React.useMemo<{ config: UIConfig } | { error: string } | { loading: true }>(() => {
|
|
437
|
-
if (isStreaming)
|
|
446
|
+
const result = React.useMemo<{ config: UIConfig } | { error: string } | { loading: true; silent?: boolean }>(() => {
|
|
447
|
+
if (isStreaming) {
|
|
448
|
+
const looksLikeCarousel = rawContent.includes('"view": "carousel"') ||
|
|
449
|
+
rawContent.includes('"view":"carousel"') ||
|
|
450
|
+
rawContent.includes('"type": "products"') ||
|
|
451
|
+
rawContent.includes('"type":"products"');
|
|
452
|
+
return { loading: true, silent: looksLikeCarousel };
|
|
453
|
+
}
|
|
438
454
|
try {
|
|
439
455
|
const clean = rawContent
|
|
440
456
|
.replace(/^```[a-z]*\s*/i, '')
|
|
@@ -495,6 +511,7 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
|
|
|
495
511
|
}, [rawContent, isStreaming]);
|
|
496
512
|
|
|
497
513
|
if ('loading' in result) {
|
|
514
|
+
if (result.silent) return null;
|
|
498
515
|
return (
|
|
499
516
|
<div className="my-4 p-8 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex flex-col items-center justify-center gap-3 animate-pulse">
|
|
500
517
|
<div className="w-5 h-5 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" />
|
|
@@ -547,16 +564,14 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
|
|
|
547
564
|
</div>
|
|
548
565
|
);
|
|
549
566
|
case 'carousel':
|
|
567
|
+
// We only render title/description here. The actual carousel is rendered separately
|
|
568
|
+
// at the bottom of the MessageBubble to avoid being trapped in the streaming loading box.
|
|
550
569
|
return (
|
|
551
570
|
<div className="my-4">
|
|
552
571
|
{config.title && <h4 className={`${isCompact ? 'mb-1.5 text-[11px]' : 'mb-2 text-xs'} font-semibold text-slate-500`}>{config.title}</h4>}
|
|
553
572
|
{hasDescription && (
|
|
554
573
|
<p className={`mb-3 ${isCompact ? 'text-xs' : 'text-sm'} text-slate-500 dark:text-white/60`}>{config.description}</p>
|
|
555
574
|
)}
|
|
556
|
-
<ProductCarousel products={(config.data as unknown as Product[]).map(item => ({
|
|
557
|
-
...item,
|
|
558
|
-
image: item.image ?? (typeof resolveImage === 'function' ? resolveImage(item as unknown as Record<string, unknown>) : undefined)
|
|
559
|
-
}))} primaryColor={primaryColor} onAddToCart={onAddToCart} />
|
|
560
575
|
</div>
|
|
561
576
|
);
|
|
562
577
|
case 'table':
|
|
@@ -599,11 +614,6 @@ export function MessageBubble({
|
|
|
599
614
|
const isCompact = viewportSize === 'compact';
|
|
600
615
|
const isMedium = viewportSize === 'medium';
|
|
601
616
|
const [showSources, setShowSources] = React.useState(false);
|
|
602
|
-
const hasStructuredProductBlock = React.useMemo(
|
|
603
|
-
() => /```(?:ui|json|chart)\s*[\s\S]*?"?(?:view|type|data|products)"?\s*:\s*"?(?:carousel|products|table)"?/i.test(message.content) ||
|
|
604
|
-
(/\{[\s\S]*?"?(?:view|type|data|products)"?\s*:\s*"?(?:carousel|products)"?[\s\S]*\}/i.test(message.content) && looksLikeStructuredPayload(message.content)),
|
|
605
|
-
[message.content],
|
|
606
|
-
);
|
|
607
617
|
const structuredContent = React.useMemo(
|
|
608
618
|
() => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
|
|
609
619
|
[isUser, message.content],
|
|
@@ -718,17 +728,23 @@ export function MessageBubble({
|
|
|
718
728
|
.replace(/(\|[^\n]*\|)\n\n+(?=\|)/g, '$1\n'); // Remove blank lines between rows
|
|
719
729
|
|
|
720
730
|
// 2. HEALING: Detect naked JSON charts/products and wrap them if markers are missing
|
|
721
|
-
// This handles models (like Llama 3.2) that often omit the ```
|
|
722
|
-
if (!isStreaming) {
|
|
731
|
+
// This handles models (like Llama 3.2) that often omit the ```ui markers.
|
|
732
|
+
if (!isStreaming && !hasStructuredUiBlock) {
|
|
723
733
|
// Look for objects that look like chart or product data
|
|
724
|
-
raw = raw.replace(/(?:^|\n)\s
|
|
734
|
+
raw = raw.replace(/(?:^|\n)\s*(\{[\s\S]+?\})(?:\s*\n|$)/g, (match, body) => {
|
|
725
735
|
// Only wrap if not already in a code block
|
|
726
736
|
if (match.includes('```')) return match;
|
|
727
|
-
|
|
728
|
-
|
|
737
|
+
const trimmedBody = body.trim();
|
|
738
|
+
if (!looksLikeStructuredPayload(trimmedBody)) return match;
|
|
739
|
+
|
|
740
|
+
const type = trimmedBody.includes('"view"') ||
|
|
741
|
+
trimmedBody.includes('"chartType"') ||
|
|
742
|
+
trimmedBody.includes('"type"') ||
|
|
743
|
+
trimmedBody.includes('"xAxisKey"')
|
|
729
744
|
? 'ui'
|
|
730
745
|
: 'json';
|
|
731
|
-
|
|
746
|
+
|
|
747
|
+
return `\n\n\`\`\`${type}\n${trimmedBody}\n\`\`\`\n\n`;
|
|
732
748
|
});
|
|
733
749
|
}
|
|
734
750
|
|
|
@@ -824,7 +840,6 @@ export function MessageBubble({
|
|
|
824
840
|
primaryColor={primaryColor}
|
|
825
841
|
accentColor={accentColor}
|
|
826
842
|
isStreaming={isStreaming}
|
|
827
|
-
onAddToCart={onAddToCart}
|
|
828
843
|
viewportSize={viewportSize}
|
|
829
844
|
/>
|
|
830
845
|
);
|
|
@@ -839,7 +854,6 @@ export function MessageBubble({
|
|
|
839
854
|
primaryColor={primaryColor}
|
|
840
855
|
accentColor={accentColor}
|
|
841
856
|
isStreaming={isStreaming}
|
|
842
|
-
onAddToCart={onAddToCart}
|
|
843
857
|
viewportSize={viewportSize}
|
|
844
858
|
/>
|
|
845
859
|
);
|
|
@@ -855,7 +869,6 @@ export function MessageBubble({
|
|
|
855
869
|
primaryColor={primaryColor}
|
|
856
870
|
accentColor={accentColor}
|
|
857
871
|
isStreaming={isStreaming}
|
|
858
|
-
onAddToCart={onAddToCart}
|
|
859
872
|
viewportSize={viewportSize}
|
|
860
873
|
/>
|
|
861
874
|
);
|
|
@@ -882,7 +895,7 @@ export function MessageBubble({
|
|
|
882
895
|
);
|
|
883
896
|
},
|
|
884
897
|
}),
|
|
885
|
-
[primaryColor, accentColor, isStreaming,
|
|
898
|
+
[primaryColor, accentColor, isStreaming, viewportSize],
|
|
886
899
|
);
|
|
887
900
|
|
|
888
901
|
// ── Render ─────────────────────────────────────────────────────────────────
|
|
@@ -924,7 +937,6 @@ export function MessageBubble({
|
|
|
924
937
|
primaryColor={primaryColor}
|
|
925
938
|
accentColor={accentColor}
|
|
926
939
|
isStreaming={isStreaming}
|
|
927
|
-
onAddToCart={onAddToCart}
|
|
928
940
|
viewportSize={viewportSize}
|
|
929
941
|
/>
|
|
930
942
|
)}
|
|
@@ -943,7 +955,7 @@ export function MessageBubble({
|
|
|
943
955
|
</div>
|
|
944
956
|
|
|
945
957
|
{/* Product Carousel */}
|
|
946
|
-
{!isUser &&
|
|
958
|
+
{!isUser && allProducts.length > 0 && (
|
|
947
959
|
<div className="w-full mt-1">
|
|
948
960
|
<ProductCarousel
|
|
949
961
|
products={allProducts}
|
package/src/core/Pipeline.ts
CHANGED
|
@@ -101,15 +101,16 @@ export class Pipeline {
|
|
|
101
101
|
|
|
102
102
|
// Augment system prompt with a Universal Visualization Protocol
|
|
103
103
|
// We use a unique marker to ensure this is only injected once but is ALWAYS present.
|
|
104
|
-
const CHART_MARKER = '<!--
|
|
104
|
+
const CHART_MARKER = '<!-- UI_PROTOCOL_V7 -->';
|
|
105
105
|
const chartInstruction = `\n\n${CHART_MARKER}
|
|
106
|
-
### UNIVERSAL UI PROTOCOL
|
|
107
|
-
When
|
|
106
|
+
### UNIVERSAL UI PROTOCOL (MANDATORY)
|
|
107
|
+
CRITICAL: When providing visual data (carousels, charts, tables), you MUST wrap the JSON payload in a \`\`\`ui\`\`\` code block.
|
|
108
|
+
NEVER output naked JSON. If you provide a visualization, do not also provide a markdown table of the same data unless explicitly asked.
|
|
108
109
|
|
|
109
110
|
1. VIEW SELECTION
|
|
110
|
-
- Use "carousel" for browsing
|
|
111
|
-
- Use "chart" ONLY for numerical aggregations
|
|
112
|
-
- Use "table" for
|
|
111
|
+
- Use "carousel" for browsing products, items, or catalogs. (e.g., "show me beauty products").
|
|
112
|
+
- Use "chart" ONLY for numerical aggregations (counts, sums, percentages).
|
|
113
|
+
- Use "table" for technical specs or row-heavy comparisons.
|
|
113
114
|
|
|
114
115
|
2. REQUIRED JSON SHAPE
|
|
115
116
|
{
|
|
@@ -117,52 +118,31 @@ When the user asks for a visual representation, comparison, distribution, breakd
|
|
|
117
118
|
"title": "Short heading",
|
|
118
119
|
"description": "One sentence describing the view",
|
|
119
120
|
"chartType": "pie" | "bar" | "line", // Required if view is "chart"
|
|
120
|
-
"
|
|
121
|
-
"dataKeys": ["value"],
|
|
122
|
-
"columns": ["dynamicFieldKey1", "dynamicFieldKey2"],
|
|
123
|
-
"insights": ["Short takeaway 1", "Short takeaway 2"],
|
|
124
|
-
"data": []
|
|
121
|
+
"data": [] // Array of objects
|
|
125
122
|
}
|
|
126
123
|
|
|
127
124
|
3. DATA RULES
|
|
128
|
-
- Build the UI payload from retrieved context
|
|
129
|
-
- For CAROUSEL: "data"
|
|
130
|
-
-
|
|
131
|
-
- If the user asks for "products in beauty", do NOT return a pie chart of categories; return a carousel of the beauty products themselves.
|
|
125
|
+
- Build the UI payload from retrieved context and metadata.
|
|
126
|
+
- For CAROUSEL: "data" items MUST have { id, name, brand, price, image, link }.
|
|
127
|
+
- IMPORTANT: Extract "image" and "price" from Source metadata. Image URLs are often in fields like "images", "thumbnail", or "img".
|
|
132
128
|
|
|
133
129
|
4. FORMAT RULES
|
|
134
|
-
-
|
|
135
|
-
-
|
|
130
|
+
- Wrap the JSON in \`\`\`ui ... \`\`\` blocks.
|
|
131
|
+
- Place natural language text OUTSIDE the block.
|
|
136
132
|
|
|
137
|
-
5.
|
|
138
|
-
User: "
|
|
139
|
-
Assistant:
|
|
133
|
+
5. EXAMPLE
|
|
134
|
+
User: "find me beauty products"
|
|
135
|
+
Assistant: "I found several beauty products in our catalog:"
|
|
140
136
|
\`\`\`ui
|
|
141
137
|
{
|
|
142
138
|
"view": "carousel",
|
|
143
139
|
"title": "Beauty Products",
|
|
144
|
-
"description": "
|
|
140
|
+
"description": "Latest skincare and makeup items.",
|
|
145
141
|
"data": [
|
|
146
|
-
{ "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "
|
|
147
|
-
{ "id": "2", "name": "Lipstick", "brand": "Chic", "price": "$12.99", "image": "...", "link": "..." }
|
|
142
|
+
{ "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "https://placehold.co/600x400?text=Product", "link": "https://example.com/product" }
|
|
148
143
|
]
|
|
149
144
|
}
|
|
150
145
|
\`\`\`
|
|
151
|
-
|
|
152
|
-
User: "breakdown of products by category"
|
|
153
|
-
Assistant:
|
|
154
|
-
\`\`\`ui
|
|
155
|
-
{
|
|
156
|
-
"view": "chart",
|
|
157
|
-
"title": "Category Breakdown",
|
|
158
|
-
"chartType": "pie",
|
|
159
|
-
"xAxisKey": "label",
|
|
160
|
-
"dataKeys": ["value"],
|
|
161
|
-
"data": [
|
|
162
|
-
{ "label": "Beauty", "value": 15 },
|
|
163
|
-
{ "label": "Fragrances", "value": 8 }
|
|
164
|
-
]
|
|
165
|
-
}
|
|
166
146
|
\`\`\``;
|
|
167
147
|
|
|
168
148
|
if (!this.config.llm.systemPrompt?.includes(CHART_MARKER)) {
|
|
@@ -407,7 +387,7 @@ Assistant:
|
|
|
407
387
|
|
|
408
388
|
// 4. Context Augmentation
|
|
409
389
|
let context = sources.length
|
|
410
|
-
? sources.map((m, i) => `[Source ${i + 1}]\
|
|
390
|
+
? sources.map((m, i) => `[Source ${i + 1}]\nContent: ${m.content}\nMetadata: ${JSON.stringify(m.metadata)}`).join('\n\n---\n\n')
|
|
411
391
|
: 'No relevant context found.';
|
|
412
392
|
|
|
413
393
|
if (graphData && graphData.nodes.length > 0) {
|