@retrivora-ai/rag-engine 1.5.5 → 1.5.6

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>
@@ -229,12 +243,45 @@ interface UIConfig {
229
243
  items?: Record<string, unknown>[];
230
244
  }
231
245
 
232
- function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAddToCart }: {
246
+ interface RawUIConfig extends Omit<UIConfig, 'data'> {
247
+ data: Array<Record<string, unknown> | unknown[]>;
248
+ }
249
+
250
+ function normalizeTabularRows(
251
+ rows: Array<Record<string, unknown> | unknown[]> | undefined,
252
+ columns?: string[],
253
+ ): Record<string, unknown>[] {
254
+ if (!Array.isArray(rows)) return [];
255
+
256
+ return rows
257
+ .map((row) => {
258
+ if (Array.isArray(row)) {
259
+ const keys = Array.isArray(columns) && columns.length > 0
260
+ ? columns
261
+ : row.map((_, index) => `column_${index + 1}`);
262
+
263
+ return keys.reduce<Record<string, unknown>>((acc, key, index) => {
264
+ acc[key] = row[index];
265
+ return acc;
266
+ }, {});
267
+ }
268
+
269
+ if (row && typeof row === 'object') {
270
+ return row as Record<string, unknown>;
271
+ }
272
+
273
+ return { value: row };
274
+ })
275
+ .filter((row) => Object.keys(row).length > 0);
276
+ }
277
+
278
+ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAddToCart, viewportSize = 'large' }: {
233
279
  rawContent: string;
234
280
  primaryColor?: string;
235
281
  accentColor?: string;
236
282
  isStreaming?: boolean;
237
283
  onAddToCart?: (product: Product) => void;
284
+ viewportSize?: ChatViewportSize;
238
285
  }) {
239
286
  const result = React.useMemo<{ config: UIConfig } | { error: string } | { loading: true }>(() => {
240
287
  if (isStreaming) return { loading: true };
@@ -243,17 +290,26 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
243
290
  .replace(/^```[a-z]*\s*/i, '')
244
291
  .replace(/```\s*$/, '')
245
292
  .trim();
246
- const parsed = JSON.parse(sanitizeJson(clean));
247
- const config = { ...parsed };
293
+ const parsed = JSON.parse(sanitizeJson(extractLikelyJsonBlock(clean)));
294
+ const config = { ...parsed } as Partial<RawUIConfig> & Record<string, unknown>;
248
295
 
249
296
  // Smart Auto-Detection for legacy or simpler formats
250
297
  if (!config.view) {
251
298
  if (config.type === 'products' || Array.isArray(config.items)) {
252
299
  config.view = 'carousel';
253
300
  config.data = config.data || config.items || [];
254
- } else if (['pie', 'bar', 'line'].includes(config.type) || ['pie', 'bar', 'line'].includes(config.chartType)) {
301
+ } else if (
302
+ (typeof config.type === 'string' && ['pie', 'bar', 'line'].includes(config.type)) ||
303
+ (typeof config.chartType === 'string' && ['pie', 'bar', 'line'].includes(config.chartType))
304
+ ) {
305
+ const resolvedChartType =
306
+ config.chartType === 'pie' || config.chartType === 'bar' || config.chartType === 'line'
307
+ ? config.chartType
308
+ : config.type === 'pie' || config.type === 'bar' || config.type === 'line'
309
+ ? config.type
310
+ : 'bar';
255
311
  config.view = 'chart';
256
- config.chartType = config.chartType || config.type || 'bar';
312
+ config.chartType = resolvedChartType;
257
313
  config.data = config.data || [];
258
314
  } else {
259
315
  config.view = 'table';
@@ -261,7 +317,12 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
261
317
  }
262
318
  }
263
319
 
264
- return { config: config as UIConfig };
320
+ const normalizedConfig: UIConfig = {
321
+ ...(config as Omit<UIConfig, 'data'>),
322
+ data: normalizeTabularRows(config.data, config.columns),
323
+ };
324
+
325
+ return { config: normalizedConfig };
265
326
  } catch (err) {
266
327
  return { error: String(err) };
267
328
  }
@@ -281,16 +342,17 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
281
342
  }
282
343
 
283
344
  const { config } = result;
345
+ const isCompact = viewportSize === 'compact';
284
346
  const hasInsights = Array.isArray(config.insights) && config.insights.length > 0;
285
347
  const hasDescription = typeof config.description === 'string' && config.description.trim().length > 0;
286
348
 
287
349
  switch (config.view) {
288
350
  case 'chart':
289
351
  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>}
352
+ <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`}>
353
+ {config.title && <h4 className={`${isCompact ? 'text-[11px] mb-3' : 'text-xs mb-4'} font-semibold text-slate-500 px-2`}>{config.title}</h4>}
292
354
  {hasDescription && (
293
- <p className="px-2 text-sm text-slate-500 dark:text-white/60">{config.description}</p>
355
+ <p className={`px-2 ${isCompact ? 'text-xs' : 'text-sm'} text-slate-500 dark:text-white/60`}>{config.description}</p>
294
356
  )}
295
357
  <DynamicChart
296
358
  config={{
@@ -302,13 +364,14 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
302
364
  }}
303
365
  primaryColor={primaryColor}
304
366
  accentColor={accentColor}
367
+ viewportSize={viewportSize}
305
368
  />
306
369
  {hasInsights && (
307
370
  <div className="mt-4 flex flex-wrap gap-2 px-2">
308
371
  {config.insights?.map((insight) => (
309
372
  <span
310
373
  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"
374
+ 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
375
  >
313
376
  {insight}
314
377
  </span>
@@ -320,9 +383,9 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
320
383
  case 'carousel':
321
384
  return (
322
385
  <div className="my-4">
323
- {config.title && <h4 className="mb-2 text-xs font-semibold text-slate-500">{config.title}</h4>}
386
+ {config.title && <h4 className={`${isCompact ? 'mb-1.5 text-[11px]' : 'mb-2 text-xs'} font-semibold text-slate-500`}>{config.title}</h4>}
324
387
  {hasDescription && (
325
- <p className="mb-3 text-sm text-slate-500 dark:text-white/60">{config.description}</p>
388
+ <p className={`mb-3 ${isCompact ? 'text-xs' : 'text-sm'} text-slate-500 dark:text-white/60`}>{config.description}</p>
326
389
  )}
327
390
  <ProductCarousel products={(config.data as unknown as Product[]).map(item => ({
328
391
  ...item,
@@ -332,10 +395,10 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
332
395
  );
333
396
  case 'table':
334
397
  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>}
398
+ <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`}>
399
+ {config.title && <h4 className={`${isCompact ? 'text-[11px]' : 'text-xs'} font-semibold text-slate-500 mb-2`}>{config.title}</h4>}
337
400
  {hasDescription && (
338
- <p className="mb-3 text-sm text-slate-500 dark:text-white/60">{config.description}</p>
401
+ <p className={`mb-3 ${isCompact ? 'text-xs' : 'text-sm'} text-slate-500 dark:text-white/60`}>{config.description}</p>
339
402
  )}
