@retrivora-ai/rag-engine 1.5.3 → 1.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.
@@ -123,6 +123,7 @@ function normaliseChild(children: React.ReactNode): React.ReactNode {
123
123
  interface TableConfig {
124
124
  type: 'table';
125
125
  title?: string;
126
+ description?: string;
126
127
  columns?: string[]; // preferred: explicit ordered column labels
127
128
  dataKeys?: string[]; // fallback column keys (may omit xAxisKey)
128
129
  xAxisKey?: string; // row-label key to prepend when using dataKeys
@@ -210,72 +211,150 @@ function DataTable({ config }: { config: TableConfig }) {
210
211
  );
211
212
  }
212
213
 
213
- // ─── Chart / table code block dispatcher ─────────────────────────────────────
214
+ // ─── Universal UI Dispatcher ────────────────────────────────────────────────
215
+ // Handles charts, product carousels, and data tables via a single protocol.
214
216
 
215
- type ParsedConfig = ChartConfig | TableConfig;
217
+ interface UIConfig {
218
+ view: 'chart' | 'carousel' | 'table';
219
+ chartType?: 'pie' | 'bar' | 'line';
220
+ xAxisKey?: string;
221
+ dataKeys?: string[];
222
+ columns?: string[];
223
+ colors?: string[];
224
+ data: Record<string, unknown>[];
225
+ title?: string;
226
+ description?: string;
227
+ insights?: string[];
228
+ type?: string;
229
+ items?: Record<string, unknown>[];
230
+ }
216
231
 
217
- interface ChartBlockProps {
232
+ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAddToCart }: {
218
233
  rawContent: string;
219
- primaryColor: string;
220
- accentColor: string;
234
+ primaryColor?: string;
235
+ accentColor?: string;
221
236
  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
- >(() => {
237
+ onAddToCart?: (product: Product) => void;
238
+ }) {
239
+ const result = React.useMemo<{ config: UIConfig } | { error: string } | { loading: true }>(() => {
228
240
  if (isStreaming) return { loading: true };
229
- if (!rawContent || rawContent === 'undefined') return { error: 'Empty visualization config.' };
230
241
  try {
231
242
  const clean = rawContent
232
243
  .replace(/^```[a-z]*\s*/i, '')
233
244
  .replace(/```\s*$/, '')
234
245
  .trim();
235
- const config = JSON.parse(sanitizeJson(clean)) as ParsedConfig;
236
- return { config };
246
+ const parsed = JSON.parse(sanitizeJson(clean));
247
+ const config = { ...parsed };
248
+
249
+ // Smart Auto-Detection for legacy or simpler formats
250
+ if (!config.view) {
251
+ if (config.type === 'products' || Array.isArray(config.items)) {
252
+ config.view = 'carousel';
253
+ config.data = config.data || config.items || [];
254
+ } else if (['pie', 'bar', 'line'].includes(config.type) || ['pie', 'bar', 'line'].includes(config.chartType)) {
255
+ config.view = 'chart';
256
+ config.chartType = config.chartType || config.type || 'bar';
257
+ config.data = config.data || [];
258
+ } else {
259
+ config.view = 'table';
260
+ config.data = Array.isArray(config.data) ? config.data : (Array.isArray(config) ? config : []);
261
+ }
262
+ }
263
+
264
+ return { config: config as UIConfig };
237
265
  } catch (err) {
238
- console.error('[ChartBlock] Parsing failed.\nError:', err);
239
266
  return { error: String(err) };
240
267
  }
241
268
  }, [rawContent, isStreaming]);
242
269
 
243
270
  if ('loading' in result) {
244
271
  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>
272
+ <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">
273
+ <div className="w-5 h-5 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" />
274
+ <p className="text-xs text-slate-500 font-medium italic">Preparing view...</p>
248
275
  </div>
249
276
  );
250
277
  }
251
278
 
252
279
  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
- );
280
+ 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
281
  }
260
282
 
261
283
  const { config } = result;
