@retrivora-ai/rag-engine 1.5.3 → 1.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-C4UPQX3E.mjs → chunk-Q4MDH6C4.mjs} +59 -8
- package/dist/handlers/index.js +59 -8
- package/dist/handlers/index.mjs +1 -1
- package/dist/index.js +143 -42
- package/dist/index.mjs +143 -42
- package/dist/server.js +59 -8
- package/dist/server.mjs +1 -1
- package/package.json +1 -1
- package/src/app/api/chat/route.ts +2 -2
- package/src/components/DynamicChart.tsx +81 -14
- package/src/components/MessageBubble.tsx +143 -44
- package/src/core/Pipeline.ts +59 -8
|
@@ -7,7 +7,7 @@ import remarkGfm from 'remark-gfm';
|
|
|
7
7
|
import { MessageBubbleProps, Product } from '../types';
|
|
8
8
|
import { SourceCard } from './SourceCard';
|
|
9
9
|
import { ProductCarousel } from './ProductCarousel';
|
|
10
|
-
import { DynamicChart
|
|
10
|
+
import { DynamicChart } from './DynamicChart';
|
|
11
11
|
|
|
12
12
|
// ─── JSON sanitization ────────────────────────────────────────────────────────
|
|
13
13
|
// Order matters: each step is a targeted fix, no step undoes a previous one.
|
|
@@ -123,6 +123,7 @@ function normaliseChild(children: React.ReactNode): React.ReactNode {
|
|
|
123
123
|
interface TableConfig {
|
|
124
124
|
type: 'table';
|
|
125
125
|
title?: string;
|
|
126
|
+
description?: string;
|
|
126
127
|
columns?: string[]; // preferred: explicit ordered column labels
|
|
127
128
|
dataKeys?: string[]; // fallback column keys (may omit xAxisKey)
|
|
128
129
|
xAxisKey?: string; // row-label key to prepend when using dataKeys
|
|
@@ -210,72 +211,150 @@ function DataTable({ config }: { config: TableConfig }) {
|
|
|
210
211
|
);
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
// ───
|
|
214
|
+
// ─── Universal UI Dispatcher ────────────────────────────────────────────────
|
|
215
|
+
// Handles charts, product carousels, and data tables via a single protocol.
|
|
214
216
|
|
|
215
|
-
|
|
217
|
+
interface UIConfig {
|
|
218
|
+
view: 'chart' | 'carousel' | 'table';
|
|
219
|
+
chartType?: 'pie' | 'bar' | 'line';
|
|
220
|
+
xAxisKey?: string;
|
|
221
|
+
dataKeys?: string[];
|
|
222
|
+
columns?: string[];
|
|
223
|
+
colors?: string[];
|
|
224
|
+
data: Record<string, unknown>[];
|
|
225
|
+
title?: string;
|
|
226
|
+
description?: string;
|
|
227
|
+
insights?: string[];
|
|
228
|
+
type?: string;
|
|
229
|
+
items?: Record<string, unknown>[];
|
|
230
|
+
}
|
|
216
231
|
|
|
217
|
-
|
|
232
|
+
function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAddToCart }: {
|
|
218
233
|
rawContent: string;
|
|
219
|
-
primaryColor
|
|
220
|
-
accentColor
|
|
234
|
+
primaryColor?: string;
|
|
235
|
+
accentColor?: string;
|
|
221
236
|
isStreaming?: boolean;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const result = React.useMemo<
|
|
226
|
-
{ config: ParsedConfig } | { error: string } | { loading: true }
|
|
227
|
-
>(() => {
|
|
237
|
+
onAddToCart?: (product: Product) => void;
|
|
238
|
+
}) {
|
|
239
|
+
const result = React.useMemo<{ config: UIConfig } | { error: string } | { loading: true }>(() => {
|
|
228
240
|
if (isStreaming) return { loading: true };
|
|
229
|
-
if (!rawContent || rawContent === 'undefined') return { error: 'Empty visualization config.' };
|
|
230
241
|
try {
|
|
231
242
|
const clean = rawContent
|
|
232
243
|
.replace(/^```[a-z]*\s*/i, '')
|
|
233
244
|
.replace(/```\s*$/, '')
|
|
234
245
|
.trim();
|
|
235
|
-
const
|
|
236
|
-
|
|
246
|
+
const parsed = JSON.parse(sanitizeJson(clean));
|
|
247
|
+
const config = { ...parsed };
|
|
248
|
+
|
|
249
|
+
// Smart Auto-Detection for legacy or simpler formats
|
|
250
|
+
if (!config.view) {
|
|
251
|
+
if (config.type === 'products' || Array.isArray(config.items)) {
|
|
252
|
+
config.view = 'carousel';
|
|
253
|
+
config.data = config.data || config.items || [];
|
|
254
|
+
} else if (['pie', 'bar', 'line'].includes(config.type) || ['pie', 'bar', 'line'].includes(config.chartType)) {
|
|
255
|
+
config.view = 'chart';
|
|
256
|
+
config.chartType = config.chartType || config.type || 'bar';
|
|
257
|
+
config.data = config.data || [];
|
|
258
|
+
} else {
|
|
259
|
+
config.view = 'table';
|
|
260
|
+
config.data = Array.isArray(config.data) ? config.data : (Array.isArray(config) ? config : []);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { config: config as UIConfig };
|
|
237
265
|
} catch (err) {
|
|
238
|
-
console.error('[ChartBlock] Parsing failed.\nError:', err);
|
|
239
266
|
return { error: String(err) };
|
|
240
267
|
}
|
|
241
268
|
}, [rawContent, isStreaming]);
|
|
242
269
|
|
|
243
270
|
if ('loading' in result) {
|
|
244
271
|
return (
|
|
245
|
-
<div className="my-4 p-8 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex flex-col items-center justify-center gap-3
|
|
246
|
-
<div className="w-
|
|
247
|
-
<p className="text-xs text-slate-500 font-medium
|
|
272
|
+
<div className="my-4 p-8 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex flex-col items-center justify-center gap-3 animate-pulse">
|
|
273
|
+
<div className="w-5 h-5 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" />
|
|
274
|
+
<p className="text-xs text-slate-500 font-medium italic">Preparing view...</p>
|
|
248
275
|
</div>
|
|
249
276
|
);
|
|
250
277
|
}
|
|
251
278
|
|
|
252
279
|
if ('error' in result) {
|
|
253
|
-
return
|
|
254
|
-
<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">
|
|
255
|
-
<p className="font-medium mb-1">Failed to render visualization</p>
|
|
256
|
-
<p className="opacity-70 text-xs">The generated configuration is invalid. Check the console for details.</p>
|
|
257
|
-
</div>
|
|
258
|
-
);
|
|
280
|
+
return <pre className="p-4 my-2 bg-slate-900 text-slate-100 rounded-lg text-[10px] overflow-auto max-h-40"><code>{rawContent}</code></pre>;
|
|
259
281
|
}
|
|
260
282
|
|
|
261
283
|
const { config } = result;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
284
|
+
const hasInsights = Array.isArray(config.insights) && config.insights.length > 0;
|
|
285
|
+
const hasDescription = typeof config.description === 'string' && config.description.trim().length > 0;
|
|
286
|
+
|
|
287
|
+
switch (config.view) {
|
|
288
|
+
case 'chart':
|
|
289
|
+
return (
|
|
290
|
+
<div className="my-6 p-4 bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-white/10 shadow-sm overflow-hidden">
|
|
291
|
+
{config.title && <h4 className="text-xs font-semibold text-slate-500 mb-4 px-2">{config.title}</h4>}
|
|
292
|
+
{hasDescription && (
|
|
293
|
+
<p className="px-2 text-sm text-slate-500 dark:text-white/60">{config.description}</p>
|
|
294
|
+
)}
|
|
295
|
+
<DynamicChart
|
|
296
|
+
config={{
|
|
297
|
+
type: (config.chartType || 'bar') as 'bar' | 'line' | 'pie',
|
|
298
|
+
data: config.data as unknown as Record<string, string | number>[],
|
|
299
|
+
xAxisKey: config.xAxisKey,
|
|
300
|
+
dataKeys: config.dataKeys,
|
|
301
|
+
colors: config.colors,
|
|
302
|
+
}}
|
|
303
|
+
primaryColor={primaryColor}
|
|
304
|
+
accentColor={accentColor}
|
|
305
|
+
/>
|
|
306
|
+
{hasInsights && (
|
|
307
|
+
<div className="mt-4 flex flex-wrap gap-2 px-2">
|
|
308
|
+
{config.insights?.map((insight) => (
|
|
309
|
+
<span
|
|
310
|
+
key={insight}
|
|
311
|
+
className="rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-[11px] font-medium text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-300"
|
|
312
|
+
>
|
|
313
|
+
{insight}
|
|
314
|
+
</span>
|
|
315
|
+
))}
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
);
|
|
320
|
+
case 'carousel':
|
|
321
|
+
return (
|
|
322
|
+
<div className="my-4">
|
|
323
|
+
{config.title && <h4 className="mb-2 text-xs font-semibold text-slate-500">{config.title}</h4>}
|
|
324
|
+
{hasDescription && (
|
|
325
|
+
<p className="mb-3 text-sm text-slate-500 dark:text-white/60">{config.description}</p>
|
|
326
|
+
)}
|
|
327
|
+
<ProductCarousel products={(config.data as unknown as Product[]).map(item => ({
|
|
328
|
+
...item,
|
|
329
|
+
image: item.image ?? (typeof resolveImage === 'function' ? resolveImage(item as unknown as Record<string, unknown>) : undefined)
|
|
330
|
+
}))} primaryColor={primaryColor} onAddToCart={onAddToCart} />
|
|
331
|
+
</div>
|
|
332
|
+
);
|
|
333
|
+
case 'table':
|
|
334
|
+
return (
|
|
335
|
+
<div className="my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm overflow-auto max-h-[400px]">
|
|
336
|
+
{config.title && <h4 className="text-xs font-semibold text-slate-500 mb-2">{config.title}</h4>}
|
|
337
|
+
{hasDescription && (
|
|
338
|
+
<p className="mb-3 text-sm text-slate-500 dark:text-white/60">{config.description}</p>
|
|
339
|
+
)}
|
|
340
|
+
<DataTable config={{
|
|
341
|
+
type: 'table',
|
|
342
|
+
title: config.title,
|
|
343
|
+
description: config.description,
|
|
344
|
+
columns: config.columns,
|
|
345
|
+
dataKeys: config.dataKeys,
|
|
346
|
+
xAxisKey: config.xAxisKey,
|
|
347
|
+
data: config.data,
|
|
348
|
+
}} />
|
|
349
|
+
</div>
|
|
350
|
+
);
|
|
351
|
+
default:
|
|
352
|
+
return null;
|
|
266
353
|
}
|
|
267
|
-
|
|
268
|
-
return (
|
|
269
|
-
<div className="my-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm">
|
|
270
|
-
<DynamicChart
|
|
271
|
-
config={config as ChartConfig}
|
|
272
|
-
primaryColor={primaryColor}
|
|
273
|
-
accentColor={accentColor}
|
|
274
|
-
/>
|
|
275
|
-
</div>
|
|
276
|
-
);
|
|
277
354
|
}
|
|
278
355
|
|
|
356
|
+
|
|
357
|
+
|
|
279
358
|
// ─── Main component ───────────────────────────────────────────────────────────
|
|
280
359
|
|
|
281
360
|
export function MessageBubble({
|
|
@@ -288,6 +367,10 @@ export function MessageBubble({
|
|
|
288
367
|
}: MessageBubbleProps) {
|
|
289
368
|
const isUser = message.role === 'user';
|
|
290
369
|
const [showSources, setShowSources] = React.useState(false);
|
|
370
|
+
const hasStructuredProductBlock = React.useMemo(
|
|
371
|
+
() => /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|type)"\s*:\s*"(?:carousel|products)"/i.test(message.content),
|
|
372
|
+
[message.content],
|
|
373
|
+
);
|
|
291
374
|
|
|
292
375
|
// ── Products from sources ──────────────────────────────────────────────────
|
|
293
376
|
const productsFromSources = React.useMemo<Product[]>(() => {
|
|
@@ -457,17 +540,33 @@ export function MessageBubble({
|
|
|
457
540
|
}: React.HTMLAttributes<HTMLElement> & { inline?: boolean }) {
|
|
458
541
|
const lang = /language-(\w+)/.exec(className ?? '')?.[1];
|
|
459
542
|
|
|
460
|
-
if (!inline && lang === 'chart') {
|
|
543
|
+
if (!inline && (lang === 'ui' || lang === 'chart')) {
|
|
461
544
|
return (
|
|
462
|
-
<
|
|
545
|
+
<UIDispatcher
|
|
463
546
|
rawContent={String(children ?? '').trim()}
|
|
464
547
|
primaryColor={primaryColor}
|
|
465
548
|
accentColor={accentColor}
|
|
466
549
|
isStreaming={isStreaming}
|
|
550
|
+
onAddToCart={onAddToCart}
|
|
467
551
|
/>
|
|
468
552
|
);
|
|
469
553
|
}
|
|
470
554
|
|
|
555
|
+
if (!inline && lang === 'json') {
|
|
556
|
+
const content = String(children ?? '').trim();
|
|
557
|
+
if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
|
|
558
|
+
return (
|
|
559
|
+
<UIDispatcher
|
|
560
|
+
rawContent={content}
|
|
561
|
+
primaryColor={primaryColor}
|
|
562
|
+
accentColor={accentColor}
|
|
563
|
+
isStreaming={isStreaming}
|
|
564
|
+
onAddToCart={onAddToCart}
|
|
565
|
+
/>
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
471
570
|
if (!inline && lang === 'table-loading') {
|
|
472
571
|
return (
|
|
473
572
|
<div className="my-5 p-6 bg-slate-50 dark:bg-white/5 rounded-xl border border-dashed border-slate-300 dark:border-white/10 flex items-center justify-center gap-3 select-none">
|
|
@@ -488,7 +587,7 @@ export function MessageBubble({
|
|
|
488
587
|
);
|
|
489
588
|
},
|
|
490
589
|
}),
|
|
491
|
-
[primaryColor, accentColor, isStreaming],
|
|
590
|
+
[primaryColor, accentColor, isStreaming, onAddToCart],
|
|
492
591
|
);
|
|
493
592
|
|
|
494
593
|
// ── Render ─────────────────────────────────────────────────────────────────
|
|
@@ -536,7 +635,7 @@ export function MessageBubble({
|
|
|
536
635
|
</div>
|
|
537
636
|
|
|
538
637
|
{/* Product Carousel */}
|
|
539
|
-
{!isUser && allProducts.length > 0 && (
|
|
638
|
+
{!isUser && !hasStructuredProductBlock && allProducts.length > 0 && (
|
|
540
639
|
<div className="w-full mt-1">
|
|
541
640
|
<ProductCarousel
|
|
542
641
|
products={allProducts}
|
|
@@ -569,4 +668,4 @@ export function MessageBubble({
|
|
|
569
668
|
</div>
|
|
570
669
|
</div>
|
|
571
670
|
);
|
|
572
|
-
}
|
|
671
|
+
}
|
package/src/core/Pipeline.ts
CHANGED
|
@@ -101,15 +101,66 @@ export class Pipeline {
|
|
|
101
101
|
|
|
102
102
|
// Augment system prompt with a Universal Visualization Protocol
|
|
103
103
|
// We use a unique marker to ensure this is only injected once but is ALWAYS present.
|
|
104
|
-
const CHART_MARKER = '<!--
|
|
104
|
+
const CHART_MARKER = '<!-- UI_PROTOCOL_V5 -->';
|
|
105
105
|
const chartInstruction = `\n\n${CHART_MARKER}
|
|
106
|
-
###
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
### UNIVERSAL UI PROTOCOL
|
|
107
|
+
When the user asks for a visual representation, comparison, distribution, breakdown, table, list, catalog, products, carousel, stock view, or category summary, you MUST include exactly one \`\`\`ui\`\`\` block.
|
|
108
|
+
|
|
109
|
+
1. VIEW SELECTION
|
|
110
|
+
- Use "chart" for distributions, percentages, counts, comparisons, trends, or "show me in a chart".
|
|
111
|
+
- Use "carousel" for browseable product recommendations or when the user asks to see products/items/cards.
|
|
112
|
+
- Use "table" for tabular/raw detail, explicit table/list requests, or when there are many attributes to compare.
|
|
113
|
+
|
|
114
|
+
2. REQUIRED JSON SHAPE
|
|
115
|
+
{
|
|
116
|
+
"view": "chart" | "carousel" | "table",
|
|
117
|
+
"title": "Short heading",
|
|
118
|
+
"description": "One sentence describing the view",
|
|
119
|
+
"chartType": "pie" | "bar" | "line",
|
|
120
|
+
"xAxisKey": "label",
|
|
121
|
+
"dataKeys": ["value"],
|
|
122
|
+
"columns": ["name", "category", "price", "stockStatus"],
|
|
123
|
+
"insights": ["Short takeaway 1", "Short takeaway 2"],
|
|
124
|
+
"data": []
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
3. DATA RULES
|
|
128
|
+
- Build the UI payload from retrieved context only. Do not invent products, categories, or stock values.
|
|
129
|
+
- Aggregate raw product rows when the user asks for a chart.
|
|
130
|
+
- For charts, every row in "data" must be flat JSON with primitive values only.
|
|
131
|
+
- Prefer { "label": "...", "value": number } for pie charts.
|
|
132
|
+
- If stock information exists, preserve it in each row with fields like "inStock", "inStockCount", "outOfStockCount", or "stockStatus".
|
|
133
|
+
- If the user asks to highlight what is in stock, include those stock-related fields in the chart data and mention the pattern in "insights".
|
|
134
|
+
- For carousel rows, include product-friendly fields such as id, name, brand, price, image, link, category, stockStatus.
|
|
135
|
+
- For tables, keep "data" row-oriented and include "columns" when possible.
|
|
136
|
+
|
|
137
|
+
4. FORMAT RULES
|
|
138
|
+
- Never use ASCII art, markdown tables as the primary answer, or text-only fake charts when a UI block is requested.
|
|
139
|
+
- Put any short natural-language explanation before or after the \`\`\`ui\`\`\` block, but keep it concise.
|
|
140
|
+
- Return valid JSON inside the \`\`\`ui\`\`\` block.
|
|
141
|
+
|
|
142
|
+
5. EXAMPLE
|
|
143
|
+
User: "provide me the distribution of products across categories and highlight which ones are in stock in a pie chart"
|
|
144
|
+
Assistant:
|
|
145
|
+
\`\`\`ui
|
|
146
|
+
{
|
|
147
|
+
"view": "chart",
|
|
148
|
+
"title": "Product Distribution Across Categories",
|
|
149
|
+
"description": "Pie chart showing product count by category with stock signals preserved per slice.",
|
|
150
|
+
"chartType": "pie",
|
|
151
|
+
"xAxisKey": "label",
|
|
152
|
+
"dataKeys": ["value"],
|
|
153
|
+
"insights": [
|
|
154
|
+
"Beauty has the highest product count.",
|
|
155
|
+
"Electronics has the largest number of in-stock products."
|
|
156
|
+
],
|
|
157
|
+
"data": [
|
|
158
|
+
{ "label": "Beauty", "value": 12, "inStockCount": 9, "outOfStockCount": 3 },
|
|
159
|
+
{ "label": "Electronics", "value": 8, "inStockCount": 7, "outOfStockCount": 1 },
|
|
160
|
+
{ "label": "Home", "value": 5, "inStockCount": 2, "outOfStockCount": 3 }
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
\`\`\``;
|
|
113
164
|
|
|
114
165
|
if (!this.config.llm.systemPrompt?.includes(CHART_MARKER)) {
|
|
115
166
|
this.config.llm.systemPrompt = (this.config.llm.systemPrompt || '') + chartInstruction;
|