@retrivora-ai/rag-engine 1.5.2 → 1.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2259,17 +2259,23 @@ var Pipeline = class {
2259
2259
  async initialize() {
2260
2260
  var _a, _b;
2261
2261
  if (this.initialised) return;
2262
- const CHART_MARKER = "<!-- VIZ_PROTOCOL_V2 -->";
2262
+ const CHART_MARKER = "<!-- UI_PROTOCOL_V3 -->";
2263
2263
  const chartInstruction = `
2264
2264
 
2265
2265
  ${CHART_MARKER}
2266
- ### MANDATORY VISUALIZATION RULES:
2267
- - If asked for statistics, distribution, or comparisons: You MUST use the \`\`\`chart block.
2268
- - FORMAT: \`\`\`chart { "type": "pie" | "bar" | "line", "data": [ { "label": "X", "value": 10 } ] } \`\`\`
2269
- - RULES:
2270
- 1. ALWAYS include the \`\`\`chart and \`\`\` markers.
2271
- 2. NEVER use text, tables, or ASCII art for charts.
2272
- 3. Output ONLY the chart block. No Python, no text descriptions.`;
2266
+ ### UNIVERSAL UI PROTOCOL:
2267
+ You have a specialized UI system. For any structured data (charts, product lists, or tables), you MUST use a single \`\`\`ui\`\`\` block.
2268
+ 1. DECIDE the best "view" for the user's query:
2269
+ - "chart": For statistics, distributions, or comparisons. (Requires "chartType": "pie" | "bar" | "line")
2270
+ - "carousel": For browsing specific product items.
2271
+ - "table": For detailed raw data comparisons.
2272
+ 2. OUTPUT a \`\`\`ui\`\`\` block with this schema:
2273
+ {
2274
+ "view": "chart" | "carousel" | "table",
2275
+ "chartType": "pie" | "bar" | "line" (only for view: "chart"),
2276
+ "data": [ ... objects ... ]
2277
+ }
2278
+ 3. RULES: Valid JSON only. Data must be FLAT. Never draw charts/tables with text. Choose ONLY the most useful view.`;
2273
2279
  if (!((_a = this.config.llm.systemPrompt) == null ? void 0 : _a.includes(CHART_MARKER))) {
2274
2280
  this.config.llm.systemPrompt = (this.config.llm.systemPrompt || "") + chartInstruction;
2275
2281
  }
@@ -3822,17 +3822,23 @@ var Pipeline = class {
3822
3822
  async initialize() {
3823
3823
  var _a, _b;
3824
3824
  if (this.initialised) return;
3825
- const CHART_MARKER = "<!-- VIZ_PROTOCOL_V2 -->";
3825
+ const CHART_MARKER = "<!-- UI_PROTOCOL_V3 -->";
3826
3826
  const chartInstruction = `
3827
3827
 
3828
3828
  ${CHART_MARKER}
3829
- ### MANDATORY VISUALIZATION RULES:
3830
- - If asked for statistics, distribution, or comparisons: You MUST use the \`\`\`chart block.
3831
- - FORMAT: \`\`\`chart { "type": "pie" | "bar" | "line", "data": [ { "label": "X", "value": 10 } ] } \`\`\`
3832
- - RULES:
3833
- 1. ALWAYS include the \`\`\`chart and \`\`\` markers.
3834
- 2. NEVER use text, tables, or ASCII art for charts.
3835
- 3. Output ONLY the chart block. No Python, no text descriptions.`;
3829
+ ### UNIVERSAL UI PROTOCOL:
3830
+ You have a specialized UI system. For any structured data (charts, product lists, or tables), you MUST use a single \`\`\`ui\`\`\` block.
3831
+ 1. DECIDE the best "view" for the user's query:
3832
+ - "chart": For statistics, distributions, or comparisons. (Requires "chartType": "pie" | "bar" | "line")
3833
+ - "carousel": For browsing specific product items.
3834
+ - "table": For detailed raw data comparisons.
3835
+ 2. OUTPUT a \`\`\`ui\`\`\` block with this schema:
3836
+ {
3837
+ "view": "chart" | "carousel" | "table",
3838
+ "chartType": "pie" | "bar" | "line" (only for view: "chart"),
3839
+ "data": [ ... objects ... ]
3840
+ }
3841
+ 3. RULES: Valid JSON only. Data must be FLAT. Never draw charts/tables with text. Choose ONLY the most useful view.`;
3836
3842
  if (!((_a = this.config.llm.systemPrompt) == null ? void 0 : _a.includes(CHART_MARKER))) {
3837
3843
  this.config.llm.systemPrompt = (this.config.llm.systemPrompt || "") + chartInstruction;
3838
3844
  }
@@ -9,7 +9,7 @@ import {
9
9
  sseFrame,
10
10
  sseMetaFrame,
11
11
  sseTextFrame
12
- } from "../chunk-C4UPQX3E.mjs";
12
+ } from "../chunk-BLLMNP77.mjs";
13
13
  import "../chunk-YLTMFW4M.mjs";
14
14
  import "../chunk-X4TOT24V.mjs";
15
15
  export {
package/dist/index.js CHANGED
@@ -440,38 +440,63 @@ function DataTable({ config }) {
440
440
  ))
441
441
  ))))), /* @__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
442
  }
443
- function ChartBlock({ rawContent, primaryColor, accentColor, isStreaming }) {
443
+ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming }) {
444
444
  const result = import_react5.default.useMemo(() => {
445
445
  if (isStreaming) return { loading: true };
446
- if (!rawContent || rawContent === "undefined") return { error: "Empty visualization config." };
447
446
  try {
448
- const sanitized = sanitizeJson(rawContent);
449
- const config2 = JSON.parse(sanitized);
447
+ const clean = rawContent.replace(/^```[a-z]*\s*/i, "").replace(/```\s*$/, "").trim();
448
+ const parsed = JSON.parse(sanitizeJson(clean));
449
+ const config2 = __spreadValues({}, parsed);
450
+ if (!config2.view) {
451
+ if (config2.type === "products" || Array.isArray(config2.items)) {
452
+ config2.view = "carousel";
453
+ config2.data = config2.data || config2.items || [];
454
+ } else if (["pie", "bar", "line"].includes(config2.type) || ["pie", "bar", "line"].includes(config2.chartType)) {
455
+ config2.view = "chart";
456
+ config2.chartType = config2.chartType || config2.type || "bar";
457
+ config2.data = config2.data || [];
458
+ } else {
459
+ config2.view = "table";
460
+ config2.data = Array.isArray(config2.data) ? config2.data : Array.isArray(config2) ? config2 : [];
461
+ }
462
+ }
450
463
  return { config: config2 };
451
464
  } catch (err) {
452
- const sanitized = sanitizeJson(rawContent);
453
- console.error("[ChartBlock] Parsing failed.\nError:", err, "\nSanitized:\n", sanitized);
454
465
  return { error: String(err) };
455
466
  }
456
467
  }, [rawContent, isStreaming]);
457
468
  if ("loading" in result) {
458
- 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 select-none" }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "w-6 h-6 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 animate-pulse" }, "Preparing data visualization\u2026"));
469
+ 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..."));
459
470
  }
460
471
  if ("error" in result) {
461
- return /* @__PURE__ */ import_react5.default.createElement("div", { className: "p-4 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg text-sm border border-red-100 dark:border-red-900/30" }, /* @__PURE__ */ import_react5.default.createElement("p", { className: "font-medium mb-1" }, "Failed to render visualization"), /* @__PURE__ */ import_react5.default.createElement("p", { className: "opacity-70 text-xs" }, "The generated configuration is invalid. Check the console for details."));
472
+ 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));
462
473
  }
463
474
  const { config } = result;
464
- if (config.type === "table") {
465
- return /* @__PURE__ */ import_react5.default.createElement(DataTable, { config });
475
+ switch (config.view) {
476
+ 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(
478
+ DynamicChart,
479
+ {
480
+ config: {
481
+ type: config.chartType || "bar",
482
+ data: config.data
483
+ },
484
+ primaryColor,
485
+ accentColor
486
+ }
487
+ ));
488
+ case "carousel":
489
+ return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-4" }, /* @__PURE__ */ import_react5.default.createElement(ProductCarousel, { products: config.data.map((item) => {
490
+ var _a;
491
+ return __spreadProps(__spreadValues({}, item), {
492
+ image: (_a = item.image) != null ? _a : typeof resolveImage === "function" ? resolveImage(item) : void 0
493
+ });
494
+ }) }));
495
+ case "table":
496
+ return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm overflow-auto max-h-[400px]" }, config.title && /* @__PURE__ */ import_react5.default.createElement("h4", { className: "text-xs font-semibold text-slate-500 mb-2" }, config.title), /* @__PURE__ */ import_react5.default.createElement(DataTable, { config: { type: "table", data: config.data } }));
497
+ default:
498
+ return null;
466
499
  }
467
- 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" }, /* @__PURE__ */ import_react5.default.createElement(
468
- DynamicChart,
469
- {
470
- config,
471
- primaryColor,
472
- accentColor
473
- }
474
- ));
475
500
  }
476
501
  function MessageBubble({
477
502
  message,
@@ -646,9 +671,9 @@ ${match.trim()}
646
671
  ]);
647
672
  var _a;
648
673
  const lang = (_a = /language-(\w+)/.exec(className != null ? className : "")) == null ? void 0 : _a[1];
649
- if (!inline && lang === "chart") {
674
+ if (!inline && (lang === "ui" || lang === "chart")) {
650
675
  return /* @__PURE__ */ import_react5.default.createElement(
651
- ChartBlock,
676
+ UIDispatcher,
652
677
  {
653
678
  rawContent: String(children != null ? children : "").trim(),
654
679
  primaryColor,
@@ -657,6 +682,20 @@ ${match.trim()}
657
682
  }
658
683
  );
659
684
  }
685
+ if (!inline && lang === "json") {
686
+ const content = String(children != null ? children : "").trim();
687
+ if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
688
+ return /* @__PURE__ */ import_react5.default.createElement(
689
+ UIDispatcher,
690
+ {
691
+ rawContent: content,
692
+ primaryColor,
693
+ accentColor,
694
+ isStreaming
695
+ }
696
+ );
697
+ }
698
+ }
660
699
  if (!inline && lang === "table-loading") {
661
700
  return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-5 p-6 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex items-center justify-center gap-3 select-none" }, /* @__PURE__ */ import_react5.default.createElement("div", { className: "w-4 h-4 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 animate-pulse" }, "Generating data table..."));
662
701
  }
package/dist/index.mjs CHANGED
@@ -403,38 +403,63 @@ function DataTable({ config }) {
403
403
  ))
404
404
  ))))), /* @__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
405
  }
406
- function ChartBlock({ rawContent, primaryColor, accentColor, isStreaming }) {
406
+ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming }) {
407
407
  const result = React5.useMemo(() => {
408
408
  if (isStreaming) return { loading: true };
409
- if (!rawContent || rawContent === "undefined") return { error: "Empty visualization config." };
410
409
  try {
411
- const sanitized = sanitizeJson(rawContent);
412
- const config2 = JSON.parse(sanitized);
410
+ const clean = rawContent.replace(/^```[a-z]*\s*/i, "").replace(/```\s*$/, "").trim();
411
+ const parsed = JSON.parse(sanitizeJson(clean));
412
+ const config2 = __spreadValues({}, parsed);
413
+ if (!config2.view) {
414
+ if (config2.type === "products" || Array.isArray(config2.items)) {
415
+ config2.view = "carousel";
416
+ config2.data = config2.data || config2.items || [];
417
+ } else if (["pie", "bar", "line"].includes(config2.type) || ["pie", "bar", "line"].includes(config2.chartType)) {
418
+ config2.view = "chart";
419
+ config2.chartType = config2.chartType || config2.type || "bar";
420
+ config2.data = config2.data || [];
421
+ } else {
422
+ config2.view = "table";
423
+ config2.data = Array.isArray(config2.data) ? config2.data : Array.isArray(config2) ? config2 : [];
424
+ }
425
+ }
413
426
  return { config: config2 };
414
427
  } catch (err) {
415
- const sanitized = sanitizeJson(rawContent);
416
- console.error("[ChartBlock] Parsing failed.\nError:", err, "\nSanitized:\n", sanitized);
417
428
  return { error: String(err) };
418
429
  }
419
430
  }, [rawContent, isStreaming]);
420
431
  if ("loading" in result) {
421
- return /* @__PURE__ */ React5.createElement("div", { className: "my-4 p-8 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex flex-col items-center justify-center gap-3 select-none" }, /* @__PURE__ */ React5.createElement("div", { className: "w-6 h-6 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" }), /* @__PURE__ */ React5.createElement("p", { className: "text-xs text-slate-500 font-medium animate-pulse" }, "Preparing data visualization\u2026"));
432
+ 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..."));
422
433
  }
423
434
  if ("error" in result) {
424
- return /* @__PURE__ */ React5.createElement("div", { className: "p-4 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg text-sm border border-red-100 dark:border-red-900/30" }, /* @__PURE__ */ React5.createElement("p", { className: "font-medium mb-1" }, "Failed to render visualization"), /* @__PURE__ */ React5.createElement("p", { className: "opacity-70 text-xs" }, "The generated configuration is invalid. Check the console for details."));
435
+ 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));
425
436
  }
426
437
  const { config } = result;
427
- if (config.type === "table") {
428
- return /* @__PURE__ */ React5.createElement(DataTable, { config });
438
+ switch (config.view) {
439
+ 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(
441
+ DynamicChart,
442
+ {
443
+ config: {
444
+ type: config.chartType || "bar",
445
+ data: config.data
446
+ },
447
+ primaryColor,
448
+ accentColor
449
+ }
450
+ ));
451
+ case "carousel":
452
+ return /* @__PURE__ */ React5.createElement("div", { className: "my-4" }, /* @__PURE__ */ React5.createElement(ProductCarousel, { products: config.data.map((item) => {
453
+ var _a;
454
+ return __spreadProps(__spreadValues({}, item), {
455
+ image: (_a = item.image) != null ? _a : typeof resolveImage === "function" ? resolveImage(item) : void 0
456
+ });
457
+ }) }));
458
+ case "table":
459
+ return /* @__PURE__ */ React5.createElement("div", { className: "my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm overflow-auto max-h-[400px]" }, config.title && /* @__PURE__ */ React5.createElement("h4", { className: "text-xs font-semibold text-slate-500 mb-2" }, config.title), /* @__PURE__ */ React5.createElement(DataTable, { config: { type: "table", data: config.data } }));
460
+ default:
461
+ return null;
429
462
  }
430
- return /* @__PURE__ */ React5.createElement("div", { className: "my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm" }, /* @__PURE__ */ React5.createElement(
431
- DynamicChart,
432
- {
433
- config,
434
- primaryColor,
435
- accentColor
436
- }
437
- ));
438
463
  }
439
464
  function MessageBubble({
440
465
  message,
@@ -609,9 +634,9 @@ ${match.trim()}
609
634
  ]);
610
635
  var _a;
611
636
  const lang = (_a = /language-(\w+)/.exec(className != null ? className : "")) == null ? void 0 : _a[1];
612
- if (!inline && lang === "chart") {
637
+ if (!inline && (lang === "ui" || lang === "chart")) {
613
638
  return /* @__PURE__ */ React5.createElement(
614
- ChartBlock,
639
+ UIDispatcher,
615
640
  {
616
641
  rawContent: String(children != null ? children : "").trim(),
617
642
  primaryColor,
@@ -620,6 +645,20 @@ ${match.trim()}
620
645
  }
621
646
  );
622
647
  }
648
+ if (!inline && lang === "json") {
649
+ const content = String(children != null ? children : "").trim();
650
+ if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
651
+ return /* @__PURE__ */ React5.createElement(
652
+ UIDispatcher,
653
+ {
654
+ rawContent: content,
655
+ primaryColor,
656
+ accentColor,
657
+ isStreaming
658
+ }
659
+ );
660
+ }
661
+ }
623
662
  if (!inline && lang === "table-loading") {
624
663
  return /* @__PURE__ */ React5.createElement("div", { className: "my-5 p-6 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex items-center justify-center gap-3 select-none" }, /* @__PURE__ */ React5.createElement("div", { className: "w-4 h-4 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" }), /* @__PURE__ */ React5.createElement("p", { className: "text-xs text-slate-500 font-medium italic animate-pulse" }, "Generating data table..."));
625
664
  }
package/dist/server.js CHANGED
@@ -3913,17 +3913,23 @@ var Pipeline = class {
3913
3913
  async initialize() {
3914
3914
  var _a, _b;
3915
3915
  if (this.initialised) return;
3916
- const CHART_MARKER = "<!-- VIZ_PROTOCOL_V2 -->";
3916
+ const CHART_MARKER = "<!-- UI_PROTOCOL_V3 -->";
3917
3917
  const chartInstruction = `
3918
3918
 
3919
3919
  ${CHART_MARKER}
3920
- ### MANDATORY VISUALIZATION RULES:
3921
- - If asked for statistics, distribution, or comparisons: You MUST use the \`\`\`chart block.
3922
- - FORMAT: \`\`\`chart { "type": "pie" | "bar" | "line", "data": [ { "label": "X", "value": 10 } ] } \`\`\`
3923
- - RULES:
3924
- 1. ALWAYS include the \`\`\`chart and \`\`\` markers.
3925
- 2. NEVER use text, tables, or ASCII art for charts.
3926
- 3. Output ONLY the chart block. No Python, no text descriptions.`;
3920
+ ### UNIVERSAL UI PROTOCOL:
3921
+ You have a specialized UI system. For any structured data (charts, product lists, or tables), you MUST use a single \`\`\`ui\`\`\` block.
3922
+ 1. DECIDE the best "view" for the user's query:
3923
+ - "chart": For statistics, distributions, or comparisons. (Requires "chartType": "pie" | "bar" | "line")
3924
+ - "carousel": For browsing specific product items.
3925
+ - "table": For detailed raw data comparisons.
3926
+ 2. OUTPUT a \`\`\`ui\`\`\` block with this schema:
3927
+ {
3928
+ "view": "chart" | "carousel" | "table",
3929
+ "chartType": "pie" | "bar" | "line" (only for view: "chart"),
3930
+ "data": [ ... objects ... ]
3931
+ }
3932
+ 3. RULES: Valid JSON only. Data must be FLAT. Never draw charts/tables with text. Choose ONLY the most useful view.`;
3927
3933
  if (!((_a = this.config.llm.systemPrompt) == null ? void 0 : _a.includes(CHART_MARKER))) {
3928
3934
  this.config.llm.systemPrompt = (this.config.llm.systemPrompt || "") + chartInstruction;
3929
3935
  }
package/dist/server.mjs CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  sseFrame,
40
40
  sseMetaFrame,
41
41
  sseTextFrame
42
- } from "./chunk-C4UPQX3E.mjs";
42
+ } from "./chunk-BLLMNP77.mjs";
43
43
  import "./chunk-YLTMFW4M.mjs";
