@retrivora-ai/rag-engine 1.5.3 → 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,37 +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
447
  const clean = rawContent.replace(/^```[a-z]*\s*/i, "").replace(/```\s*$/, "").trim();
449
- const config2 = JSON.parse(sanitizeJson(clean));
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
- console.error("[ChartBlock] Parsing failed.\nError:", err);
453
465
  return { error: String(err) };
454
466
  }
455
467
  }, [rawContent, isStreaming]);
456
468
  if ("loading" in result) {
457
- 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..."));
458
470
  }
459
471
  if ("error" in result) {
460
- 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));
461
473
  }
462
474
  const { config } = result;
463
- if (config.type === "table") {
464
- 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;
465
499
  }
466
- 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(
467
- DynamicChart,
468
- {
469
- config,
470
- primaryColor,
471
- accentColor
472
- }
473
- ));
474
500
  }
475
501
  function MessageBubble({
476
502
  message,
@@ -645,9 +671,9 @@ ${match.trim()}
645
671
  ]);
646
672
  var _a;
647
673
  const lang = (_a = /language-(\w+)/.exec(className != null ? className : "")) == null ? void 0 : _a[1];
648
- if (!inline && lang === "chart") {
674
+ if (!inline && (lang === "ui" || lang === "chart")) {
649
675
  return /* @__PURE__ */ import_react5.default.createElement(
650
- ChartBlock,
676
+ UIDispatcher,
651
677
  {
652
678
  rawContent: String(children != null ? children : "").trim(),
653
679
  primaryColor,
@@ -656,6 +682,20 @@ ${match.trim()}
656
682
  }
657
683
  );
658
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
+ }
659
699
  if (!inline && lang === "table-loading") {
660
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..."));
661
701
  }
package/dist/index.mjs CHANGED
@@ -403,37 +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
410
  const clean = rawContent.replace(/^```[a-z]*\s*/i, "").replace(/```\s*$/, "").trim();
412
- const config2 = JSON.parse(sanitizeJson(clean));
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
- console.error("[ChartBlock] Parsing failed.\nError:", err);
416
428
  return { error: String(err) };
417
429
  }
418
430
  }, [rawContent, isStreaming]);
419
431
  if ("loading" in result) {
420
- return /* @__PURE__ */ React5.createElement("div", { className: "my-4 p-8 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex flex-col items-center justify-center gap-3 select-none" }, /* @__PURE__ */ React5.createElement("div", { className: "w-6 h-6 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" }), /* @__PURE__ */ React5.createElement("p", { className: "text-xs text-slate-500 font-medium animate-pulse" }, "Preparing data visualization\u2026"));
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..."));
421
433
  }
422
434
  if ("error" in result) {
423
- return /* @__PURE__ */ React5.createElement("div", { className: "p-4 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg text-sm border border-red-100 dark:border-red-900/30" }, /* @__PURE__ */ React5.createElement("p", { className: "font-medium mb-1" }, "Failed to render visualization"), /* @__PURE__ */ React5.createElement("p", { className: "opacity-70 text-xs" }, "The generated configuration is invalid. Check the console for details."));
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));
424
436
  }
425
437
  const { config } = result;
426
- if (config.type === "table") {
427
- 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;
428
462
  }
429
- return /* @__PURE__ */ React5.createElement("div", { className: "my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm" }, /* @__PURE__ */ React5.createElement(
430
- DynamicChart,
431
- {
432
- config,
433
- primaryColor,
434
- accentColor
435
- }
436
- ));
437
463
  }
438
464
  function MessageBubble({
439
465
  message,
@@ -608,9 +634,9 @@ ${match.trim()}
608
634
  ]);
609
635
  var _a;
610
636
  const lang = (_a = /language-(\w+)/.exec(className != null ? className : "")) == null ? void 0 : _a[1];
611
- if (!inline && lang === "chart") {
637
+ if (!inline && (lang === "ui" || lang === "chart")) {
612
638
  return /* @__PURE__ */ React5.createElement(
613
- ChartBlock,
639
+ UIDispatcher,
614
640
  {
615
641
  rawContent: String(children != null ? children : "").trim(),
616
642
  primaryColor,
@@ -619,6 +645,20 @@ ${match.trim()}
619
645
  }
620
646
  );
621
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
+ }
622
662
  if (!inline && lang === "table-loading") {
623
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..."));
624
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.3",
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,72 +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
234
  const clean = rawContent
232
235
  .replace(/^```[a-z]*\s*/i, '')
233
236
  .replace(/```\s*$/, '')
234
237
  .trim();
235
- const config = JSON.parse(sanitizeJson(clean)) as ParsedConfig;
236
- return { config };
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 };
237
257
  } catch (err) {
238
- console.error('[ChartBlock] Parsing failed.\nError:', err);
239
258
  return { error: String(err) };
240
259
  }
241
260
  }, [rawContent, isStreaming]);
242
261
 
243
262
  if ('loading' in result) {
244
263
  return (
245
- <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">
246
- <div className="w-6 h-6 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" />
247
- <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>
248
267
  </div>
249
268
  );
250
269
  }
251
270
 
252
271
  if ('error' in result) {
253
- return (
254
- <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">
255
- <p className="font-medium mb-1">Failed to render visualization</p>
256
- <p className="opacity-70 text-xs">The generated configuration is invalid. Check the console for details.</p>
257
- </div>
258
- );
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>;
259
273
  }
260
274
 
261
275
  const { config } = result;
262
276
 
263
- // Route table type to dedicated renderer; everything else goes to DynamicChart
264
- if (config.type === 'table') {
265
- 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;
266
310
  }
267
-
268
- return (
269
- <div className="my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm">
270
- <DynamicChart
271
- config={config as ChartConfig}
272
- primaryColor={primaryColor}
273
- accentColor={accentColor}
274
- />
275
- </div>
276
- );
277
311
  }
278
312
 
313
+
314
+
279
315
  // ─── Main component ───────────────────────────────────────────────────────────
280
316
 
281
317
  export function MessageBubble({
@@ -457,9 +493,9 @@ export function MessageBubble({
457
493
  }: React.HTMLAttributes<HTMLElement> & { inline?: boolean }) {
458
494
  const lang = /language-(\w+)/.exec(className ?? '')?.[1];
459
495
 
460
- if (!inline && lang === 'chart') {
496
+ if (!inline && (lang === 'ui' || lang === 'chart')) {
461
497
  return (
462
- <ChartBlock
498
+ <UIDispatcher
463
499
  rawContent={String(children ?? '').trim()}
464
500
  primaryColor={primaryColor}
465
501
  accentColor={accentColor}
@@ -468,6 +504,20 @@ export function MessageBubble({
468
504
  );
469
505
  }
470
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
+
471
521
  if (!inline && lang === 'table-loading') {
472
522
  return (
473
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;