@retrivora-ai/rag-engine 1.5.5 → 1.5.7

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.
@@ -5,6 +5,7 @@ import { Bot, User, ChevronDown, ChevronRight } from 'lucide-react';
5
5
  import ReactMarkdown from 'react-markdown';
6
6
  import remarkGfm from 'remark-gfm';
7
7
  import { MessageBubbleProps, Product } from '../types';
8
+ import { ChatViewportSize } from '../types/props';
8
9
  import { SourceCard } from './SourceCard';
9
10
  import { ProductCarousel } from './ProductCarousel';
10
11
  import { DynamicChart } from './DynamicChart';
@@ -90,6 +91,17 @@ function sanitizeJson(raw: string): string {
90
91
  return s;
91
92
  }
92
93
 
94
+ function extractLikelyJsonBlock(raw: string): string {
95
+ const trimmed = raw.trim();
96
+ const objectStart = trimmed.indexOf('{');
97
+ const arrayStart = trimmed.indexOf('[');
98
+
99
+ const starts = [objectStart, arrayStart].filter((index) => index >= 0);
100
+ if (starts.length === 0) return trimmed;
101
+
102
+ return trimmed.slice(Math.min(...starts));
103
+ }
104
+
93
105
  // ─── Tiny helpers ─────────────────────────────────────────────────────────────
94
106
 
