@retrivora-ai/rag-engine 1.7.3 → 1.7.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-M32ZXLTT.mjs → chunk-QOO37S2Z.mjs} +177 -53
- package/dist/handlers/index.js +172 -48
- package/dist/handlers/index.mjs +1 -1
- package/dist/index.js +17 -3
- package/dist/index.mjs +17 -3
- package/dist/server.js +179 -51
- package/dist/server.mjs +8 -4
- package/package.json +1 -1
- package/src/components/MessageBubble.tsx +58 -5
- package/src/core/Pipeline.ts +179 -53
- package/src/llm/providers/GeminiProvider.ts +7 -2
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +13 -3
package/dist/server.js
CHANGED
|
@@ -2482,7 +2482,8 @@ ${context}`;
|
|
|
2482
2482
|
stopSequences: options == null ? void 0 : options.stop
|
|
2483
2483
|
}
|
|
2484
2484
|
});
|
|
2485
|
-
|
|
2485
|
+
const text = typeof response.text === "function" ? response.text() : (_f = response.text) != null ? _f : "";
|
|
2486
|
+
return text;
|
|
2486
2487
|
}
|
|
2487
2488
|
chatStream(messages, context, options) {
|
|
2488
2489
|
return __asyncGenerator(this, null, function* () {
|
|
@@ -2516,7 +2517,7 @@ ${context}`;
|
|
|
2516
2517
|
try {
|
|
2517
2518
|
for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
|
|
2518
2519
|
const chunk = temp.value;
|
|
2519
|
-
const text = chunk.text;
|
|
2520
|
+
const text = typeof chunk.text === "function" ? chunk.text() : chunk.text;
|
|
2520
2521
|
if (text) yield text;
|
|
2521
2522
|
}
|
|
2522
2523
|
} catch (temp) {
|
|
@@ -3913,73 +3914,196 @@ var Pipeline = class {
|
|
|
3913
3914
|
async initialize() {
|
|
3914
3915
|
var _a, _b;
|
|
3915
3916
|
if (this.initialised) return;
|
|
3916
|
-
const CHART_MARKER = "<!--
|
|
3917
|
+
const CHART_MARKER = "<!-- UI_PROTOCOL_V7 -->";
|
|
3917
3918
|
const chartInstruction = `
|
|
3918
3919
|
|
|
3919
3920
|
${CHART_MARKER}
|
|
3920
|
-
### UNIVERSAL UI PROTOCOL
|
|
3921
|
-
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.
|
|
3921
|
+
### UNIVERSAL UI PROTOCOL V7 (STRICT MODE)
|
|
3922
3922
|
|
|
3923
|
-
|
|
3924
|
-
- Use "carousel" for browsing specific products, items, or a catalog. This is the DEFAULT for "show me", "what are", or "find me" requests for a specific category (e.g. "beauty").
|
|
3925
|
-
- Use "chart" ONLY for numerical aggregations: distributions, percentages, counts, comparisons over time, or trends (e.g. "how many beauty products?").
|
|
3926
|
-
- Use "table" for detailed technical specifications, row-heavy comparisons, or when the user explicitly asks for a list/table format.
|
|
3923
|
+
You are responsible for returning a SINGLE structured UI block when visualization is required.
|
|
3927
3924
|
|
|
3928
|
-
|
|
3929
|
-
{
|
|
3930
|
-
"view": "chart" | "carousel" | "table",
|
|
3931
|
-
"title": "Short heading",
|
|
3932
|
-
"description": "One sentence describing the view",
|
|
3933
|
-
"chartType": "pie" | "bar" | "line", // Required if view is "chart"
|
|
3934
|
-
"xAxisKey": "label",
|
|
3935
|
-
"dataKeys": ["value"],
|
|
3936
|
-
"columns": ["dynamicFieldKey1", "dynamicFieldKey2"],
|
|
3937
|
-
"insights": ["Short takeaway 1", "Short takeaway 2"],
|
|
3938
|
-
"data": []
|
|
3939
|
-
}
|
|
3925
|
+
---
|
|
3940
3926
|
|
|
3941
|
-
|
|
3942
|
-
- Build the UI payload from retrieved context only. Do not invent products or values.
|
|
3943
|
-
- For CAROUSEL: "data" must be a list of product objects (id, name, brand, price, image, link).
|
|
3944
|
-
- For CHART: "data" must be aggregated counts or sums. Never put raw product lists in a chart view.
|
|
3945
|
-
- If the user asks for "products in beauty", do NOT return a pie chart of categories; return a carousel of the beauty products themselves.
|
|
3927
|
+
## 1. INTENT CLASSIFICATION (MANDATORY)
|
|
3946
3928
|
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3929
|
+
Classify the query into ONE of the following intents:
|
|
3930
|
+
|
|
3931
|
+
- "explore" \u2192 user wants to browse items (e.g., "show me products")
|
|
3932
|
+
- "analyze" \u2192 user wants aggregation/distribution (e.g., "distribution of products")
|
|
3933
|
+
- "compare" \u2192 user wants side-by-side comparison
|
|
3934
|
+
- "detail" \u2192 user wants structured/tabular data
|
|
3935
|
+
|
|
3936
|
+
---
|
|
3937
|
+
|
|
3938
|
+
## 2. VIEW MAPPING (STRICT)
|
|
3939
|
+
|
|
3940
|
+
| Intent | View |
|
|
3941
|
+
|----------|-----------|
|
|
3942
|
+
| explore | carousel |
|
|
3943
|
+
| analyze | chart |
|
|
3944
|
+
| compare | table |
|
|
3945
|
+
| detail | table |
|
|
3946
|
+
|
|
3947
|
+
\u26A0\uFE0F Override Rules:
|
|
3948
|
+
- If query contains "distribution", "percentage", "ratio" \u2192 ALWAYS chart
|
|
3949
|
+
- If query contains "show products", "list items" \u2192 ALWAYS carousel
|
|
3950
|
+
- NEVER mix views
|
|
3951
|
+
|
|
3952
|
+
---
|
|
3953
|
+
|
|
3954
|
+
## 3. RESPONSE FORMAT (STRICT JSON ONLY INSIDE BLOCK)
|
|
3955
|
+
|
|
3956
|
+
\`\`\`ui
|
|
3957
|
+
{
|
|
3958
|
+
"view": "carousel" | "chart" | "table",
|
|
3959
|
+
"title": "string",
|
|
3960
|
+
"description": "string",
|
|
3961
|
+
"metadata": {
|
|
3962
|
+
"intent": "explore" | "analyze" | "compare" | "detail",
|
|
3963
|
+
"confidence": 0.0-1.0
|
|
3964
|
+
},
|
|
3965
|
+
|
|
3966
|
+
// CHART ONLY
|
|
3967
|
+
"chart": {
|
|
3968
|
+
"type": "pie" | "bar" | "line",
|
|
3969
|
+
"xKey": "label",
|
|
3970
|
+
"yKeys": ["value"],
|
|
3971
|
+
"data": []
|
|
3972
|
+
},
|
|
3973
|
+
|
|
3974
|
+
// TABLE ONLY
|
|
3975
|
+
"table": {
|
|
3976
|
+
"columns": [],
|
|
3977
|
+
"rows": []
|
|
3978
|
+
},
|
|
3979
|
+
|
|
3980
|
+
// CAROUSEL ONLY
|
|
3981
|
+
"carousel": {
|
|
3982
|
+
"items": []
|
|
3983
|
+
},
|
|
3984
|
+
|
|
3985
|
+
"insights": []
|
|
3986
|
+
}
|
|
3987
|
+
\`\`\`
|
|
3988
|
+
|
|
3989
|
+
---
|
|
3990
|
+
|
|
3991
|
+
## 4. DATA CONTRACT (CRITICAL)
|
|
3992
|
+
|
|
3993
|
+
### \u{1F539} CAROUSEL ITEM FORMAT
|
|
3994
|
+
{
|
|
3995
|
+
"id": "string",
|
|
3996
|
+
"name": "string",
|
|
3997
|
+
"brand": "string",
|
|
3998
|
+
"price": number,
|
|
3999
|
+
"image": "url",
|
|
4000
|
+
"link": "url",
|
|
4001
|
+
"inStock": boolean
|
|
4002
|
+
}
|
|
4003
|
+
|
|
4004
|
+
### \u{1F539} CHART DATA FORMAT
|
|
4005
|
+
{
|
|
4006
|
+
"label": "string",
|
|
4007
|
+
"value": number,
|
|
4008
|
+
"inStock": boolean (optional)
|
|
4009
|
+
}
|
|
4010
|
+
|
|
4011
|
+
### \u{1F539} TABLE FORMAT
|
|
4012
|
+
- columns MUST match row keys
|
|
4013
|
+
- rows MUST be flat objects
|
|
4014
|
+
|
|
4015
|
+
---
|
|
4016
|
+
|
|
4017
|
+
## 5. DATA RULES
|
|
4018
|
+
|
|
4019
|
+
- NEVER hallucinate fields
|
|
4020
|
+
- ONLY use retrieved vector DB data
|
|
4021
|
+
- NO placeholders ("...", "N/A")
|
|
4022
|
+
- If image/link missing \u2192 REMOVE FIELD
|
|
4023
|
+
- Aggregate BEFORE rendering chart
|
|
4024
|
+
|
|
4025
|
+
---
|
|
4026
|
+
|
|
4027
|
+
## 6. SMART RULES (IMPORTANT)
|
|
4028
|
+
|
|
4029
|
+
- If user asks BOTH:
|
|
4030
|
+
"show products AND distribution"
|
|
4031
|
+
\u2192 PRIORITIZE "explore" \u2192 carousel
|
|
4032
|
+
|
|
4033
|
+
- If dataset size > 20:
|
|
4034
|
+
\u2192 aggregate \u2192 chart or table
|
|
4035
|
+
|
|
4036
|
+
---
|
|
4037
|
+
|
|
4038
|
+
## 7. OUTPUT RULES
|
|
4039
|
+
|
|
4040
|
+
- ALWAYS return EXACTLY ONE \`\`\`ui\`\`\` block
|
|
4041
|
+
- JSON must be VALID
|
|
4042
|
+
- No explanation inside block
|
|
4043
|
+
|
|
4044
|
+
---
|
|
4045
|
+
|
|
4046
|
+
## 8. EXAMPLES
|
|
3950
4047
|
|
|
3951
|
-
5. EXAMPLES
|
|
3952
4048
|
User: "show me beauty products"
|
|
3953
|
-
|
|
4049
|
+
|
|
3954
4050
|
\`\`\`ui
|
|
3955
4051
|
{
|
|
3956
4052
|
"view": "carousel",
|
|
3957
4053
|
"title": "Beauty Products",
|
|
3958
|
-
"description": "Browse
|
|
3959
|
-
"
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
4054
|
+
"description": "Browse available beauty items",
|
|
4055
|
+
"metadata": {
|
|
4056
|
+
"intent": "explore",
|
|
4057
|
+
"confidence": 0.95
|
|
4058
|
+
},
|
|
4059
|
+
"carousel": {
|
|
4060
|
+
"items": [
|
|
4061
|
+
{
|
|
4062
|
+
"id": "1",
|
|
4063
|
+
"name": "Mascara",
|
|
4064
|
+
"brand": "Essence",
|
|
4065
|
+
"price": 9.99,
|
|
4066
|
+
"image": "https://example.com/mascara.jpg",
|
|
4067
|
+
"link": "https://example.com/p1",
|
|
4068
|
+
"inStock": true
|
|
4069
|
+
}
|
|
4070
|
+
]
|
|
4071
|
+
},
|
|
4072
|
+
"insights": ["Most products are affordable"]
|
|
3963
4073
|
}
|
|
3964
4074
|
\`\`\`
|
|
3965
4075
|
|
|
3966
|
-
|
|
3967
|
-
|
|
4076
|
+
---
|
|
4077
|
+
|
|
4078
|
+
User: "distribution of products across categories"
|
|
4079
|
+
|
|
3968
4080
|
\`\`\`ui
|
|
3969
4081
|
{
|
|
3970
4082
|
"view": "chart",
|
|
3971
|
-
"title": "
|
|
3972
|
-
"
|
|
3973
|
-
"
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
4083
|
+
"title": "Product Distribution",
|
|
4084
|
+
"description": "Category-wise product distribution",
|
|
4085
|
+
"metadata": {
|
|
4086
|
+
"intent": "analyze",
|
|
4087
|
+
"confidence": 0.98
|
|
4088
|
+
},
|
|
4089
|
+
"chart": {
|
|
4090
|
+
"type": "pie",
|
|
4091
|
+
"xKey": "label",
|
|
4092
|
+
"yKeys": ["value"],
|
|
4093
|
+
"data": [
|
|
4094
|
+
{ "label": "Beauty", "value": 15 },
|
|
4095
|
+
{ "label": "Electronics", "value": 10 }
|
|
4096
|
+
]
|
|
4097
|
+
},
|
|
4098
|
+
"insights": ["Beauty dominates inventory"]
|
|
3979
4099
|
}
|
|
3980
|
-
|
|
4100
|
+
\`\`\`
|
|
4101
|
+
`;
|
|
3981
4102
|
if (!((_a = this.config.llm.systemPrompt) == null ? void 0 : _a.includes(CHART_MARKER))) {
|
|
3982
|
-
|
|
4103
|
+
let cleanPrompt = this.config.llm.systemPrompt || "";
|
|
4104
|
+
cleanPrompt = cleanPrompt.replace(/\n\n### UNIVERSAL UI PROTOCOL[\s\S]*?(?=\n\n|$)/g, "");
|
|
4105
|
+
cleanPrompt = cleanPrompt.replace(/<!-- UI_PROTOCOL_V\d+ -->[\s\S]*?(?=\n\n|$)/g, "");
|
|
4106
|
+
this.config.llm.systemPrompt = cleanPrompt.trim() + chartInstruction;
|
|
3983
4107
|
}
|
|
3984
4108
|
console.log(`[Pipeline] Initializing with provider: ${this.config.vectorDb.provider}...`);
|
|
3985
4109
|
this.vectorDB = await ProviderRegistry.createVectorProvider(this.config.vectorDb);
|
|
@@ -4803,11 +4927,15 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
4803
4927
|
).map((hint) => hint.value.trim().toLowerCase()).filter(Boolean) : [];
|
|
4804
4928
|
console.log(`[MultiTablePostgresProvider] queryText: "${queryText}"`);
|
|
4805
4929
|
console.log(`[MultiTablePostgresProvider] entityHints: [${entityHints.join(", ")}]`);
|
|
4930
|
+
const cleanKeywordQuery = (text) => {
|
|
4931
|
+
return text.toLowerCase().replace(/\b(show|me|the|product|cards|of|category|find|list|browse|what|are|display)\b/g, "").trim().replace(/\s+/g, " & ");
|
|
4932
|
+
};
|
|
4806
4933
|
const queryPromises = this.tables.map(async (table) => {
|
|
4807
4934
|
try {
|
|
4808
4935
|
let sqlQuery = "";
|
|
4809
4936
|
let params = [];
|
|
4810
4937
|
if (queryText) {
|
|
4938
|
+
const cleanedText = cleanKeywordQuery(queryText) || queryText;
|
|
4811
4939
|
const hasEntityHints = entityHints.length > 0;
|
|
4812
4940
|
const exactNameScoreExpr = hasEntityHints ? `+ (
|
|
4813
4941
|
SELECT COALESCE(MAX(
|
|
@@ -4820,13 +4948,13 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
4820
4948
|
sqlQuery = `
|
|
4821
4949
|
SELECT *,
|
|
4822
4950
|
(1 - (embedding <=> $1::vector)) AS vector_score,
|
|
4823
|
-
COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text),
|
|
4824
|
-
((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text),
|
|
4951
|
+
COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) AS keyword_score,
|
|
4952
|
+
((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
|
|
4825
4953
|
FROM "${table}" t
|
|
4826
4954
|
ORDER BY hybrid_score DESC
|
|
4827
4955
|
LIMIT 50
|
|
4828
4956
|
`;
|
|
4829
|
-
params = [vectorLiteral,
|
|
4957
|
+
params = [vectorLiteral, cleanedText];
|
|
4830
4958
|
} else {
|
|
4831
4959
|
sqlQuery = `
|
|
4832
4960
|
SELECT *,
|
package/dist/server.mjs
CHANGED
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
sseFrame,
|
|
40
40
|
sseMetaFrame,
|
|
41
41
|
sseTextFrame
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-QOO37S2Z.mjs";
|
|
43
43
|
import "./chunk-YLTMFW4M.mjs";
|
|
44
44
|
import {
|
|
45
45
|
PineconeProvider
|
|
@@ -325,11 +325,15 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
325
325
|
).map((hint) => hint.value.trim().toLowerCase()).filter(Boolean) : [];
|
|
326
326
|
console.log(`[MultiTablePostgresProvider] queryText: "${queryText}"`);
|
|
327
327
|
console.log(`[MultiTablePostgresProvider] entityHints: [${entityHints.join(", ")}]`);
|
|
328
|
+
const cleanKeywordQuery = (text) => {
|
|
329
|
+
return text.toLowerCase().replace(/\b(show|me|the|product|cards|of|category|find|list|browse|what|are|display)\b/g, "").trim().replace(/\s+/g, " & ");
|
|
330
|
+
};
|
|
328
331
|
const queryPromises = this.tables.map(async (table) => {
|
|
329
332
|
try {
|
|
330
333
|
let sqlQuery = "";
|
|
331
334
|
let params = [];
|
|
332
335
|
if (queryText) {
|
|
336
|
+
const cleanedText = cleanKeywordQuery(queryText) || queryText;
|
|
333
337
|
const hasEntityHints = entityHints.length > 0;
|
|
334
338
|
const exactNameScoreExpr = hasEntityHints ? `+ (
|
|
335
339
|
SELECT COALESCE(MAX(
|
|
@@ -342,13 +346,13 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
342
346
|
sqlQuery = `
|
|
343
347
|
SELECT *,
|
|
344
348
|
(1 - (embedding <=> $1::vector)) AS vector_score,
|
|
345
|
-
COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text),
|
|
346
|
-
((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text),
|
|
349
|
+
COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) AS keyword_score,
|
|
350
|
+
((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
|
|
347
351
|
FROM "${table}" t
|
|
348
352
|
ORDER BY hybrid_score DESC
|
|
349
353
|
LIMIT 50
|
|
350
354
|
`;
|
|
351
|
-
params = [vectorLiteral,
|
|
355
|
+
params = [vectorLiteral, cleanedText];
|
|
352
356
|
} else {
|
|
353
357
|
sqlQuery = `
|
|
354
358
|
SELECT *,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retrivora-ai/rag-engine",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.5",
|
|
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",
|
|
@@ -32,8 +32,8 @@ function sanitizeJson(raw: string): string {
|
|
|
32
32
|
},
|
|
33
33
|
);
|
|
34
34
|
|
|
35
|
-
// 2. Strip JS-style
|
|
36
|
-
s = s.replace(
|
|
35
|
+
// 2. Strip JS-style comments while preserving strings (e.g. URLs)
|
|
36
|
+
s = s.replace(/("(?:\\.|[^\\"])*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (m, g1) => g1 || '');
|
|
37
37
|
|
|
38
38
|
// 3. Quote bare (unquoted) object keys
|
|
39
39
|
s = s.replace(/([{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/g, '$1"$2":');
|
|
@@ -204,11 +204,14 @@ function isLikelyUiOnlyMessage(text: string): boolean {
|
|
|
204
204
|
// ─── Tiny helpers ─────────────────────────────────────────────────────────────
|
|
205
205
|
|
|
206
206
|
function resolveImage(data: Record<string, unknown>): string | undefined {
|
|
207
|
+
const isPlaceholder = (val: unknown) =>
|
|
208
|
+
typeof val === 'string' && (val === '...' || val === '' || val.toLowerCase() === 'null' || val.toLowerCase() === 'undefined');
|
|
209
|
+
|
|
207
210
|
for (const key of ['image', 'img', 'thumbnail']) {
|
|
208
211
|
const v = data[key];
|
|
209
|
-
if (typeof v === 'string' && v) return v;
|
|
212
|
+
if (typeof v === 'string' && v && !isPlaceholder(v)) return v;
|
|
210
213
|
}
|
|
211
|
-
if (Array.isArray(data.images) && typeof data.images[0] === 'string') {
|
|
214
|
+
if (Array.isArray(data.images) && typeof data.images[0] === 'string' && !isPlaceholder(data.images[0])) {
|
|
212
215
|
return data.images[0];
|
|
213
216
|
}
|
|
214
217
|
return undefined;
|
|
@@ -378,14 +381,46 @@ interface UIConfig {
|
|
|
378
381
|
rows?: Record<string, unknown>[];
|
|
379
382
|
products?: Record<string, unknown>[];
|
|
380
383
|
results?: Record<string, unknown>[];
|
|
384
|
+
|
|
385
|
+
// V7 Protocol Fields
|
|
386
|
+
carousel?: {
|
|
387
|
+
items: Record<string, unknown>[];
|
|
388
|
+
};
|
|
389
|
+
chart?: {
|
|
390
|
+
type: 'pie' | 'bar' | 'line';
|
|
391
|
+
xKey: string;
|
|
392
|
+
yKeys: string[];
|
|
393
|
+
data: Record<string, unknown>[];
|
|
394
|
+
};
|
|
395
|
+
table?: {
|
|
396
|
+
columns: string[];
|
|
397
|
+
rows?: Record<string, unknown>[];
|
|
398
|
+
data?: Record<string, unknown>[];
|
|
399
|
+
};
|
|
381
400
|
}
|
|
382
401
|
|
|
383
|
-
interface RawUIConfig extends Omit<UIConfig, 'data' | 'items' | 'rows' | 'products' | 'results'> {
|
|
402
|
+
interface RawUIConfig extends Omit<UIConfig, 'data' | 'items' | 'rows' | 'products' | 'results' | 'carousel' | 'chart' | 'table'> {
|
|
384
403
|
data: Array<Record<string, unknown> | unknown[]>;
|
|
385
404
|
items?: Array<Record<string, unknown> | unknown[]>;
|
|
386
405
|
rows?: Array<Record<string, unknown> | unknown[]>;
|
|
387
406
|
products?: Array<Record<string, unknown> | unknown[]>;
|
|
388
407
|
results?: Array<Record<string, unknown> | unknown[]>;
|
|
408
|
+
|
|
409
|
+
// V7 Protocol Fields (Raw)
|
|
410
|
+
carousel?: {
|
|
411
|
+
items: Array<Record<string, unknown> | unknown[]>;
|
|
412
|
+
};
|
|
413
|
+
chart?: {
|
|
414
|
+
type: 'pie' | 'bar' | 'line';
|
|
415
|
+
xKey: string;
|
|
416
|
+
yKeys: string[];
|
|
417
|
+
data: Array<Record<string, unknown> | unknown[]>;
|
|
418
|
+
};
|
|
419
|
+
table?: {
|
|
420
|
+
columns: string[];
|
|
421
|
+
rows?: Array<Record<string, unknown> | unknown[]>;
|
|
422
|
+
data?: Array<Record<string, unknown> | unknown[]>;
|
|
423
|
+
};
|
|
389
424
|
}
|
|
390
425
|
|
|
391
426
|
function normalizeTabularRows(
|
|
@@ -417,6 +452,13 @@ function normalizeTabularRows(
|
|
|
417
452
|
}
|
|
418
453
|
|
|
419
454
|
function resolveStructuredRows(config: Partial<RawUIConfig> & Record<string, unknown>) {
|
|
455
|
+
// V7 Nested Structures
|
|
456
|
+
if (config.carousel?.items && Array.isArray(config.carousel.items)) return config.carousel.items;
|
|
457
|
+
if (config.chart?.data && Array.isArray(config.chart.data)) return config.chart.data;
|
|
458
|
+
if (config.table?.rows && Array.isArray(config.table.rows)) return config.table.rows;
|
|
459
|
+
if (config.table?.data && Array.isArray(config.table.data)) return config.table.data;
|
|
460
|
+
|
|
461
|
+
// V6 Flat Structures
|
|
420
462
|
if (Array.isArray(config.data) && config.data.length > 0) return config.data;
|
|
421
463
|
if (Array.isArray(config.items) && config.items.length > 0) return config.items;
|
|
422
464
|
if (Array.isArray(config.products) && config.products.length > 0) return config.products;
|
|
@@ -442,6 +484,17 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
|
|
|
442
484
|
.trim();
|
|
443
485
|
const parsed = JSON.parse(sanitizeJson(extractLikelyJsonBlock(clean)));
|
|
444
486
|
const config = { ...parsed } as Partial<RawUIConfig> & Record<string, unknown>;
|
|
487
|
+
|
|
488
|
+
// V7 Field Mapping
|
|
489
|
+
if (config.chart) {
|
|
490
|
+
if (!config.chartType) config.chartType = config.chart.type;
|
|
491
|
+
if (!config.xAxisKey) config.xAxisKey = config.chart.xKey;
|
|
492
|
+
if (!config.dataKeys) config.dataKeys = config.chart.yKeys;
|
|
493
|
+
}
|
|
494
|
+
if (config.table) {
|
|
495
|
+
if (!config.columns) config.columns = config.table.columns;
|
|
496
|
+
}
|
|
497
|
+
|
|
445
498
|
const resolvedRows = resolveStructuredRows(config);
|
|
446
499
|
|
|
447
500
|
// ─── Intent Healing & Auto-Detection ───────────────────────────────────
|