262
-
263
- // Route table type to dedicated renderer; everything else goes to DynamicChart
264
- if (config.type === 'table') {
265
- return <DataTable config={config as TableConfig} />;
284
+ const hasInsights = Array.isArray(config.insights) && config.insights.length > 0;
285
+ const hasDescription = typeof config.description === 'string' && config.description.trim().length > 0;
286
+
287
+ switch (config.view) {
288
+ case 'chart':
289
+ return (
290
+ <div className="my-6 p-4 bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-white/10 shadow-sm overflow-hidden">
291
+ {config.title && <h4 className="text-xs font-semibold text-slate-500 mb-4 px-2">{config.title}</h4>}
292
+ {hasDescription && (
293
+ <p className="px-2 text-sm text-slate-500 dark:text-white/60">{config.description}</p>
294
+ )}
295
+ <DynamicChart
296
+ config={{
297
+ type: (config.chartType || 'bar') as 'bar' | 'line' | 'pie',
298
+ data: config.data as unknown as Record<string, string | number>[],
299
+ xAxisKey: config.xAxisKey,
300
+ dataKeys: config.dataKeys,
301
+ colors: config.colors,
302
+ }}
303
+ primaryColor={primaryColor}
304
+ accentColor={accentColor}
305
+ />
306
+ {hasInsights && (
307
+ <div className="mt-4 flex flex-wrap gap-2 px-2">
308
+ {config.insights?.map((insight) => (
309
+ <span
310
+ key={insight}
311
+ className="rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-[11px] font-medium text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-300"
312
+ >
313
+ {insight}
314
+ </span>
315
+ ))}
316
+ </div>
317
+ )}
318
+ </div>
319
+ );
320
+ case 'carousel':
321
+ return (
322
+ <div className="my-4">
323
+ {config.title && <h4 className="mb-2 text-xs font-semibold text-slate-500">{config.title}</h4>}
324
+ {hasDescription && (
325
+ <p className="mb-3 text-sm text-slate-500 dark:text-white/60">{config.description}</p>
326
+ )}
327
+ <ProductCarousel products={(config.data as unknown as Product[]).map(item => ({
328
+ ...item,
329
+ image: item.image ?? (typeof resolveImage === 'function' ? resolveImage(item as unknown as Record<string, unknown>) : undefined)
330
+ }))} primaryColor={primaryColor} onAddToCart={onAddToCart} />
331
+ </div>
332
+ );
333
+ case 'table':
334
+ return (
335
+ <div className="my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm overflow-auto max-h-[400px]">
336
+ {config.title && <h4 className="text-xs font-semibold text-slate-500 mb-2">{config.title}</h4>}
337
+ {hasDescription && (
338
+ <p className="mb-3 text-sm text-slate-500 dark:text-white/60">{config.description}</p>
339
+ )}
340
+ <DataTable config={{
341
+ type: 'table',
342
+ title: config.title,
343
+ description: config.description,
344
+ columns: config.columns,
345
+ dataKeys: config.dataKeys,
346
+ xAxisKey: config.xAxisKey,
347
+ data: config.data,
348
+ }} />
349
+ </div>
350
+ );
351
+ default:
352
+ return null;
266
353
  }
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
354
  }
278
355
 
356
+
357
+
279
358
  // ─── Main component ───────────────────────────────────────────────────────────
280
359
 
