@retrivora-ai/rag-engine 1.8.0 → 1.8.2

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.
Files changed (71) hide show
  1. package/dist/DocumentChunker-Dh9TvmGG.d.mts +45 -0
  2. package/dist/DocumentChunker-Dh9TvmGG.d.ts +45 -0
  3. package/dist/{index-DPsQodME.d.mts → ILLMProvider-BOJFz3Na.d.mts} +104 -1
  4. package/dist/{index-DPsQodME.d.ts → ILLMProvider-BOJFz3Na.d.ts} +104 -1
  5. package/dist/MultiTablePostgresProvider-ZLGSKTJR.mjs +8 -0
  6. package/dist/chunk-ICKRMZQK.mjs +76 -0
  7. package/dist/{chunk-PV3MFHWU.mjs → chunk-LZVVLSDN.mjs} +977 -516
  8. package/dist/chunk-OZFBG4BA.mjs +291 -0
  9. package/dist/handlers/index.d.mts +2 -2
  10. package/dist/handlers/index.d.ts +2 -2
  11. package/dist/handlers/index.js +1269 -656
  12. package/dist/handlers/index.mjs +4 -1
  13. package/dist/{index-Bb2yEopi.d.mts → index-BwpcaziY.d.ts} +10 -2
  14. package/dist/{index-CkbTzj9J.d.ts → index-D3V9Et2M.d.mts} +10 -2
  15. package/dist/index.d.mts +24 -4
  16. package/dist/index.d.ts +24 -4
  17. package/dist/index.js +1354 -826
  18. package/dist/index.mjs +1284 -795
  19. package/dist/server.d.mts +47 -27
  20. package/dist/server.d.ts +47 -27
  21. package/dist/server.js +1417 -829
  22. package/dist/server.mjs +164 -176
  23. package/package.json +6 -2
  24. package/src/app/api/upload/route.ts +4 -0
  25. package/src/app/constants.tsx +2 -2
  26. package/src/app/page.tsx +12 -322
  27. package/src/components/AmbientBackground.tsx +29 -0
  28. package/src/components/ArchitectureCard.tsx +17 -0
  29. package/src/components/ArchitectureCardsSection.tsx +15 -0
  30. package/src/components/ChatWindow.tsx +32 -0
  31. package/src/components/CodeViewer.tsx +51 -0
  32. package/src/components/ConfigProvider.tsx +1 -0
  33. package/src/components/DocViewer.tsx +37 -0
  34. package/src/components/DocumentUpload.tsx +44 -1
  35. package/src/components/Documentation.tsx +58 -0
  36. package/src/components/DynamicChart.tsx +27 -2
  37. package/src/components/Hero.tsx +59 -0
  38. package/src/components/HourglassLoader.tsx +87 -0
  39. package/src/components/Lifecycle.tsx +37 -0
  40. package/src/components/MarkdownComponents.tsx +140 -0
  41. package/src/components/MessageBubble.tsx +124 -904
  42. package/src/components/Navbar.tsx +55 -0
  43. package/src/components/ObservabilityPanel.tsx +374 -0
  44. package/src/components/ProductCard.tsx +5 -3
  45. package/src/components/UIDispatcher.tsx +344 -0
  46. package/src/components/VisualizationRenderer.tsx +372 -250
  47. package/src/config/RagConfig.ts +5 -0
  48. package/src/config/serverConfig.ts +3 -1
  49. package/src/core/Pipeline.ts +240 -271
  50. package/src/core/ProviderRegistry.ts +2 -2
  51. package/src/core/VectorPlugin.ts +9 -0
  52. package/src/handlers/index.ts +91 -15
  53. package/src/hooks/useRagChat.ts +21 -11
  54. package/src/index.ts +9 -1
  55. package/src/llm/LLMFactory.ts +54 -2
  56. package/src/llm/providers/AnthropicProvider.ts +12 -8
  57. package/src/llm/providers/GeminiProvider.ts +188 -143
  58. package/src/llm/providers/OllamaProvider.ts +7 -3
  59. package/src/llm/providers/OpenAIProvider.ts +12 -8
  60. package/src/providers/vectordb/MultiTablePostgresProvider.ts +150 -64
  61. package/src/types/chat.ts +8 -0
  62. package/src/types/index.ts +132 -0
  63. package/src/types/props.ts +9 -1
  64. package/src/utils/ProductExtractor.ts +347 -0
  65. package/src/utils/SchemaMapper.ts +129 -0
  66. package/src/utils/UITransformer.ts +470 -209
  67. package/src/utils/synonyms.ts +78 -0
  68. package/dist/DocumentChunker-C1GEEosY.d.ts +0 -93
  69. package/dist/DocumentChunker-CFEiRopR.d.mts +0 -93
  70. package/dist/PostgreSQLProvider-BMOETDZA.mjs +0 -8
  71. package/dist/chunk-FLOSGE6A.mjs +0 -202
