@retrivora-ai/rag-engine 1.8.0 → 1.8.1

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