95
107
  function resolveImage(data: Record<string, unknown>): string | undefined {
@@ -130,7 +142,9 @@ interface TableConfig {
130
142
  data: Record<string, unknown>[];
131
143
  }
132
144
 
133
- function DataTable({ config }: { config: TableConfig }) {
145
+ function DataTable({ config, viewportSize = 'large' }: { config: TableConfig; viewportSize?: ChatViewportSize }) {
146
+ const isCompact = viewportSize === 'compact';
147
+ const isMedium = viewportSize === 'medium';
134
148
  const keys = React.useMemo<string[]>(() => {
135
149
  // Priority 1: explicit columns list
136
150
  if (Array.isArray(config.columns) && config.columns.length) {
@@ -169,13 +183,13 @@ function DataTable({ config }: { config: TableConfig }) {
169
183
  )}
170
184
 
171
185
  <div className="overflow-x-auto">
172
- <table className="w-full text-left border-collapse min-w-[320px] text-sm">
186
+ <table className={`w-full text-left border-collapse ${isCompact ? 'min-w-[240px] text-xs' : isMedium ? 'min-w-[280px] text-[13px]' : 'min-w-[320px] text-sm'}`}>
173
187
  <thead className="bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10">
174
188
  <tr>
175
189
  {keys.map(k => (
176
190
  <th
177
191
  key={k}
178
- className="px-4 py-3 font-semibold text-slate-700 dark:text-white/90 whitespace-nowrap"
192
+ className={`${isCompact ? 'px-2.5 py-2' : 'px-4 py-3'} font-semibold text-slate-700 dark:text-white/90 whitespace-nowrap`}
179
193
  >
180
194
  {k}
181
195
  </th>
@@ -191,7 +205,7 @@ function DataTable({ config }: { config: TableConfig }) {
191
205
  {keys.map(k => (
192
206
  <td
193
207
  key={k}
194
- className="px-4 py-3 text-slate-600 dark:text-white/70 whitespace-nowrap"
208
+ className={`${isCompact ? 'px-2.5 py-2' : 'px-4 py-3'} text-slate-600 dark:text-white/70 whitespace-nowrap`}
195
209
  >
196
210
  {formatCell(row[k])}
197
211
  </td>
@@ -227,14 +241,63 @@ interface UIConfig {
227
241
  insights?: string[];
228
242
  type?: string;
229
243
  items?: Record<string, unknown>[];
244
+ rows?: Record<string, unknown>[];
245
+ products?: Record<string, unknown>[];
246
+ results?: Record<string, unknown>[];
230
247
  }
231
248
 
232
- function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAddToCart }: {
249
+ interface RawUIConfig extends Omit<UIConfig, 'data' | 'items' | 'rows' | 'products' | 'results'> {
250
+ data: Array<Record<string, unknown> | unknown[]>;
251
+ items?: Array<Record<string, unknown> | unknown[]>;
252
+ rows?: Array<Record<string, unknown> | unknown[]>;
253
+ products?: Array<Record<string, unknown> | unknown[]>;
254
+ results?: Array<Record<string, unknown> | unknown[]>;
255
+ }
256
+
257
+ function normalizeTabularRows(
258
+ rows: Array<Record<string, unknown> | unknown[]> | undefined,
259
+ columns?: string[],
260
+ ): Record<string, unknown>[] {
261
+ if (!Array.isArray(rows)) return [];
262
+
263
+ return rows
264
+ .map((row) => {
265
+ if (Array.isArray(row)) {
266
+ const keys = Array.isArray(columns) && columns.length > 0
267
+ ? columns
268
+ : row.map((_, index) => `column_${index + 1}`);
269
+
270
+ return keys.reduce<Record<string, unknown>>((acc, key, index) => {
271
+ acc[key] = row[index];
272
+ return acc;
273
+ }, {});
274
+ }
275
+
276
+ if (row && typeof row === 'object') {
277
+ return row as Record<string, unknown>;
278
+ }
279
+
280
+ return { value: row };
281
+ })
282
+ .filter((row) => Object.keys(row).length > 0);
283
+ }
284
+
285
+ function resolveStructuredRows(config: Partial<RawUIConfig> & Record<string, unknown>) {
286
+ if (Array.isArray(config.data) && config.data.length > 0) return config.data;
287
+ if (Array.isArray(config.rows) && config.rows.length > 0) return config.rows;
288
+ if (Array.isArray(config.items) && config.items.length > 0) return config.items;
289
+ if (Array.isArray(config.products) && config.products.length > 0) return config.products;
290
+ if (Array.isArray(config.results) && config.results.length > 0) return config.results;
291
+ return [];
292
+ }
293
+
294
+ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAddToCart, viewportSize = 'large' }: {
233
295
  rawContent: string;
234
296
  primaryColor?: string;
235
297
  accentColor?: string;
236
298
  isStreaming?: boolean;
237
299
  onAddToCart?: (product: Product) => void;
300
+ viewportSize?: ChatViewportSize;
238
301
  }) {
239
302
  const result = React.useMemo<{ config: UIConfig } | { error: string } | { loading: true }>(() => {
240
303
  if (isStreaming) return { loading: true };
@@ -243,17 +306,26 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
243
306
  .replace(/^```[a-z]*\s*/i, '')
244
307
  .replace(/```\s*$/, '')
245
308
  .trim();
246
- const parsed = JSON.parse(sanitizeJson(clean));
247
- const config = { ...parsed };
309
+ const parsed = JSON.parse(sanitizeJson(extractLikelyJsonBlock(clean)));
310
+ const config = { ...parsed } as Partial<RawUIConfig> & Record<string, unknown>;
248
311
 
249
312
  // Smart Auto-Detection for legacy or simpler formats
250
313
  if (!config.view) {
251
314
  if (config.type === 'products' || Array.isArray(config.items)) {
252
315
  config.view = 'carousel';
253
316
  config.data = config.data || config.items || [];
254
- } else if (['pie', 'bar', 'line'].includes(config.type) || ['pie', 'bar', 'line'].includes(config.chartType)) {
317
+ } else if (
318
+ (typeof config.type === 'string' && ['pie', 'bar', 'line'].includes(config.type)) ||
319
+ (typeof config.chartType === 'string' && ['pie', 'bar', 'line'].includes(config.chartType))
320
+ ) {
321
+ const resolvedChartType =
322
+ config.chartType === 'pie' || config.chartType === 'bar' || config.chartType === 'line'
323
+ ? config.chartType
324
+ : config.type === 'pie' || config.type === 'bar' || config.type === 'line'
325
+ ? config.type
326
+ : 'bar';
255
327
  config.view = 'chart';
256
- config.chartType = config.chartType || config.type || 'bar';
328
+ config.chartType = resolvedChartType;
257
329
  config.data = config.data || [];
258
330
  } else {
259
331
  config.view = 'table';
@@ -261,7 +333,13 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
261
333
  }
262
334
  }
263
335
 
264
- return { config: config as UIConfig };
336
+ const resolvedRows = resolveStructuredRows(config);
337
+ const normalizedConfig: UIConfig = {
338
+ ...(config as Omit<UIConfig, 'data'>),
339
+ data: normalizeTabularRows(resolvedRows, config.columns),
340
+ };
341
+
342
+ return { config: normalizedConfig };
265
343
  } catch (err) {
266
344
  return { error: String(err) };
267
345
  }
@@ -281,16 +359,17 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
281
359
  }
282
360
 
283
361
  const { config } = result;
362
+ const isCompact = viewportSize === 'compact';
284
363
  const hasInsights = Array.isArray(config.insights) && config.insights.length > 0;
285
364
  const hasDescription = typeof config.description === 'string' && config.description.trim().length > 0;
286
365
 
287
366
  switch (config.view) {
288
367
  case 'chart':
289
368
  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>}
369
+ <div className={`${isCompact ? 'my-4 p-3' : 'my-6 p-4'} bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-white/10 shadow-sm overflow-hidden`}>
370
+ {config.title && <h4 className={`${isCompact ? 'text-[11px] mb-3' : 'text-xs mb-4'} font-semibold text-slate-500 px-2`}>{config.title}</h4>}
292
371
  {hasDescription && (
293
- <p className="px-2 text-sm text-slate-500 dark:text-white/60">{config.description}</p>
372
+ <p className={`px-2 ${isCompact ? 'text-xs' : 'text-sm'} text-slate-500 dark:text-white/60`}>{config.description}</p>
294
373
  )}
295
374
  <DynamicChart
296
375
  config={{
@@ -302,13 +381,14 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
302
381
  }}
303
382
  primaryColor={primaryColor}
304
383
  accentColor={accentColor}
384
+ viewportSize={viewportSize}
305
385
  />
306
386
  {hasInsights && (
307
387
  <div className="mt-4 flex flex-wrap gap-2 px-2">
308
388
  {config.insights?.map((insight) => (
309
389
  <span
310
390
  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"
391
+ className={`rounded-full border border-emerald-200 bg-emerald-50 ${isCompact ? 'px-2.5 py-1 text-[10px]' : '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
392
  >
313
393
  {insight}
314
394
  </span>
@@ -320,9 +400,9 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
320
400
  case 'carousel':
321
401
  return (
322
402
  <div className="my-4">
323
- {config.title && <h4 className="mb-2 text-xs font-semibold text-slate-500">{config.title}</h4>}
403
+ {config.title && <h4 className={`${isCompact ? 'mb-1.5 text-[11px]' : 'mb-2 text-xs'} font-semibold text-slate-500`}>{config.title}</h4>}
324
404
  {hasDescription && (
325
- <p className="mb-3 text-sm text-slate-500 dark:text-white/60">{config.description}</p>
405
+ <p className={`mb-3 ${isCompact ? 'text-xs' : 'text-sm'} text-slate-500 dark:text-white/60`}>{config.description}</p>
326
406
  )}
327
407
  <ProductCarousel products={(config.data as unknown as Product[]).map(item => ({
328
408
  ...item,
@@ -332,10 +412,10 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
332
412
  );
333
413
  case 'table':
334
414
  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>}
415
+ <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`}>
416
+ {config.title && <h4 className={`${isCompact ? 'text-[11px]' : 'text-xs'} font-semibold text-slate-500 mb-2`}>{config.title}</h4>}
337
417
  {hasDescription && (
338
- <p className="mb-3 text-sm text-slate-500 dark:text-white/60">{config.description}</p>
418
+ <p className={`mb-3 ${isCompact ? 'text-xs' : 'text-sm'} text-slate-500 dark:text-white/60`}>{config.description}</p>
339
419
  )}
340
420
  <DataTable config={{
341
421
  type: 'table',
@@ -345,7 +425,7 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
345
425
  dataKeys: config.dataKeys,
346
426
  xAxisKey: config.xAxisKey,
347
427
  data: config.data,
348
- }} />
428
+ }} viewportSize={viewportSize} />
349
429
  </div>
350
430
  );
351
431
  default:
@@ -364,8 +444,11 @@ export function MessageBubble({
364
444
  primaryColor = '#6366f1',
365
445
  accentColor = '#8b5cf6',
366
446
  onAddToCart,
447
+ viewportSize = 'large',
367
448
  }: MessageBubbleProps) {
368
449
  const isUser = message.role === 'user';
450
+ const isCompact = viewportSize === 'compact';
451
+ const isMedium = viewportSize === 'medium';
369
452
  const [showSources, setShowSources] = React.useState(false);
370
453
  const hasStructuredProductBlock = React.useMemo(
371
454
  () => /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|type)"\s*:\s*"(?:carousel|products)"/i.test(message.content),
@@ -548,6 +631,7 @@ export function MessageBubble({
548
631
  accentColor={accentColor}
549
632
  isStreaming={isStreaming}
550
633
  onAddToCart={onAddToCart}
634
+ viewportSize={viewportSize}
551
635
  />
552
636
  );
553
637
  }
@@ -562,6 +646,7 @@ export function MessageBubble({
562
646
  accentColor={accentColor}
563
647
  isStreaming={isStreaming}
564
648
  onAddToCart={onAddToCart}
649
+ viewportSize={viewportSize}
565
650
  />
566
651
  );
567
652
  }
@@ -587,27 +672,27 @@ export function MessageBubble({
587
672
  );
588
673
  },
589
674
  }),
590
- [primaryColor, accentColor, isStreaming, onAddToCart],
675
+ [primaryColor, accentColor, isStreaming, onAddToCart, viewportSize],
591
676
  );
592
677
 
593
678
  // ── Render ─────────────────────────────────────────────────────────────────
594
679
  return (
595
- <div className={`flex gap-3 ${isUser ? 'flex-row-reverse' : 'flex-row'} items-start`}>
680
+ <div className={`flex ${isCompact ? 'gap-2' : 'gap-3'} ${isUser ? 'flex-row-reverse' : 'flex-row'} items-start`}>
596
681
  {/* Avatar */}
597
682
  <div
598
- className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center shadow-lg ${isUser
683
+ className={`flex-shrink-0 ${isCompact ? 'w-7 h-7' : 'w-8 h-8'} rounded-full flex items-center justify-center shadow-lg ${isUser
599
684
  ? 'text-white'
600
685
  : 'bg-slate-100 dark:bg-white/10 text-slate-700 dark:text-white/80'
601
686
  }`}
602
687
  style={isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}}
603
688
  >
604
- {isUser ? <User className="w-4 h-4 text-white" /> : <Bot className="w-4 h-4" />}
689
+ {isUser ? <User className={`${isCompact ? 'w-3.5 h-3.5' : 'w-4 h-4'} text-white`} /> : <Bot className={`${isCompact ? 'w-3.5 h-3.5' : 'w-4 h-4'}`} />}
605
690
  </div>
606
691
 
607
- <div className={`flex flex-col gap-1 max-w-[90%] ${isUser ? 'items-end' : 'items-start'}`}>
692
+ <div className={`flex flex-col gap-1 ${isCompact ? 'max-w-[94%]' : isMedium ? 'max-w-[92%]' : 'max-w-[90%]'} ${isUser ? 'items-end' : 'items-start'}`}>
608
693
  {/* Bubble */}
609
694
  <div
610
- className={`relative px-4 py-3 rounded-2xl text-sm leading-relaxed shadow-sm dark:shadow-lg ${isUser
695
+ className={`relative ${isCompact ? 'px-3 py-2.5 text-[13px]' : 'px-4 py-3 text-sm'} rounded-2xl leading-relaxed shadow-sm dark:shadow-lg ${isUser
611
696
  ? 'text-white rounded-tr-sm'
612
697
  : 'bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-slate-800 dark:text-white/90 rounded-tl-sm'
613
698
  }`}
@@ -622,7 +707,7 @@ export function MessageBubble({
622
707
  <span className="w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:300ms]" />
623
708
  </span>
624
709
  ) : (
625
- <div className={`prose prose-sm max-w-none ${isUser ? 'prose-invert' : 'dark:prose-invert'}`}>
710
+ <div className={`prose ${isCompact ? 'prose-xs' : 'prose-sm'} max-w-none break-words ${isUser ? 'prose-invert' : 'dark:prose-invert'}`}>
626
711
  <ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
627
712
  {processedMarkdown}
628
713
  </ReactMarkdown>
@@ -44,7 +44,7 @@ export function ProductCarousel({ products, primaryColor = '#6366f1', onAddToCar
44
44
  // Single product: No carousel needed
45
45
  if (products.length === 1) {
46
46
  return (
47
- <div className="my-4 w-[85%] sm:w-[48%] md:w-[31%] min-w-[200px] max-w-[300px] animate-fade-in-up">
47
+ <div className="my-4 w-[min(88%,18rem)] min-w-0 max-w-full animate-fade-in-up">
48
48
  <ProductCard product={products[0]} primaryColor={primaryColor} onAddToCart={onAddToCart} />
49
49
  </div>
50
50
  );
@@ -86,7 +86,7 @@ export function ProductCarousel({ products, primaryColor = '#6366f1', onAddToCar
86
86
  {products.map((product) => (
87
87
  <div
88
88
  key={product.id}
89
- className="snap-start flex-shrink-0 w-[85%] sm:w-[48%] md:w-[31%] min-w-[200px] max-w-[300px]"
89
+ className="snap-start flex-shrink-0 w-[min(88%,18rem)] min-w-[11rem] max-w-full"
90
90
  >
91
91
  <ProductCard product={product} primaryColor={primaryColor} onAddToCart={onAddToCart} />
92
92
  </div>
@@ -138,6 +138,8 @@ When the user asks for a visual representation, comparison, distribution, breakd
138
138
  - Never use ASCII art, markdown tables as the primary answer, or text-only fake charts when a UI block is requested.
139
139
  - Put any short natural-language explanation before or after the \`\`\`ui\`\`\` block, but keep it concise.
140
140
  - Return valid JSON inside the \`\`\`ui\`\`\` block.
141
+ - If the user explicitly asks for a pie chart, return "view": "chart" with "chartType": "pie", not a table fallback.
142
+ - Do not prefix the JSON with labels like "Table View", "Alternative", or explanatory text inside the code block.
141
143
 
142
144
  5. EXAMPLE
143
145
  User: "provide me the distribution of products across categories and highlight which ones are in stock in a pie chart"
@@ -3,6 +3,8 @@ import { Product, VectorMatch } from './index';
3
3
  import { RagMessage } from './chat';
4
4
  import { UIConfig } from '../config/RagConfig';
5
5
 
6
+ export type ChatViewportSize = 'compact' | 'medium' | 'large';
7
+
6
8
  export interface ChatWindowProps {
7
9
  /** Additional className for the wrapper div */
8
10
  className?: string;
@@ -40,6 +42,7 @@ export interface MessageBubbleProps {
40
42
  primaryColor: string;
41
43
  accentColor: string;
42
44
  onAddToCart?: (product: Product) => void;
45
+ viewportSize?: ChatViewportSize;
43
46
  }
44
47
 
45
48
  export interface ProductCardProps {