@@ -1,37 +1,108 @@
1
1
  'use client';
2
2
 
3
- import Image from 'next/image';
3
+ import React from 'react';
4
4
  import {
5
- UITransformationResponse,
6
- VisualizationType,
7
- PieChartData,
8
- BarChartData,
9
- LineChartDataPoint,
10
- TableData,
11
- CarouselProduct,
12
- } from '@/utils/UITransformer';
5
+ PieChart,
6
+ Pie,
7
+ Cell,
8
+ BarChart,
9
+ Bar,
10
+ LineChart,
11
+ Line,
12
+ XAxis,
13
+ YAxis,
14
+ CartesianGrid,
15
+ Tooltip,
16
+ Legend,
17
+ ResponsiveContainer,
18
+ RadarChart,
19
+ Radar,
20
+ PolarGrid,
21
+ PolarAngleAxis,
22
+ PolarRadiusAxis,
23
+ } from 'recharts';
24
+ import { ProductCarousel } from './ProductCarousel';
25
+ import { Product, UITransformationResponse, VisualizationType, PieChartData, BarChartData, LineChartDataPoint, TableData, CarouselProduct } from '../types';
26
+
27
+ import { CHART_COLORS } from './DynamicChart';
28
+
29
+ function getColor(index: number): string {
30
+ return CHART_COLORS[index % CHART_COLORS.length];
31
+ }
32
+
33
+ // ─── Custom Tooltip ───────────────────────────────────────────────────────────
34
+
35
+ interface TooltipPayloadItem {
36
+ name: string;
37
+ value: string | number;
38
+ color?: string;
39
+ }
40
+
41
+ function CustomTooltip({
42
+ active,
43
+ payload,
44
+ label,
45
+ }: {
46
+ active?: boolean;
47
+ payload?: TooltipPayloadItem[];
48
+ label?: string;
49
+ }) {
50
+ if (!active || !payload || payload.length === 0) return null;
51
+
52
+ return (
53
+ <div className="rounded-xl border border-slate-200 bg-white px-3 py-2 text-xs shadow-xl dark:border-white/10 dark:bg-slate-950">
54
+ {label && (
55
+ <p className="mb-2 font-semibold text-slate-800 dark:text-white/90">{label}</p>
56
+ )}
57
+ {payload.map((entry, i) => (
58
+ <div key={i} className="flex items-center justify-between gap-3 text-slate-600 dark:text-white/70">
59
+ <span className="flex items-center gap-1.5">
60
+ {entry.color && (
61
+ <span
62
+ className="inline-block w-2 h-2 rounded-full flex-shrink-0"
63
+ style={{ background: entry.color }}
64
+ />
65
+ )}
66
+ {entry.name}
67
+ </span>
68
+ <span className="font-medium text-slate-900 dark:text-white/90">{entry.value}</span>
69
+ </div>
70
+ ))}
71
+ </div>
72
+ );
73
+ }
74
+
75
+ // ─── Props ────────────────────────────────────────────────────────────────────
13
76
 
14
- /**
15
- * Props for the VisualizationRenderer component
16
- */
17
77
  interface VisualizationRendererProps {
18
78
  data: UITransformationResponse;
19
79
  className?: string;
80
+ primaryColor?: string;
81
+ onAddToCart?: (product: Product) => void;
82
+ isStreaming?: boolean;
20
83
  }
21
84
 