44
44
  import {
45
45
  PineconeProvider
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retrivora-ai/rag-engine",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
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",
@@ -7,7 +7,7 @@ import remarkGfm from 'remark-gfm';
7
7
  import { MessageBubbleProps, Product } from '../types';
8
8
  import { SourceCard } from './SourceCard';
9
9
  import { ProductCarousel } from './ProductCarousel';
10
- import { DynamicChart, ChartConfig } from './DynamicChart';
10
+ import { DynamicChart } from './DynamicChart';
11
11
 
12
12
  // ─── JSON sanitization ────────────────────────────────────────────────────────
13
13
  // Order matters: each step is a targeted fix, no step undoes a previous one.
@@ -210,70 +210,108 @@ function DataTable({ config }: { config: TableConfig }) {
210
210
  );
211
211
  }
212
212
 
213
- // ─── Chart / table code block dispatcher ─────────────────────────────────────
213
+ // ─── Universal UI Dispatcher ────────────────────────────────────────────────
214
+ // Handles charts, product carousels, and data tables via a single protocol.
214
215
 
215
- type ParsedConfig = ChartConfig | TableConfig;
216
+ interface UIConfig {
217
+ view: 'chart' | 'carousel' | 'table';
218
+ chartType?: 'pie' | 'bar' | 'line';
219
+ data: Record<string, unknown>[];
220
+ title?: string;
221
+ type?: string;
222
+ items?: Record<string, unknown>[];
223
+ }
216
224
 
