@retrivora-ai/rag-engine 1.3.2 → 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/index.js +181 -106
- package/dist/index.mjs +181 -106
- package/package.json +1 -1
- package/src/components/MessageBubble.tsx +246 -172
package/dist/index.js
CHANGED
|
@@ -318,6 +318,90 @@ 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
|
+
let inString = false;
|
|
339
|
+
let escape = false;
|
|
340
|
+
let braces = 0;
|
|
341
|
+
let brackets = 0;
|
|
342
|
+
for (const ch of s) {
|
|
343
|
+
if (escape) {
|
|
344
|
+
escape = false;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (ch === "\\" && inString) {
|
|
348
|
+
escape = true;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (ch === '"') {
|
|
352
|
+
inString = !inString;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (inString) continue;
|
|
356
|
+
if (ch === "{") braces++;
|
|
357
|
+
else if (ch === "}") braces--;
|
|
358
|
+
else if (ch === "[") brackets++;
|
|
359
|
+
else if (ch === "]") brackets--;
|
|
360
|
+
}
|
|
361
|
+
if (inString) s += '"';
|
|
362
|
+
while (brackets > 0) {
|
|
363
|
+
s += "]";
|
|
364
|
+
brackets--;
|
|
365
|
+
}
|
|
366
|
+
while (braces > 0) {
|
|
367
|
+
s += "}";
|
|
368
|
+
braces--;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const lastClose = Math.max(s.lastIndexOf("}"), s.lastIndexOf("]"));
|
|
372
|
+
if (lastClose !== -1 && lastClose < s.length - 1) {
|
|
373
|
+
s = s.substring(0, lastClose + 1);
|
|
374
|
+
}
|
|
375
|
+
return s;
|
|
376
|
+
}
|
|
377
|
+
function resolveImage(data) {
|
|
378
|
+
for (const key of ["image", "img", "thumbnail"]) {
|
|
379
|
+
const v = data[key];
|
|
380
|
+
if (typeof v === "string" && v) return v;
|
|
381
|
+
}
|
|
382
|
+
if (Array.isArray(data.images) && typeof data.images[0] === "string") {
|
|
383
|
+
return data.images[0];
|
|
384
|
+
}
|
|
385
|
+
return void 0;
|
|
386
|
+
}
|
|
387
|
+
function ChartBlock({ rawContent, primaryColor, accentColor }) {
|
|
388
|
+
const result = import_react5.default.useMemo(() => {
|
|
389
|
+
if (!rawContent || rawContent === "undefined") return { error: "Empty chart config." };
|
|
390
|
+
try {
|
|
391
|
+
const sanitized = sanitizeJson(rawContent);
|
|
392
|
+
const config = JSON.parse(sanitized);
|
|
393
|
+
return { config };
|
|
394
|
+
} catch (err) {
|
|
395
|
+
const sanitized = sanitizeJson(rawContent);
|
|
396
|
+
console.error("[ChartBlock] Parsing failed.\nError:", err, "\nSanitized:\n", sanitized);
|
|
397
|
+
return { error: String(err) };
|
|
398
|
+
}
|
|
399
|
+
}, [rawContent]);
|
|
400
|
+
if ("error" in result) {
|
|
401
|
+
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."));
|
|
402
|
+
}
|
|
403
|
+
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 }));
|
|
404
|
+
}
|
|
321
405
|
function MessageBubble({
|
|
322
406
|
message,
|
|
323
407
|
sources,
|
|
@@ -328,28 +412,18 @@ function MessageBubble({
|
|
|
328
412
|
}) {
|
|
329
413
|
const isUser = message.role === "user";
|
|
330
414
|
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
415
|
const productsFromSources = import_react5.default.useMemo(() => {
|
|
344
416
|
if (isUser || !sources) return [];
|
|
345
417
|
return sources.filter((s) => {
|
|
346
|
-
|
|
418
|
+
var _a;
|
|
419
|
+
const m = (_a = s.metadata) != null ? _a : {};
|
|
347
420
|
return m.price || m.image || m.img || m.thumbnail || m.images || m.brand || m.type === "product";
|
|
348
421
|
}).map((s) => {
|
|
349
|
-
|
|
422
|
+
var _a, _b, _c, _d;
|
|
423
|
+
const m = (_a = s.metadata) != null ? _a : {};
|
|
350
424
|
return {
|
|
351
425
|
id: s.id,
|
|
352
|
-
name: m.name
|
|
426
|
+
name: (_d = (_c = (_b = m.name) != null ? _b : m.title) != null ? _c : s.content.split("\n")[0]) != null ? _d : "Unknown Product",
|
|
353
427
|
brand: m.brand,
|
|
354
428
|
price: m.price,
|
|
355
429
|
image: resolveImage(m),
|
|
@@ -364,16 +438,19 @@ function MessageBubble({
|
|
|
364
438
|
const products = [];
|
|
365
439
|
let content = message.content;
|
|
366
440
|
if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
|
|
367
|
-
const
|
|
368
|
-
for (const m of matches) {
|
|
441
|
+
for (const match of content.matchAll(jsonRegex)) {
|
|
369
442
|
try {
|
|
370
|
-
const data = JSON.parse(
|
|
443
|
+
const data = JSON.parse(match[1]);
|
|
371
444
|
if (data.type === "products" && Array.isArray(data.items)) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
445
|
+
products.push(
|
|
446
|
+
...data.items.map((item) => {
|
|
447
|
+
var _a;
|
|
448
|
+
return __spreadProps(__spreadValues({}, item), {
|
|
449
|
+
image: (_a = item.image) != null ? _a : resolveImage(item)
|
|
450
|
+
});
|
|
451
|
+
})
|
|
452
|
+
);
|
|
453
|
+
content = content.replace(match[0], "");
|
|
377
454
|
}
|
|
378
455
|
} catch (e) {
|
|
379
456
|
}
|
|
@@ -383,22 +460,82 @@ function MessageBubble({
|
|
|
383
460
|
}, [message.content, isUser]);
|
|
384
461
|
const allProducts = import_react5.default.useMemo(() => {
|
|
385
462
|
if (productsFromContent.length > 0) return productsFromContent;
|
|
386
|
-
const
|
|
387
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
463
|
+
const seen = /* @__PURE__ */ new Set();
|
|
388
464
|
const contentLower = message.content.toLowerCase();
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
|
|
465
|
+
return productsFromSources.filter((p) => {
|
|
466
|
+
var _a;
|
|
467
|
+
const id = String((_a = p.id) != null ? _a : p.name);
|
|
468
|
+
if (seen.has(id)) return false;
|
|
469
|
+
const mentioned = contentLower.includes(p.name.toLowerCase()) || (p.brand ? contentLower.includes(p.brand.toLowerCase()) : false);
|
|
470
|
+
if (mentioned) {
|
|
471
|
+
seen.add(id);
|
|
472
|
+
return true;
|
|
397
473
|
}
|
|
398
|
-
|
|
399
|
-
|
|
474
|
+
return false;
|
|
475
|
+
});
|
|
400
476
|
}, [productsFromSources, productsFromContent, message.content]);
|
|
401
|
-
const
|
|
477
|
+
const markdownComponents = import_react5.default.useMemo(
|
|
478
|
+
() => ({
|
|
479
|
+
table: (_a) => {
|
|
480
|
+
var props = __objRest(_a, []);
|
|
481
|
+
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)));
|
|
482
|
+
},
|
|
483
|
+
thead: (_b) => {
|
|
484
|
+
var props = __objRest(_b, []);
|
|
485
|
+
return /* @__PURE__ */ import_react5.default.createElement(
|
|
486
|
+
"thead",
|
|
487
|
+
__spreadValues({
|
|
488
|
+
className: "bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10"
|
|
489
|
+
}, props)
|
|
490
|
+
);
|
|
491
|
+
},
|
|
492
|
+
th: (_c) => {
|
|
493
|
+
var _d = _c, { children } = _d, props = __objRest(_d, ["children"]);
|
|
494
|
+
return /* @__PURE__ */ import_react5.default.createElement(
|
|
495
|
+
"th",
|
|
496
|
+
__spreadValues({
|
|
497
|
+
className: "p-3 font-semibold text-slate-700 dark:text-white/90 first:rounded-tl-xl last:rounded-tr-xl"
|
|
498
|
+
}, props),
|
|
499
|
+
normaliseChild(children)
|
|
500
|
+
);
|
|
501
|
+
},
|
|
502
|
+
td: (_e) => {
|
|
503
|
+
var _f = _e, { children } = _f, props = __objRest(_f, ["children"]);
|
|
504
|
+
return /* @__PURE__ */ import_react5.default.createElement(
|
|
505
|
+
"td",
|
|
506
|
+
__spreadValues({
|
|
507
|
+
className: "p-3 border-b border-slate-100 dark:border-white/5 last:border-0 text-slate-600 dark:text-white/70"
|
|
508
|
+
}, props),
|
|
509
|
+
normaliseChild(children)
|
|
510
|
+
);
|
|
511
|
+
},
|
|
512
|
+
code(_g) {
|
|
513
|
+
var _h = _g, {
|
|
514
|
+
inline,
|
|
515
|
+
className,
|
|
516
|
+
children
|
|
517
|
+
} = _h, props = __objRest(_h, [
|
|
518
|
+
"inline",
|
|
519
|
+
"className",
|
|
520
|
+
"children"
|
|
521
|
+
]);
|
|
522
|
+
var _a;
|
|
523
|
+
const lang = (_a = /language-(\w+)/.exec(className != null ? className : "")) == null ? void 0 : _a[1];
|
|
524
|
+
if (!inline && lang === "chart") {
|
|
525
|
+
return /* @__PURE__ */ import_react5.default.createElement(
|
|
526
|
+
ChartBlock,
|
|
527
|
+
{
|
|
528
|
+
rawContent: String(children != null ? children : "").trim(),
|
|
529
|
+
primaryColor,
|
|
530
|
+
accentColor
|
|
531
|
+
}
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
return /* @__PURE__ */ import_react5.default.createElement("code", __spreadValues({ className }, props), typeof children === "string" || typeof children === "number" ? children : JSON.stringify(children));
|
|
535
|
+
}
|
|
536
|
+
}),
|
|
537
|
+
[primaryColor, accentColor]
|
|
538
|
+
);
|
|
402
539
|
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
540
|
"div",
|
|
404
541
|
{
|
|
@@ -412,76 +549,8 @@ function MessageBubble({
|
|
|
412
549
|
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
550
|
style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
|
|
414
551
|
},
|
|
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(
|
|
552
|
+
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" }))
|
|
553
|
+
), !isUser && allProducts.length > 0 && /* @__PURE__ */ import_react5.default.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ import_react5.default.createElement(
|
|
485
554
|
ProductCarousel,
|
|
486
555
|
{
|
|
487
556
|
products: allProducts,
|
|
@@ -501,6 +570,12 @@ function MessageBubble({
|
|
|
501
570
|
" used"
|
|
502
571
|
), 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
572
|
}
|
|
573
|
+
function normaliseChild(children) {
|
|
574
|
+
if (typeof children === "object" && children !== null && !Array.isArray(children) && !import_react5.default.isValidElement(children)) {
|
|
575
|
+
return JSON.stringify(children);
|
|
576
|
+
}
|
|
577
|
+
return children;
|
|
578
|
+
}
|
|
504
579
|
|
|
505
580
|
// src/components/ConfigProvider.tsx
|
|
506
581
|
var import_react6 = __toESM(require("react"));
|
package/dist/index.mjs
CHANGED
|
@@ -281,6 +281,90 @@ 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
|
+
let inString = false;
|
|
302
|
+
let escape = false;
|
|
303
|
+
let braces = 0;
|
|
304
|
+
let brackets = 0;
|
|
305
|
+
for (const ch of s) {
|
|
306
|
+
if (escape) {
|
|
307
|
+
escape = false;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (ch === "\\" && inString) {
|
|
311
|
+
escape = true;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (ch === '"') {
|
|
315
|
+
inString = !inString;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (inString) continue;
|
|
319
|
+
if (ch === "{") braces++;
|
|
320
|
+
else if (ch === "}") braces--;
|
|
321
|
+
else if (ch === "[") brackets++;
|
|
322
|
+
else if (ch === "]") brackets--;
|
|
323
|
+
}
|
|
324
|
+
if (inString) s += '"';
|
|
325
|
+
while (brackets > 0) {
|
|
326
|
+
s += "]";
|
|
327
|
+
brackets--;
|
|
328
|
+
}
|
|
329
|
+
while (braces > 0) {
|
|
330
|
+
s += "}";
|
|
331
|
+
braces--;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const lastClose = Math.max(s.lastIndexOf("}"), s.lastIndexOf("]"));
|
|
335
|
+
if (lastClose !== -1 && lastClose < s.length - 1) {
|
|
336
|
+
s = s.substring(0, lastClose + 1);
|
|
337
|
+
}
|
|
338
|
+
return s;
|
|
339
|
+
}
|
|
340
|
+
function resolveImage(data) {
|
|
341
|
+
for (const key of ["image", "img", "thumbnail"]) {
|
|
342
|
+
const v = data[key];
|
|
343
|
+
if (typeof v === "string" && v) return v;
|
|
344
|
+
}
|
|
345
|
+
if (Array.isArray(data.images) && typeof data.images[0] === "string") {
|
|
346
|
+
return data.images[0];
|
|
347
|
+
}
|
|
348
|
+
return void 0;
|
|
349
|
+
}
|
|
350
|
+
function ChartBlock({ rawContent, primaryColor, accentColor }) {
|
|
351
|
+
const result = React5.useMemo(() => {
|
|
352
|
+
if (!rawContent || rawContent === "undefined") return { error: "Empty chart config." };
|
|
353
|
+
try {
|
|
354
|
+
const sanitized = sanitizeJson(rawContent);
|
|
355
|
+
const config = JSON.parse(sanitized);
|
|
356
|
+
return { config };
|
|
357
|
+
} catch (err) {
|
|
358
|
+
const sanitized = sanitizeJson(rawContent);
|
|
359
|
+
console.error("[ChartBlock] Parsing failed.\nError:", err, "\nSanitized:\n", sanitized);
|
|
360
|
+
return { error: String(err) };
|
|
361
|
+
}
|
|
362
|
+
}, [rawContent]);
|
|
363
|
+
if ("error" in result) {
|
|
364
|
+
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."));
|
|
365
|
+
}
|
|
366
|
+
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 }));
|
|
367
|
+
}
|
|
284
368
|
function MessageBubble({
|
|
285
369
|
message,
|
|
286
370
|
sources,
|
|
@@ -291,28 +375,18 @@ function MessageBubble({
|
|
|
291
375
|
}) {
|
|
292
376
|
const isUser = message.role === "user";
|
|
293
377
|
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
378
|
const productsFromSources = React5.useMemo(() => {
|
|
307
379
|
if (isUser || !sources) return [];
|
|
308
380
|
return sources.filter((s) => {
|
|
309
|
-
|
|
381
|
+
var _a;
|
|
382
|
+
const m = (_a = s.metadata) != null ? _a : {};
|
|
310
383
|
return m.price || m.image || m.img || m.thumbnail || m.images || m.brand || m.type === "product";
|
|
311
384
|
}).map((s) => {
|
|
312
|
-
|
|
385
|
+
var _a, _b, _c, _d;
|
|
386
|
+
const m = (_a = s.metadata) != null ? _a : {};
|
|
313
387
|
return {
|
|
314
388
|
id: s.id,
|
|
315
|
-
name: m.name
|
|
389
|
+
name: (_d = (_c = (_b = m.name) != null ? _b : m.title) != null ? _c : s.content.split("\n")[0]) != null ? _d : "Unknown Product",
|
|
316
390
|
brand: m.brand,
|
|
317
391
|
price: m.price,
|
|
318
392
|
image: resolveImage(m),
|
|
@@ -327,16 +401,19 @@ function MessageBubble({
|
|
|
327
401
|
const products = [];
|
|
328
402
|
let content = message.content;
|
|
329
403
|
if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
|
|
330
|
-
const
|
|
331
|
-
for (const m of matches) {
|
|
404
|
+
for (const match of content.matchAll(jsonRegex)) {
|
|
332
405
|
try {
|
|
333
|
-
const data = JSON.parse(
|
|
406
|
+
const data = JSON.parse(match[1]);
|
|
334
407
|
if (data.type === "products" && Array.isArray(data.items)) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
408
|
+
products.push(
|
|
409
|
+
...data.items.map((item) => {
|
|
410
|
+
var _a;
|
|
411
|
+
return __spreadProps(__spreadValues({}, item), {
|
|
412
|
+
image: (_a = item.image) != null ? _a : resolveImage(item)
|
|
413
|
+
});
|
|
414
|
+
})
|
|
415
|
+
);
|
|
416
|
+
content = content.replace(match[0], "");
|
|
340
417
|
}
|
|
341
418
|
} catch (e) {
|
|
342
419
|
}
|
|
@@ -346,22 +423,82 @@ function MessageBubble({
|
|
|
346
423
|
}, [message.content, isUser]);
|
|
347
424
|
const allProducts = React5.useMemo(() => {
|
|
348
425
|
if (productsFromContent.length > 0) return productsFromContent;
|
|
349
|
-
const
|
|
350
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
426
|
+
const seen = /* @__PURE__ */ new Set();
|
|
351
427
|
const contentLower = message.content.toLowerCase();
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
if (
|
|
358
|
-
|
|
359
|
-
|
|
428
|
+
return productsFromSources.filter((p) => {
|
|
429
|
+
var _a;
|
|
430
|
+
const id = String((_a = p.id) != null ? _a : p.name);
|
|
431
|
+
if (seen.has(id)) return false;
|
|
432
|
+
const mentioned = contentLower.includes(p.name.toLowerCase()) || (p.brand ? contentLower.includes(p.brand.toLowerCase()) : false);
|
|
433
|
+
if (mentioned) {
|
|
434
|
+
seen.add(id);
|
|
435
|
+
return true;
|
|
360
436
|
}
|
|
361
|
-
|
|
362
|
-
|
|
437
|
+
return false;
|
|
438
|
+
});
|
|
363
439
|
}, [productsFromSources, productsFromContent, message.content]);
|
|
364
|
-
const
|
|
440
|
+
const markdownComponents = React5.useMemo(
|
|
441
|
+
() => ({
|
|
442
|
+
table: (_a) => {
|
|
443
|
+
var props = __objRest(_a, []);
|
|
444
|
+
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)));
|
|
445
|
+
},
|
|
446
|
+
thead: (_b) => {
|
|
447
|
+
var props = __objRest(_b, []);
|
|
448
|
+
return /* @__PURE__ */ React5.createElement(
|
|
449
|
+
"thead",
|
|
450
|
+
__spreadValues({
|
|
451
|
+
className: "bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/10"
|
|
452
|
+
}, props)
|
|
453
|
+
);
|
|
454
|
+
},
|
|
455
|
+
th: (_c) => {
|
|
456
|
+
var _d = _c, { children } = _d, props = __objRest(_d, ["children"]);
|
|
457
|
+
return /* @__PURE__ */ React5.createElement(
|
|
458
|
+
"th",
|
|
459
|
+
__spreadValues({
|
|
460
|
+
className: "p-3 font-semibold text-slate-700 dark:text-white/90 first:rounded-tl-xl last:rounded-tr-xl"
|
|
461
|
+
}, props),
|
|
462
|
+
normaliseChild(children)
|
|
463
|
+
);
|
|
464
|
+
},
|
|
465
|
+
td: (_e) => {
|
|
466
|
+
var _f = _e, { children } = _f, props = __objRest(_f, ["children"]);
|
|
467
|
+
return /* @__PURE__ */ React5.createElement(
|
|
468
|
+
"td",
|
|
469
|
+
__spreadValues({
|
|
470
|
+
className: "p-3 border-b border-slate-100 dark:border-white/5 last:border-0 text-slate-600 dark:text-white/70"
|
|
471
|
+
}, props),
|
|
472
|
+
normaliseChild(children)
|
|
473
|
+
);
|
|
474
|
+
},
|
|
475
|
+
code(_g) {
|
|
476
|
+
var _h = _g, {
|
|
477
|
+
inline,
|
|
478
|
+
className,
|
|
479
|
+
children
|
|
480
|
+
} = _h, props = __objRest(_h, [
|
|
481
|
+
"inline",
|
|
482
|
+
"className",
|
|
483
|
+
"children"
|
|
484
|
+
]);
|
|
485
|
+
var _a;
|
|
486
|
+
const lang = (_a = /language-(\w+)/.exec(className != null ? className : "")) == null ? void 0 : _a[1];
|
|
487
|
+
if (!inline && lang === "chart") {
|
|
488
|
+
return /* @__PURE__ */ React5.createElement(
|
|
489
|
+
ChartBlock,
|
|
490
|
+
{
|
|
491
|
+
rawContent: String(children != null ? children : "").trim(),
|
|
492
|
+
primaryColor,
|
|
493
|
+
accentColor
|
|
494
|
+
}
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
return /* @__PURE__ */ React5.createElement("code", __spreadValues({ className }, props), typeof children === "string" || typeof children === "number" ? children : JSON.stringify(children));
|
|
498
|
+
}
|
|
499
|
+
}),
|
|
500
|
+
[primaryColor, accentColor]
|
|
501
|
+
);
|
|
365
502
|
return /* @__PURE__ */ React5.createElement("div", { className: `flex gap-3 ${isUser ? "flex-row-reverse" : "flex-row"} items-start` }, /* @__PURE__ */ React5.createElement(
|
|
366
503
|
"div",
|
|
367
504
|
{
|
|
@@ -375,76 +512,8 @@ function MessageBubble({
|
|
|
375
512
|
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
513
|
style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
|
|
377
514
|
},
|
|
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(
|
|
515
|
+
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" }))
|
|
516
|
+
), !isUser && allProducts.length > 0 && /* @__PURE__ */ React5.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ React5.createElement(
|
|
448
517
|
ProductCarousel,
|
|
449
518
|
{
|
|
450
519
|
products: allProducts,
|
|
@@ -464,6 +533,12 @@ function MessageBubble({
|
|
|
464
533
|
" used"
|
|
465
534
|
), 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
535
|
}
|
|
536
|
+
function normaliseChild(children) {
|
|
537
|
+
if (typeof children === "object" && children !== null && !Array.isArray(children) && !React5.isValidElement(children)) {
|
|
538
|
+
return JSON.stringify(children);
|
|
539
|
+
}
|
|
540
|
+
return children;
|
|
541
|
+
}
|
|
467
542
|
|
|
468
543
|
// src/components/ConfigProvider.tsx
|
|
469
544
|
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.3",
|
|
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,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,101 +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) && !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
|
-
>
|
|
315
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
|
254
316
|
{cleanContent || message.content}
|
|
255
317
|
</ReactMarkdown>
|
|
256
318
|
|
|
@@ -261,8 +323,8 @@ export function MessageBubble({
|
|
|
261
323
|
)}
|
|
262
324
|
</div>
|
|
263
325
|
|
|
264
|
-
{/* Product Carousel
|
|
265
|
-
{!isUser &&
|
|
326
|
+
{/* Product Carousel */}
|
|
327
|
+
{!isUser && allProducts.length > 0 && (
|
|
266
328
|
<div className="w-full mt-1">
|
|
267
329
|
<ProductCarousel
|
|
268
330
|
products={allProducts}
|
|
@@ -272,17 +334,14 @@ export function MessageBubble({
|
|
|
272
334
|
</div>
|
|
273
335
|
)}
|
|
274
336
|
|
|
275
|
-
{/* Sources
|
|
337
|
+
{/* Sources */}
|
|
276
338
|
{!isUser && sources && sources.length > 0 && (
|
|
277
339
|
<div className="w-full">
|
|
278
340
|
<button
|
|
279
|
-
onClick={() => setShowSources(
|
|
341
|
+
onClick={() => setShowSources(s => !s)}
|
|
280
342
|
className="text-[11px] text-indigo-400 hover:text-indigo-300 transition-colors flex items-center gap-1 mt-1"
|
|
281
343
|
>
|
|
282
|
-
{showSources
|
|
283
|
-
? <ChevronDown className="w-3 h-3" />
|
|
284
|
-
: <ChevronRight className="w-3 h-3" />
|
|
285
|
-
}
|
|
344
|
+
{showSources ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
|
286
345
|
{sources.length} source{sources.length !== 1 ? 's' : ''} used
|
|
287
346
|
</button>
|
|
288
347
|
|
|
@@ -299,3 +358,18 @@ export function MessageBubble({
|
|
|
299
358
|
</div>
|
|
300
359
|
);
|
|
301
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
|
+
}
|