@retrivora-ai/rag-engine 1.3.2 → 1.3.4
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/index.js +178 -106
- package/dist/index.mjs +178 -106
- package/package.json +1 -1
- package/src/components/MessageBubble.tsx +252 -172
package/dist/index.js
CHANGED
|
@@ -318,6 +318,87 @@ function DynamicChart({ config, primaryColor = "#6366f1", accentColor = "#8b5cf6
|
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
// src/components/MessageBubble.tsx
|
|
321
|
+
function sanitizeJson(raw) {
|
|
322
|
+
let s = raw.trim();
|
|
323
|
+
s = s.replace(
|
|
324
|
+
/:\s*(-?\d+(?:\.\d+)?)\s*([+\-*/])\s*(-?\d+(?:\.\d+)?)/g,
|
|
325
|
+
(_, a, op, b) => {
|
|
326
|
+
const n1 = parseFloat(a);
|
|
327
|
+
const n2 = parseFloat(b);
|
|
328
|
+
const result = op === "+" ? n1 + n2 : op === "-" ? n1 - n2 : op === "*" ? n1 * n2 : n2 !== 0 ? n1 / n2 : n1;
|
|
329
|
+
return `: ${result}`;
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
s = s.replace(/\/\/[^\n]*/g, "");
|
|
333
|
+
s = s.replace(/([{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/g, '$1"$2":');
|
|
334
|
+
s = s.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"');
|
|
335
|
+
s = s.replace(/,\s*([}\]])/g, "$1");
|
|
336
|
+
s = s.replace(/([{,])\s*""\s*(?=[,}])/g, "$1");
|
|
337
|
+
{
|
|
338
|
+
const stack = [];
|
|
339
|
+
let inString = false;
|
|
340
|
+
let escape = false;
|
|
341
|
+
for (const ch of s) {
|
|
342
|
+
if (escape) {
|
|
343
|
+
escape = false;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (ch === "\\" && inString) {
|
|
347
|
+
escape = true;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (ch === '"') {
|
|
351
|
+
inString = !inString;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (inString) continue;
|
|
355
|
+
if (ch === "{" || ch === "[") {
|
|
356
|
+
stack.push(ch);
|
|
357
|
+
} else if (ch === "}") {
|
|
358
|
+
if (stack[stack.length - 1] === "{") stack.pop();
|
|
359
|
+
} else if (ch === "]") {
|
|
360
|
+
if (stack[stack.length - 1] === "[") stack.pop();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (inString) s += '"';
|
|
364
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
365
|
+
s += stack[i] === "{" ? "}" : "]";
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const lastClose = Math.max(s.lastIndexOf("}"), s.lastIndexOf("]"));
|
|
369
|
+
if (lastClose !== -1 && lastClose < s.length - 1 && /\S/.test(s.slice(lastClose + 1))) {
|
|
370
|
+
s = s.substring(0, lastClose + 1);
|
|
371
|
+
}
|
|
372
|
+
return s;
|
|
373
|
+
}
|
|
374
|
+
function resolveImage(data) {
|
|
375
|
+
for (const key of ["image", "img", "thumbnail"]) {
|
|
376
|
+
const v = data[key];
|
|
377
|
+
if (typeof v === "string" && v) return v;
|
|
378
|
+
}
|
|
379
|
+
if (Array.isArray(data.images) && typeof data.images[0] === "string") {
|
|
380
|
+
return data.images[0];
|
|
381
|
+
}
|
|
382
|
+
return void 0;
|
|
383
|
+
}
|
|
384
|
+
function ChartBlock({ rawContent, primaryColor, accentColor }) {
|
|
385
|
+
const result = import_react5.default.useMemo(() => {
|
|
386
|
+
if (!rawContent || rawContent === "undefined") return { error: "Empty chart config." };
|
|
387
|
+
try {
|
|
388
|
+
const sanitized = sanitizeJson(rawContent);
|
|
389
|
+
const config = JSON.parse(sanitized);
|
|
390
|
+
return { config };
|
|
391
|
+
} catch (err) {
|
|
392
|
+
const sanitized = sanitizeJson(rawContent);
|
|
393
|
+
console.error("[ChartBlock] Parsing failed.\nError:", err, "\nSanitized:\n", sanitized);
|
|
394
|
+
return { error: String(err) };
|
|
395
|
+
}
|
|
396
|
+
}, [rawContent]);
|
|
397
|
+
if ("error" in result) {
|
|
398
|
+
return /* @__PURE__ */ import_react5.default.createElement("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" }, /* @__PURE__ */ import_react5.default.createElement("p", { className: "font-medium mb-1" }, "Failed to render chart"), /* @__PURE__ */ import_react5.default.createElement("p", { className: "opacity-70 text-xs" }, "The generated configuration is invalid. Check the console for details."));
|
|
399
|
+
}
|
|
400
|
+
return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm" }, /* @__PURE__ */ import_react5.default.createElement(DynamicChart, { config: result.config, primaryColor, accentColor }));
|
|
401
|
+
}
|
|
321
402
|
function MessageBubble({
|
|
322
403
|
message,
|
|
323
404
|
sources,
|
|
@@ -328,28 +409,18 @@ function MessageBubble({
|
|
|
328
409
|
}) {
|
|
329
410
|
const isUser = message.role === "user";
|
|
330
411
|
const [showSources, setShowSources] = import_react5.default.useState(false);
|
|
331
|
-
const resolveImage = (data) => {
|
|
332
|
-
if (!data) return void 0;
|
|
333
|
-
const singleFields = ["image", "img", "thumbnail"];
|
|
334
|
-
for (const field of singleFields) {
|
|
335
|
-
const val = data[field];
|
|
336
|
-
if (typeof val === "string" && val) return val;
|
|
337
|
-
}
|
|
338
|
-
if (Array.isArray(data.images) && data.images.length > 0) {
|
|
339
|
-
if (typeof data.images[0] === "string") return data.images[0];
|
|
340
|
-
}
|
|
341
|
-
return void 0;
|
|
342
|
-
};
|
|
343
412
|
const productsFromSources = import_react5.default.useMemo(() => {
|
|
344
413
|
if (isUser || !sources) return [];
|
|
345
414
|
return sources.filter((s) => {
|
|
346
|
-
|
|
415
|
+
var _a;
|
|
416
|
+
const m = (_a = s.metadata) != null ? _a : {};
|
|
347
417
|
return m.price || m.image || m.img || m.thumbnail || m.images || m.brand || m.type === "product";
|
|
348
418
|
}).map((s) => {
|
|
349
|
-
|
|
419
|
+
var _a, _b, _c, _d;
|
|
420
|
+
const m = (_a = s.metadata) != null ? _a : {};
|
|
350
421
|
return {
|
|
351
422
|
id: s.id,
|
|
352
|
-
name: m.name
|
|
423
|
+
name: (_d = (_c = (_b = m.name) != null ? _b : m.title) != null ? _c : s.content.split("\n")[0]) != null ? _d : "Unknown Product",
|
|
353
424
|
brand: m.brand,
|
|
354
425
|
price: m.price,
|
|
355
426
|
image: resolveImage(m),
|
|
@@ -364,16 +435,19 @@ function MessageBubble({
|
|
|
364
435
|
const products = [];
|
|
365
436
|
let content = message.content;
|
|
366
437
|
if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
|
|
367
|
-
const
|
|
368
|
-
for (const m of matches) {
|
|
438
|
+
for (const match of content.matchAll(jsonRegex)) {
|
|
369
439
|
try {
|
|
370
|
-
const data = JSON.parse(
|
|
440
|
+
const data = JSON.parse(match[1]);
|
|
371
441
|
if (data.type === "products" && Array.isArray(data.items)) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
442
|
+
products.push(
|
|
443
|
+
...data.items.map((item) => {
|
|
444
|
+
var _a;
|
|
445
|
+
return __spreadProps(__spreadValues({}, item), {
|
|
446
|
+
image: (_a = item.image) != null ? _a : resolveImage(item)
|
|
447
|
+
});
|
|
448
|
+
})
|
|
449
|
+
);
|
|
450
|
+
content = content.replace(match[0], "");
|
|
377
451
|
}
|
|
378
452
|
} catch (e) {
|
|
379
453
|
}
|
|
@@ -383,22 +457,82 @@ function MessageBubble({
|
|
|
383
457
|
}, [message.content, isUser]);
|
|
384
458
|
const allProducts = import_react5.default.useMemo(() => {
|
|
385
459
|
if (productsFromContent.length > 0) return productsFromContent;
|
|
386
|
-
const
|
|
387
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
460
|
+
const seen = /* @__PURE__ */ new Set();
|
|
388
461
|
const contentLower = message.content.toLowerCase();
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
|
|
462
|
+
return productsFromSources.filter((p) => {
|
|
463
|
+
var _a;
|
|
464
|
+
const id = String((_a = p.id) != null ? _a : p.name);
|
|
465
|
+
if (seen.has(id)) return false;
|
|
466
|
+
const mentioned = contentLower.includes(p.name.toLowerCase()) || (p.brand ? contentLower.includes(p.brand.toLowerCase()) : false);
|
|
467
|
+
if (mentioned) {
|
|
468
|
+
seen.add(id);
|
|
469
|
+
return true;
|
|
397
470
|
}
|
|
398
|
-
|
|
399
|
-
|
|
471
|
+
return false;
|
|
472
|
+
});
|
|
400
473
|
}, [productsFromSources, productsFromContent, message.content]);
|
|
401
|
-
const
|
|
474
|
+
const markdownComponents = import_react5.default.useMemo(
|
|
475
|
+
() => ({
|
|
476
|
+
table: (_a) => {
|
|
477
|
+
var props = __objRest(_a, []);
|
|
478
|
+
return /* @__PURE__ */ import_react5.default.createElement("div", { className: "overflow-x-auto my-4 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm" }, /* @__PURE__ */ import_react5.default.createElement("table", __spreadValues({ className: "w-full text-left border-collapse min-w-[400px]" }, props)));
|
|
479
|
+
},
|
|
480
|
+
thead: (_b) => {
|
|
481
|
+
var props = __objRest(_b, []);
|
|
482
|
+
return /* @__PURE__ */ import_react5.default.createElement(
|
|
483
|
+
"thead",
|
|
484
|
+
__spreadValues({
|
|
485
|
+
className: "bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10"
|
|
486
|
+
}, props)
|
|
487
|
+
);
|
|
488
|
+
},
|
|
489
|
+
th: (_c) => {
|
|
490
|
+
var _d = _c, { children } = _d, props = __objRest(_d, ["children"]);
|
|
491
|
+
return /* @__PURE__ */ import_react5.default.createElement(
|
|
492
|
+
"th",
|
|
493
|
+
__spreadValues({
|
|
494
|
+
className: "p-3 font-semibold text-slate-700 dark:text-white/90 first:rounded-tl-xl last:rounded-tr-xl"
|
|
495
|
+
}, props),
|
|
496
|
+
normaliseChild(children)
|
|
497
|
+
);
|
|
498
|
+
},
|
|
499
|
+
td: (_e) => {
|
|
500
|
+
var _f = _e, { children } = _f, props = __objRest(_f, ["children"]);
|
|
501
|
+
return /* @__PURE__ */ import_react5.default.createElement(
|
|
502
|
+
"td",
|
|
503
|
+
__spreadValues({
|
|
504
|
+
className: "p-3 border-b border-slate-100 dark:border-white/5 last:border-0 text-slate-600 dark:text-white/70"
|
|
505
|
+
}, props),
|
|
506
|
+
normaliseChild(children)
|
|
507
|
+
);
|
|
508
|
+
},
|
|
509
|
+
code(_g) {
|
|
510
|
+
var _h = _g, {
|
|
511
|
+
inline,
|
|
512
|
+
className,
|
|
513
|
+
children
|
|
514
|
+
} = _h, props = __objRest(_h, [
|
|
515
|
+
"inline",
|
|
516
|
+
"className",
|
|
517
|
+
"children"
|
|
518
|
+
]);
|
|
519
|
+
var _a;
|
|
520
|
+
const lang = (_a = /language-(\w+)/.exec(className != null ? className : "")) == null ? void 0 : _a[1];
|
|
521
|
+
if (!inline && lang === "chart") {
|
|
522
|
+
return /* @__PURE__ */ import_react5.default.createElement(
|
|
523
|
+
ChartBlock,
|
|
524
|
+
{
|
|
525
|
+
rawContent: String(children != null ? children : "").trim(),
|
|
526
|
+
primaryColor,
|
|
527
|
+
accentColor
|
|
528
|
+
}
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
return /* @__PURE__ */ import_react5.default.createElement("code", __spreadValues({ className }, props), typeof children === "string" || typeof children === "number" ? children : JSON.stringify(children));
|
|
532
|
+
}
|
|
533
|
+
}),
|
|
534
|
+
[primaryColor, accentColor]
|
|
535
|
+
);
|
|
402
536
|
return /* @__PURE__ */ import_react5.default.createElement("div", { className: `flex gap-3 ${isUser ? "flex-row-reverse" : "flex-row"} items-start` }, /* @__PURE__ */ import_react5.default.createElement(
|
|
403
537
|
"div",
|
|
404
538
|
{
|
|
@@ -412,76 +546,8 @@ function MessageBubble({
|
|
|
412
546
|
className: `relative px-4 py-3 rounded-2xl text-sm leading-relaxed shadow-sm dark:shadow-lg ${isUser ? "text-white rounded-tr-sm" : "bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-slate-800 dark:text-white/90 rounded-tl-sm"}`,
|
|
413
547
|
style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
|
|
414
548
|
},
|
|
415
|
-
isStreaming && !message.content ? /* @__PURE__ */ import_react5.default.createElement("span", { className: "flex items-center gap-1 py-1" }, /* @__PURE__ */ import_react5.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:0ms]" }), /* @__PURE__ */ import_react5.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:150ms]" }), /* @__PURE__ */ import_react5.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:300ms]" })) : /* @__PURE__ */ import_react5.default.createElement("div", { className: `prose prose-sm max-w-none ${isUser ? "prose-invert" : "dark:prose-invert"}` }, /* @__PURE__ */ import_react5.default.createElement(
|
|
416
|
-
|
|
417
|
-
{
|
|
418
|
-
remarkPlugins: [import_remark_gfm.default],
|
|
419
|
-
components: {
|
|
420
|
-
table: (_a) => {
|
|
421
|
-
var props = __objRest(_a, []);
|
|
422
|
-
return /* @__PURE__ */ import_react5.default.createElement("div", { className: "overflow-x-auto my-4 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm" }, /* @__PURE__ */ import_react5.default.createElement("table", __spreadValues({ className: "w-full text-left border-collapse min-w-[400px]" }, props)));
|
|
423
|
-
},
|
|
424
|
-
thead: (_b) => {
|
|
425
|
-
var props = __objRest(_b, []);
|
|
426
|
-
return /* @__PURE__ */ import_react5.default.createElement("thead", __spreadValues({ className: "bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10" }, props));
|
|
427
|
-
},
|
|
428
|
-
th: (_c) => {
|
|
429
|
-
var _d = _c, { children } = _d, props = __objRest(_d, ["children"]);
|
|
430
|
-
return /* @__PURE__ */ import_react5.default.createElement("th", __spreadValues({ className: "p-3 font-semibold text-slate-700 dark:text-white/90 first:rounded-tl-xl last:rounded-tr-xl" }, props), typeof children === "object" && children !== null && !Array.isArray(children) && !import_react5.default.isValidElement(children) ? JSON.stringify(children) : children);
|
|
431
|
-
},
|
|
432
|
-
td: (_e) => {
|
|
433
|
-
var _f = _e, { children } = _f, props = __objRest(_f, ["children"]);
|
|
434
|
-
return /* @__PURE__ */ import_react5.default.createElement("td", __spreadValues({ className: "p-3 border-b border-slate-100 dark:border-white/5 last:border-0 text-slate-600 dark:text-white/70" }, props), typeof children === "object" && children !== null && !Array.isArray(children) && !import_react5.default.isValidElement(children) ? JSON.stringify(children) : children);
|
|
435
|
-
},
|
|
436
|
-
code(_g) {
|
|
437
|
-
var _h = _g, { inline, className, children } = _h, props = __objRest(_h, ["inline", "className", "children"]);
|
|
438
|
-
const match = /language-(\w+)/.exec(className || "");
|
|
439
|
-
const isChart = match && match[1] === "chart";
|
|
440
|
-
if (!inline && isChart) {
|
|
441
|
-
const rawContent = String(children || "").trim();
|
|
442
|
-
if (!rawContent || rawContent === "undefined") {
|
|
443
|
-
return null;
|
|
444
|
-
}
|
|
445
|
-
const sanitizeJson = (json) => {
|
|
446
|
-
const firstBrace = json.indexOf("{");
|
|
447
|
-
const firstBracket = json.indexOf("[");
|
|
448
|
-
const start = firstBrace !== -1 && (firstBracket === -1 || firstBrace < firstBracket) ? firstBrace : firstBracket;
|
|
449
|
-
const lastBrace = json.lastIndexOf("}");
|
|
450
|
-
const lastBracket = json.lastIndexOf("]");
|
|
451
|
-
const end = Math.max(lastBrace, lastBracket);
|
|
452
|
-
if (start === -1 || end === -1 || end < start) return json;
|
|
453
|
-
const trimmed = json.substring(start, end + 1);
|
|
454
|
-
return trimmed.replace(/:\s*(\d+(\.\d+)?)\s*([\+\-\*\/])\s*(\d+(\.\d+)?)/g, (_, n1, __, op, n2) => {
|
|
455
|
-
var _a;
|
|
456
|
-
const v1 = parseFloat(n1);
|
|
457
|
-
const v2 = parseFloat(n2);
|
|
458
|
-
const ops = { "+": v1 + v2, "-": v1 - v2, "*": v1 * v2, "/": v1 / v2 };
|
|
459
|
-
return `: ${(_a = ops[op]) != null ? _a : v1}`;
|
|
460
|
-
}).replace(/([{,])\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1 "$2":').replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"').replace(/,\s*([\]}])/g, "$1").replace(/}\s*{/g, "},{").replace(/]\s*\[/g, "],[").replace(/\/\/.*$/gm, "").replace(/([{,])\s*""\s*([,}])/g, "$1$2");
|
|
461
|
-
};
|
|
462
|
-
try {
|
|
463
|
-
const sanitized = sanitizeJson(rawContent);
|
|
464
|
-
const config = JSON.parse(sanitized);
|
|
465
|
-
return /* @__PURE__ */ import_react5.default.createElement("div", { className: "my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm" }, /* @__PURE__ */ import_react5.default.createElement(
|
|
466
|
-
DynamicChart,
|
|
467
|
-
{
|
|
468
|
-
config,
|
|
469
|
-
primaryColor,
|
|
470
|
-
accentColor
|
|
471
|
-
}
|
|
472
|
-
));
|
|
473
|
-
} catch (err) {
|
|
474
|
-
console.error("[MessageBubble] Chart parsing failed:", err, "\nSanitized content:", sanitizeJson(rawContent));
|
|
475
|
-
return /* @__PURE__ */ import_react5.default.createElement("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" }, /* @__PURE__ */ import_react5.default.createElement("p", { className: "font-medium mb-1" }, "Failed to render chart"), /* @__PURE__ */ import_react5.default.createElement("p", { className: "opacity-70 text-xs" }, "The generated configuration is invalid. Check console for details."));
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
return /* @__PURE__ */ import_react5.default.createElement("code", __spreadValues({ className }, props), typeof children === "string" || typeof children === "number" ? children : JSON.stringify(children));
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
},
|
|
482
|
-
cleanContent || message.content
|
|
483
|
-
), isStreaming && message.content && /* @__PURE__ */ import_react5.default.createElement("span", { className: "inline-block w-1.5 h-3.5 ml-1 bg-emerald-500 animate-pulse align-middle" }))
|
|
484
|
-
), !isUser && hasProducts && /* @__PURE__ */ import_react5.default.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ import_react5.default.createElement(
|
|
549
|
+
isStreaming && !message.content ? /* @__PURE__ */ import_react5.default.createElement("span", { className: "flex items-center gap-1 py-1" }, /* @__PURE__ */ import_react5.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:0ms]" }), /* @__PURE__ */ import_react5.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:150ms]" }), /* @__PURE__ */ import_react5.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:300ms]" })) : /* @__PURE__ */ import_react5.default.createElement("div", { className: `prose prose-sm max-w-none ${isUser ? "prose-invert" : "dark:prose-invert"}` }, /* @__PURE__ */ import_react5.default.createElement(import_react_markdown.default, { remarkPlugins: [import_remark_gfm.default], components: markdownComponents }, cleanContent || message.content), isStreaming && message.content && /* @__PURE__ */ import_react5.default.createElement("span", { className: "inline-block w-1.5 h-3.5 ml-1 bg-emerald-500 animate-pulse align-middle" }))
|
|
550
|
+
), !isUser && allProducts.length > 0 && /* @__PURE__ */ import_react5.default.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ import_react5.default.createElement(
|
|
485
551
|
ProductCarousel,
|
|
486
552
|
{
|
|
487
553
|
products: allProducts,
|
|
@@ -501,6 +567,12 @@ function MessageBubble({
|
|
|
501
567
|
" used"
|
|
502
568
|
), showSources && /* @__PURE__ */ import_react5.default.createElement("div", { className: "mt-2 flex flex-col gap-2" }, sources.map((src, i) => /* @__PURE__ */ import_react5.default.createElement(SourceCard, { key: src.id, source: src, index: i }))))));
|
|
503
569
|
}
|
|
570
|
+
function normaliseChild(children) {
|
|
571
|
+
if (typeof children === "object" && children !== null && !Array.isArray(children) && !import_react5.default.isValidElement(children)) {
|
|
572
|
+
return JSON.stringify(children);
|
|
573
|
+
}
|
|
574
|
+
return children;
|
|
575
|
+
}
|
|
504
576
|
|
|
505
577
|
// src/components/ConfigProvider.tsx
|
|
506
578
|
var import_react6 = __toESM(require("react"));
|
package/dist/index.mjs
CHANGED
|
@@ -281,6 +281,87 @@ function DynamicChart({ config, primaryColor = "#6366f1", accentColor = "#8b5cf6
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
// src/components/MessageBubble.tsx
|
|
284
|
+
function sanitizeJson(raw) {
|
|
285
|
+
let s = raw.trim();
|
|
286
|
+
s = s.replace(
|
|
287
|
+
/:\s*(-?\d+(?:\.\d+)?)\s*([+\-*/])\s*(-?\d+(?:\.\d+)?)/g,
|
|
288
|
+
(_, a, op, b) => {
|
|
289
|
+
const n1 = parseFloat(a);
|
|
290
|
+
const n2 = parseFloat(b);
|
|
291
|
+
const result = op === "+" ? n1 + n2 : op === "-" ? n1 - n2 : op === "*" ? n1 * n2 : n2 !== 0 ? n1 / n2 : n1;
|
|
292
|
+
return `: ${result}`;
|
|
293
|
+
}
|
|
294
|
+
);
|
|
295
|
+
s = s.replace(/\/\/[^\n]*/g, "");
|
|
296
|
+
s = s.replace(/([{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/g, '$1"$2":');
|
|
297
|
+
s = s.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"');
|
|
298
|
+
s = s.replace(/,\s*([}\]])/g, "$1");
|
|
299
|
+
s = s.replace(/([{,])\s*""\s*(?=[,}])/g, "$1");
|
|
300
|
+
{
|
|
301
|
+
const stack = [];
|
|
302
|
+
let inString = false;
|
|
303
|
+
let escape = false;
|
|
304
|
+
for (const ch of s) {
|
|
305
|
+
if (escape) {
|
|
306
|
+
escape = false;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (ch === "\\" && inString) {
|
|
310
|
+
escape = true;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (ch === '"') {
|
|
314
|
+
inString = !inString;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (inString) continue;
|
|
318
|
+
if (ch === "{" || ch === "[") {
|
|
319
|
+
stack.push(ch);
|
|
320
|
+
} else if (ch === "}") {
|
|
321
|
+
if (stack[stack.length - 1] === "{") stack.pop();
|
|
322
|
+
} else if (ch === "]") {
|
|
323
|
+
if (stack[stack.length - 1] === "[") stack.pop();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (inString) s += '"';
|
|
327
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
328
|
+
s += stack[i] === "{" ? "}" : "]";
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const lastClose = Math.max(s.lastIndexOf("}"), s.lastIndexOf("]"));
|
|
332
|
+
if (lastClose !== -1 && lastClose < s.length - 1 && /\S/.test(s.slice(lastClose + 1))) {
|
|
333
|
+
s = s.substring(0, lastClose + 1);
|
|
334
|
+
}
|
|
335
|
+
return s;
|
|
336
|
+
}
|
|
337
|
+
function resolveImage(data) {
|
|
338
|
+
for (const key of ["image", "img", "thumbnail"]) {
|
|
339
|
+
const v = data[key];
|
|
340
|
+
if (typeof v === "string" && v) return v;
|
|
341
|
+
}
|
|
342
|
+
if (Array.isArray(data.images) && typeof data.images[0] === "string") {
|
|
343
|
+
return data.images[0];
|
|
344
|
+
}
|
|
345
|
+
return void 0;
|
|
346
|
+
}
|
|
347
|
+
function ChartBlock({ rawContent, primaryColor, accentColor }) {
|
|
348
|
+
const result = React5.useMemo(() => {
|
|
349
|
+
if (!rawContent || rawContent === "undefined") return { error: "Empty chart config." };
|
|
350
|
+
try {
|
|
351
|
+
const sanitized = sanitizeJson(rawContent);
|
|
352
|
+
const config = JSON.parse(sanitized);
|
|
353
|
+
return { config };
|
|
354
|
+
} catch (err) {
|
|
355
|
+
const sanitized = sanitizeJson(rawContent);
|
|
356
|
+
console.error("[ChartBlock] Parsing failed.\nError:", err, "\nSanitized:\n", sanitized);
|
|
357
|
+
return { error: String(err) };
|
|
358
|
+
}
|
|
359
|
+
}, [rawContent]);
|
|
360
|
+
if ("error" in result) {
|
|
361
|
+
return /* @__PURE__ */ React5.createElement("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" }, /* @__PURE__ */ React5.createElement("p", { className: "font-medium mb-1" }, "Failed to render chart"), /* @__PURE__ */ React5.createElement("p", { className: "opacity-70 text-xs" }, "The generated configuration is invalid. Check the console for details."));
|
|
362
|
+
}
|
|
363
|
+
return /* @__PURE__ */ React5.createElement("div", { className: "my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm" }, /* @__PURE__ */ React5.createElement(DynamicChart, { config: result.config, primaryColor, accentColor }));
|
|
364
|
+
}
|
|
284
365
|
function MessageBubble({
|
|
285
366
|
message,
|
|
286
367
|
sources,
|
|
@@ -291,28 +372,18 @@ function MessageBubble({
|
|
|
291
372
|
}) {
|
|
292
373
|
const isUser = message.role === "user";
|
|
293
374
|
const [showSources, setShowSources] = React5.useState(false);
|
|
294
|
-
const resolveImage = (data) => {
|
|
295
|
-
if (!data) return void 0;
|
|
296
|
-
const singleFields = ["image", "img", "thumbnail"];
|
|
297
|
-
for (const field of singleFields) {
|
|
298
|
-
const val = data[field];
|
|
299
|
-
if (typeof val === "string" && val) return val;
|
|
300
|
-
}
|
|
301
|
-
if (Array.isArray(data.images) && data.images.length > 0) {
|
|
302
|
-
if (typeof data.images[0] === "string") return data.images[0];
|
|
303
|
-
}
|
|
304
|
-
return void 0;
|
|
305
|
-
};
|
|
306
375
|
const productsFromSources = React5.useMemo(() => {
|
|
307
376
|
if (isUser || !sources) return [];
|
|
308
377
|
return sources.filter((s) => {
|
|
309
|
-
|
|
378
|
+
var _a;
|
|
379
|
+
const m = (_a = s.metadata) != null ? _a : {};
|
|
310
380
|
return m.price || m.image || m.img || m.thumbnail || m.images || m.brand || m.type === "product";
|
|
311
381
|
}).map((s) => {
|
|
312
|
-
|
|
382
|
+
var _a, _b, _c, _d;
|
|
383
|
+
const m = (_a = s.metadata) != null ? _a : {};
|
|
313
384
|
return {
|
|
314
385
|
id: s.id,
|
|
315
|
-
name: m.name
|
|
386
|
+
name: (_d = (_c = (_b = m.name) != null ? _b : m.title) != null ? _c : s.content.split("\n")[0]) != null ? _d : "Unknown Product",
|
|
316
387
|
brand: m.brand,
|
|
317
388
|
price: m.price,
|
|
318
389
|
image: resolveImage(m),
|
|
@@ -327,16 +398,19 @@ function MessageBubble({
|
|
|
327
398
|
const products = [];
|
|
328
399
|
let content = message.content;
|
|
329
400
|
if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
|
|
330
|
-
const
|
|
331
|
-
for (const m of matches) {
|
|
401
|
+
for (const match of content.matchAll(jsonRegex)) {
|
|
332
402
|
try {
|
|
333
|
-
const data = JSON.parse(
|
|
403
|
+
const data = JSON.parse(match[1]);
|
|
334
404
|
if (data.type === "products" && Array.isArray(data.items)) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
405
|
+
products.push(
|
|
406
|
+
...data.items.map((item) => {
|
|
407
|
+
var _a;
|
|
408
|
+
return __spreadProps(__spreadValues({}, item), {
|
|
409
|
+
image: (_a = item.image) != null ? _a : resolveImage(item)
|
|
410
|
+
});
|
|
411
|
+
})
|
|
412
|
+
);
|
|
413
|
+
content = content.replace(match[0], "");
|
|
340
414
|
}
|
|
341
415
|
} catch (e) {
|
|
342
416
|
}
|
|
@@ -346,22 +420,82 @@ function MessageBubble({
|
|
|
346
420
|
}, [message.content, isUser]);
|
|
347
421
|
const allProducts = React5.useMemo(() => {
|
|
348
422
|
if (productsFromContent.length > 0) return productsFromContent;
|
|
349
|
-
const
|
|
350
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
423
|
+
const seen = /* @__PURE__ */ new Set();
|
|
351
424
|
const contentLower = message.content.toLowerCase();
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
if (
|
|
358
|
-
|
|
359
|
-
|
|
425
|
+
return productsFromSources.filter((p) => {
|
|
426
|
+
var _a;
|
|
427
|
+
const id = String((_a = p.id) != null ? _a : p.name);
|
|
428
|
+
if (seen.has(id)) return false;
|
|
429
|
+
const mentioned = contentLower.includes(p.name.toLowerCase()) || (p.brand ? contentLower.includes(p.brand.toLowerCase()) : false);
|
|
430
|
+
if (mentioned) {
|
|
431
|
+
seen.add(id);
|
|
432
|
+
return true;
|
|
360
433
|
}
|
|
361
|
-
|
|
362
|
-
|
|
434
|
+
return false;
|
|
435
|
+
});
|
|
363
436
|
}, [productsFromSources, productsFromContent, message.content]);
|
|
364
|
-
const
|
|
437
|
+
const markdownComponents = React5.useMemo(
|
|
438
|
+
() => ({
|
|
439
|
+
table: (_a) => {
|
|
440
|
+
var props = __objRest(_a, []);
|
|
441
|
+
return /* @__PURE__ */ React5.createElement("div", { className: "overflow-x-auto my-4 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm" }, /* @__PURE__ */ React5.createElement("table", __spreadValues({ className: "w-full text-left border-collapse min-w-[400px]" }, props)));
|
|
442
|
+
},
|
|
443
|
+
thead: (_b) => {
|
|
444
|
+
var props = __objRest(_b, []);
|
|
445
|
+
return /* @__PURE__ */ React5.createElement(
|
|
446
|
+
"thead",
|
|
447
|
+
__spreadValues({
|
|
448
|
+
className: "bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10"
|
|
449
|
+
}, props)
|
|
450
|
+
);
|
|
451
|
+
},
|
|
452
|
+
th: (_c) => {
|
|
453
|
+
var _d = _c, { children } = _d, props = __objRest(_d, ["children"]);
|
|
454
|
+
return /* @__PURE__ */ React5.createElement(
|
|
455
|
+
"th",
|
|
456
|
+
__spreadValues({
|
|
457
|
+
className: "p-3 font-semibold text-slate-700 dark:text-white/90 first:rounded-tl-xl last:rounded-tr-xl"
|
|
458
|
+
}, props),
|
|
459
|
+
normaliseChild(children)
|
|
460
|
+
);
|
|
461
|
+
},
|
|
462
|
+
td: (_e) => {
|
|
463
|
+
var _f = _e, { children } = _f, props = __objRest(_f, ["children"]);
|
|
464
|
+
return /* @__PURE__ */ React5.createElement(
|
|
465
|
+
"td",
|
|
466
|
+
__spreadValues({
|
|
467
|
+
className: "p-3 border-b border-slate-100 dark:border-white/5 last:border-0 text-slate-600 dark:text-white/70"
|
|
468
|
+
}, props),
|
|
469
|
+
normaliseChild(children)
|
|
470
|
+
);
|
|
471
|
+
},
|
|
472
|
+
code(_g) {
|
|
473
|
+
var _h = _g, {
|
|
474
|
+
inline,
|
|
475
|
+
className,
|
|
476
|
+
children
|
|
477
|
+
} = _h, props = __objRest(_h, [
|
|
478
|
+
"inline",
|
|
479
|
+
"className",
|
|
480
|
+
"children"
|
|
481
|
+
]);
|
|
482
|
+
var _a;
|
|
483
|
+
const lang = (_a = /language-(\w+)/.exec(className != null ? className : "")) == null ? void 0 : _a[1];
|
|
484
|
+
if (!inline && lang === "chart") {
|
|
485
|
+
return /* @__PURE__ */ React5.createElement(
|
|
486
|
+
ChartBlock,
|
|
487
|
+
{
|
|
488
|
+
rawContent: String(children != null ? children : "").trim(),
|
|
489
|
+
primaryColor,
|
|
490
|
+
accentColor
|
|
491
|
+
}
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
return /* @__PURE__ */ React5.createElement("code", __spreadValues({ className }, props), typeof children === "string" || typeof children === "number" ? children : JSON.stringify(children));
|
|
495
|
+
}
|
|
496
|
+
}),
|
|
497
|
+
[primaryColor, accentColor]
|
|
498
|
+
);
|
|
365
499
|
return /* @__PURE__ */ React5.createElement("div", { className: `flex gap-3 ${isUser ? "flex-row-reverse" : "flex-row"} items-start` }, /* @__PURE__ */ React5.createElement(
|
|
366
500
|
"div",
|
|
367
501
|
{
|
|
@@ -375,76 +509,8 @@ function MessageBubble({
|
|
|
375
509
|
className: `relative px-4 py-3 rounded-2xl text-sm leading-relaxed shadow-sm dark:shadow-lg ${isUser ? "text-white rounded-tr-sm" : "bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-slate-800 dark:text-white/90 rounded-tl-sm"}`,
|
|
376
510
|
style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
|
|
377
511
|
},
|
|
378
|
-
isStreaming && !message.content ? /* @__PURE__ */ React5.createElement("span", { className: "flex items-center gap-1 py-1" }, /* @__PURE__ */ React5.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:0ms]" }), /* @__PURE__ */ React5.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:150ms]" }), /* @__PURE__ */ React5.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:300ms]" })) : /* @__PURE__ */ React5.createElement("div", { className: `prose prose-sm max-w-none ${isUser ? "prose-invert" : "dark:prose-invert"}` }, /* @__PURE__ */ React5.createElement(
|
|
379
|
-
|
|
380
|
-
{
|
|
381
|
-
remarkPlugins: [remarkGfm],
|
|
382
|
-
components: {
|
|
383
|
-
table: (_a) => {
|
|
384
|
-
var props = __objRest(_a, []);
|
|
385
|
-
return /* @__PURE__ */ React5.createElement("div", { className: "overflow-x-auto my-4 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm" }, /* @__PURE__ */ React5.createElement("table", __spreadValues({ className: "w-full text-left border-collapse min-w-[400px]" }, props)));
|
|
386
|
-
},
|
|
387
|
-
thead: (_b) => {
|
|
388
|
-
var props = __objRest(_b, []);
|
|
389
|
-
return /* @__PURE__ */ React5.createElement("thead", __spreadValues({ className: "bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10" }, props));
|
|
390
|
-
},
|
|
391
|
-
th: (_c) => {
|
|
392
|
-
var _d = _c, { children } = _d, props = __objRest(_d, ["children"]);
|
|
393
|
-
return /* @__PURE__ */ React5.createElement("th", __spreadValues({ className: "p-3 font-semibold text-slate-700 dark:text-white/90 first:rounded-tl-xl last:rounded-tr-xl" }, props), typeof children === "object" && children !== null && !Array.isArray(children) && !React5.isValidElement(children) ? JSON.stringify(children) : children);
|
|
394
|
-
},
|
|
395
|
-
td: (_e) => {
|
|
396
|
-
var _f = _e, { children } = _f, props = __objRest(_f, ["children"]);
|
|
397
|
-
return /* @__PURE__ */ React5.createElement("td", __spreadValues({ className: "p-3 border-b border-slate-100 dark:border-white/5 last:border-0 text-slate-600 dark:text-white/70" }, props), typeof children === "object" && children !== null && !Array.isArray(children) && !React5.isValidElement(children) ? JSON.stringify(children) : children);
|
|
398
|
-
},
|
|
399
|
-
code(_g) {
|
|
400
|
-
var _h = _g, { inline, className, children } = _h, props = __objRest(_h, ["inline", "className", "children"]);
|
|
401
|
-
const match = /language-(\w+)/.exec(className || "");
|
|
402
|
-
const isChart = match && match[1] === "chart";
|
|
403
|
-
if (!inline && isChart) {
|
|
404
|
-
const rawContent = String(children || "").trim();
|
|
405
|
-
if (!rawContent || rawContent === "undefined") {
|
|
406
|
-
return null;
|
|
407
|
-
}
|
|
408
|
-
const sanitizeJson = (json) => {
|
|
409
|
-
const firstBrace = json.indexOf("{");
|
|
410
|
-
const firstBracket = json.indexOf("[");
|
|
411
|
-
const start = firstBrace !== -1 && (firstBracket === -1 || firstBrace < firstBracket) ? firstBrace : firstBracket;
|
|
412
|
-
const lastBrace = json.lastIndexOf("}");
|
|
413
|
-
const lastBracket = json.lastIndexOf("]");
|
|
414
|
-
const end = Math.max(lastBrace, lastBracket);
|
|
415
|
-
if (start === -1 || end === -1 || end < start) return json;
|
|
416
|
-
const trimmed = json.substring(start, end + 1);
|
|
417
|
-
return trimmed.replace(/:\s*(\d+(\.\d+)?)\s*([\+\-\*\/])\s*(\d+(\.\d+)?)/g, (_, n1, __, op, n2) => {
|
|
418
|
-
var _a;
|
|
419
|
-
const v1 = parseFloat(n1);
|
|
420
|
-
const v2 = parseFloat(n2);
|
|
421
|
-
const ops = { "+": v1 + v2, "-": v1 - v2, "*": v1 * v2, "/": v1 / v2 };
|
|
422
|
-
return `: ${(_a = ops[op]) != null ? _a : v1}`;
|
|
423
|
-
}).replace(/([{,])\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1 "$2":').replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"').replace(/,\s*([\]}])/g, "$1").replace(/}\s*{/g, "},{").replace(/]\s*\[/g, "],[").replace(/\/\/.*$/gm, "").replace(/([{,])\s*""\s*([,}])/g, "$1$2");
|
|
424
|
-
};
|
|
425
|
-
try {
|
|
426
|
-
const sanitized = sanitizeJson(rawContent);
|
|
427
|
-
const config = JSON.parse(sanitized);
|
|
428
|
-
return /* @__PURE__ */ React5.createElement("div", { className: "my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm" }, /* @__PURE__ */ React5.createElement(
|
|
429
|
-
DynamicChart,
|
|
430
|
-
{
|
|
431
|
-
config,
|
|
432
|
-
primaryColor,
|
|
433
|
-
accentColor
|
|
434
|
-
}
|
|
435
|
-
));
|
|
436
|
-
} catch (err) {
|
|
437
|
-
console.error("[MessageBubble] Chart parsing failed:", err, "\nSanitized content:", sanitizeJson(rawContent));
|
|
438
|
-
return /* @__PURE__ */ React5.createElement("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" }, /* @__PURE__ */ React5.createElement("p", { className: "font-medium mb-1" }, "Failed to render chart"), /* @__PURE__ */ React5.createElement("p", { className: "opacity-70 text-xs" }, "The generated configuration is invalid. Check console for details."));
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
return /* @__PURE__ */ React5.createElement("code", __spreadValues({ className }, props), typeof children === "string" || typeof children === "number" ? children : JSON.stringify(children));
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
},
|
|
445
|
-
cleanContent || message.content
|
|
446
|
-
), isStreaming && message.content && /* @__PURE__ */ React5.createElement("span", { className: "inline-block w-1.5 h-3.5 ml-1 bg-emerald-500 animate-pulse align-middle" }))
|
|
447
|
-
), !isUser && hasProducts && /* @__PURE__ */ React5.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ React5.createElement(
|
|
512
|
+
isStreaming && !message.content ? /* @__PURE__ */ React5.createElement("span", { className: "flex items-center gap-1 py-1" }, /* @__PURE__ */ React5.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:0ms]" }), /* @__PURE__ */ React5.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:150ms]" }), /* @__PURE__ */ React5.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:300ms]" })) : /* @__PURE__ */ React5.createElement("div", { className: `prose prose-sm max-w-none ${isUser ? "prose-invert" : "dark:prose-invert"}` }, /* @__PURE__ */ React5.createElement(ReactMarkdown, { remarkPlugins: [remarkGfm], components: markdownComponents }, cleanContent || message.content), isStreaming && message.content && /* @__PURE__ */ React5.createElement("span", { className: "inline-block w-1.5 h-3.5 ml-1 bg-emerald-500 animate-pulse align-middle" }))
|
|
513
|
+
), !isUser && allProducts.length > 0 && /* @__PURE__ */ React5.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ React5.createElement(
|
|
448
514
|
ProductCarousel,
|
|
449
515
|
{
|
|
450
516
|
products: allProducts,
|
|
@@ -464,6 +530,12 @@ function MessageBubble({
|
|
|
464
530
|
" used"
|
|
465
531
|
), showSources && /* @__PURE__ */ React5.createElement("div", { className: "mt-2 flex flex-col gap-2" }, sources.map((src, i) => /* @__PURE__ */ React5.createElement(SourceCard, { key: src.id, source: src, index: i }))))));
|
|
466
532
|
}
|
|
533
|
+
function normaliseChild(children) {
|
|
534
|
+
if (typeof children === "object" && children !== null && !Array.isArray(children) && !React5.isValidElement(children)) {
|
|
535
|
+
return JSON.stringify(children);
|
|
536
|
+
}
|
|
537
|
+
return children;
|
|
538
|
+
}
|
|
467
539
|
|
|
468
540
|
// src/components/ConfigProvider.tsx
|
|
469
541
|
import React6, { createContext, useContext } from "react";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retrivora-ai/rag-engine",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"description": "Retrivora AI is a plug-and-play AI engine for RAG chat experiences — generic vector DB + LLM provider, embeddable or standalone.",
|
|
5
5
|
"author": "Abhinav Alkuchi",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,6 +9,141 @@ 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. Stack-based structure balancer.
|
|
50
|
+
// Walk every character, maintain a stack of openers so we close each
|
|
51
|
+
// unclosed structure with its exact matching closer — in the right order.
|
|
52
|
+
// This is the critical fix: a flat counter approach appends all ]'s then
|
|
53
|
+
// all }'s, which corrupts nested structures like {..., [..., {...}]}.
|
|
54
|
+
{
|
|
55
|
+
const stack: ('{' | '[')[] = [];
|
|
56
|
+
let inString = false;
|
|
57
|
+
let escape = false;
|
|
58
|
+
|
|
59
|
+
for (const ch of s) {
|
|
60
|
+
if (escape) { escape = false; continue; }
|
|
61
|
+
if (ch === '\\' && inString) { escape = true; continue; }
|
|
62
|
+
if (ch === '"') { inString = !inString; continue; }
|
|
63
|
+
if (inString) continue;
|
|
64
|
+
|
|
65
|
+
if (ch === '{' || ch === '[') {
|
|
66
|
+
stack.push(ch);
|
|
67
|
+
} else if (ch === '}') {
|
|
68
|
+
if (stack[stack.length - 1] === '{') stack.pop();
|
|
69
|
+
// mismatched closer — leave stack alone (sanitize step 5 handles trailing commas)
|
|
70
|
+
} else if (ch === ']') {
|
|
71
|
+
if (stack[stack.length - 1] === '[') stack.pop();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Close any unterminated string literal first
|
|
76
|
+
if (inString) s += '"';
|
|
77
|
+
|
|
78
|
+
// Close every unclosed opener in reverse (innermost first)
|
|
79
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
80
|
+
s += stack[i] === '{' ? '}' : ']';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 8. Trim trailing garbage that appears after the outermost closing char.
|
|
85
|
+
// Only fires when something non-whitespace follows the last } or ].
|
|
86
|
+
const lastClose = Math.max(s.lastIndexOf('}'), s.lastIndexOf(']'));
|
|
87
|
+
if (lastClose !== -1 && lastClose < s.length - 1 && /\S/.test(s.slice(lastClose + 1))) {
|
|
88
|
+
s = s.substring(0, lastClose + 1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return s;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── Tiny helpers ─────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
function resolveImage(data: Record<string, unknown>): string | undefined {
|
|
97
|
+
for (const key of ['image', 'img', 'thumbnail']) {
|
|
98
|
+
const v = data[key];
|
|
99
|
+
if (typeof v === 'string' && v) return v;
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(data.images) && typeof data.images[0] === 'string') {
|
|
102
|
+
return data.images[0];
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── Chart code block renderer ────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
interface ChartBlockProps {
|
|
110
|
+
rawContent: string;
|
|
111
|
+
primaryColor: string;
|
|
112
|
+
accentColor: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function ChartBlock({ rawContent, primaryColor, accentColor }: ChartBlockProps) {
|
|
116
|
+
const result = React.useMemo<{ config: ChartConfig } | { error: string }>(() => {
|
|
117
|
+
if (!rawContent || rawContent === 'undefined') return { error: 'Empty chart config.' };
|
|
118
|
+
try {
|
|
119
|
+
const sanitized = sanitizeJson(rawContent);
|
|
120
|
+
const config = JSON.parse(sanitized) as ChartConfig;
|
|
121
|
+
return { config };
|
|
122
|
+
} catch (err) {
|
|
123
|
+
const sanitized = sanitizeJson(rawContent);
|
|
124
|
+
console.error('[ChartBlock] Parsing failed.\nError:', err, '\nSanitized:\n', sanitized);
|
|
125
|
+
return { error: String(err) };
|
|
126
|
+
}
|
|
127
|
+
}, [rawContent]);
|
|
128
|
+
|
|
129
|
+
if ('error' in result) {
|
|
130
|
+
return (
|
|
131
|
+
<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">
|
|
132
|
+
<p className="font-medium mb-1">Failed to render chart</p>
|
|
133
|
+
<p className="opacity-70 text-xs">The generated configuration is invalid. Check the console for details.</p>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div className="my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm">
|
|
140
|
+
<DynamicChart config={result.config} primaryColor={primaryColor} accentColor={accentColor} />
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
12
147
|
export function MessageBubble({
|
|
13
148
|
message,
|
|
14
149
|
sources,
|
|
@@ -20,71 +155,51 @@ export function MessageBubble({
|
|
|
20
155
|
const isUser = message.role === 'user';
|
|
21
156
|
const [showSources, setShowSources] = React.useState(false);
|
|
22
157
|
|
|
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(() => {
|
|
158
|
+
// ── Products from sources ──────────────────────────────────────────────────
|
|
159
|
+
const productsFromSources = React.useMemo<Product[]>(() => {
|
|
44
160
|
if (isUser || !sources) return [];
|
|
45
161
|
return sources
|
|
46
162
|
.filter(s => {
|
|
47
|
-
const m = s.metadata
|
|
163
|
+
const m = s.metadata ?? {};
|
|
48
164
|
return m.price || m.image || m.img || m.thumbnail || m.images || m.brand || m.type === 'product';
|
|
49
165
|
})
|
|
50
166
|
.map(s => {
|
|
51
|
-
const m = (s.metadata
|
|
167
|
+
const m = (s.metadata ?? {}) as Record<string, unknown>;
|
|
52
168
|
return {
|
|
53
169
|
id: s.id,
|
|
54
|
-
name: (m.name
|
|
170
|
+
name: (m.name ?? m.title ?? s.content.split('\n')[0] ?? 'Unknown Product') as string,
|
|
55
171
|
brand: m.brand as string,
|
|
56
172
|
price: m.price as string | number,
|
|
57
173
|
image: resolveImage(m),
|
|
58
174
|
link: m.link as string,
|
|
59
|
-
description: s.content
|
|
175
|
+
description: s.content,
|
|
60
176
|
};
|
|
61
177
|
});
|
|
62
178
|
}, [sources, isUser]);
|
|
63
179
|
|
|
64
|
-
//
|
|
180
|
+
// ── Products + cleaned content from structured JSON blocks in the message ──
|
|
65
181
|
const { productsFromContent, cleanContent } = React.useMemo(() => {
|
|
66
|
-
if (isUser) return { productsFromContent: [], cleanContent: message.content };
|
|
182
|
+
if (isUser) return { productsFromContent: [] as Product[], cleanContent: message.content };
|
|
67
183
|
|
|
68
184
|
const jsonRegex = /```json\s*([\s\S]*?)\s*```/g;
|
|
69
185
|
const products: Product[] = [];
|
|
70
186
|
let content = message.content;
|
|
71
187
|
|
|
72
|
-
// Only process if it looks like it might contain product data
|
|
73
188
|
if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
|
|
74
|
-
const
|
|
75
|
-
for (const m of matches) {
|
|
189
|
+
for (const match of content.matchAll(jsonRegex)) {
|
|
76
190
|
try {
|
|
77
|
-
const data = JSON.parse(
|
|
191
|
+
const data = JSON.parse(match[1]);
|
|
78
192
|
if (data.type === 'products' && Array.isArray(data.items)) {
|
|
79
|
-
|
|
80
|
-
...item,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
193
|
+
products.push(
|
|
194
|
+
...data.items.map((item: Record<string, unknown>) => ({
|
|
195
|
+
...item,
|
|
196
|
+
image: (item.image as string) ?? resolveImage(item),
|
|
197
|
+
})) as Product[],
|
|
198
|
+
);
|
|
199
|
+
content = content.replace(match[0], '');
|
|
85
200
|
}
|
|
86
201
|
} catch {
|
|
87
|
-
//
|
|
202
|
+
// Malformed product block — skip silently
|
|
88
203
|
}
|
|
89
204
|
}
|
|
90
205
|
}
|
|
@@ -92,60 +207,107 @@ export function MessageBubble({
|
|
|
92
207
|
return { productsFromContent: products, cleanContent: content.trim() };
|
|
93
208
|
}, [message.content, isUser]);
|
|
94
209
|
|
|
95
|
-
|
|
96
|
-
|
|
210
|
+
// ── Merge & deduplicate products ───────────────────────────────────────────
|
|
211
|
+
const allProducts = React.useMemo<Product[]>(() => {
|
|
97
212
|
if (productsFromContent.length > 0) return productsFromContent;
|
|
98
213
|
|
|
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();
|
|
214
|
+
const seen = new Set<string>();
|
|
103
215
|
const contentLower = message.content.toLowerCase();
|
|
104
216
|
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
217
|
+
return productsFromSources.filter(p => {
|
|
218
|
+
const id = String(p.id ?? p.name);
|
|
219
|
+
if (seen.has(id)) return false;
|
|
220
|
+
const mentioned =
|
|
221
|
+
contentLower.includes(p.name.toLowerCase()) ||
|
|
222
|
+
(p.brand ? contentLower.includes(p.brand.toLowerCase()) : false);
|
|
223
|
+
if (mentioned) { seen.add(id); return true; }
|
|
224
|
+
return false;
|
|
225
|
+
});
|
|
226
|
+
}, [productsFromSources, productsFromContent, message.content]);
|
|
112
227
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
228
|
+
// ── Markdown component overrides ───────────────────────────────────────────
|
|
229
|
+
const markdownComponents = React.useMemo(
|
|
230
|
+
() => ({
|
|
231
|
+
table: ({ ...props }: React.HTMLAttributes<HTMLTableElement>) => (
|
|
232
|
+
<div className="overflow-x-auto my-4 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm">
|
|
233
|
+
<table className="w-full text-left border-collapse min-w-[400px]" {...props} />
|
|
234
|
+
</div>
|
|
235
|
+
),
|
|
236
|
+
thead: ({ ...props }: React.HTMLAttributes<HTMLTableSectionElement>) => (
|
|
237
|
+
<thead
|
|
238
|
+
className="bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10"
|
|
239
|
+
{...props}
|
|
240
|
+
/>
|
|
241
|
+
),
|
|
242
|
+
th: ({ children, ...props }: React.ThHTMLAttributes<HTMLTableCellElement>) => (
|
|
243
|
+
<th
|
|
244
|
+
className="p-3 font-semibold text-slate-700 dark:text-white/90 first:rounded-tl-xl last:rounded-tr-xl"
|
|
245
|
+
{...props}
|
|
246
|
+
>
|
|
247
|
+
{normaliseChild(children)}
|
|
248
|
+
</th>
|
|
249
|
+
),
|
|
250
|
+
td: ({ children, ...props }: React.TdHTMLAttributes<HTMLTableCellElement>) => (
|
|
251
|
+
<td
|
|
252
|
+
className="p-3 border-b border-slate-100 dark:border-white/5 last:border-0 text-slate-600 dark:text-white/70"
|
|
253
|
+
{...props}
|
|
254
|
+
>
|
|
255
|
+
{normaliseChild(children)}
|
|
256
|
+
</td>
|
|
257
|
+
),
|
|
258
|
+
code({
|
|
259
|
+
inline,
|
|
260
|
+
className,
|
|
261
|
+
children,
|
|
262
|
+
...props
|
|
263
|
+
}: React.HTMLAttributes<HTMLElement> & { inline?: boolean }) {
|
|
264
|
+
const lang = /language-(\w+)/.exec(className ?? '')?.[1];
|
|
118
265
|
|
|
119
|
-
|
|
120
|
-
|
|
266
|
+
if (!inline && lang === 'chart') {
|
|
267
|
+
return (
|
|
268
|
+
<ChartBlock
|
|
269
|
+
rawContent={String(children ?? '').trim()}
|
|
270
|
+
primaryColor={primaryColor}
|
|
271
|
+
accentColor={accentColor}
|
|
272
|
+
/>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
121
275
|
|
|
122
|
-
|
|
276
|
+
return (
|
|
277
|
+
<code className={className} {...props}>
|
|
278
|
+
{typeof children === 'string' || typeof children === 'number'
|
|
279
|
+
? children
|
|
280
|
+
: JSON.stringify(children)}
|
|
281
|
+
</code>
|
|
282
|
+
);
|
|
283
|
+
},
|
|
284
|
+
}),
|
|
285
|
+
[primaryColor, accentColor],
|
|
286
|
+
);
|
|
123
287
|
|
|
288
|
+
// ── Render ─────────────────────────────────────────────────────────────────
|
|
124
289
|
return (
|
|
125
290
|
<div className={`flex gap-3 ${isUser ? 'flex-row-reverse' : 'flex-row'} items-start`}>
|
|
126
291
|
{/* Avatar */}
|
|
127
292
|
<div
|
|
128
|
-
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center shadow-lg ${isUser
|
|
293
|
+
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center shadow-lg ${isUser
|
|
294
|
+
? 'text-white'
|
|
295
|
+
: 'bg-slate-100 dark:bg-white/10 text-slate-700 dark:text-white/80'
|
|
129
296
|
}`}
|
|
130
297
|
style={isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}}
|
|
131
298
|
>
|
|
132
|
-
{isUser
|
|
133
|
-
? <User className="w-4 h-4 text-white" />
|
|
134
|
-
: <Bot className="w-4 h-4" />
|
|
135
|
-
}
|
|
299
|
+
{isUser ? <User className="w-4 h-4 text-white" /> : <Bot className="w-4 h-4" />}
|
|
136
300
|
</div>
|
|
137
301
|
|
|
138
302
|
<div className={`flex flex-col gap-1 max-w-[90%] ${isUser ? 'items-end' : 'items-start'}`}>
|
|
139
303
|
{/* Bubble */}
|
|
140
304
|
<div
|
|
141
305
|
className={`relative px-4 py-3 rounded-2xl text-sm leading-relaxed shadow-sm dark:shadow-lg ${isUser
|
|
142
|
-
|
|
143
|
-
|
|
306
|
+
? 'text-white rounded-tr-sm'
|
|
307
|
+
: '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
308
|
}`}
|
|
145
309
|
style={
|
|
146
|
-
isUser
|
|
147
|
-
? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` }
|
|
148
|
-
: {}
|
|
310
|
+
isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
|
|
149
311
|
}
|
|
150
312
|
>
|
|
151
313
|
{isStreaming && !message.content ? (
|
|
@@ -156,101 +318,7 @@ export function MessageBubble({
|
|
|
156
318
|
</span>
|
|
157
319
|
) : (
|
|
158
320
|
<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) && !React.isValidElement(children) ? 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) && !React.isValidElement(children) ? 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
|
-
// 0. Boundary detection: find the actual JSON object/array start/end
|
|
194
|
-
// This removes stray backticks or text the LLM might have included
|
|
195
|
-
const firstBrace = json.indexOf('{');
|
|
196
|
-
const firstBracket = json.indexOf('[');
|
|
197
|
-
const start = (firstBrace !== -1 && (firstBracket === -1 || firstBrace < firstBracket)) ? firstBrace : firstBracket;
|
|
198
|
-
const lastBrace = json.lastIndexOf('}');
|
|
199
|
-
const lastBracket = json.lastIndexOf(']');
|
|
200
|
-
const end = Math.max(lastBrace, lastBracket);
|
|
201
|
-
|
|
202
|
-
if (start === -1 || end === -1 || end < start) return json;
|
|
203
|
-
const trimmed = json.substring(start, end + 1);
|
|
204
|
-
|
|
205
|
-
return trimmed
|
|
206
|
-
// 1. Resolve arithmetic: "value": 495 / 1000 => "value": 0.495
|
|
207
|
-
.replace(/:\s*(\d+(\.\d+)?)\s*([\+\-\*\/])\s*(\d+(\.\d+)?)/g, (_, n1, __, op, n2) => {
|
|
208
|
-
const v1 = parseFloat(n1); const v2 = parseFloat(n2);
|
|
209
|
-
const ops: Record<string, number> = { '+': v1 + v2, '-': v1 - v2, '*': v1 * v2, '/': v1 / v2 };
|
|
210
|
-
return `: ${ops[op] ?? v1}`;
|
|
211
|
-
})
|
|
212
|
-
// 2. Quote unquoted keys and fix single quotes
|
|
213
|
-
.replace(/([{,])\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1 "$2":')
|
|
214
|
-
.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"')
|
|
215
|
-
// 3. Syntax cleanup: trailing/missing commas and comments
|
|
216
|
-
.replace(/,\s*([\]}])/g, '$1')
|
|
217
|
-
.replace(/}\s*{/g, '},{')
|
|
218
|
-
.replace(/]\s*\[/g, '],[')
|
|
219
|
-
.replace(/\/\/.*$/gm, '')
|
|
220
|
-
// 4. Remove dangling empty strings
|
|
221
|
-
.replace(/([{,])\s*""\s*([,}])/g, '$1$2');
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
try {
|
|
225
|
-
const sanitized = sanitizeJson(rawContent);
|
|
226
|
-
const config = JSON.parse(sanitized) as ChartConfig;
|
|
227
|
-
return (
|
|
228
|
-
<div className="my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm">
|
|
229
|
-
<DynamicChart
|
|
230
|
-
config={config}
|
|
231
|
-
primaryColor={primaryColor}
|
|
232
|
-
accentColor={accentColor}
|
|
233
|
-
/>
|
|
234
|
-
</div>
|
|
235
|
-
);
|
|
236
|
-
} catch (err) {
|
|
237
|
-
console.error('[MessageBubble] Chart parsing failed:', err, '\nSanitized content:', sanitizeJson(rawContent));
|
|
238
|
-
return (
|
|
239
|
-
<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">
|
|
240
|
-
<p className="font-medium mb-1">Failed to render chart</p>
|
|
241
|
-
<p className="opacity-70 text-xs">The generated configuration is invalid. Check console for details.</p>
|
|
242
|
-
</div>
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return (
|
|
247
|
-
<code className={className} {...props}>
|
|
248
|
-
{typeof children === 'string' || typeof children === 'number' ? children : JSON.stringify(children)}
|
|
249
|
-
</code>
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
}}
|
|
253
|
-
>
|
|
321
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
|
254
322
|
{cleanContent || message.content}
|
|
255
323
|
</ReactMarkdown>
|
|
256
324
|
|
|
@@ -261,8 +329,8 @@ export function MessageBubble({
|
|
|
261
329
|
)}
|
|
262
330
|
</div>
|
|
263
331
|
|
|
264
|
-
{/* Product Carousel
|
|
265
|
-
{!isUser &&
|
|
332
|
+
{/* Product Carousel */}
|
|
333
|
+
{!isUser && allProducts.length > 0 && (
|
|
266
334
|
<div className="w-full mt-1">
|
|
267
335
|
<ProductCarousel
|
|
268
336
|
products={allProducts}
|
|
@@ -272,17 +340,14 @@ export function MessageBubble({
|
|
|
272
340
|
</div>
|
|
273
341
|
)}
|
|
274
342
|
|
|
275
|
-
{/* Sources
|
|
343
|
+
{/* Sources */}
|
|
276
344
|
{!isUser && sources && sources.length > 0 && (
|
|
277
345
|
<div className="w-full">
|
|
278
346
|
<button
|
|
279
|
-
onClick={() => setShowSources(
|
|
347
|
+
onClick={() => setShowSources(s => !s)}
|
|
280
348
|
className="text-[11px] text-indigo-400 hover:text-indigo-300 transition-colors flex items-center gap-1 mt-1"
|
|
281
349
|
>
|
|
282
|
-
{showSources
|
|
283
|
-
? <ChevronDown className="w-3 h-3" />
|
|
284
|
-
: <ChevronRight className="w-3 h-3" />
|
|
285
|
-
}
|
|
350
|
+
{showSources ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
|
286
351
|
{sources.length} source{sources.length !== 1 ? 's' : ''} used
|
|
287
352
|
</button>
|
|
288
353
|
|
|
@@ -299,3 +364,18 @@ export function MessageBubble({
|
|
|
299
364
|
</div>
|
|
300
365
|
);
|
|
301
366
|
}
|
|
367
|
+
|
|
368
|
+
// ─── Util ─────────────────────────────────────────────────────────────────────
|
|
369
|
+
|
|
370
|
+
/** Safely render table cell children that might be plain objects. */
|
|
371
|
+
function normaliseChild(children: React.ReactNode): React.ReactNode {
|
|
372
|
+
if (
|
|
373
|
+
typeof children === 'object' &&
|
|
374
|
+
children !== null &&
|
|
375
|
+
!Array.isArray(children) &&
|
|
376
|
+
!React.isValidElement(children)
|
|
377
|
+
) {
|
|
378
|
+
return JSON.stringify(children);
|
|
379
|
+
}
|
|
380
|
+
return children;
|
|
381
|
+
}
|