85
+ // ─── Main component ───────────────────────────────────────────────────────────
86
+
22
87
  /**
23
- * VisualizationRenderer — Renders UI transformations as React components
88
+ * VisualizationRenderer — Renders UI transformations as React components.
24
89
  *
25
90
  * Dynamically selects the appropriate visualization component based on the
26
- * transformation type returned by UITransformer.
91
+ * transformation type returned by UITransformer. All charts are rendered via
92
+ * Recharts (chart.js has been removed).
93
+ *
94
+ * Supported types: bar_chart · line_chart · pie_chart · table · product_carousel · text
27
95
  *
28
96
  * @example
29
- * const transformation = UITransformer.transform(query, results);
97
+ * const transformation = await UITransformer.analyzeAndDecide(query, sources, llm);
30
98
  * return <VisualizationRenderer data={transformation} />;
31
99
  */
32
100
  export function VisualizationRenderer({
33
101
  data,
34
102
  className = '',
103
+ primaryColor,
104
+ onAddToCart,
105
+ isStreaming,
35
106
  }: VisualizationRendererProps) {
36
107
  if (!data) {
37
108
  return (
@@ -43,299 +114,350 @@ export function VisualizationRenderer({
43
114
 
44
115
  return (
45
116
  <div className={`space-y-4 ${className}`}>
46
- <div className="space-y-2">
47
- <h3 className="text-lg font-semibold text-gray-900 dark:text-white">
117
+ <div className="space-y-1">
118
+ <h3 className="text-sm font-semibold text-slate-900 dark:text-white">
48
119
  {data.title}
49
120
  </h3>
50
- {data.description && (
51
- <p className="text-sm text-gray-600 dark:text-gray-400">
121
+ {data.description && data.description !== (data.data as { content?: string })?.content && (
122
+ <p className="text-xs text-slate-500 dark:text-slate-400">
52
123
  {data.description}
53
124
  </p>
54
125
  )}
55
126
  </div>
56
127
 
57
- {renderVisualization(data.type, data.data)}
128
+ {renderVisualization(data.type, data.data, onAddToCart, primaryColor, isStreaming)}
58
129
  </div>
59
130
  );
60
131
  }
61
132
 
62
- /**
63
- * Internal helper to render the appropriate visualization
64
- */
65
- function renderVisualization(type: VisualizationType, data: unknown): React.ReactNode {
66
- switch (type) {
67
- case 'pie_chart':
68
- return renderPieChart(data as PieChartData[]);
69
- case 'bar_chart':
70
- return renderBarChart(data as BarChartData[]);
71
- case 'line_chart':
72
- return renderLineChart(data as LineChartDataPoint[]);
73
- case 'table':
74
- return renderTable(data as TableData);
75
- case 'product_carousel':
76
- return renderProductCarousel(data as CarouselProduct[]);
77
- case 'text':
78
- default:
79
- return renderText(data);
133
+ // ─── Visualization dispatcher ─────────────────────────────────────────────────
134
+
135
+ function renderVisualization(
136
+ type: VisualizationType,
137
+ data: unknown,
138
+ onAddToCart?: (product: Product) => void,
139
+ primaryColor?: string,
140
+ isStreaming?: boolean,
141
+ ): React.ReactNode {
142
+ try {
143
+ switch (type) {
144
+ case 'pie_chart':
145
+ return <PieChartView data={data as PieChartData[]} isStreaming={isStreaming} />;
146
+ case 'bar_chart':
147
+ return <BarChartView data={data as BarChartData[]} primaryColor={primaryColor} isStreaming={isStreaming} />;
148
+ case 'line_chart':
149
+ return <LineChartView data={data as LineChartDataPoint[]} primaryColor={primaryColor} isStreaming={isStreaming} />;
150
+ case 'radar_chart':
151
+ return <RadarChartView data={data as Record<string, unknown>[]} isStreaming={isStreaming} />;
152
+ case 'table':
153
+ return <TableView data={data as TableData} isStreaming={isStreaming} />;
154
+ case 'product_carousel':
155
+ case 'carousel':
156
+ return <CarouselView data={data as CarouselProduct[]} onAddToCart={onAddToCart} primaryColor={primaryColor} />;
157
+ case 'text':
158
+ default:
159
+ return <TextView data={data} />;
160
+ }
161
+ } catch (error) {
162
+ console.warn('[VisualizationRenderer] Rendering failed, falling back to text:', error);
163
+ return <TextView data={{ content: 'Unable to render the requested visualization.' }} />;
80
164
  }
81
165
  }
82
166
 
83
- /**
84
- * Render pie chart
85
- */
86
- function renderPieChart(data: PieChartData[]): React.ReactNode {
87
- if (!Array.isArray(data)) {
88
- return <div className="text-red-500">Invalid pie chart data</div>;
167
+ // ─── Chart Skeleton ────────────────────────────────────────────────────────────
168
+
169
+ function ChartSkeleton({ type }: { type: 'pie' | 'bar' | 'table' }) {
170
+ if (type === 'pie') {
171
+ return (
172
+ <div className="w-full mt-4 mb-2 animate-pulse">
173
+ <div className="mx-auto w-40 h-40 rounded-full bg-slate-100 dark:bg-white/10" />
174
+ </div>
175
+ );
176
+ }
177
+ if (type === 'table') {
178
+ return (
179
+ <div className="w-full mt-4 mb-2 animate-pulse space-y-2">
180
+ <div className="h-8 bg-slate-100 dark:bg-white/10 rounded" />
181
+ <div className="h-8 bg-slate-100 dark:bg-white/10 rounded" />
182
+ <div className="h-8 bg-slate-100 dark:bg-white/10 rounded" />
183
+ </div>
184
+ );
185
+ }
186
+ return (
187
+ <div className="w-full mt-4 mb-2 animate-pulse flex items-end gap-2 h-32 px-2">
188
+ {Array.from({ length: 5 }).map((_, i) => (
189
+ <div key={i} className="flex-1 rounded-t-md bg-slate-100 dark:bg-white/10" style={{ height: `${40 + (i % 3) * 25}%` }} />
190
+ ))}
191
+ </div>
192
+ );
193
+ }
194
+
195
+ // ─── Pie Chart ────────────────────────────────────────────────────────────────
196
+
197
+ function PieChartView({ data, isStreaming }: { data: PieChartData[]; isStreaming?: boolean }) {
198
+ if (isStreaming) return <ChartSkeleton type="pie" />;
199
+ if (!Array.isArray(data) || data.length === 0) {
200
+ return <div className="text-xs text-red-500">Invalid pie chart data</div>;
89
201
  }
90
202
 
203
+ // Recharts Pie uses flat key/value, so map to { name, value }
204
+ const chartData = data.map((item) => ({
205
+ name: item.label,
206
+ value: item.value,
207
+ }));
208
+
91
209
  return (
92
- <div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow">
93
- <div className="flex flex-wrap gap-2">
94
- {data.map((item: PieChartData, idx: number) => (
95
- <div
96
- key={idx}
97
- className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-700 rounded"
210
+ <div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 p-4 shadow-sm">
211
+ <ResponsiveContainer width="100%" height={280}>
212
+ <PieChart>
213
+ <Pie
214
+ data={chartData}
215
+ dataKey="value"
216
+ nameKey="name"
217
+ cx="50%"
218
+ cy="50%"
219
+ outerRadius="68%"
220
+ label={false}
221
+ labelLine={false}
98
222
  >
99
- <div
100
- className="w-3 h-3 rounded-full"
101
- style={{ backgroundColor: generateColor(idx) }}
102
- />
103
- <span className="text-sm font-medium text-gray-700 dark:text-gray-300">
104
- {item.label}: {item.value}
105
- </span>
106
- {item.inStock !== undefined && (
107
- <span
108
- className={`text-xs px-2 py-1 rounded ${
109
- item.inStock
110
- ? 'bg-green-100 text-green-800'
111
- : 'bg-red-100 text-red-800'
112
- }`}
113
- >
114
- {item.inStock ? 'In Stock' : 'Out of Stock'}
115
- </span>
116
- )}
117
- </div>
118
- ))}
119
- </div>
223
+ {chartData.map((_entry, index) => (
224
+ <Cell key={`cell-${index}`} fill={getColor(index)} />
225
+ ))}
226
+ </Pie>
227
+ <Tooltip content={<CustomTooltip />} />
228
+ <Legend wrapperStyle={{ fontSize: 12 }} verticalAlign="bottom" />
229
+ </PieChart>
230
+ </ResponsiveContainer>
120
231
  </div>
121
232
  );
122
233
  }
123
234
 
124
- /**
125
- * Render bar chart
126
- */
127
- function renderBarChart(data: BarChartData[]): React.ReactNode {
128
- if (!Array.isArray(data)) {
129
- return <div className="text-red-500">Invalid bar chart data</div>;
235
+ // ─── Bar Chart ────────────────────────────────────────────────────────────────
236
+
237
+ function BarChartView({ data, primaryColor, isStreaming }: { data: BarChartData[]; primaryColor?: string; isStreaming?: boolean }) {
238
+ if (isStreaming) return <ChartSkeleton type="bar" />;
239
+ if (!Array.isArray(data) || data.length === 0) {
240
+ return <div className="text-xs text-red-500">Invalid bar chart data</div>;
130
241
  }
131
242
 
243
+ const chartData = data.map((item) => ({
244
+ category: item.category,
245
+ value: item.value,
246
+ ...(item.inStockCount !== undefined && { inStockCount: item.inStockCount }),
247
+ ...(item.outOfStockCount !== undefined && { outOfStockCount: item.outOfStockCount }),
248
+ }));
249
+
250
+ const barColor = primaryColor ?? getColor(0);
251
+ const hasStock = chartData.some((d) => 'inStockCount' in d);
252
+
132
253
  return (
133
- <div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow overflow-x-auto">
134
- <div className="space-y-3">
135
- {data.map((item: BarChartData, idx: number) => (
136
- <div key={idx} className="space-y-1">
137
- <div className="flex justify-between items-center">
138
- <span className="text-sm font-medium text-gray-700 dark:text-gray-300">
139
- {item.category}
140
- </span>
141
- <span className="text-sm text-gray-600 dark:text-gray-400">
142
- {item.value}
143
- </span>
144
- </div>
145
- <div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
146
- <div
147
- className="h-full rounded-full transition-all"
148
- style={{
149
- width: `${(item.value / Math.max(...data.map((d: BarChartData) => d.value))) * 100}%`,
150
- backgroundColor: generateColor(idx),
151
- }}
152
- />
153
- </div>
154
- {item.inStock !== undefined && (
155
- <span
156
- className={`text-xs px-2 py-1 rounded inline-block ${
157
- item.inStock
158
- ? 'bg-green-100 text-green-800'
159
- : 'bg-red-100 text-red-800'
160
- }`}
161
- >
162
- {item.inStock ? 'In Stock' : 'Out of Stock'}
163
- </span>
164
- )}
165
- </div>
166
- ))}
167
- </div>
254
+ <div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 p-4 shadow-sm overflow-x-auto">
255
+ <ResponsiveContainer width="100%" height={260}>
256
+ <BarChart data={chartData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
257
+ <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e2e8f0" />
258
+ <XAxis
259
+ dataKey="category"
260
+ tick={{ fontSize: 11, fill: '#64748b' }}
261
+ tickLine={false}
262
+ axisLine={{ stroke: '#cbd5e1' }}
263
+ />
264
+ <YAxis tick={{ fontSize: 11, fill: '#64748b' }} tickLine={false} axisLine={false} />
265
+ <Tooltip content={<CustomTooltip />} cursor={{ fill: '#f1f5f9' }} />
266
+ <Legend wrapperStyle={{ fontSize: 12 }} />
267
+ {hasStock ? (
268
+ <>
269
+ <Bar dataKey="inStockCount" name="In Stock" fill={getColor(1)} radius={[4, 4, 0, 0]} />
270
+ <Bar dataKey="outOfStockCount" name="Out of Stock" fill={getColor(3)} radius={[4, 4, 0, 0]} />
271
+ </>
272
+ ) : (
273
+ <Bar dataKey="value" name="Value" fill={barColor} radius={[4, 4, 0, 0]} />
274
+ )}
275
+ </BarChart>
276
+ </ResponsiveContainer>
168
277
  </div>
169
278
  );
170
279
  }
171
280
 
172
- /**
173
- * Render line chart (simplified text representation)
174
- */
175
- function renderLineChart(data: LineChartDataPoint[]): React.ReactNode {
176
- if (!Array.isArray(data)) {
177
- return <div className="text-red-500">Invalid line chart data</div>;
281
+ // ─── Line Chart ───────────────────────────────────────────────────────────────
282
+
283
+ function LineChartView({ data, primaryColor, isStreaming }: { data: LineChartDataPoint[]; primaryColor?: string; isStreaming?: boolean }) {
284
+ if (isStreaming) return <ChartSkeleton type="bar" />;
285
+ if (!Array.isArray(data) || data.length === 0) {
286
+ return <div className="text-xs text-red-500">Invalid line chart data</div>;
178
287
  }
179
288
 
289
+ const chartData = data.map((point) => ({
290
+ label: String(point.label ?? point.timestamp),
291
+ value: Number(point.value),
292
+ }));
293
+
294
+ const lineColor = primaryColor ?? getColor(0);
295
+
180
296
  return (
181
- <div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow overflow-x-auto">
182
- <div className="space-y-2">
183
- {data.map((point: LineChartDataPoint, idx: number) => (
184
- <div
185
- key={idx}
186
- className="flex justify-between items-center p-2 bg-gray-50 dark:bg-gray-700 rounded"
187
- >
188
- <span className="text-sm text-gray-600 dark:text-gray-400">
189
- {point.label || point.timestamp}
190
- </span>
191
- <span className="text-sm font-semibold text-gray-900 dark:text-white">
192
- {typeof point.value === 'number' ? point.value.toFixed(2) : point.value}
193
- </span>
194
- </div>
195
- ))}
196
- </div>
297
+ <div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 p-4 shadow-sm overflow-x-auto">
298
+ <ResponsiveContainer width="100%" height={260}>
299
+ <LineChart data={chartData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
300
+ <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e2e8f0" />
301
+ <XAxis
302
+ dataKey="label"
303
+ tick={{ fontSize: 11, fill: '#64748b' }}
304
+ tickLine={false}
305
+ axisLine={{ stroke: '#cbd5e1' }}
306
+ />
307
+ <YAxis tick={{ fontSize: 11, fill: '#64748b' }} tickLine={false} axisLine={false} />
308
+ <Tooltip content={<CustomTooltip />} />
309
+ <Line
310
+ type="monotone"
311
+ dataKey="value"
312
+ name="Value"
313
+ stroke={lineColor}
314
+ strokeWidth={2.5}
315
+ dot={{ r: 3.5, strokeWidth: 2, fill: lineColor }}
316
+ activeDot={{ r: 5 }}
317
+ />
318
+ </LineChart>
319
+ </ResponsiveContainer>
197
320
  </div>
198
321
  );
199
322
  }
200
323
 
201
- /**
202
- * Render table
203
- */
204
- function renderTable(data: TableData): React.ReactNode {
205
- if (!data.columns || !data.rows) {
206
- return <div className="text-red-500">Invalid table data</div>;
324
+ // ─── Radar Chart ──────────────────────────────────────────────────────────────
325
+
326
+ function RadarChartView({ data, isStreaming }: { data: Record<string, unknown>[]; isStreaming?: boolean }) {
327
+ if (isStreaming) return <ChartSkeleton type="pie" />;
328
+ if (!Array.isArray(data) || data.length === 0) {
329
+ return <div className="text-xs text-red-500">Invalid radar chart data</div>;
207
330
  }
208
331
 
332
+ // Find all keys that are not 'attribute' to use as radar data keys (product names)
333
+ const productNames = Array.from(
334
+ new Set(data.flatMap((item) => Object.keys(item).filter((key) => key !== 'attribute')))
335
+ );
336
+
209
337
  return (
210
- <div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-x-auto">
211
- <table className="w-full text-sm">
212
- <thead className="bg-gray-100 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
213
- <tr>
214
- {data.columns.map((col: string, idx: number) => (
215
- <th
216
- key={idx}
217
- className="px-4 py-2 text-left font-semibold text-gray-900 dark:text-white"
218
- >
219
- {col}
220
- </th>
221
- ))}
222
- </tr>
223
- </thead>
224
- <tbody className="divide-y divide-gray-200 dark:divide-gray-600">
225
- {data.rows.map((row: (string | number | boolean)[], rowIdx: number) => (
226
- <tr
227
- key={rowIdx}
228
- className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
229
- >
230
- {row.map((cell: string | number | boolean, cellIdx: number) => (
231
- <td
232
- key={cellIdx}
233
- className="px-4 py-2 text-gray-700 dark:text-gray-300"
338
+ <div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 p-4 shadow-sm">
339
+ <ResponsiveContainer width="100%" height={300}>
340
+ <RadarChart cx="50%" cy="50%" outerRadius="80%" data={data}>
341
+ <PolarGrid stroke="#e2e8f0" />
342
+ <PolarAngleAxis dataKey="attribute" tick={{ fontSize: 11, fill: '#64748b' }} />
343
+ <PolarRadiusAxis tick={{ fontSize: 10, fill: '#64748b' }} />
344
+ {productNames.map((productName, index) => (
345
+ <Radar
346
+ key={productName}
347
+ name={productName}
348
+ dataKey={productName}
349
+ stroke={getColor(index)}
350
+ fill={getColor(index)}
351
+ fillOpacity={0.4}
352
+ />
353
+ ))}
354
+ <Tooltip content={<CustomTooltip />} />
355
+ <Legend wrapperStyle={{ fontSize: 12 }} />
356
+ </RadarChart>
357
+ </ResponsiveContainer>
358
+ </div>
359
+ );
360
+ }
361
+
362
+ // ─── Table ────────────────────────────────────────────────────────────────────
363
+
364
+ function TableView({ data, isStreaming }: { data: TableData; isStreaming?: boolean }) {
365
+ if (isStreaming) return <ChartSkeleton type="table" />;
366
+ if (!data?.columns || !data?.rows) {
367
+ return <div className="text-xs text-red-500">Invalid table data</div>;
368
+ }
369
+
370
+ return (
371
+ <div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm overflow-hidden">
372
+ <div className="overflow-x-auto">
373
+ <table className="w-full text-sm border-collapse">
374
+ <thead className="bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10">
375
+ <tr>
376
+ {data.columns.map((col: string, idx: number) => (
377
+ <th
378
+ key={idx}
379
+ className="px-4 py-3 text-left text-xs font-semibold text-slate-700 dark:text-white/90 whitespace-nowrap uppercase tracking-wider"
234
380
  >
235
- {String(cell)}
236
- </td>
381
+ {col}
382
+ </th>
237
383
  ))}
238
384
  </tr>
239
- ))}
240
- </tbody>
241
- </table>
385
+ </thead>
386
+ <tbody className="divide-y divide-slate-100 dark:divide-white/5">
387
+ {data.rows.map((row: (string | number | boolean)[], rowIdx: number) => (
388
+ <tr
389
+ key={rowIdx}
390
+ className="hover:bg-slate-50/60 dark:hover:bg-white/5 transition-colors"
391
+ >
392
+ {row.map((cell: string | number | boolean, cellIdx: number) => (
393
+ <td
394
+ key={cellIdx}
395
+ className="px-4 py-3 text-slate-600 dark:text-white/70 text-sm"
396
+ >
397
+ {String(cell)}
398
+ </td>
399
+ ))}
400
+ </tr>
401
+ ))}
402
+ </tbody>
403
+ </table>
404
+ </div>
405
+ <div className="px-4 py-2 bg-slate-50 dark:bg-white/5 border-t border-slate-100 dark:border-white/5">
406
+ <p className="text-[11px] text-slate-400 dark:text-white/30">
407
+ {data.rows.length} row{data.rows.length !== 1 ? 's' : ''}
408
+ </p>
409
+ </div>
242
410
  </div>
243
411
  );
244
412
  }
245
413
 
246
- /**
247
- * Render product carousel
248
- */
249
- function renderProductCarousel(data: CarouselProduct[]): React.ReactNode {
414
+ // ─── Product Carousel ─────────────────────────────────────────────────────────
415
+
416
+ function CarouselView({
417
+ data,
418
+ onAddToCart,
419
+ primaryColor,
420
+ }: {
421
+ data: CarouselProduct[];
422
+ onAddToCart?: (product: Product) => void;
423
+ primaryColor?: string;
424
+ }) {
250
425
  if (!Array.isArray(data)) {
251
- return <div className="text-red-500">Invalid carousel data</div>;
426
+ return <div className="text-xs text-red-500">Invalid carousel data</div>;
252
427
  }
253
428
 
429
+ const products: Product[] = data.map((product) => ({
430
+ id: product.id,
431
+ name: product.name,
432
+ brand: product.brand,
433
+ price: product.price,
434
+ image: product.image,
435
+ link: typeof product.link === 'string' ? product.link : undefined,
436
+ description: product.description,
437
+ inStock: product.inStock,
438
+ }));
439
+
254
440
  return (
255
- <div className="overflow-x-auto">
256
- <div className="flex gap-4 pb-4">
257
- {data.map((product: CarouselProduct) => (
258
- <div
259
- key={product.id}
260
- className="flex-shrink-0 w-64 bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"
261
- >
262
- {product.image && (
263
- <div className="relative w-full h-40 bg-gray-200">
264
- <Image
265
- src={product.image}
266
- alt={product.name}
267
- fill
268
- className="object-cover"
269
- sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
270
- />
271
- </div>
272
- )}
273
- <div className="p-4 space-y-2">
274
- <h4 className="font-semibold text-gray-900 dark:text-white line-clamp-2">
275
- {product.name}
276
- </h4>
277
- {product.brand && (
278
- <p className="text-xs text-gray-500 dark:text-gray-400">
279
- Brand: {product.brand}
280
- </p>
281
- )}
282
- {product.price && (
283
- <p className="text-lg font-bold text-gray-900 dark:text-white">
284
- ${typeof product.price === 'number' ? product.price.toFixed(2) : product.price}
285
- </p>
286
- )}
287
- {product.description && (
288
- <p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
289
- {product.description}
290
- </p>
291
- )}
292
- <div className="flex gap-2 pt-2">
293
- <span
294
- className={`text-xs px-2 py-1 rounded flex-1 text-center ${
295
- product.inStock
296
- ? 'bg-green-100 text-green-800'
297
- : 'bg-red-100 text-red-800'
298
- }`}
299
- >
300
- {product.inStock ? 'In Stock' : 'Out of Stock'}
301
- </span>
302
- </div>
303
- </div>
304
- </div>
305
- ))}
306
- </div>
441
+ <div className="w-full">
442
+ <ProductCarousel
443
+ products={products}
444
+ primaryColor={primaryColor ?? '#6366f1'}
445
+ onAddToCart={(p) => onAddToCart?.(p)}
446
+ />
307
447
  </div>
308
448
  );
309
449
  }
310
450
 
311
- /**
312
- * Render text
313
- */
314
- function renderText(data: unknown): React.ReactNode {
315
- const content = (data as { content?: string })?.content || String(data);
451
+ // ─── Text / Prose ─────────────────────────────────────────────────────────────
452
+
453
+ function TextView({ data }: { data: unknown }) {
454
+ const content = (data as { content?: string })?.content || String(data ?? '');
316
455
 
317
456
  return (
318
- <div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow prose dark:prose-invert max-w-none">
319
- <p className="text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
457
+ <div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 p-4 shadow-sm">
458
+ <p className="text-sm text-slate-700 dark:text-slate-300 whitespace-pre-wrap leading-relaxed">
320
459
  {content}
321
460
  </p>
322
461
  </div>
323
462
  );
324
463
  }
325
-
326
- /**
327
- * Utility to generate consistent colors for charts
328
- */
329
- function generateColor(index: number): string {
330
- const colors = [
331
- '#3B82F6', // blue
332
- '#10B981', // green
333
- '#F59E0B', // amber
334
- '#EF4444', // red
335
- '#8B5CF6', // purple
336
- '#EC4899', // pink
337
- '#06B6D4', // cyan
338
- '#F97316', // orange
339
- ];
340
- return colors[index % colors.length];
341
- }