@retrivora-ai/rag-engine 1.7.1 → 1.7.3
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.
- package/dist/{chunk-OHXRQTQH.mjs → chunk-M32ZXLTT.mjs} +399 -20
- package/dist/handlers/index.d.mts +1 -1
- package/dist/handlers/index.d.ts +1 -1
- package/dist/handlers/index.js +402 -22
- package/dist/handlers/index.mjs +5 -3
- package/dist/{index-kUXnRvuI.d.mts → index-BejNscWZ.d.mts} +4 -2
- package/dist/{index-B67KQ9NN.d.ts → index-CbkMJvu5.d.ts} +4 -2
- package/dist/index.js +37 -46
- package/dist/index.mjs +37 -46
- package/dist/server.d.mts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +400 -20
- package/dist/server.mjs +1 -1
- package/package.json +1 -1
- package/src/components/MessageBubble.tsx +54 -80
- package/src/components/VisualizationRenderer.tsx +341 -0
- package/src/core/Pipeline.ts +39 -19
- package/src/handlers/index.ts +21 -3
- package/src/utils/UITransformer.ts +535 -0
|
@@ -32,8 +32,8 @@ function sanitizeJson(raw: string): string {
|
|
|
32
32
|
},
|
|
33
33
|
);
|
|
34
34
|
|
|
35
|
-
// 2. Strip JS-style single-line comments
|
|
36
|
-
s = s.replace(
|
|
35
|
+
// 2. Strip JS-style single-line comments
|
|
36
|
+
s = s.replace(/\/\/[^\n]*/g, '');
|
|
37
37
|
|
|
38
38
|
// 3. Quote bare (unquoted) object keys
|
|
39
39
|
s = s.replace(/([{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/g, '$1"$2":');
|
|
@@ -103,7 +103,7 @@ function extractLikelyJsonBlock(raw: string): string {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
function looksLikeStructuredPayload(raw: string): boolean {
|
|
106
|
-
return /"?(view|chartType|type|data|items|rows|products|results
|
|
106
|
+
return /"?(view|chartType|type|data|items|rows|products|results)"?\s*:/.test(raw);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
function stripMarkdownTables(raw: string): string {
|
|
@@ -204,23 +204,12 @@ function isLikelyUiOnlyMessage(text: string): boolean {
|
|
|
204
204
|
// ─── Tiny helpers ─────────────────────────────────────────────────────────────
|
|
205
205
|
|
|
206
206
|
function resolveImage(data: Record<string, unknown>): string | undefined {
|
|
207
|
-
const
|
|
208
|
-
for (const key of candidates) {
|
|
207
|
+
for (const key of ['image', 'img', 'thumbnail']) {
|
|
209
208
|
const v = data[key];
|
|
210
|
-
if (typeof v === 'string' && v
|
|
211
|
-
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
212
|
-
const subVal = (v as Record<string, unknown>).url || (v as Record<string, unknown>).link || (v as Record<string, unknown>).src;
|
|
213
|
-
if (typeof subVal === 'string' && subVal.startsWith('http')) return subVal;
|
|
214
|
-
}
|
|
209
|
+
if (typeof v === 'string' && v) return v;
|
|
215
210
|
}
|
|
216
|
-
if (Array.isArray(data.images)) {
|
|
217
|
-
|
|
218
|
-
if (typeof item === 'string' && item.startsWith('http')) return item;
|
|
219
|
-
if (item && typeof item === 'object') {
|
|
220
|
-
const url = item.url || item.link || item.src;
|
|
221
|
-
if (typeof url === 'string' && url.startsWith('http')) return url;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
211
|
+
if (Array.isArray(data.images) && typeof data.images[0] === 'string') {
|
|
212
|
+
return data.images[0];
|
|
224
213
|
}
|
|
225
214
|
return undefined;
|
|
226
215
|
}
|
|
@@ -436,21 +425,16 @@ function resolveStructuredRows(config: Partial<RawUIConfig> & Record<string, unk
|
|
|
436
425
|
return [];
|
|
437
426
|
}
|
|
438
427
|
|
|
439
|
-
function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, viewportSize = 'large' }: {
|
|
428
|
+
function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAddToCart, viewportSize = 'large' }: {
|
|
440
429
|
rawContent: string;
|
|
441
430
|
primaryColor?: string;
|
|
442
431
|
accentColor?: string;
|
|
443
432
|
isStreaming?: boolean;
|
|
433
|
+
onAddToCart?: (product: Product) => void;
|
|
444
434
|
viewportSize?: ChatViewportSize;
|
|
445
435
|
}) {
|
|
446
|
-
const result = React.useMemo<{ config: UIConfig } | { error: string } | { loading: true
|
|
447
|
-
if (isStreaming) {
|
|
448
|
-
const looksLikeCarousel = rawContent.includes('"view": "carousel"') ||
|
|
449
|
-
rawContent.includes('"view":"carousel"') ||
|
|
450
|
-
rawContent.includes('"type": "products"') ||
|
|
451
|
-
rawContent.includes('"type":"products"');
|
|
452
|
-
return { loading: true, silent: looksLikeCarousel };
|
|
453
|
-
}
|
|
436
|
+
const result = React.useMemo<{ config: UIConfig } | { error: string } | { loading: true }>(() => {
|
|
437
|
+
if (isStreaming) return { loading: true };
|
|
454
438
|
try {
|
|
455
439
|
const clean = rawContent
|
|
456
440
|
.replace(/^```[a-z]*\s*/i, '')
|
|
@@ -461,18 +445,18 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, view
|
|
|
461
445
|
const resolvedRows = resolveStructuredRows(config);
|
|
462
446
|
|
|
463
447
|
// ─── Intent Healing & Auto-Detection ───────────────────────────────────
|
|
464
|
-
|
|
465
|
-
const hasProductLikeData = resolvedRows.length > 0 &&
|
|
466
|
-
typeof resolvedRows[0] === 'object' && resolvedRows[0] !== null &&
|
|
448
|
+
|
|
449
|
+
const hasProductLikeData = resolvedRows.length > 0 &&
|
|
450
|
+
typeof resolvedRows[0] === 'object' && resolvedRows[0] !== null &&
|
|
467
451
|
!Array.isArray(resolvedRows[0]) &&
|
|
468
|
-
(['price', 'brand', 'image', 'link', 'thumbnail'].some(key => key in resolvedRows[0]) ||
|
|
469
|
-
|
|
452
|
+
(['price', 'brand', 'image', 'link', 'thumbnail'].some(key => key in resolvedRows[0]) ||
|
|
453
|
+
Object.keys(resolvedRows[0]).length >= 4);
|
|
470
454
|
|
|
471
|
-
const hasAggregationLikeData = resolvedRows.length > 0 &&
|
|
472
|
-
typeof resolvedRows[0] === 'object' && resolvedRows[0] !== null &&
|
|
455
|
+
const hasAggregationLikeData = resolvedRows.length > 0 &&
|
|
456
|
+
typeof resolvedRows[0] === 'object' && resolvedRows[0] !== null &&
|
|
473
457
|
!Array.isArray(resolvedRows[0]) &&
|
|
474
|
-
(['value', 'count', 'total', 'percentage'].some(key => key in resolvedRows[0]) ||
|
|
475
|
-
|
|
458
|
+
(['value', 'count', 'total', 'percentage'].some(key => key in resolvedRows[0]) ||
|
|
459
|
+
(Object.keys(resolvedRows[0]).length <= 3 && Object.values(resolvedRows[0]).some(v => typeof v === 'number')));
|
|
476
460
|
|
|
477
461
|
// 1. Healing: Override explicit view if it contradicts the data shape
|
|
478
462
|
if (config.view === 'chart' && hasProductLikeData && !hasAggregationLikeData) {
|
|
@@ -492,8 +476,8 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, view
|
|
|
492
476
|
(typeof config.chartType === 'string' && ['pie', 'bar', 'line'].includes(config.chartType))
|
|
493
477
|
) {
|
|
494
478
|
config.view = 'chart';
|
|
495
|
-
config.chartType = (config.chartType as 'pie' | 'bar' | 'line') ||
|
|
496
|
-
|
|
479
|
+
config.chartType = (config.chartType as 'pie' | 'bar' | 'line') ||
|
|
480
|
+
(config.type as 'pie' | 'bar' | 'line') || 'bar';
|
|
497
481
|
} else {
|
|
498
482
|
config.view = 'table';
|
|
499
483
|
}
|
|
@@ -511,7 +495,6 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, view
|
|
|
511
495
|
}, [rawContent, isStreaming]);
|
|
512
496
|
|
|
513
497
|
if ('loading' in result) {
|
|
514
|
-
if (result.silent) return null;
|
|
515
498
|
return (
|
|
516
499
|
<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">
|
|
517
500
|
<div className="w-5 h-5 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" />
|
|
@@ -564,14 +547,16 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, view
|
|
|
564
547
|
</div>
|
|
565
548
|
);
|
|
566
549
|
case 'carousel':
|
|
567
|
-
// We only render title/description here. The actual carousel is rendered separately
|
|
568
|
-
// at the bottom of the MessageBubble to avoid being trapped in the streaming loading box.
|
|
569
550
|
return (
|
|
570
551
|
<div className="my-4">
|
|
571
552
|
{config.title && <h4 className={`${isCompact ? 'mb-1.5 text-[11px]' : 'mb-2 text-xs'} font-semibold text-slate-500`}>{config.title}</h4>}
|
|
572
553
|
{hasDescription && (
|
|
573
554
|
<p className={`mb-3 ${isCompact ? 'text-xs' : 'text-sm'} text-slate-500 dark:text-white/60`}>{config.description}</p>
|
|
574
555
|
)}
|
|
556
|
+
<ProductCarousel products={(config.data as unknown as Product[]).map(item => ({
|
|
557
|
+
...item,
|
|
558
|
+
image: item.image ?? (typeof resolveImage === 'function' ? resolveImage(item as unknown as Record<string, unknown>) : undefined)
|
|
559
|
+
}))} primaryColor={primaryColor} onAddToCart={onAddToCart} />
|
|
575
560
|
</div>
|
|
576
561
|
);
|
|
577
562
|
case 'table':
|
|
@@ -614,6 +599,11 @@ export function MessageBubble({
|
|
|
614
599
|
const isCompact = viewportSize === 'compact';
|
|
615
600
|
const isMedium = viewportSize === 'medium';
|
|
616
601
|
const [showSources, setShowSources] = React.useState(false);
|
|
602
|
+
const hasStructuredProductBlock = React.useMemo(
|
|
603
|
+
() => /```(?:ui|json|chart)\s*[\s\S]*?"?(?:view|type|data|products)"?\s*:\s*"?(?:carousel|products|table)"?/i.test(message.content) ||
|
|
604
|
+
(/\{[\s\S]*?"?(?:view|type|data|products)"?\s*:\s*"?(?:carousel|products)"?[\s\S]*\}/i.test(message.content) && looksLikeStructuredPayload(message.content)),
|
|
605
|
+
[message.content],
|
|
606
|
+
);
|
|
617
607
|
const structuredContent = React.useMemo(
|
|
618
608
|
() => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
|
|
619
609
|
[isUser, message.content],
|
|
@@ -703,34 +693,20 @@ export function MessageBubble({
|
|
|
703
693
|
|
|
704
694
|
// ── Merge & deduplicate products ───────────────────────────────────────────
|
|
705
695
|
const allProducts = React.useMemo<Product[]>(() => {
|
|
706
|
-
|
|
707
|
-
const merged: Product[] = [];
|
|
708
|
-
|
|
709
|
-
// 1. Add products extracted from the LLM content (highest priority)
|
|
710
|
-
for (const p of productsFromContent) {
|
|
711
|
-
const id = String(p.id || p.name);
|
|
712
|
-
if (!seen.has(id)) {
|
|
713
|
-
seen.add(id);
|
|
714
|
-
merged.push(p);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
696
|
+
if (productsFromContent.length > 0) return productsFromContent;
|
|
717
697
|
|
|
718
|
-
|
|
698
|
+
const seen = new Set<string>();
|
|
719
699
|
const contentLower = message.content.toLowerCase();
|
|
720
|
-
for (const p of productsFromSources) {
|
|
721
|
-
const id = String(p.id || p.name);
|
|
722
|
-
if (seen.has(id)) continue;
|
|
723
|
-
|
|
724
|
-
const mentioned = contentLower.includes(p.name.toLowerCase()) ||
|
|
725
|
-
(p.brand ? contentLower.includes(p.brand.toLowerCase()) : false);
|
|
726
|
-
|
|
727
|
-
if (mentioned) {
|
|
728
|
-
seen.add(id);
|
|
729
|
-
merged.push(p);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
700
|
|
|
733
|
-
return
|
|
701
|
+
return productsFromSources.filter(p => {
|
|
702
|
+
const id = String(p.id ?? p.name);
|
|
703
|
+
if (seen.has(id)) return false;
|
|
704
|
+
const mentioned =
|
|
705
|
+
contentLower.includes(p.name.toLowerCase()) ||
|
|
706
|
+
(p.brand ? contentLower.includes(p.brand.toLowerCase()) : false);
|
|
707
|
+
if (mentioned) { seen.add(id); return true; }
|
|
708
|
+
return false;
|
|
709
|
+
});
|
|
734
710
|
}, [productsFromSources, productsFromContent, message.content]);
|
|
735
711
|
|
|
736
712
|
const processedMarkdown = React.useMemo(() => {
|
|
@@ -742,23 +718,17 @@ export function MessageBubble({
|
|
|
742
718
|
.replace(/(\|[^\n]*\|)\n\n+(?=\|)/g, '$1\n'); // Remove blank lines between rows
|
|
743
719
|
|
|
744
720
|
// 2. HEALING: Detect naked JSON charts/products and wrap them if markers are missing
|
|
745
|
-
// This handles models (like Llama 3.2) that often omit the ```
|
|
746
|
-
if (!isStreaming
|
|
721
|
+
// This handles models (like Llama 3.2) that often omit the ```chart markers.
|
|
722
|
+
if (!isStreaming) {
|
|
747
723
|
// Look for objects that look like chart or product data
|
|
748
|
-
raw = raw.replace(/(?:^|\n)\s
|
|
724
|
+
raw = raw.replace(/(?:^|\n)\s*\{[\s\S]*\}\s*(?:\n|$)/g, (match) => {
|
|
749
725
|
// Only wrap if not already in a code block
|
|
750
726
|
if (match.includes('```')) return match;
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
const type = trimmedBody.includes('"view"') ||
|
|
755
|
-
trimmedBody.includes('"chartType"') ||
|
|
756
|
-
trimmedBody.includes('"type"') ||
|
|
757
|
-
trimmedBody.includes('"xAxisKey"')
|
|
727
|
+
if (!looksLikeStructuredPayload(match)) return match;
|
|
728
|
+
const type = match.includes('"view"') || match.includes('"chartType"') || match.includes('"type": "pie"') || match.includes('"type":"pie"') || match.includes('"type": "bar"') || match.includes('"type":"bar"') || match.includes('"type": "line"') || match.includes('"type":"line"')
|
|
758
729
|
? 'ui'
|
|
759
730
|
: 'json';
|
|
760
|
-
|
|
761
|
-
return `\n\n\`\`\`${type}\n${trimmedBody}\n\`\`\`\n\n`;
|
|
731
|
+
return `\n\n\`\`\`${type}\n${match.trim()}\n\`\`\`\n\n`;
|
|
762
732
|
});
|
|
763
733
|
}
|
|
764
734
|
|
|
@@ -854,6 +824,7 @@ export function MessageBubble({
|
|
|
854
824
|
primaryColor={primaryColor}
|
|
855
825
|
accentColor={accentColor}
|
|
856
826
|
isStreaming={isStreaming}
|
|
827
|
+
onAddToCart={onAddToCart}
|
|
857
828
|
viewportSize={viewportSize}
|
|
858
829
|
/>
|
|
859
830
|
);
|
|
@@ -868,6 +839,7 @@ export function MessageBubble({
|
|
|
868
839
|
primaryColor={primaryColor}
|
|
869
840
|
accentColor={accentColor}
|
|
870
841
|
isStreaming={isStreaming}
|
|
842
|
+
onAddToCart={onAddToCart}
|
|
871
843
|
viewportSize={viewportSize}
|
|
872
844
|
/>
|
|
873
845
|
);
|
|
@@ -883,6 +855,7 @@ export function MessageBubble({
|
|
|
883
855
|
primaryColor={primaryColor}
|
|
884
856
|
accentColor={accentColor}
|
|
885
857
|
isStreaming={isStreaming}
|
|
858
|
+
onAddToCart={onAddToCart}
|
|
886
859
|
viewportSize={viewportSize}
|
|
887
860
|
/>
|
|
888
861
|
);
|
|
@@ -909,7 +882,7 @@ export function MessageBubble({
|
|
|
909
882
|
);
|
|
910
883
|
},
|
|
911
884
|
}),
|
|
912
|
-
[primaryColor, accentColor, isStreaming, viewportSize],
|
|
885
|
+
[primaryColor, accentColor, isStreaming, onAddToCart, viewportSize],
|
|
913
886
|
);
|
|
914
887
|
|
|
915
888
|
// ── Render ─────────────────────────────────────────────────────────────────
|
|
@@ -951,6 +924,7 @@ export function MessageBubble({
|
|
|
951
924
|
primaryColor={primaryColor}
|
|
952
925
|
accentColor={accentColor}
|
|
953
926
|
isStreaming={isStreaming}
|
|
927
|
+
onAddToCart={onAddToCart}
|
|
954
928
|
viewportSize={viewportSize}
|
|
955
929
|
/>
|
|
956
930
|
)}
|
|
@@ -969,7 +943,7 @@ export function MessageBubble({
|
|
|
969
943
|
</div>
|
|
970
944
|
|
|
971
945
|
{/* Product Carousel */}
|
|
972
|
-
{!isUser && allProducts.length > 0 && (
|
|
946
|
+
{!isUser && !hasStructuredProductBlock && allProducts.length > 0 && (
|
|
973
947
|
<div className="w-full mt-1">
|
|
974
948
|
<ProductCarousel
|
|
975
949
|
products={allProducts}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Image from 'next/image';
|
|
4
|
+
import {
|
|
5
|
+
UITransformationResponse,
|
|
6
|
+
VisualizationType,
|
|
7
|
+
PieChartData,
|
|
8
|
+
BarChartData,
|
|
9
|
+
LineChartDataPoint,
|
|
10
|
+
TableData,
|
|
11
|
+
CarouselProduct,
|
|
12
|
+
} from '@/utils/UITransformer';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Props for the VisualizationRenderer component
|
|
16
|
+
*/
|
|
17
|
+
interface VisualizationRendererProps {
|
|
18
|
+
data: UITransformationResponse;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* VisualizationRenderer — Renders UI transformations as React components
|
|
24
|
+
*
|
|
25
|
+
* Dynamically selects the appropriate visualization component based on the
|
|
26
|
+
* transformation type returned by UITransformer.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* const transformation = UITransformer.transform(query, results);
|
|
30
|
+
* return <VisualizationRenderer data={transformation} />;
|
|
31
|
+
*/
|
|
32
|
+
export function VisualizationRenderer({
|
|
33
|
+
data,
|
|
34
|
+
className = '',
|
|
35
|
+
}: VisualizationRendererProps) {
|
|
36
|
+
if (!data) {
|
|
37
|
+
return (
|
|
38
|
+
<div className={`p-4 text-gray-500 ${className}`}>
|
|
39
|
+
No data available for visualization
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<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">
|
|
48
|
+
{data.title}
|
|
49
|
+
</h3>
|
|
50
|
+
{data.description && (
|
|
51
|
+
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
52
|
+
{data.description}
|
|
53
|
+
</p>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
{renderVisualization(data.type, data.data)}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
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);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
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>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
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"
|
|
98
|
+
>
|
|
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>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
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>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
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>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
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>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
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>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
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>;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
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"
|
|
234
|
+
>
|
|
235
|
+
{String(cell)}
|
|
236
|
+
</td>
|
|
237
|
+
))}
|
|
238
|
+
</tr>
|
|
239
|
+
))}
|
|
240
|
+
</tbody>
|
|
241
|
+
</table>
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Render product carousel
|
|
248
|
+
*/
|
|
249
|
+
function renderProductCarousel(data: CarouselProduct[]): React.ReactNode {
|
|
250
|
+
if (!Array.isArray(data)) {
|
|
251
|
+
return <div className="text-red-500">Invalid carousel data</div>;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
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>
|
|
307
|
+
</div>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Render text
|
|
313
|
+
*/
|
|
314
|
+
function renderText(data: unknown): React.ReactNode {
|
|
315
|
+
const content = (data as { content?: string })?.content || String(data);
|
|
316
|
+
|
|
317
|
+
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">
|
|
320
|
+
{content}
|
|
321
|
+
</p>
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
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
|
+
}
|