@retrivora-ai/rag-engine 1.3.1 → 1.3.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-AYOQ4LF4.mjs → chunk-FQ7OYZLF.mjs} +4 -2
- package/dist/handlers/index.js +4 -2
- package/dist/handlers/index.mjs +1 -1
- package/dist/index.js +205 -110
- package/dist/index.mjs +205 -110
- package/dist/server.js +4 -2
- package/dist/server.mjs +1 -1
- package/package.json +1 -1
- package/src/components/DynamicChart.tsx +29 -15
- package/src/components/MessageBubble.tsx +246 -160
- package/src/core/LangChainAgent.ts +2 -1
- package/src/core/Pipeline.ts +2 -1
|
@@ -9,6 +9,132 @@ import { SourceCard } from './SourceCard';
|
|
|
9
9
|
import { ProductCarousel } from './ProductCarousel';
|
|
10
10
|
import { DynamicChart, ChartConfig } from './DynamicChart';
|
|
11
11
|
|
|
12
|
+
// ─── JSON sanitization ────────────────────────────────────────────────────────
|
|
13
|
+
// Order matters: each step is a targeted fix, no step undoes a previous one.
|
|
14
|
+
|
|
15
|
+
function sanitizeJson(raw: string): string {
|
|
16
|
+
let s = raw.trim();
|
|
17
|
+
|
|
18
|
+
// 1. Evaluate simple arithmetic expressions in values e.g. "v": 495 / 1000
|
|
19
|
+
s = s.replace(
|
|
20
|
+
/:\s*(-?\d+(?:\.\d+)?)\s*([+\-*/])\s*(-?\d+(?:\.\d+)?)/g,
|
|
21
|
+
(_, a, op, b) => {
|
|
22
|
+
const n1 = parseFloat(a);
|
|
23
|
+
const n2 = parseFloat(b);
|
|
24
|
+
const result =
|
|
25
|
+
op === '+' ? n1 + n2
|
|
26
|
+
: op === '-' ? n1 - n2
|
|
27
|
+
: op === '*' ? n1 * n2
|
|
28
|
+
: n2 !== 0 ? n1 / n2
|
|
29
|
+
: n1;
|
|
30
|
+
return `: ${result}`;
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// 2. Strip JS-style single-line comments
|
|
35
|
+
s = s.replace(/\/\/[^\n]*/g, '');
|
|
36
|
+
|
|
37
|
+
// 3. Quote bare (unquoted) object keys
|
|
38
|
+
s = s.replace(/([{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/g, '$1"$2":');
|
|
39
|
+
|
|
40
|
+
// 4. Replace single-quoted strings with double-quoted ones
|
|
41
|
+
s = s.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"');
|
|
42
|
+
|
|
43
|
+
// 5. Remove trailing commas before ] or }
|
|
44
|
+
s = s.replace(/,\s*([}\]])/g, '$1');
|
|
45
|
+
|
|
46
|
+
// 6. Remove completely empty key-value pairs left by prior cleanup
|
|
47
|
+
s = s.replace(/([{,])\s*""\s*(?=[,}])/g, '$1');
|
|
48
|
+
|
|
49
|
+
// 7. Balance braces/brackets: walk char-by-char, track depth, append closers
|
|
50
|
+
// Do this BEFORE the lastValidIndex trim so we don't cut valid JSON.
|
|
51
|
+
{
|
|
52
|
+
let inString = false;
|
|
53
|
+
let escape = false;
|
|
54
|
+
let braces = 0;
|
|
55
|
+
let brackets = 0;
|
|
56
|
+
|
|
57
|
+
for (const ch of s) {
|
|
58
|
+
if (escape) { escape = false; continue; }
|
|
59
|
+
if (ch === '\\' && inString) { escape = true; continue; }
|
|
60
|
+
if (ch === '"') { inString = !inString; continue; }
|
|
61
|
+
if (inString) continue;
|
|
62
|
+
if (ch === '{') braces++;
|
|
63
|
+
else if (ch === '}') braces--;
|
|
64
|
+
else if (ch === '[') brackets++;
|
|
65
|
+
else if (ch === ']') brackets--;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Close any unclosed string first
|
|
69
|
+
if (inString) s += '"';
|
|
70
|
+
// Then close structures in the correct order
|
|
71
|
+
while (brackets > 0) { s += ']'; brackets--; }
|
|
72
|
+
while (braces > 0) { s += '}'; braces--; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 8. Trim to the last valid closing char only if there is trailing garbage
|
|
76
|
+
// (e.g. a partial second object after the first one ends)
|
|
77
|
+
const lastClose = Math.max(s.lastIndexOf('}'), s.lastIndexOf(']'));
|
|
78
|
+
if (lastClose !== -1 && lastClose < s.length - 1) {
|
|
79
|
+
s = s.substring(0, lastClose + 1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return s;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Tiny helpers ─────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
function resolveImage(data: Record<string, unknown>): string | undefined {
|
|
88
|
+
for (const key of ['image', 'img', 'thumbnail']) {
|
|
89
|
+
const v = data[key];
|
|
90
|
+
if (typeof v === 'string' && v) return v;
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(data.images) && typeof data.images[0] === 'string') {
|
|
93
|
+
return data.images[0];
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── Chart code block renderer ────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
interface ChartBlockProps {
|
|
101
|
+
rawContent: string;
|
|
102
|
+
primaryColor: string;
|
|
103
|
+
accentColor: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ChartBlock({ rawContent, primaryColor, accentColor }: ChartBlockProps) {
|
|
107
|
+
const result = React.useMemo<{ config: ChartConfig } | { error: string }>(() => {
|
|
108
|
+
if (!rawContent || rawContent === 'undefined') return { error: 'Empty chart config.' };
|
|
109
|
+
try {
|
|
110
|
+
const sanitized = sanitizeJson(rawContent);
|
|
111
|
+
const config = JSON.parse(sanitized) as ChartConfig;
|
|
112
|
+
return { config };
|
|
113
|
+
} catch (err) {
|
|
114
|
+
const sanitized = sanitizeJson(rawContent);
|
|
115
|
+
console.error('[ChartBlock] Parsing failed.\nError:', err, '\nSanitized:\n', sanitized);
|
|
116
|
+
return { error: String(err) };
|
|
117
|
+
}
|
|
118
|
+
}, [rawContent]);
|
|
119
|
+
|
|
120
|
+
if ('error' in result) {
|
|
121
|
+
return (
|
|
122
|
+
<div className="p-4 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg text-sm border border-red-100 dark:border-red-900/30">
|
|
123
|
+
<p className="font-medium mb-1">Failed to render chart</p>
|
|
124
|
+
<p className="opacity-70 text-xs">The generated configuration is invalid. Check the console for details.</p>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div className="my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm">
|
|
131
|
+
<DynamicChart config={result.config} primaryColor={primaryColor} accentColor={accentColor} />
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
12
138
|
export function MessageBubble({
|
|
13
139
|
message,
|
|
14
140
|
sources,
|
|
@@ -20,71 +146,51 @@ export function MessageBubble({
|
|
|
20
146
|
const isUser = message.role === 'user';
|
|
21
147
|
const [showSources, setShowSources] = React.useState(false);
|
|
22
148
|
|
|
23
|
-
//
|
|
24
|
-
const
|
|
25
|
-
if (!data) return undefined;
|
|
26
|
-
|
|
27
|
-
// Check single string fields
|
|
28
|
-
const singleFields = ['image', 'img', 'thumbnail'];
|
|
29
|
-
for (const field of singleFields) {
|
|
30
|
-
const val = data[field];
|
|
31
|
-
if (typeof val === 'string' && val) return val;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Check images array
|
|
35
|
-
if (Array.isArray(data.images) && data.images.length > 0) {
|
|
36
|
-
if (typeof data.images[0] === 'string') return data.images[0];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return undefined;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// Extract products from sources
|
|
43
|
-
const productsFromSources = React.useMemo(() => {
|
|
149
|
+
// ── Products from sources ──────────────────────────────────────────────────
|
|
150
|
+
const productsFromSources = React.useMemo<Product[]>(() => {
|
|
44
151
|
if (isUser || !sources) return [];
|
|
45
152
|
return sources
|
|
46
153
|
.filter(s => {
|
|
47
|
-
const m = s.metadata
|
|
154
|
+
const m = s.metadata ?? {};
|
|
48
155
|
return m.price || m.image || m.img || m.thumbnail || m.images || m.brand || m.type === 'product';
|
|
49
156
|
})
|
|
50
157
|
.map(s => {
|
|
51
|
-
const m = (s.metadata
|
|
158
|
+
const m = (s.metadata ?? {}) as Record<string, unknown>;
|
|
52
159
|
return {
|
|
53
160
|
id: s.id,
|
|
54
|
-
name: (m.name
|
|
161
|
+
name: (m.name ?? m.title ?? s.content.split('\n')[0] ?? 'Unknown Product') as string,
|
|
55
162
|
brand: m.brand as string,
|
|
56
163
|
price: m.price as string | number,
|
|
57
164
|
image: resolveImage(m),
|
|
58
165
|
link: m.link as string,
|
|
59
|
-
description: s.content
|
|
166
|
+
description: s.content,
|
|
60
167
|
};
|
|
61
168
|
});
|
|
62
169
|
}, [sources, isUser]);
|
|
63
170
|
|
|
64
|
-
//
|
|
171
|
+
// ── Products + cleaned content from structured JSON blocks in the message ──
|
|
65
172
|
const { productsFromContent, cleanContent } = React.useMemo(() => {
|
|
66
|
-
if (isUser) return { productsFromContent: [], cleanContent: message.content };
|
|
173
|
+
if (isUser) return { productsFromContent: [] as Product[], cleanContent: message.content };
|
|
67
174
|
|
|
68
175
|
const jsonRegex = /```json\s*([\s\S]*?)\s*```/g;
|
|
69
176
|
const products: Product[] = [];
|
|
70
177
|
let content = message.content;
|
|
71
178
|
|
|
72
|
-
// Only process if it looks like it might contain product data
|
|
73
179
|
if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
|
|
74
|
-
const
|
|
75
|
-
for (const m of matches) {
|
|
180
|
+
for (const match of content.matchAll(jsonRegex)) {
|
|
76
181
|
try {
|
|
77
|
-
const data = JSON.parse(
|
|
182
|
+
const data = JSON.parse(match[1]);
|
|
78
183
|
if (data.type === 'products' && Array.isArray(data.items)) {
|
|
79
|
-
|
|
80
|
-
...item,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
184
|
+
products.push(
|
|
185
|
+
...data.items.map((item: Record<string, unknown>) => ({
|
|
186
|
+
...item,
|
|
187
|
+
image: (item.image as string) ?? resolveImage(item),
|
|
188
|
+
})) as Product[],
|
|
189
|
+
);
|
|
190
|
+
content = content.replace(match[0], '');
|
|
85
191
|
}
|
|
86
192
|
} catch {
|
|
87
|
-
//
|
|
193
|
+
// Malformed product block — skip silently
|
|
88
194
|
}
|
|
89
195
|
}
|
|
90
196
|
}
|
|
@@ -92,60 +198,110 @@ export function MessageBubble({
|
|
|
92
198
|
return { productsFromContent: products, cleanContent: content.trim() };
|
|
93
199
|
}, [message.content, isUser]);
|
|
94
200
|
|
|
95
|
-
|
|
96
|
-
|
|
201
|
+
// ── Merge & deduplicate products ───────────────────────────────────────────
|
|
202
|
+
const allProducts = React.useMemo<Product[]>(() => {
|
|
97
203
|
if (productsFromContent.length > 0) return productsFromContent;
|
|
98
204
|
|
|
99
|
-
|
|
100
|
-
// This aligns the carousel with what the assistant is actually "displaying" in its response.
|
|
101
|
-
const uniqueProducts: Product[] = [];
|
|
102
|
-
const seenIds = new Set();
|
|
205
|
+
const seen = new Set<string>();
|
|
103
206
|
const contentLower = message.content.toLowerCase();
|
|
104
207
|
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
seenIds.add(identifier);
|
|
115
|
-
uniqueProducts.push(p);
|
|
208
|
+
return productsFromSources.filter(p => {
|
|
209
|
+
const id = String(p.id ?? p.name);
|
|
210
|
+
if (seen.has(id)) return false;
|
|
211
|
+
const mentioned =
|
|
212
|
+
contentLower.includes(p.name.toLowerCase()) ||
|
|
213
|
+
(p.brand ? contentLower.includes(p.brand.toLowerCase()) : false);
|
|
214
|
+
if (mentioned) {
|
|
215
|
+
seen.add(id);
|
|
216
|
+
return true;
|
|
116
217
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return uniqueProducts;
|
|
218
|
+
return false;
|
|
219
|
+
});
|
|
120
220
|
}, [productsFromSources, productsFromContent, message.content]);
|
|
121
221
|
|
|
122
|
-
|
|
222
|
+
// ── Markdown component overrides ───────────────────────────────────────────
|
|
223
|
+
const markdownComponents = React.useMemo(
|
|
224
|
+
() => ({
|
|
225
|
+
table: ({ ...props }: React.HTMLAttributes<HTMLTableElement>) => (
|
|
226
|
+
<div className="overflow-x-auto my-4 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm">
|
|
227
|
+
<table className="w-full text-left border-collapse min-w-[400px]" {...props} />
|
|
228
|
+
</div>
|
|
229
|
+
),
|
|
230
|
+
thead: ({ ...props }: React.HTMLAttributes<HTMLTableSectionElement>) => (
|
|
231
|
+
<thead
|
|
232
|
+
className="bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10"
|
|
233
|
+
{...props}
|
|
234
|
+
/>
|
|
235
|
+
),
|
|
236
|
+
th: ({ children, ...props }: React.ThHTMLAttributes<HTMLTableCellElement>) => (
|
|
237
|
+
<th
|
|
238
|
+
className="p-3 font-semibold text-slate-700 dark:text-white/90 first:rounded-tl-xl last:rounded-tr-xl"
|
|
239
|
+
{...props}
|
|
240
|
+
>
|
|
241
|
+
{normaliseChild(children)}
|
|
242
|
+
</th>
|
|
243
|
+
),
|
|
244
|
+
td: ({ children, ...props }: React.TdHTMLAttributes<HTMLTableCellElement>) => (
|
|
245
|
+
<td
|
|
246
|
+
className="p-3 border-b border-slate-100 dark:border-white/5 last:border-0 text-slate-600 dark:text-white/70"
|
|
247
|
+
{...props}
|
|
248
|
+
>
|
|
249
|
+
{normaliseChild(children)}
|
|
250
|
+
</td>
|
|
251
|
+
),
|
|
252
|
+
code({
|
|
253
|
+
inline,
|
|
254
|
+
className,
|
|
255
|
+
children,
|
|
256
|
+
...props
|
|
257
|
+
}: React.HTMLAttributes<HTMLElement> & { inline?: boolean }) {
|
|
258
|
+
const lang = /language-(\w+)/.exec(className ?? '')?.[1];
|
|
123
259
|
|
|
260
|
+
if (!inline && lang === 'chart') {
|
|
261
|
+
return (
|
|
262
|
+
<ChartBlock
|
|
263
|
+
rawContent={String(children ?? '').trim()}
|
|
264
|
+
primaryColor={primaryColor}
|
|
265
|
+
accentColor={accentColor}
|
|
266
|
+
/>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<code className={className} {...props}>
|
|
272
|
+
{typeof children === 'string' || typeof children === 'number'
|
|
273
|
+
? children
|
|
274
|
+
: JSON.stringify(children)}
|
|
275
|
+
</code>
|
|
276
|
+
);
|
|
277
|
+
},
|
|
278
|
+
}),
|
|
279
|
+
[primaryColor, accentColor],
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// ── Render ─────────────────────────────────────────────────────────────────
|
|
124
283
|
return (
|
|
125
284
|
<div className={`flex gap-3 ${isUser ? 'flex-row-reverse' : 'flex-row'} items-start`}>
|
|
126
285
|
{/* Avatar */}
|
|
127
286
|
<div
|
|
128
|
-
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center shadow-lg ${isUser
|
|
287
|
+
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center shadow-lg ${isUser
|
|
288
|
+
? 'text-white'
|
|
289
|
+
: 'bg-slate-100 dark:bg-white/10 text-slate-700 dark:text-white/80'
|
|
129
290
|
}`}
|
|
130
291
|
style={isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}}
|
|
131
292
|
>
|
|
132
|
-
{isUser
|
|
133
|
-
? <User className="w-4 h-4 text-white" />
|
|
134
|
-
: <Bot className="w-4 h-4" />
|
|
135
|
-
}
|
|
293
|
+
{isUser ? <User className="w-4 h-4 text-white" /> : <Bot className="w-4 h-4" />}
|
|
136
294
|
</div>
|
|
137
295
|
|
|
138
296
|
<div className={`flex flex-col gap-1 max-w-[90%] ${isUser ? 'items-end' : 'items-start'}`}>
|
|
139
297
|
{/* Bubble */}
|
|
140
298
|
<div
|
|
141
299
|
className={`relative px-4 py-3 rounded-2xl text-sm leading-relaxed shadow-sm dark:shadow-lg ${isUser
|
|
142
|
-
|
|
143
|
-
|
|
300
|
+
? 'text-white rounded-tr-sm'
|
|
301
|
+
: 'bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-slate-800 dark:text-white/90 rounded-tl-sm'
|
|
144
302
|
}`}
|
|
145
303
|
style={
|
|
146
|
-
isUser
|
|
147
|
-
? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` }
|
|
148
|
-
: {}
|
|
304
|
+
isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
|
|
149
305
|
}
|
|
150
306
|
>
|
|
151
307
|
{isStreaming && !message.content ? (
|
|
@@ -156,89 +312,7 @@ export function MessageBubble({
|
|
|
156
312
|
</span>
|
|
157
313
|
) : (
|
|
158
314
|
<div className={`prose prose-sm max-w-none ${isUser ? 'prose-invert' : 'dark:prose-invert'}`}>
|
|
159
|
-
<ReactMarkdown
|
|
160
|
-
remarkPlugins={[remarkGfm]}
|
|
161
|
-
components={{
|
|
162
|
-
table: ({...props}) => (
|
|
163
|
-
<div className="overflow-x-auto my-4 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm">
|
|
164
|
-
<table className="w-full text-left border-collapse min-w-[400px]" {...props} />
|
|
165
|
-
</div>
|
|
166
|
-
),
|
|
167
|
-
thead: ({...props}) => (
|
|
168
|
-
<thead className="bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10" {...props} />
|
|
169
|
-
),
|
|
170
|
-
th: ({children, ...props}) => (
|
|
171
|
-
<th className="p-3 font-semibold text-slate-700 dark:text-white/90 first:rounded-tl-xl last:rounded-tr-xl" {...props}>
|
|
172
|
-
{typeof children === 'object' && children !== null && !Array.isArray(children) && !(children as any).$$typeof ? JSON.stringify(children) : children}
|
|
173
|
-
</th>
|
|
174
|
-
),
|
|
175
|
-
td: ({children, ...props}) => (
|
|
176
|
-
<td className="p-3 border-b border-slate-100 dark:border-white/5 last:border-0 text-slate-600 dark:text-white/70" {...props}>
|
|
177
|
-
{typeof children === 'object' && children !== null && !Array.isArray(children) && !(children as any).$$typeof ? JSON.stringify(children) : children}
|
|
178
|
-
</td>
|
|
179
|
-
),
|
|
180
|
-
code({ inline, className, children, ...props }: React.HTMLAttributes<HTMLElement> & { inline?: boolean }) {
|
|
181
|
-
const match = /language-(\w+)/.exec(className || '');
|
|
182
|
-
const isChart = match && match[1] === 'chart';
|
|
183
|
-
|
|
184
|
-
if (!inline && isChart) {
|
|
185
|
-
const rawContent = String(children || '').trim();
|
|
186
|
-
|
|
187
|
-
if (!rawContent || rawContent === 'undefined') {
|
|
188
|
-
return null;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Optimized sanitization helper to handle LLM quirks
|
|
192
|
-
const sanitizeJson = (json: string) => {
|
|
193
|
-
return json
|
|
194
|
-
// 1. Resolve arithmetic: "value": 495 / 1000 => "value": 0.495
|
|
195
|
-
.replace(/:\s*(\d+(\.\d+)?)\s*([\+\-\*\/])\s*(\d+(\.\d+)?)/g, (_, n1, __, op, n2) => {
|
|
196
|
-
const v1 = parseFloat(n1); const v2 = parseFloat(n2);
|
|
197
|
-
const ops: Record<string, number> = { '+': v1 + v2, '-': v1 - v2, '*': v1 * v2, '/': v1 / v2 };
|
|
198
|
-
return `: ${ops[op] ?? v1}`;
|
|
199
|
-
})
|
|
200
|
-
// 2. Quote unquoted keys and fix single quotes
|
|
201
|
-
.replace(/([{,])\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1 "$2":')
|
|
202
|
-
.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"')
|
|
203
|
-
// 3. Syntax cleanup: trailing/missing commas and comments
|
|
204
|
-
.replace(/,\s*([\]}])/g, '$1')
|
|
205
|
-
.replace(/}\s*{/g, '},{')
|
|
206
|
-
.replace(/]\s*\[/g, '],[')
|
|
207
|
-
.replace(/\/\/.*$/gm, '')
|
|
208
|
-
// 4. Remove dangling empty strings
|
|
209
|
-
.replace(/([{,])\s*""\s*([,}])/g, '$1$2');
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
try {
|
|
213
|
-
const sanitized = sanitizeJson(rawContent);
|
|
214
|
-
const config = JSON.parse(sanitized) as ChartConfig;
|
|
215
|
-
return (
|
|
216
|
-
<div className="my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm">
|
|
217
|
-
<DynamicChart
|
|
218
|
-
config={config}
|
|
219
|
-
primaryColor={primaryColor}
|
|
220
|
-
accentColor={accentColor}
|
|
221
|
-
/>
|
|
222
|
-
</div>
|
|
223
|
-
);
|
|
224
|
-
} catch (err) {
|
|
225
|
-
console.error('[MessageBubble] Chart parsing failed:', err, '\nSanitized content:', sanitizeJson(rawContent));
|
|
226
|
-
return (
|
|
227
|
-
<div className="p-4 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg text-sm border border-red-100 dark:border-red-900/30">
|
|
228
|
-
<p className="font-medium mb-1">Failed to render chart</p>
|
|
229
|
-
<p className="opacity-70 text-xs">The generated configuration is invalid. Check console for details.</p>
|
|
230
|
-
</div>
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return (
|
|
235
|
-
<code className={className} {...props}>
|
|
236
|
-
{typeof children === 'string' || typeof children === 'number' ? children : JSON.stringify(children)}
|
|
237
|
-
</code>
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
}}
|
|
241
|
-
>
|
|
315
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
|
242
316
|
{cleanContent || message.content}
|
|
243
317
|
</ReactMarkdown>
|
|
244
318
|
|
|
@@ -249,8 +323,8 @@ export function MessageBubble({
|
|
|
249
323
|
)}
|
|
250
324
|
</div>
|
|
251
325
|
|
|
252
|
-
{/* Product Carousel
|
|
253
|
-
{!isUser &&
|
|
326
|
+
{/* Product Carousel */}
|
|
327
|
+
{!isUser && allProducts.length > 0 && (
|
|
254
328
|
<div className="w-full mt-1">
|
|
255
329
|
<ProductCarousel
|
|
256
330
|
products={allProducts}
|
|
@@ -260,17 +334,14 @@ export function MessageBubble({
|
|
|
260
334
|
</div>
|
|
261
335
|
)}
|
|
262
336
|
|
|
263
|
-
{/* Sources
|
|
337
|
+
{/* Sources */}
|
|
264
338
|
{!isUser && sources && sources.length > 0 && (
|
|
265
339
|
<div className="w-full">
|
|
266
340
|
<button
|
|
267
|
-
onClick={() => setShowSources(
|
|
341
|
+
onClick={() => setShowSources(s => !s)}
|
|
268
342
|
className="text-[11px] text-indigo-400 hover:text-indigo-300 transition-colors flex items-center gap-1 mt-1"
|
|
269
343
|
>
|
|
270
|
-
{showSources
|
|
271
|
-
? <ChevronDown className="w-3 h-3" />
|
|
272
|
-
: <ChevronRight className="w-3 h-3" />
|
|
273
|
-
}
|
|
344
|
+
{showSources ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
|
274
345
|
{sources.length} source{sources.length !== 1 ? 's' : ''} used
|
|
275
346
|
</button>
|
|
276
347
|
|
|
@@ -287,3 +358,18 @@ export function MessageBubble({
|
|
|
287
358
|
</div>
|
|
288
359
|
);
|
|
289
360
|
}
|
|
361
|
+
|
|
362
|
+
// ─── Util ─────────────────────────────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
/** Safely render table cell children that might be plain objects. */
|
|
365
|
+
function normaliseChild(children: React.ReactNode): React.ReactNode {
|
|
366
|
+
if (
|
|
367
|
+
typeof children === 'object' &&
|
|
368
|
+
children !== null &&
|
|
369
|
+
!Array.isArray(children) &&
|
|
370
|
+
!React.isValidElement(children)
|
|
371
|
+
) {
|
|
372
|
+
return JSON.stringify(children);
|
|
373
|
+
}
|
|
374
|
+
return children;
|
|
375
|
+
}
|
|
@@ -57,7 +57,8 @@ export class LangChainAgent {
|
|
|
57
57
|
1. Use ONLY valid JSON (double quotes, no trailing commas).
|
|
58
58
|
2. NO math expressions (e.g., use 100, NOT 50+50).
|
|
59
59
|
3. dataKeys MUST exactly match the property names in the data objects.
|
|
60
|
-
4.
|
|
60
|
+
4. data objects MUST be FLAT (no nested objects or arrays).
|
|
61
|
+
5. DO NOT output naked JSON; it MUST be wrapped in \`\`\`chart ... \`\`\`.
|
|
61
62
|
\`\`\`chart
|
|
62
63
|
{
|
|
63
64
|
"type": "bar" | "line" | "pie",
|
package/src/core/Pipeline.ts
CHANGED
|
@@ -107,7 +107,8 @@ export class Pipeline {
|
|
|
107
107
|
1. Use ONLY valid JSON (double quotes, no trailing commas).
|
|
108
108
|
2. NO math expressions (e.g., use 100, NOT 50+50).
|
|
109
109
|
3. dataKeys MUST exactly match the property names in the data objects.
|
|
110
|
-
4.
|
|
110
|
+
4. data objects MUST be FLAT (no nested objects or arrays).
|
|
111
|
+
5. DO NOT output naked JSON; it MUST be wrapped in \`\`\`chart ... \`\`\`.
|
|
111
112
|
\`\`\`chart
|
|
112
113
|
{
|
|
113
114
|
"type": "bar" | "line" | "pie",
|