@retrivora-ai/rag-engine 1.8.3 → 1.8.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/server.mjs CHANGED
@@ -353,10 +353,12 @@ var init_MultiTablePostgresProvider = __esm({
353
353
  embedding VECTOR(${this.dimensions})
354
354
  )
355
355
  `);
356
+ const metric = this.config.distanceMetric || "cosine";
357
+ const indexOps = metric === "euclidean" ? "vector_l2_ops" : metric === "dotproduct" ? "vector_ip_ops" : "vector_cosine_ops";
356
358
  await client.query(`
357
359
  CREATE INDEX IF NOT EXISTS ${this.uploadTable}_embedding_idx
358
360
  ON ${this.uploadTable}
359
- USING hnsw (embedding vector_cosine_ops)
361
+ USING hnsw (embedding ${indexOps})
360
362
  `);
361
363
  const res = await client.query(`
362
364
  SELECT table_name
@@ -418,10 +420,12 @@ var init_MultiTablePostgresProvider = __esm({
418
420
  )
419
421
  `;
420
422
  await client.query(createTableSql);
423
+ const metric = this.config.distanceMetric || "cosine";
424
+ const indexOps = metric === "euclidean" ? "vector_l2_ops" : metric === "dotproduct" ? "vector_ip_ops" : "vector_cosine_ops";
421
425
  await client.query(`
422
426
  CREATE INDEX IF NOT EXISTS "${tableName}_embedding_idx"
423
427
  ON "${tableName}"
424
- USING hnsw (embedding vector_cosine_ops)
428
+ USING hnsw (embedding ${indexOps})
425
429
  `);
426
430
  if (!this.tables.includes(tableName)) {
427
431
  this.tables.push(tableName);
@@ -503,6 +507,8 @@ var init_MultiTablePostgresProvider = __esm({
503
507
  try {
504
508
  let sqlQuery = "";
505
509
  let params = [];
510
+ const metric = this.config.distanceMetric || "cosine";
511
+ const scoreExpr = metric === "euclidean" ? `1 / (1 + (embedding <-> $1::vector))` : metric === "dotproduct" ? `(embedding <#> $1::vector) * -1` : `1 - (embedding <=> $1::vector)`;
506
512
  if (queryText) {
507
513
  const hasEntityHints = entityHints.length > 0;
508
514
  const exactNameScoreExpr = hasEntityHints ? `+ (
@@ -515,9 +521,9 @@ var init_MultiTablePostgresProvider = __esm({
515
521
  ) * 3.0` : "";
516
522
  sqlQuery = `
517
523
  SELECT *,
518
- (1 - (embedding <=> $1::vector)) AS vector_score,
524
+ (${scoreExpr}) AS vector_score,
519
525
  COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) AS keyword_score,
520
- ((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
526
+ ((${scoreExpr}) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
521
527
  FROM "${table}" t
522
528
  ORDER BY hybrid_score DESC
523
529
  LIMIT 50
@@ -526,7 +532,7 @@ var init_MultiTablePostgresProvider = __esm({
526
532
  } else {
527
533
  sqlQuery = `
528
534
  SELECT *,
529
- (1 - (embedding <=> $1::vector)) AS hybrid_score
535
+ (${scoreExpr}) AS hybrid_score
530
536
  FROM "${table}" t
531
537
  ORDER BY hybrid_score DESC
532
538
  LIMIT 50
@@ -1000,11 +1006,13 @@ var init_QdrantProvider = __esm({
1000
1006
  if (axios4.isAxiosError(err) && ((_a = err.response) == null ? void 0 : _a.status) === 404) {
1001
1007
  const opts = this.config.options;
1002
1008
  const dimensionsForCreate = opts.dimensions || 1536;
1009
+ const metric = this.config.distanceMetric || "cosine";
1010
+ const distance = metric === "euclidean" ? "Euclid" : metric === "dotproduct" ? "Dot" : "Cosine";
1003
1011
  console.log(`[QdrantProvider] \u23F3 Creating collection "${this.indexName}" (dimensions: ${dimensionsForCreate})...`);
1004
1012
  await this.http.put(`/collections/${this.indexName}`, {
1005
1013
  vectors: {
1006
1014
  size: dimensionsForCreate,
1007
- distance: "Cosine"
1015
+ distance
1008
1016
  }
1009
1017
  });
1010
1018
  console.log(`[QdrantProvider] \u2705 Created collection "${this.indexName}"`);
@@ -1182,9 +1190,12 @@ var init_ChromaDBProvider = __esm({
1182
1190
  console.log(`[ChromaDBProvider] \u2705 Collection "${this.indexName}" found (id: ${this.collectionId})`);
1183
1191
  } catch (err) {
1184
1192
  if (axios5.isAxiosError(err) && ((_a = err.response) == null ? void 0 : _a.status) === 404) {
1193
+ const metric = this.config.distanceMetric || "cosine";
1194
+ const space = metric === "euclidean" ? "l2" : metric === "dotproduct" ? "ip" : "cosine";
1185
1195
  console.log(`[ChromaDBProvider] \u23F3 Collection "${this.indexName}" not found. Creating...`);
1186
1196
  const { data } = await this.http.post("/api/v1/collections", {
1187
- name: this.indexName
1197
+ name: this.indexName,
1198
+ metadata: { "hnsw:space": space }
1188
1199
  });
1189
1200
  this.collectionId = data.id;
1190
1201
  console.log(`[ChromaDBProvider] \u2705 Created collection "${this.indexName}" (id: ${this.collectionId})`);
@@ -4173,18 +4184,20 @@ var UITransformer = class {
4173
4184
  }
4174
4185
  const isStockRequest = this.isStockQuery(userQuery);
4175
4186
  const filteredData = isStockRequest ? retrievedData.filter((item) => this.determineStockStatus(item)) : retrievedData;
4176
- const categories = this.detectCategories(filteredData);
4187
+ const groupByField = this.determineGroupByField(userQuery, filteredData);
4188
+ const categories = this.detectCategories(filteredData, groupByField);
4177
4189
  const hasProducts = filteredData.some((item) => this.isProductData(item));
4178
4190
  const isTimeSeries = filteredData.some((item) => this.isTimeSeriesData(item));
4179
4191
  const isTrendQuery = this.isTrendQuery(userQuery);
4180
4192
  if (isTrendQuery && isTimeSeries) {
4181
4193
  return this.transformToLineChart(filteredData);
4182
4194
  }
4183
- if (hasProducts && !this.shouldShowCategoryChart(userQuery, categories)) {
4195
+ const explicitChartRequest = this.isExplicitChartQuery(userQuery);
4196
+ if (hasProducts && !explicitChartRequest && !this.shouldShowCategoryChart(userQuery)) {
4184
4197
  return this.transformToProductCarousel(filteredData, config, trainedSchema);
4185
4198
  }
4186
- if (categories.length > 1 && this.shouldShowCategoryChart(userQuery, categories)) {
4187
- return this.transformToPieChart(filteredData);
4199
+ if (explicitChartRequest || categories.length > 1 && this.shouldShowCategoryChart(userQuery)) {
4200
+ return this.transformToPieChart(filteredData, groupByField);
4188
4201
  }
4189
4202
  if (hasProducts) {
4190
4203
  return this.transformToProductCarousel(filteredData, config, trainedSchema);
@@ -4209,11 +4222,12 @@ var UITransformer = class {
4209
4222
  /**
4210
4223
  * Transform data to pie chart format
4211
4224
  */
4212
- static transformToPieChart(data) {
4213
- const categories = this.detectCategories(data);
4214
- const categoryData = this.aggregateByCategory(data, categories);
4225
+ static transformToPieChart(data, groupByField) {
4226
+ let categories = this.detectCategories(data, groupByField);
4227
+ if (categories.length === 0) categories = ["All Data"];
4228
+ const categoryData = this.aggregateByCategory(data, categories, groupByField);
4215
4229
  const pieData = Object.entries(categoryData).map(([label, count]) => {
4216
- const { inStockCount, outOfStockCount } = this.calculateStockCounts(label, data);
4230
+ const { inStockCount, outOfStockCount } = this.calculateStockCounts(label, data, groupByField);
4217
4231
  return {
4218
4232
  label,
4219
4233
  value: count,
@@ -4223,7 +4237,7 @@ var UITransformer = class {
4223
4237
  });
4224
4238
  return {
4225
4239
  type: "pie_chart",
4226
- title: "Distribution by Category",
4240
+ title: `Distribution by ${groupByField.charAt(0).toUpperCase() + groupByField.slice(1)}`,
4227
4241
  description: `Showing breakdown across ${categories.length} categories`,
4228
4242
  data: pieData
4229
4243
  };
@@ -4446,10 +4460,11 @@ var UITransformer = class {
4446
4460
  });
4447
4461
  return hasTimeKeyword || hasValidDateValue;
4448
4462
  }
4449
- static shouldShowCategoryChart(query, categories) {
4450
- if (categories.length < 2) {
4451
- return false;
4452
- }
4463
+ static isExplicitChartQuery(query) {
4464
+ const normalized = query.toLowerCase();
4465
+ return normalized.includes("pie chart") || normalized.includes("piechart") || normalized.includes("bar chart") || normalized.includes("barchart") || normalized.includes("radar chart");
4466
+ }
4467
+ static shouldShowCategoryChart(query) {
4453
4468
  const normalized = query.toLowerCase();
4454
4469
  const chartKeywords = [
4455
4470
  "distribution",
@@ -4462,7 +4477,11 @@ var UITransformer = class {
4462
4477
  "segmentation",
4463
4478
  "split",
4464
4479
  "category breakdown",
4465
- "category distribution"
4480
+ "category distribution",
4481
+ "pie chart",
4482
+ "piechart",
4483
+ "bar chart",
4484
+ "barchart"
4466
4485
  ];
4467
4486
  return chartKeywords.some((keyword) => normalized.includes(keyword));
4468
4487
  }
@@ -4538,44 +4557,67 @@ var UITransformer = class {
4538
4557
  return null;
4539
4558
  }
4540
4559
  /**
4541
- * Helper: Detect categories in data
4560
+ * Helper: Dynamically determine what field to group by based on user query
4561
+ */
4562
+ static determineGroupByField(query, data) {
4563
+ const normalized = query.toLowerCase();
4564
+ const match = normalized.match(/(?:by|across|per|based on)\s+([a-zA-Z]+)/i);
4565
+ let targetField = "category";
4566
+ if (match && match[1]) {
4567
+ let field = match[1].toLowerCase();
4568
+ if (field.endsWith("ies")) field = field.replace(/ies$/, "y");
4569
+ else if (field.endsWith("s") && field !== "status") field = field.slice(0, -1);
4570
+ const stopWords = ["the", "all", "different", "various", "some", "these", "those"];
4571
+ if (!stopWords.includes(field)) {
4572
+ targetField = field;
4573
+ }
4574
+ }
4575
+ const hasTargetField = data.some((d) => d.metadata && d.metadata[targetField] !== void 0);
4576
+ if (!hasTargetField && targetField === "category") {
4577
+ if (data.some((d) => d.metadata && d.metadata["type"] !== void 0)) return "type";
4578
+ if (data.some((d) => d.metadata && d.metadata["brand"] !== void 0)) return "brand";
4579
+ }
4580
+ return targetField;
4581
+ }
4582
+ /**
4583
+ * Helper: Detect grouping values dynamically
4542
4584
  */
4543
- static detectCategories(data) {
4585
+ static detectCategories(data, groupByField) {
4544
4586
  const categories = /* @__PURE__ */ new Set();
4545
4587
  data.forEach((item) => {
4546
4588
  const meta = item.metadata || {};
4547
- if (meta.category) {
4548
- categories.add(String(meta.category));
4549
- }
4550
- if (meta.type) {
4551
- categories.add(String(meta.type));
4552
- }
4553
- if (meta.tag) {
4554
- const tags = Array.isArray(meta.tag) ? meta.tag : [meta.tag];
4555
- tags.forEach((t) => categories.add(String(t)));
4556
- }
4557
- const contentCategories = Array.from(new Set(
4558
- Array.from(item.content.matchAll(/^\s*([^:\n]+):\s*(?:•|\*|-)*/gm)).map((match) => match[1].trim()).filter(Boolean)
4559
- ));
4560
- contentCategories.forEach((category) => categories.add(category));
4561
- const categoryMatch = item.content.match(/(?:Category|Type|Class):\s*([^\n]+)/i);
4562
- if (categoryMatch) {
4563
- categories.add(categoryMatch[1].trim());
4589
+ if (meta[groupByField] !== void 0) {
4590
+ const val = meta[groupByField];
4591
+ if (Array.isArray(val)) val.forEach((v) => categories.add(String(v)));
4592
+ else categories.add(String(val));
4593
+ } else if (groupByField === "category") {
4594
+ if (meta.type) categories.add(String(meta.type));
4595
+ if (meta.tag) {
4596
+ const tags = Array.isArray(meta.tag) ? meta.tag : [meta.tag];
4597
+ tags.forEach((t) => categories.add(String(t)));
4598
+ }
4599
+ const categoryMatch = item.content.match(/(?:Category|Type|Class):\s*([^\n]+)/i);
4600
+ if (categoryMatch) categories.add(categoryMatch[1].trim());
4564
4601
  }
4565
4602
  });
4566
4603
  return Array.from(categories);
4567
4604
  }
4568
4605
  /**
4569
- * Helper: Aggregate data by category
4606
+ * Helper: Aggregate data dynamically
4570
4607
  */
4571
- static aggregateByCategory(data, categories) {
4608
+ static aggregateByCategory(data, categories, groupByField) {
4572
4609
  const result = {};
4573
4610
  categories.forEach((cat) => {
4574
4611
  result[cat] = 0;
4575
4612
  });
4576
4613
  data.forEach((item) => {
4577
4614
  const meta = item.metadata || {};
4578
- const itemCategory = meta.category || meta.type || "Other";
4615
+ let itemCategory = "Other";
4616
+ if (meta[groupByField] !== void 0) {
4617
+ itemCategory = String(meta[groupByField]);
4618
+ } else if (groupByField === "category" && meta.type) {
4619
+ itemCategory = String(meta.type);
4620
+ }
4579
4621
  if (Object.prototype.hasOwnProperty.call(result, itemCategory)) {
4580
4622
  result[itemCategory]++;
4581
4623
  } else {
@@ -4625,14 +4667,19 @@ var UITransformer = class {
4625
4667
  });
4626
4668
  }
4627
4669
  /**
4628
- * Helper: Calculate stock counts by category
4670
+ * Helper: Calculate stock counts dynamically
4629
4671
  */
4630
- static calculateStockCounts(category, data) {
4672
+ static calculateStockCounts(category, data, groupByField) {
4631
4673
  let inStock = 0;
4632
4674
  let outOfStock = 0;
4633
4675
  data.forEach((d) => {
4634
4676
  const meta = d.metadata || {};
4635
- const itemCategory = meta.category || meta.type || "Other";
4677
+ let itemCategory = "Other";
4678
+ if (meta[groupByField] !== void 0) {
4679
+ itemCategory = String(meta[groupByField]);
4680
+ } else if (groupByField === "category" && meta.type) {
4681
+ itemCategory = String(meta.type);
4682
+ }
4636
4683
  if (itemCategory === category) {
4637
4684
  if (this.determineStockStatus(d)) {
4638
4685
  inStock++;
@@ -4757,12 +4804,13 @@ DATA SHAPE per type:
4757
4804
  - text: { "content": "<prose answer>" }
4758
4805
 
4759
4806
  DECISION RULES (follow strictly):
4760
- 1. bar_chart \u2192 comparing quantities across categories (e.g. sales by region, price by product). Use this when there is only ONE value per category.
4761
- 2. line_chart \u2192 trends or changes over time (dates, months, years, sequential events)
4762
- 3. pie_chart \u2192 proportional breakdown or percentage distribution
4763
- 4. radar_chart \u2192 comparing 2 or more products across MULTIPLE attributes (e.g. comparing features, ratings, or characteristics of 2 specific products). If the user asks to "compare" products and the data contains multiple dimensions or ratings for them, you MUST use this.
4764
- 5. table \u2192 multi-field structured records where each item has \u2265 3 attributes
4765
- 6. text \u2192 conversational or free-form answers where no chart adds value
4807
+ 1. IF the user explicitly asks for a "pie chart", "bar chart", or "table", you MUST output that exact type. Do not deviate from their request.
4808
+ 2. bar_chart \u2192 comparing quantities across categories (e.g. sales by region, price by product). Use this when there is only ONE value per category.
4809
+ 3. line_chart \u2192 trends or changes over time (dates, months, years, sequential events)
4810
+ 4. pie_chart \u2192 proportional breakdown or percentage distribution
4811
+ 5. radar_chart \u2192 comparing 2 or more products across MULTIPLE attributes.
4812
+ 6. table \u2192 multi-field structured records where each item has \u2265 3 attributes
4813
+ 7. text \u2192 conversational or free-form answers where no chart adds value
4766
4814
 
4767
4815
  IMPORTANT:
4768
4816
  - Aggregate numeric values from the raw data \u2014 do not pass raw object arrays as data.
@@ -5920,10 +5968,12 @@ var PostgreSQLProvider = class extends BaseVectorProvider {
5920
5968
  embedding VECTOR(${this.dimensions})
5921
5969
  )
5922
5970
  `);
5971
+ const metric = this.config.distanceMetric || "cosine";
5972
+ const indexOps = metric === "euclidean" ? "vector_l2_ops" : metric === "dotproduct" ? "vector_ip_ops" : "vector_cosine_ops";
5923
5973
  await client.query(`
5924
5974
  CREATE INDEX IF NOT EXISTS ${this.tableName}_embedding_idx
5925
5975
  ON ${this.tableName}
5926
- USING hnsw (embedding vector_cosine_ops)
5976
+ USING hnsw (embedding ${indexOps})
5927
5977
  `);
5928
5978
  } finally {
5929
5979
  client.release();
@@ -5995,11 +6045,14 @@ var PostgreSQLProvider = class extends BaseVectorProvider {
5995
6045
  try {
5996
6046
  const efSearch = this.config.options.efSearch || Math.max(topK * 10, 40);
5997
6047
  await client.query(`SET LOCAL hnsw.ef_search = ${efSearch}`);
6048
+ const metric = this.config.distanceMetric || "cosine";
6049
+ const queryOp = metric === "euclidean" ? "<->" : metric === "dotproduct" ? "<#>" : "<=>";
6050
+ const scoreExpr = metric === "euclidean" ? `1 / (1 + (embedding <-> $1::vector))` : metric === "dotproduct" ? `(embedding <#> $1::vector) * -1` : `1 - (embedding <=> $1::vector)`;
5998
6051
  const result = await client.query(
5999
- `SELECT id, content, metadata, 1 - (embedding <=> $1::vector) AS score
6052
+ `SELECT id, content, metadata, ${scoreExpr} AS score
6000
6053
  FROM ${this.tableName}
6001
6054
  ${whereClause}
6002
- ORDER BY embedding <=> $1::vector
6055
+ ORDER BY embedding ${queryOp} $1::vector
6003
6056
  LIMIT $2`,
6004
6057
  params
6005
6058
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retrivora-ai/rag-engine",
3
- "version": "1.8.3",
3
+ "version": "1.8.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",
@@ -36,6 +36,7 @@
36
36
  "require": "./dist/index.js",
37
37
  "import": "./dist/index.mjs"
38
38
  },
39
+ "./style.css": "./dist/index.css",
39
40
  "./handlers": {
40
41
  "types": "./dist/handlers/index.d.ts",
41
42
  "require": "./dist/handlers/index.js",
@@ -73,7 +74,7 @@
73
74
  "scripts": {
74
75
  "dev": "next dev",
75
76
  "build": "next build",
76
- "build:pkg": "tsup src/index.ts src/handlers/index.ts src/server.ts --format cjs,esm --dts --clean --no-splitting --tsconfig tsconfig.build.json --external react,react-dom,next,mongodb,pg,openai,@anthropic-ai/sdk,@pinecone-database/pinecone,axios,lucide-react,mammoth,pdf-parse,react-markdown,remark-gfm,next-themes,langchain,@langchain/core,@langchain/openai,llamaindex",
77
+ "build:pkg": "tsup src/index.ts src/handlers/index.ts src/server.ts --format cjs,esm --dts --clean --no-splitting --tsconfig tsconfig.build.json --external react,react-dom,next,mongodb,pg,openai,@anthropic-ai/sdk,@pinecone-database/pinecone,axios,lucide-react,mammoth,pdf-parse,react-markdown,remark-gfm,next-themes,langchain,@langchain/core,@langchain/openai,llamaindex && npx @tailwindcss/cli -i src/tailwind.css -o dist/index.css",
77
78
  "start": "next start",
78
79
  "lint": "eslint",
79
80
  "prepublishOnly": "npm run build:pkg"
@@ -112,6 +113,7 @@
112
113
  "pg": "^8.20.0"
113
114
  },
114
115
  "devDependencies": {
116
+ "@tailwindcss/cli": "^4",
115
117
  "@tailwindcss/postcss": "^4",
116
118
  "@types/estree": "^1.0.9",
117
119
  "@types/node": "^20",
@@ -2,7 +2,104 @@
2
2
 
3
3
  @custom-variant dark (&:where(.dark, .dark *));
4
4
 
5
+ @theme {
6
+ --animate-shapeshift: shapeshift 5s ease-in-out infinite forwards;
7
+ --animate-gradientMove: gradientMove 3.5s ease-in-out infinite alternate;
8
+
9
+ @keyframes shapeshift {
10
+ 0% { clip-path: var(--circle); rotate: 0turn; }
11
+ 25% { clip-path: var(--flower); }
12
+ 50% { clip-path: var(--cylinder); }
13
+ 75% { clip-path: var(--hexagon); }
14
+ 100% { clip-path: var(--circle); rotate: 1turn; }
15
+ }
16
+
17
+ @keyframes gradientMove {
18
+ 0% { translate: 0 0; }
19
+ 100% { translate: -75% -75%; }
20
+ }
21
+ }
5
22
 
23
+ @layer base {
24
+ :root {
25
+ --star: shape(
26
+ evenodd from 50% 24.787%,
27
+ curve by 7.143% 18.016% with 0% 0% / 2.9725% 13.814%,
28
+ curve by 17.882% 7.197% with 4.171% 4.2025% / 17.882% 7.197%,
29
+ curve by -17.882% 8.6765% with 0% 0% / -13.711% 4.474%,
30
+ curve by -7.143% 16.5365% with -4.1705% 4.202% / -7.143% 16.5365%,
31
+ curve by -8.6115% -16.5365% with 0% 0% / -4.441% -12.3345%,
32
+ curve by -16.4135% -8.6765% with -4.171% -4.2025% / -16.4135% -8.6765%,
33
+ curve by 16.4135% -7.197% with 0% 0% / 12.2425% -2.9945%,
34
+ curve by 8.6115% -18.016% with 4.1705% -4.202% / 8.6115% -18.016%,
35
+ close
36
+ );
37
+ --flower: shape(
38
+ evenodd from 17.9665% 82.0335%,
39
+ curve by -12.349% -32.0335% with -13.239% -5.129% / -18.021% -15.402%,
40
+ curve by -0.0275% -22.203% with -3.1825% -9.331% / -3.074% -16.6605%,
41
+ curve by 12.3765% -9.8305% with 2.3835% -4.3365% / 6.565% -7.579%,
42
+ curve by 32.0335% -12.349% with 5.129% -13.239% / 15.402% -18.021%,
43
+ curve by 20.4535% -0.8665% with 8.3805% -2.858% / 15.1465% -3.062%,
44
+ curve by 11.58% 13.2155% with 5.225% 2.161% / 9.0355% 6.6475%,
45
+ curve by 12.349% 32.0335% with 13.239% 5.129% / 18.021% 15.402%,
46
+ curve by 0.5715% 21.1275% with 2.9805% 8.7395% / 3.0745% 15.723%,
47
+ curve by -12.9205% 10.906% with -2.26% 4.88% / -6.638% 8.472%,
48
+ curve by -32.0335% 12.349% with -5.129% 13.239% / -15.402% 18.021%,
49
+ curve by -21.1215% 0.5745% with -8.736% 2.9795% / -15.718% 3.0745%,
50
+ curve by -10.912% -12.9235% with -4.883% -2.2595% / -8.477% -6.6385%,
51
+ close
52
+ );
53
+ --hexagon: shape(
54
+ evenodd from 6.47% 67.001%,
55
+ curve by 0% -34.002% with -1.1735% -7.7% / -1.1735% -26.302%,
56
+ curve by 7.0415% -12.1965% with 0.7075% -4.641% / 3.3765% -9.2635%,
57
+ curve by 29.447% -17.001% with 6.0815% -4.8665% / 22.192% -14.1675%,
58
+ curve by 14.083% 0% with 4.3725% -1.708% / 9.7105% -1.708%,
59
+ curve by 29.447% 17.001% with 7.255% 2.8335% / 23.3655% 12.1345%,
60
+ curve by 7.0415% 12.1965% with 3.665% 2.933% / 6.334% 7.5555%,
61
+ curve by 0% 34.002% with 1.1735% 7.7% / 1.1735% 26.302%,
62
+ curve by -7.0415% 12.1965% with -0.7075% 4.641% / -3.3765% 9.2635%,
63
+ curve by -29.447% 17.001% with -6.0815% 4.8665% / -22.192% 14.1675%,
64
+ curve by -14.083% 0% with -4.3725% 1.708% / -9.7105% 1.708%,
65
+ curve by -29.447% -17.001% with -7.255% -2.8335% / -23.3655% -12.1345%,
66
+ curve by -7.0415% -12.1965% with -3.665% -2.933% / -6.334% -7.5555%,
67
+ close
68
+ );
69
+ --cylinder: shape(
70
+ evenodd from 10.5845% 59.7305%,
71
+ curve by 0% -19.461% with -0.113% -1.7525% / -0.11% -18.14%,
72
+ curve by 10.098% -26.213% with 0.837% -10.0375% / 3.821% -19.2625%,
73
+ curve by 29.3175% -13.0215% with 7.2175% -7.992% / 17.682% -13.0215%,
74
+ curve by 19.5845% 5.185% with 7.1265% 0% / 13.8135% 1.887%,
75
+ curve by 9.8595% 7.9775% with 3.7065% 2.1185% / 7.035% 4.8195%,
76
+ curve by 9.9715% 26.072% with 6.2015% 6.933% / 9.4345% 16.082%,
77
+ curve by 0% 19.461% with 0.074% 1.384% / 0.0745% 17.7715%,
78
+ curve by -13.0065% 29.1155% with -0.511% 11.5345% / -5.021% 21.933%,
79
+ curve by -26.409% 10.119% with -6.991% 6.288% / -16.254% 10.119%,
80
+ curve by -20.945% -5.9995% with -7.6935% 0% / -14.8755% -2.199%,
81
+ curve by -8.713% -7.404% with -3.255% -2.0385% / -6.1905% -4.537%,
82
+ curve by -9.7575% -25.831% with -6.074% -6.9035% / -9.1205% -15.963%,
83
+ close
84
+ );
85
+ --circle: shape(
86
+ evenodd from 13.482% 79.505%,
87
+ curve by -7.1945% -12.47% with -1.4985% -1.8575% / -6.328% -10.225%,
88
+ curve by 0.0985% -33.8965% with -4.1645% -10.7945% / -4.1685% -23.0235%,
89
+ curve by 6.9955% -12.101% with 1.72% -4.3825% / 4.0845% -8.458%,
90
+ curve by 30.125% -17.119% with 7.339% -9.1825% / 18.4775% -15.5135%,
91
+ curve by 13.4165% 0.095% with 4.432% -0.6105% / 8.9505% -0.5855%,
92
+ curve by 29.364% 16.9% with 11.6215% 1.77% / 22.102% 7.9015%,
93
+ curve by 7.176% 12.4145% with 3.002% 3.7195% / 5.453% 7.968%,
94
+ curve by -0.0475% 33.8925% with 4.168% 10.756% / 4.2305% 22.942%,
95
+ curve by -7.1135% 12.2825% with -1.74% 4.4535% / -4.1455% 8.592%,
96
+ curve by -29.404% 16.9075% with -7.202% 8.954% / -18.019% 15.137%,
97
+ curve by -14.19% -0.018% with -4.6635% 0.7255% / -9.4575% 0.7205%,
98
+ curve by -29.226% -16.8875% with -11.573% -1.8065% / -21.9955% -7.9235%,
99
+ close
100
+ );
101
+ }
102
+ }
6
103
 
7
104
  /* ─── Design Tokens ─────────────────────────────────────────── */
8
105
  :root {
@@ -119,25 +119,38 @@ export function ChatWidget({ position = 'bottom-right', onAddToCart }: ChatWidge
119
119
  {/* ── Floating Action Button ── */}
120
120
  <button
121
121
  onClick={isOpen ? () => setIsOpen(false) : handleOpen}
122
- className={`fixed z-[9999] w-14 h-14 shadow-2xl flex items-center justify-center transition-all duration-300 cursor-pointer hover:scale-110 active:scale-95 ${
123
- ui.borderRadius === 'full' ? 'rounded-full' : 'rounded-2xl'
124
- } ${positionClass}`}
125
- style={{ background: `linear-gradient(135deg, ${ui.primaryColor}, ${ui.accentColor})` }}
122
+ className={`group fixed z-[9999] w-14 h-14 drop-shadow-2xl flex items-center justify-center transition-all duration-300 cursor-pointer hover:scale-110 active:scale-95 ${positionClass}`}
126
123
  aria-label="Open chat"
127
124
  >
125
+ {/* Animated Background */}
126
+ <div
127
+ className={`absolute inset-0 transition-all duration-300 overflow-hidden ${
128
+ ui.borderRadius === 'full' ? 'rounded-full' : 'rounded-2xl'
129
+ } group-hover:rounded-none group-hover:animate-shapeshift`}
130
+ >
131
+ <div
132
+ className="absolute top-0 left-0 w-[400%] h-[400%] transition-transform duration-300 group-hover:animate-gradientMove"
133
+ style={{
134
+ background: `linear-gradient(135deg, ${ui.primaryColor}, ${ui.accentColor}, ${ui.primaryColor}, ${ui.accentColor})`
135
+ }}
136
+ />
137
+ </div>
138
+
128
139
  {/* Ripple ring */}
129
140
  <span
130
- className="absolute inset-0 rounded-full animate-ping opacity-20"
141
+ className="absolute inset-0 rounded-full animate-ping opacity-20 pointer-events-none"
131
142
  style={{ background: ui.primaryColor }}
132
143
  />
133
144
 
145
+ {/* Icon Layer */}
134
146
  <span
135
- className={`transition-transform duration-300 ${isOpen ? 'rotate-90 scale-90' : 'rotate-0 scale-100'}`}
147
+ className={`relative z-10 transition-transform duration-300 ${isOpen ? 'rotate-90 scale-90' : 'rotate-0 scale-100'}`}
136
148
  >
137
- {isOpen
138
- ? <X className="w-6 h-6 text-white" />
139
- : <MessageSquare className="w-6 h-6 text-white" />
140
- }
149
+ {isOpen ? (
150
+ <X className="w-6 h-6 text-white" />
151
+ ) : (
152
+ <MessageSquare className="w-6 h-6 text-white" />
153
+ )}
141
154
  </span>
142
155
 
143
156
  {/* Unread badge */}
@@ -44,6 +44,8 @@ export interface VectorDBConfig {
44
44
  * - searchFields?: string[] | string // optional override for which columns receive exact-match boosts
45
45
  */
46
46
  options: Record<string, unknown>;
47
+ /** Optional distance metric (defaults to cosine if not specified and supported by provider) */
48
+ distanceMetric?: 'cosine' | 'euclidean' | 'dotproduct';
47
49
  }
48
50
 
49
51
  // ---------------------------------------------------------------------------
@@ -41,9 +41,13 @@ export class ChromaDBProvider extends BaseVectorProvider {
41
41
  } catch (err) {
42
42
  if (axios.isAxiosError(err) && err.response?.status === 404) {
43
43
  // Create the collection if it doesn't exist
44
+ const metric = this.config.distanceMetric || 'cosine';
45
+ const space = metric === 'euclidean' ? 'l2' : metric === 'dotproduct' ? 'ip' : 'cosine';
46
+
44
47
  console.log(`[ChromaDBProvider] ⏳ Collection "${this.indexName}" not found. Creating...`);
45
48
  const { data } = await this.http.post('/api/v1/collections', {
46
49
  name: this.indexName,
50
+ metadata: { "hnsw:space": space }
47
51
  });
48
52
  this.collectionId = data.id;
49
53
  console.log(`[ChromaDBProvider] ✅ Created collection "${this.indexName}" (id: ${this.collectionId})`);
@@ -66,10 +66,13 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
66
66
  embedding VECTOR(${this.dimensions})
67
67
  )
68
68
  `);
69
+ const metric = this.config.distanceMetric || 'cosine';
70
+ const indexOps = metric === 'euclidean' ? 'vector_l2_ops' : metric === 'dotproduct' ? 'vector_ip_ops' : 'vector_cosine_ops';
71
+
69
72
  await client.query(`
70
73
  CREATE INDEX IF NOT EXISTS ${this.uploadTable}_embedding_idx
71
74
  ON ${this.uploadTable}
72
- USING hnsw (embedding vector_cosine_ops)
75
+ USING hnsw (embedding ${indexOps})
73
76
  `);
74
77
 
75
78
  const res = await client.query(`
@@ -147,10 +150,13 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
147
150
  await client.query(createTableSql);
148
151
 
149
152
  // Create Index
153
+ const metric = this.config.distanceMetric || 'cosine';
154
+ const indexOps = metric === 'euclidean' ? 'vector_l2_ops' : metric === 'dotproduct' ? 'vector_ip_ops' : 'vector_cosine_ops';
155
+
150
156
  await client.query(`
151
157
  CREATE INDEX IF NOT EXISTS "${tableName}_embedding_idx"
152
158
  ON "${tableName}"
153
- USING hnsw (embedding vector_cosine_ops)
159
+ USING hnsw (embedding ${indexOps})
154
160
  `);
155
161
 
156
162
  // Ensure this new table is added to our search scope
@@ -274,6 +280,13 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
274
280
  let sqlQuery = '';
275
281
  let params: unknown[] = [];
276
282
 
283
+ const metric = this.config.distanceMetric || 'cosine';
284
+ const scoreExpr = metric === 'euclidean'
285
+ ? `1 / (1 + (embedding <-> $1::vector))`
286
+ : metric === 'dotproduct'
287
+ ? `(embedding <#> $1::vector) * -1`
288
+ : `1 - (embedding <=> $1::vector)`;
289
+
277
290
  if (queryText) {
278
291
  const hasEntityHints = entityHints.length > 0;
279
292
  const exactNameScoreExpr = hasEntityHints
@@ -290,9 +303,9 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
290
303
  // Actual Hybrid Search: Semantic Vector Score + Full-Text Keyword Rank
291
304
  sqlQuery = `
292
305
  SELECT *,
293
- (1 - (embedding <=> $1::vector)) AS vector_score,
306
+ (${scoreExpr}) AS vector_score,
294
307
  COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) AS keyword_score,
295
- ((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
308
+ ((${scoreExpr}) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
296
309
  FROM "${table}" t
297
310
  ORDER BY hybrid_score DESC
298
311
  LIMIT 50
@@ -302,7 +315,7 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
302
315
  // Fallback to Vector-only Search
303
316
  sqlQuery = `
304
317
  SELECT *,
305
- (1 - (embedding <=> $1::vector)) AS hybrid_score
318
+ (${scoreExpr}) AS hybrid_score
306
319
  FROM "${table}" t
307
320
  ORDER BY hybrid_score DESC
308
321
  LIMIT 50
@@ -97,10 +97,13 @@ export class PostgreSQLProvider extends BaseVectorProvider {
97
97
  embedding VECTOR(${this.dimensions})
98
98
  )
99
99
  `);
100
+ const metric = this.config.distanceMetric || 'cosine';
101
+ const indexOps = metric === 'euclidean' ? 'vector_l2_ops' : metric === 'dotproduct' ? 'vector_ip_ops' : 'vector_cosine_ops';
102
+
100
103
  await client.query(`
101
104
  CREATE INDEX IF NOT EXISTS ${this.tableName}_embedding_idx
102
105
  ON ${this.tableName}
103
- USING hnsw (embedding vector_cosine_ops)
106
+ USING hnsw (embedding ${indexOps})
104
107
  `);
105
108
  } finally {
106
109
  client.release();
@@ -182,11 +185,19 @@ export class PostgreSQLProvider extends BaseVectorProvider {
182
185
  const efSearch = (this.config.options.efSearch as number) || Math.max(topK * 10, 40);
183
186
  await client.query(`SET LOCAL hnsw.ef_search = ${efSearch}`);
184
187
 
188
+ const metric = this.config.distanceMetric || 'cosine';
189
+ const queryOp = metric === 'euclidean' ? '<->' : metric === 'dotproduct' ? '<#>' : '<=>';
190
+ const scoreExpr = metric === 'euclidean'
191
+ ? `1 / (1 + (embedding <-> $1::vector))`
192
+ : metric === 'dotproduct'
193
+ ? `(embedding <#> $1::vector) * -1`
194
+ : `1 - (embedding <=> $1::vector)`;
195
+
185
196
  const result = await client.query(
186
- `SELECT id, content, metadata, 1 - (embedding <=> $1::vector) AS score
197
+ `SELECT id, content, metadata, ${scoreExpr} AS score
187
198
  FROM ${this.tableName}
188
199
  ${whereClause}
189
- ORDER BY embedding <=> $1::vector
200
+ ORDER BY embedding ${queryOp} $1::vector
190
201
  LIMIT $2`,
191
202
  params
192
203
  );