340
403
  <DataTable config={{
341
404
  type: 'table',
@@ -345,7 +408,7 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
345
408
  dataKeys: config.dataKeys,
346
409
  xAxisKey: config.xAxisKey,
347
410
  data: config.data,
348
- }} />
411
+ }} viewportSize={viewportSize} />
349
412
  </div>
350
413
  );
351
414
  default:
@@ -364,8 +427,11 @@ export function MessageBubble({
364
427
  primaryColor = '#6366f1',
365
428
  accentColor = '#8b5cf6',
366
429
  onAddToCart,
430
+ viewportSize = 'large',
367
431
  }: MessageBubbleProps) {
368
432
  const isUser = message.role === 'user';
433
+ const isCompact = viewportSize === 'compact';
434
+ const isMedium = viewportSize === 'medium';
369
435
  const [showSources, setShowSources] = React.useState(false);
370
436
  const hasStructuredProductBlock = React.useMemo(
371
437
  () => /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|type)"\s*:\s*"(?:carousel|products)"/i.test(message.content),
@@ -548,6 +614,7 @@ export function MessageBubble({
548
614
  accentColor={accentColor}
549
615
  isStreaming={isStreaming}
550
616
  onAddToCart={onAddToCart}
617
+ viewportSize={viewportSize}
551
618
  />
552
619
  );
553
620
  }
@@ -562,6 +629,7 @@ export function MessageBubble({
562
629
  accentColor={accentColor}
563
630
  isStreaming={isStreaming}
564
631
  onAddToCart={onAddToCart}
632
+ viewportSize={viewportSize}
565
633
  />
566
634
  );
567
635
  }
@@ -587,27 +655,27 @@ export function MessageBubble({
587
655
  );
588
656
  },
589
657
  }),
590
- [primaryColor, accentColor, isStreaming, onAddToCart],
658
+ [primaryColor, accentColor, isStreaming, onAddToCart, viewportSize],
591
659
  );
592
660
 
593
661
  // ── Render ─────────────────────────────────────────────────────────────────
594
662
  return (
595
- <div className={`flex gap-3 ${isUser ? 'flex-row-reverse' : 'flex-row'} items-start`}>
663
+ <div className={`flex ${isCompact ? 'gap-2' : 'gap-3'} ${isUser ? 'flex-row-reverse' : 'flex-row'} items-start`}>
596
664
  {/* Avatar */}
597
665
  <div
598
- className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center shadow-lg ${isUser
666
+ className={`flex-shrink-0 ${isCompact ? 'w-7 h-7' : 'w-8 h-8'} rounded-full flex items-center justify-center shadow-lg ${isUser
599
667
  ? 'text-white'
600
668
  : 'bg-slate-100 dark:bg-white/10 text-slate-700 dark:text-white/80'
601
669
  }`}
602
670
  style={isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}}
603
671
  >
604
- {isUser ? <User className="w-4 h-4 text-white" /> : <Bot className="w-4 h-4" />}
672
+ {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
673
  </div>
606
674
 
607
- <div className={`flex flex-col gap-1 max-w-[90%] ${isUser ? 'items-end' : 'items-start'}`}>
675
+ <div className={`flex flex-col gap-1 ${isCompact ? 'max-w-[94%]' : isMedium ? 'max-w-[92%]' : 'max-w-[90%]'} ${isUser ? 'items-end' : 'items-start'}`}>
608
676
  {/* Bubble */}
609
677
  <div
610
- className={`relative px-4 py-3 rounded-2xl text-sm leading-relaxed shadow-sm dark:shadow-lg ${isUser
678
+ 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
679
  ? 'text-white rounded-tr-sm'
612
680
  : '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
681
  }`}
@@ -622,7 +690,7 @@ export function MessageBubble({
622
690
  <span className="w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:300ms]" />
623
691
  </span>
624
692
  ) : (
625
- <div className={`prose prose-sm max-w-none ${isUser ? 'prose-invert' : 'dark:prose-invert'}`}>
693
+ <div className={`prose ${isCompact ? 'prose-xs' : 'prose-sm'} max-w-none break-words ${isUser ? 'prose-invert' : 'dark:prose-invert'}`}>
626
694
  <ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
627
695
  {processedMarkdown}
628
696
  </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 {