217
- interface ChartBlockProps {
225
+ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming }: {
218
226
  rawContent: string;
219
- primaryColor: string;
220
- accentColor: string;
227
+ primaryColor?: string;
228
+ accentColor?: string;
221
229
  isStreaming?: boolean;
222
- }
223
-
224
- function ChartBlock({ rawContent, primaryColor, accentColor, isStreaming }: ChartBlockProps) {
225
- const result = React.useMemo<
226
- { config: ParsedConfig } | { error: string } | { loading: true }
227
- >(() => {
230
+ }) {
231
+ const result = React.useMemo<{ config: UIConfig } | { error: string } | { loading: true }>(() => {
228
232
  if (isStreaming) return { loading: true };
229
- if (!rawContent || rawContent === 'undefined') return { error: 'Empty visualization config.' };
230
233
  try {
231
- const sanitized = sanitizeJson(rawContent);
232
- const config = JSON.parse(sanitized) as ParsedConfig;
233
- return { config };
234
+ const clean = rawContent
235
+ .replace(/^```[a-z]*\s*/i, '')
236
+ .replace(/```\s*$/, '')
237
+ .trim();
238
+ const parsed = JSON.parse(sanitizeJson(clean));
239
+ const config = { ...parsed };
240
+
241
+ // Smart Auto-Detection for legacy or simpler formats
242
+ if (!config.view) {
243
+ if (config.type === 'products' || Array.isArray(config.items)) {
244
+ config.view = 'carousel';
245
+ config.data = config.data || config.items || [];
246
+ } else if (['pie', 'bar', 'line'].includes(config.type) || ['pie', 'bar', 'line'].includes(config.chartType)) {
247
+ config.view = 'chart';
248
+ config.chartType = config.chartType || config.type || 'bar';
249
+ config.data = config.data || [];
250
+ } else {
251
+ config.view = 'table';
252
+ config.data = Array.isArray(config.data) ? config.data : (Array.isArray(config) ? config : []);
253
+ }
254
+ }
255
+
256
+ return { config: config as UIConfig };
234
257
  } catch (err) {
235
- const sanitized = sanitizeJson(rawContent);
236
- console.error('[ChartBlock] Parsing failed.\nError:', err, '\nSanitized:\n', sanitized);
237
258
  return { error: String(err) };
238
259
  }
239
260
  }, [rawContent, isStreaming]);
240
261
 
241
262
  if ('loading' in result) {
242
263
  return (
243
- <div className="my-4 p-8 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex flex-col items-center justify-center gap-3 select-none">
244
- <div className="w-6 h-6 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" />
245
- <p className="text-xs text-slate-500 font-medium animate-pulse">Preparing data visualization…</p>
264
+ <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">
265
+ <div className="w-5 h-5 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" />
266
+ <p className="text-xs text-slate-500 font-medium italic">Preparing view...</p>
246
267
  </div>
247
268
  );
248
269
  }
249
270
 
250
271
  if ('error' in result) {
251
- return (
252
- <div className="p-4 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg text-sm border border-red-100 dark:border-red-900/30">
253
- <p className="font-medium mb-1">Failed to render visualization</p>
254
- <p className="opacity-70 text-xs">The generated configuration is invalid. Check the console for details.</p>
255
- </div>
256
- );
272
+ return <pre className="p-4 my-2 bg-slate-900 text-slate-100 rounded-lg text-[10px] overflow-auto max-h-40"><code>{rawContent}</code></pre>;
257
273
  }
258
274
 
259
275
  const { config } = result;
260
276
 
261
- // Route table type to dedicated renderer; everything else goes to DynamicChart
262
- if (config.type === 'table') {
263
- return <DataTable config={config as TableConfig} />;
277
+ switch (config.view) {
278
+ case 'chart':
279
+ return (
280
+ <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
+ {config.title && <h4 className="text-xs font-semibold text-slate-500 mb-4 px-2">{config.title}</h4>}
282
+ <DynamicChart
283
+ config={{
284
+ type: (config.chartType || 'bar') as 'bar' | 'line' | 'pie',
285
+ data: config.data as unknown as Record<string, string | number>[]
286
+ }}
287
+ primaryColor={primaryColor}
288
+ accentColor={accentColor}
289
+ />
290
+ </div>
291
+ );
292
+ case 'carousel':
293
+ return (
294
+ <div className="my-4">
295
+ <ProductCarousel products={(config.data as unknown as Product[]).map(item => ({
296
+ ...item,
297
+ image: item.image ?? (typeof resolveImage === 'function' ? resolveImage(item as unknown as Record<string, unknown>) : undefined)
298
+ }))} />
299
+ </div>
300
+ );
301
+ case 'table':
302
+ return (
303
+ <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
+ {config.title && <h4 className="text-xs font-semibold text-slate-500 mb-2">{config.title}</h4>}
305
+ <DataTable config={{ type: 'table', data: config.data } as TableConfig} />
306
+ </div>
307
+ );
308
+ default:
309
+ return null;
264
310
  }
265
-
266
- return (
267
- <div className="my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm">
268
- <DynamicChart
269
- config={config as ChartConfig}
270
- primaryColor={primaryColor}
271
- accentColor={accentColor}
272
- />
273
- </div>
274
- );
275
311
  }
276
312
 
313
+
314
+
277
315
  // ─── Main component ───────────────────────────────────────────────────────────
278
316
 
279
317
  export function MessageBubble({
@@ -455,9 +493,9 @@ export function MessageBubble({
455
493
  }: React.HTMLAttributes<HTMLElement> & { inline?: boolean }) {
456
494
  const lang = /language-(\w+)/.exec(className ?? '')?.[1];
457
495
 
458
- if (!inline && lang === 'chart') {
496
+ if (!inline && (lang === 'ui' || lang === 'chart')) {
459
497
  return (
460
- <ChartBlock
498
+ <UIDispatcher
461
499
  rawContent={String(children ?? '').trim()}
462
500
  primaryColor={primaryColor}
463
501
  accentColor={accentColor}
@@ -466,6 +504,20 @@ export function MessageBubble({
466
504
  );
467
505
  }
468
506
 
507
+ if (!inline && lang === 'json') {
508
+ const content = String(children ?? '').trim();
509
+ if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
510
+ return (
511
+ <UIDispatcher
512
+ rawContent={content}
513
+ primaryColor={primaryColor}
514
+ accentColor={accentColor}
515
+ isStreaming={isStreaming}
516
+ />
517
+ );
518
+ }
519
+ }
520
+
469
521
  if (!inline && lang === 'table-loading') {
470
522
  return (
471
523
  <div className="my-5 p-6 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex items-center justify-center gap-3 select-none">
@@ -101,15 +101,21 @@ 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 = '<!-- VIZ_PROTOCOL_V2 -->';
104
+ const CHART_MARKER = '<!-- UI_PROTOCOL_V3 -->';
105
105
  const chartInstruction = `\n\n${CHART_MARKER}
106
- ### MANDATORY VISUALIZATION RULES:
107
- - If asked for statistics, distribution, or comparisons: You MUST use the \`\`\`chart block.
108
- - FORMAT: \`\`\`chart { "type": "pie" | "bar" | "line", "data": [ { "label": "X", "value": 10 } ] } \`\`\`
109
- - RULES:
110
- 1. ALWAYS include the \`\`\`chart and \`\`\` markers.
111
- 2. NEVER use text, tables, or ASCII art for charts.
112
- 3. Output ONLY the chart block. No Python, no text descriptions.`;
106
+ ### UNIVERSAL UI PROTOCOL:
107
+ You have a specialized UI system. For any structured data (charts, product lists, or tables), you MUST use a single \`\`\`ui\`\`\` block.
108
+ 1. DECIDE the best "view" for the user's query:
109
+ - "chart": For statistics, distributions, or comparisons. (Requires "chartType": "pie" | "bar" | "line")
110
+ - "carousel": For browsing specific product items.
111
+ - "table": For detailed raw data comparisons.
112
+ 2. OUTPUT a \`\`\`ui\`\`\` block with this schema:
113
+ {
114
+ "view": "chart" | "carousel" | "table",
115
+ "chartType": "pie" | "bar" | "line" (only for view: "chart"),
116
+ "data": [ ... objects ... ]
117
+ }
118
+ 3. RULES: Valid JSON only. Data must be FLAT. Never draw charts/tables with text. Choose ONLY the most useful view.`;
113
119
 
114
120
  if (!this.config.llm.systemPrompt?.includes(CHART_MARKER)) {
115
121
  this.config.llm.systemPrompt = (this.config.llm.systemPrompt || '') + chartInstruction;