From a1646f3af964c63ee92132f4a2d3328fdc74fb7a Mon Sep 17 00:00:00 2001 From: root Date: Mon, 11 May 2026 23:44:05 +0500 Subject: [PATCH] fixed: upselling suggestions --- src/app/storefront-chat/widget/page.tsx | 26 ++++++++++++++++---- src/lib/storefront-chat/upsells.test.ts | 32 ++++++++++++++++++++++++- src/lib/storefront-chat/upsells.ts | 26 +++++++++++++++----- 3 files changed, 73 insertions(+), 11 deletions(-) diff --git a/src/app/storefront-chat/widget/page.tsx b/src/app/storefront-chat/widget/page.tsx index da21045..84b168d 100644 --- a/src/app/storefront-chat/widget/page.tsx +++ b/src/app/storefront-chat/widget/page.tsx @@ -504,11 +504,29 @@ export default function StorefrontChatWidgetPage() { type="button" onClick={() => submitCartAction(message, action)} disabled={sending} - className="w-full rounded-xl border border-indigo-200 bg-white px-3 py-2 text-left text-xs font-bold text-indigo-700 hover:bg-indigo-50 disabled:opacity-50" + className="w-full rounded-xl border border-indigo-200 bg-white px-3 py-2 text-left hover:bg-indigo-50 disabled:opacity-50" > - {action.label} - - {action.product.title} + + {action.product.imageUrl ? ( + ) diff --git a/src/lib/storefront-chat/upsells.test.ts b/src/lib/storefront-chat/upsells.test.ts index 935a2ec..7cf969b 100644 --- a/src/lib/storefront-chat/upsells.test.ts +++ b/src/lib/storefront-chat/upsells.test.ts @@ -47,6 +47,7 @@ describe("automatic storefront upsells", () => { productId: null, relationshipType: "uncertain", confidence: 0, + catalogEvidence: "", reason: "No clearly complementary product.", }), [ @@ -72,6 +73,7 @@ describe("automatic storefront upsells", () => { productId: "candidate-1", relationshipType: "substitute", confidence: 0.9, + catalogEvidence: "Candidate Product", reason: "This is an alternative, not an add-on.", }), [ @@ -96,7 +98,8 @@ describe("automatic storefront upsells", () => { JSON.stringify({ productId: "candidate-1", relationshipType: "complementary_accessory", - confidence: 0.88, + confidence: 0.95, + catalogEvidence: "Accessory", reason: "This is used together with the base product.", }), [ @@ -122,6 +125,7 @@ describe("automatic storefront upsells", () => { productId: "unknown-product", relationshipType: "complementary_accessory", confidence: 0.9, + catalogEvidence: "Accessory", reason: "Not in the provided list.", }), [ @@ -140,4 +144,30 @@ describe("automatic storefront upsells", () => { expect(result).toBeNull() }) + + it("rejects complementary choices without exact catalog evidence", () => { + const result = parseAiUpsellChoice( + JSON.stringify({ + productId: "candidate-1", + relationshipType: "complementary_accessory", + confidence: 0.95, + catalogEvidence: "commonly worn together", + reason: "This uses generic knowledge instead of catalog text.", + }), + [ + { + title: "Plain Socks", + productType: "Accessory", + details: null, + product: { + productId: "candidate-1", + title: "Plain Socks", + variantId: "v-candidate", + }, + }, + ], + ) + + expect(result).toBeNull() + }) }) diff --git a/src/lib/storefront-chat/upsells.ts b/src/lib/storefront-chat/upsells.ts index 9b0fb7c..cccbaac 100644 --- a/src/lib/storefront-chat/upsells.ts +++ b/src/lib/storefront-chat/upsells.ts @@ -116,17 +116,30 @@ export function parseAiUpsellChoice( productId?: unknown relationshipType?: unknown confidence?: unknown + catalogEvidence?: unknown } const productId = typeof parsed.productId === "string" ? parsed.productId.trim() : "" if (!productId) return null + const candidate = candidates.find((row) => row.product.productId === productId) + if (!candidate) return null const relationshipType = typeof parsed.relationshipType === "string" ? parsed.relationshipType.trim().toLowerCase() : "" if (!ACCEPTED_UPSELL_RELATIONSHIPS.has(relationshipType)) return null const confidence = Number(parsed.confidence) - if (!Number.isFinite(confidence) || confidence < 0.75) return null - return candidates.find((candidate) => candidate.product.productId === productId)?.product ?? null + if (!Number.isFinite(confidence) || confidence < 0.9) return null + const catalogEvidence = + typeof parsed.catalogEvidence === "string" + ? parsed.catalogEvidence.trim().toLowerCase() + : "" + if (catalogEvidence.length < 8) return null + const candidateCatalogText = [candidate.title, candidate.productType, candidate.details] + .filter(Boolean) + .join(" ") + .toLowerCase() + if (!candidateCatalogText.includes(catalogEvidence)) return null + return candidate.product } catch { return null } @@ -142,13 +155,14 @@ async function selectAiUpsellCandidate(params: { const system = [ "You select one ecommerce upsell item, or no item.", "Use only the provided catalog candidates.", - "Return JSON only: {\"productId\":\"...\"|null,\"relationshipType\":\"complementary_accessory|care_item|consumable|bundle_addon|used_together|substitute|alternative|same_category|unrelated|uncertain\",\"confidence\":0-1,\"reason\":\"...\"}.", + "Return JSON only: {\"productId\":\"...\"|null,\"relationshipType\":\"complementary_accessory|care_item|consumable|bundle_addon|used_together|substitute|alternative|same_category|unrelated|uncertain\",\"confidence\":0-1,\"catalogEvidence\":\"exact text copied from the chosen candidate title/type/details, or empty string\",\"reason\":\"...\"}.", "A valid upsell must be something the customer would reasonably buy in addition to the base product and use together with it.", "Choose only these valid relationship types: complementary_accessory, care_item, consumable, bundle_addon, used_together.", + "The chosen candidate's own title/type/details must explicitly support the add-on relationship. Do not use general shopping knowledge or assumptions.", "Do not choose substitutes, alternatives, or same-category replacements, even if they share words, audience, color, brand, season, style, or price.", - "Invalid examples: sandals -> flip flops, slippers -> shoes, rain boots -> formal shoes, one backpack -> another backpack.", - "Valid examples: shoes -> socks, shoes -> laces, leather shoes -> polish, phone -> case, backpack -> lunch box.", - "If there is no clear add-on with confidence at least 0.75, return productId null with relationshipType uncertain.", + "Invalid examples: sandals -> flip flops, slippers -> shoes, rain boots -> formal shoes, one backpack -> another backpack, ballet flat shoes -> socks when the candidate only says socks.", + "Only choose socks/laces/care products when the candidate catalog text explicitly says they match, fit, complement, are for, or are bundled with the base product or its exact use case.", + "If there is no explicit catalog-backed add-on with confidence at least 0.90, return productId null with relationshipType uncertain and empty catalogEvidence.", ].join(" ") const user = JSON.stringify({