281
360
  export function MessageBubble({
@@ -288,6 +367,10 @@ export function MessageBubble({
288
367
  }: MessageBubbleProps) {
289
368
  const isUser = message.role === 'user';
290
369
  const [showSources, setShowSources] = React.useState(false);
370
+ const hasStructuredProductBlock = React.useMemo(
371
+ () => /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|type)"\s*:\s*"(?:carousel|products)"/i.test(message.content),
372
+ [message.content],
373
+ );
291
374
 
292
375
  // ── Products from sources ──────────────────────────────────────────────────
293
376
  const productsFromSources = React.useMemo<Product[]>(() => {
@@ -457,17 +540,33 @@ export function MessageBubble({
457
540
  }: React.HTMLAttributes<HTMLElement> & { inline?: boolean }) {
458
541
  const lang = /language-(\w+)/.exec(className ?? '')?.[1];
459
542
 
460
- if (!inline && lang === 'chart') {
543
+ if (!inline && (lang === 'ui' || lang === 'chart')) {
461
544
  return (
462
- <ChartBlock
545
+ <UIDispatcher
463
546
  rawContent={String(children ?? '').trim()}
464
547
  primaryColor={primaryColor}
465
548
  accentColor={accentColor}
466
549
  isStreaming={isStreaming}
550
+ onAddToCart={onAddToCart}
467
551
  />
468
552
  );
469
553
  }
470
554
 
555
+ if (!inline && lang === 'json') {
556
+ const content = String(children ?? '').trim();
557
+ if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
558
+ return (
559
+ <UIDispatcher
560
+ rawContent={content}
561
+ primaryColor={primaryColor}
562
+ accentColor={accentColor}
563
+ isStreaming={isStreaming}
564
+ onAddToCart={onAddToCart}
565
+ />
566
+ );
567
+ }
568
+ }
569
+
471
570
  if (!inline && lang === 'table-loading') {
472
571
  return (
473
572
  <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">
@@ -488,7 +587,7 @@ export function MessageBubble({
488
587
  );
489
588
  },
490
589
  }),
491
- [primaryColor, accentColor, isStreaming],
590
+ [primaryColor, accentColor, isStreaming, onAddToCart],
492
591
  );
493
592
 
494
593
  // ── Render ─────────────────────────────────────────────────────────────────
@@ -536,7 +635,7 @@ export function MessageBubble({
536
635
  </div>
537
636
 
538
637
  {/* Product Carousel */}
539
- {!isUser && allProducts.length > 0 && (
638
+ {!isUser && !hasStructuredProductBlock && allProducts.length > 0 && (
540
639
  <div className="w-full mt-1">
541
640
  <ProductCarousel
542
641
  products={allProducts}
@@ -569,4 +668,4 @@ export function MessageBubble({
569
668
  </div>
570
669
  </div>
571
670
  );
572
- }
671
+ }
@@ -101,15 +101,66 @@ export class Pipeline {
101
101
 
102
102
  // Augment system prompt with a Universal Visualization Protocol
103
103
  // We use a unique marker to ensure this is only injected once but is ALWAYS present.
104
- const CHART_MARKER = '<!-- VIZ_PROTOCOL_V2 -->';
104
+ const CHART_MARKER = '<!-- UI_PROTOCOL_V5 -->';
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
+ When the user asks for a visual representation, comparison, distribution, breakdown, table, list, catalog, products, carousel, stock view, or category summary, you MUST include exactly one \`\`\`ui\`\`\` block.
108
+
109
+ 1. VIEW SELECTION
110
+ - Use "chart" for distributions, percentages, counts, comparisons, trends, or "show me in a chart".
111
+ - Use "carousel" for browseable product recommendations or when the user asks to see products/items/cards.
112
+ - Use "table" for tabular/raw detail, explicit table/list requests, or when there are many attributes to compare.
113
+
114
+ 2. REQUIRED JSON SHAPE
115
+ {
116
+ "view": "chart" | "carousel" | "table",
117
+ "title": "Short heading",
118
+ "description": "One sentence describing the view",
119
+ "chartType": "pie" | "bar" | "line",
120
+ "xAxisKey": "label",
121
+ "dataKeys": ["value"],
122
+ "columns": ["name", "category", "price", "stockStatus"],
123
+ "insights": ["Short takeaway 1", "Short takeaway 2"],
124
+ "data": []
125
+ }
126
+
127
+ 3. DATA RULES
128
+ - Build the UI payload from retrieved context only. Do not invent products, categories, or stock values.
129
+ - Aggregate raw product rows when the user asks for a chart.
130
+ - For charts, every row in "data" must be flat JSON with primitive values only.
131
+ - Prefer { "label": "...", "value": number } for pie charts.
132
+ - If stock information exists, preserve it in each row with fields like "inStock", "inStockCount", "outOfStockCount", or "stockStatus".
133
+ - If the user asks to highlight what is in stock, include those stock-related fields in the chart data and mention the pattern in "insights".
134
+ - For carousel rows, include product-friendly fields such as id, name, brand, price, image, link, category, stockStatus.
135
+ - For tables, keep "data" row-oriented and include "columns" when possible.
136
+
137
+ 4. FORMAT RULES
138
+ - Never use ASCII art, markdown tables as the primary answer, or text-only fake charts when a UI block is requested.
139
+ - Put any short natural-language explanation before or after the \`\`\`ui\`\`\` block, but keep it concise.
140
+ - Return valid JSON inside the \`\`\`ui\`\`\` block.
141
+
142
+ 5. EXAMPLE
143
+ User: "provide me the distribution of products across categories and highlight which ones are in stock in a pie chart"
144
+ Assistant:
145
+ \`\`\`ui
146
+ {
147
+ "view": "chart",
148
+ "title": "Product Distribution Across Categories",
149
+ "description": "Pie chart showing product count by category with stock signals preserved per slice.",
150
+ "chartType": "pie",
151
+ "xAxisKey": "label",
152
+ "dataKeys": ["value"],
153
+ "insights": [
154
+ "Beauty has the highest product count.",
155
+ "Electronics has the largest number of in-stock products."
156
+ ],
157
+ "data": [
158
+ { "label": "Beauty", "value": 12, "inStockCount": 9, "outOfStockCount": 3 },
159
+ { "label": "Electronics", "value": 8, "inStockCount": 7, "outOfStockCount": 1 },
160
+ { "label": "Home", "value": 5, "inStockCount": 2, "outOfStockCount": 3 }
161
+ ]
162
+ }
163
+ \`\`\``;
113
164
 
114
165
  if (!this.config.llm.systemPrompt?.includes(CHART_MARKER)) {
115
166
  this.config.llm.systemPrompt = (this.config.llm.systemPrompt || '') + chartInstruction;