// ───────────────────────────────────────────────────────────────────────────── // SalesLab AI — v3.6 (Phase 1.5) // Updated: 2026-03-14 // Changes: // - NEW: Sales IQ Assessment — daily 5-question AI-powered sales scenario test // - Sales IQ: 3-step role selector (9 industries × 6+ roles each, including Kitchen & Bath) // - Sales IQ: Claude generates role-specific scenarios tailored to industry + role // - Sales IQ: Scores 5 skill categories: Discovery, Objection Handling, Closing, Prospecting, Negotiation // - Sales IQ: Free users see score + tier + leaderboard rank; Pro users get full skill breakdown // - Sales IQ: Viral loop — pre-written LinkedIn post, challenge link, share nudge on dashboard // - Sales IQ: 7-day streak tracker, seeded leaderboard, daily reset // - Sales IQ: Post-test share card with all skill scores and coaching pills // - Sales IQ: Plugged into dashboard home below leaderboard teaser (Pro users only shown full card) // - Sales IQ: showSalesIQ state added to main app alongside showGame, showLeaderboard, etc. // - Dashboard: Sales IQ card added to home screen with "Take today's test →" CTA // - Sales IQ sidebar entry added to PRO_SIDEBAR // ───────────────────────────────────────────────────────────────────────────── const { useState, useRef, useEffect } = React; // ─── Score Color System ─────────────────────────────────────────────────────── // 1–59 red · 60–74 orange · 75–91 yellow · 92–100 green function scoreColor(score) { if (score >= 92) return { text: "#16a34a", bar: "#22c55e", bg: "#f0fdf4", border: "#bbf7d0", label: "Elite" }; if (score >= 75) return { text: "#b45309", bar: "#f59e0b", bg: "#fffbeb", border: "#fde68a", label: "Strong" }; if (score >= 60) return { text: "#c2410c", bar: "#f97316", bg: "#fff7ed", border: "#fed7aa", label: "Developing" }; return { text: "#dc2626", bar: "#ef4444", bg: "#fef2f2", border: "#fecaca", label: "Needs Work" }; } // ─── Shuffle Array (Fisher-Yates) ───────────────────────────────────────────── function shuffleArray(arr) { const a = [...arr]; for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } // MUST be defined first — other constants reference BRAND_CONFIG.appUrl below. // To white label for a client: // 1. Duplicate this file // 2. Update the values below with the client's brand // 3. Set their own SUPABASE_URL + SUPABASE_ANON_KEY (new Supabase project) // 4. Set their Stripe links or remove billing and invoice them directly // 5. Deploy to their subdomain e.g. app.clientname.com const BRAND_CONFIG = { appName: "SalesLab AI", appNameShort: "SalesLab AI", tagline: "TRAIN · TEST · CLOSE", logoPath: "/SalesLab_AI_icon.png", appUrl: "https://thesaleslabai.com", supportEmail: "support@thesaleslabai.com", primaryColor: "#0f172a", primaryColor2: "#1e293b", }; // ─── Config ──────────────────────────────────────────────────────────────────── const SUPABASE_URL = "https://nahextkqnsmluuskjxps.supabase.co"; const SUPABASE_ANON_KEY = "sb_publishable_FhMyQlZK8F52DOfjCPlKmQ_ERuR4-cS"; const STRIPE_PRO_LINK = "https://www.paypal.com/webapps/billing/plans/subscribe?plan_id=P-5Y1401853D764390CNG4Z6GQ"; const STRIPE_TEAM_LINK = "https://www.paypal.com/webapps/billing/plans/subscribe?plan_id=P-2BB96861PG443164MNG4Z7SA"; const STRIPE_TOPUP_LINK = "https://buy.stripe.com/cNi3cueU35D4dnS8nZcIE06"; const APP_URL = BRAND_CONFIG.appUrl; // Payment link helper. // PayPal links get custom_id=userId so the webhook knows which user to activate. // Stripe topup links get client_reference_id=userId for the same reason. function stripeLink(baseLink, userId) { if (!baseLink.startsWith("https://")) return baseLink; if (baseLink.includes("paypal.com")) { if (!userId) return baseLink; try { const url = new URL(baseLink); url.searchParams.set("custom_id", userId); return url.toString(); } catch { return baseLink; } } if (!userId) return baseLink; try { const url = new URL(baseLink); url.searchParams.set("client_reference_id", userId); return url.toString(); } catch { return baseLink; } } // ─── Usage Limits ───────────────────────────────────────────────────────────── const FREE_MAX_PROMPTS_PER_DAY = 5; const PRO_MAX_TOKENS_PER_MONTH = 300000; const PRO_TOKEN_WARNING_THRESHOLD = 10000; const TOKEN_TOPUP_AMOUNT = 300000; // ─── Supabase Client ────────────────────────────────────────────────────────── const supabase = (() => { const headers = { "apikey": SUPABASE_ANON_KEY, "Authorization": `Bearer ${SUPABASE_ANON_KEY}`, "Content-Type": "application/json", }; const auth = { signInWithGoogle: async () => { const iqParam = new URLSearchParams(window.location.search).get("iq"); const redirectBase = `${APP_URL}/app`; const redirectTo = iqParam ? `${redirectBase}?iq=${iqParam}` : redirectBase; const url = `${SUPABASE_URL}/auth/v1/authorize?provider=google&redirect_to=${encodeURIComponent(redirectTo)}`; window.location.href = url; }, signInWithOtp: async (email) => { const iqParam = new URLSearchParams(window.location.search).get("iq"); const redirectTo = iqParam ? `${APP_URL}/app?iq=${iqParam}` : `${APP_URL}/app`; const res = await fetch(`${SUPABASE_URL}/auth/v1/otp`, { method: "POST", headers, body: JSON.stringify({ email, create_user: true, options: { emailRedirectTo: redirectTo } }), }); return res.ok; }, getSession: () => { try { const raw = localStorage.getItem(`sb-${SUPABASE_URL.split("//")[1].split(".")[0]}-auth-token`); if (!raw) return null; const parsed = JSON.parse(raw); if (parsed.expires_at && Date.now() / 1000 > parsed.expires_at) return null; return parsed; } catch { return null; } }, handleCallback: () => { const hash = window.location.hash; if (!hash.includes("access_token")) return null; const params = new URLSearchParams(hash.replace("#", "")); const session = { access_token: params.get("access_token"), refresh_token: params.get("refresh_token"), expires_at: Date.now() / 1000 + parseInt(params.get("expires_in") || "3600"), user: { id: params.get("user_id") || "" }, }; localStorage.setItem( `sb-${SUPABASE_URL.split("//")[1].split(".")[0]}-auth-token`, JSON.stringify(session) ); window.history.replaceState({}, document.title, window.location.pathname); return session; }, signOut: async () => { const session = auth.getSession(); if (session?.access_token) { await fetch(`${SUPABASE_URL}/auth/v1/logout`, { method: "POST", headers: { ...headers, "Authorization": `Bearer ${session.access_token}` }, }).catch(() => {}); } localStorage.removeItem(`sb-${SUPABASE_URL.split("//")[1].split(".")[0]}-auth-token`); }, getUser: async (accessToken) => { const res = await fetch(`${SUPABASE_URL}/auth/v1/user`, { headers: { ...headers, "Authorization": `Bearer ${accessToken}` }, }); if (!res.ok) return null; return res.json(); }, }; const db = { getUserTier: async (userId, accessToken) => { try { // Step 1: find an active subscription for this user const subRes = await fetch( `${SUPABASE_URL}/rest/v1/subscriptions?user_id=eq.${userId}&status=eq.active&select=plan_id&limit=1`, { headers: { ...headers, "Authorization": `Bearer ${accessToken}` } } ); if (subRes.ok) { const subs = await subRes.json(); if (subs.length && subs[0].plan_id) { // Step 2: resolve plan_id → plan name from plans table const planRes = await fetch( `${SUPABASE_URL}/rest/v1/plans?id=eq.${subs[0].plan_id}&select=name&limit=1`, { headers: { ...headers, "Authorization": `Bearer ${accessToken}` } } ); if (planRes.ok) { const plans = await planRes.json(); const name = (plans[0]?.name || "").toLowerCase(); if (name === "pro" || name === "team") return name; } } } // Step 3: fallback — check profiles.plan_id directly const profileRes = await fetch( `${SUPABASE_URL}/rest/v1/profiles?id=eq.${userId}&select=plan_id,status&limit=1`, { headers: { ...headers, "Authorization": `Bearer ${accessToken}` } } ); if (profileRes.ok) { const profiles = await profileRes.json(); const profile = profiles[0]; if (profile?.plan_id && profile?.status === "active") { const planRes2 = await fetch( `${SUPABASE_URL}/rest/v1/plans?id=eq.${profile.plan_id}&select=name&limit=1`, { headers: { ...headers, "Authorization": `Bearer ${accessToken}` } } ); if (planRes2.ok) { const plans2 = await planRes2.json(); const name2 = (plans2[0]?.name || "").toLowerCase(); if (name2 === "pro" || name2 === "team") return name2; } } } } catch (e) { console.warn("[getUserTier] Error:", e); } return "free"; }, upsertUser: async (userId, email, accessToken) => { // Upsert into profiles (not users — that table doesn't exist) await fetch(`${SUPABASE_URL}/rest/v1/profiles`, { method: "POST", headers: { ...headers, "Authorization": `Bearer ${accessToken}`, "Prefer": "resolution=merge-duplicates", }, body: JSON.stringify({ id: userId, email }), }).catch(() => {}); }, }; return { auth, db }; })(); // ─── Supabase REST helper (replaces supabase.from() calls) ──────────────────── const sbDB = (() => { const BASE = SUPABASE_URL + "/rest/v1"; const getToken = () => { try { const key = Object.keys(localStorage).find(k => k.startsWith("sb-") && k.endsWith("-auth-token")); if (key) { const session = JSON.parse(localStorage.getItem(key)); if (session?.access_token) return session.access_token; } } catch (e) {} return SUPABASE_ANON_KEY; }; const h = () => ({ "apikey": SUPABASE_ANON_KEY, "Authorization": `Bearer ${getToken()}`, "Content-Type": "application/json", "Prefer": "return=representation", }); const buildUrl = (table, filters = {}, options = {}) => { let url = `${BASE}/${table}?`; const params = []; Object.entries(filters).forEach(([k, v]) => { if (typeof v === "object" && v !== null) { Object.entries(v).forEach(([op, val]) => params.push(`${k}=${op}.${encodeURIComponent(val)}`)); } else { params.push(`${k}=eq.${encodeURIComponent(v)}`); } }); if (options.select) params.push(`select=${options.select}`); if (options.order) params.push(`order=${options.order}`); if (options.limit) params.push(`limit=${options.limit}`); return url + params.join("&"); }; return { select: async (table, filters = {}, options = {}) => { const url = buildUrl(table, filters, options); const res = await fetch(url, { headers: h() }); if (!res.ok) return { data: null, error: await res.text() }; const data = await res.json(); if (options.single) return { data: data[0] || null, error: null }; return { data, error: null }; }, insert: async (table, body) => { const res = await fetch(`${BASE}/${table}`, { method: "POST", headers: h(), body: JSON.stringify(body), }); const data = await res.json().catch(() => null); return { data, error: res.ok ? null : data }; }, update: async (table, body, filters = {}) => { const url = buildUrl(table, filters); const res = await fetch(url, { method: "PATCH", headers: h(), body: JSON.stringify(body), }); return { error: res.ok ? null : await res.text() }; }, upsert: async (table, body, options = {}) => { const headers = { ...h(), "Prefer": `resolution=${options.onConflict ? "merge-duplicates" : "merge-duplicates"},return=representation` }; const res = await fetch(`${BASE}/${table}`, { method: "POST", headers, body: JSON.stringify(body), }); return { error: res.ok ? null : await res.text() }; }, delete: async (table, filters = {}) => { const url = buildUrl(table, filters); const res = await fetch(url, { method: "DELETE", headers: h() }); return { error: res.ok ? null : await res.text() }; }, rpc: async (fn, body = {}) => { const res = await fetch(`${BASE}/rpc/${fn}`, { method: "POST", headers: h(), body: JSON.stringify(body), }); const data = await res.json().catch(() => null); return { data, error: res.ok ? null : (data || "RPC request failed") }; }, }; })(); // ─── Usage Tracking Helpers ─────────────────────────────────────────────────── const usageDB = { // Get or initialize usage record for a user getUsage: async (userId) => { const { data } = await sbDB.select("user_usage", { user_id: userId }, { single: true }); return data; }, // Initialize usage record for new user initUsage: async (userId) => { const now = new Date().toISOString(); const dayStart = getDayStart(); const monthStart = getMonthStart(); await sbDB.upsert("user_usage", { user_id: userId, day_start: dayStart, prompts_today: 0, month_start: monthStart, tokens_used_this_month: 0, topup_tokens_remaining: 0, topup_expires_at: null, updated_at: now, }, { onConflict: "user_id" }); }, // Increment daily prompt count (free users) incrementPrompt: async (userId) => { const usage = await usageDB.getUsage(userId); if (!usage) { await usageDB.initUsage(userId); return; } const dayStart = getDayStart(); const count = usage.day_start === dayStart ? (usage.prompts_today || 0) + 1 : 1; await sbDB.update("user_usage", { day_start: dayStart, prompts_today: count, updated_at: new Date().toISOString(), }, { user_id: userId }); }, // Add token usage (pro/team users) addTokens: async (userId, tokensUsed) => { const usage = await usageDB.getUsage(userId); if (!usage) { await usageDB.initUsage(userId); return; } const monthStart = getMonthStart(); // Reset if new month const base = usage.month_start === monthStart ? (usage.tokens_used_this_month || 0) : 0; // Deduct from topup first if active let topupRemaining = usage.topup_tokens_remaining || 0; const topupExpired = !usage.topup_expires_at || new Date() > new Date(usage.topup_expires_at); if (topupExpired) topupRemaining = 0; await sbDB.update("user_usage", { month_start: monthStart, tokens_used_this_month: base + tokensUsed, topup_tokens_remaining: Math.max(0, topupRemaining - (topupExpired ? 0 : tokensUsed)), updated_at: new Date().toISOString(), }, { user_id: userId }); }, // Apply token top-up after payment applyTopup: async (userId) => { const expires = new Date(); expires.setDate(expires.getDate() + 30); await sbDB.update("user_usage", { topup_tokens_remaining: TOKEN_TOPUP_AMOUNT, topup_expires_at: expires.toISOString(), updated_at: new Date().toISOString(), }, { user_id: userId }); }, }; function getWeekStart() { const d = new Date(); d.setHours(0, 0, 0, 0); d.setDate(d.getDate() - d.getDay()); // Sunday return d.toISOString().split("T")[0]; } function getMonthStart() { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-01`; } function getNextMonthReset() { const d = new Date(); const next = new Date(d.getFullYear(), d.getMonth() + 1, 1); return next.toLocaleString("en-US", { month: "long", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "short" }); } function getNextWeekReset() { const d = new Date(); const daysUntilSunday = 7 - d.getDay(); const next = new Date(d); next.setDate(d.getDate() + daysUntilSunday); next.setHours(0, 0, 0, 0); return next.toLocaleString("en-US", { weekday: "long", month: "long", day: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "short" }); } function getDayStart() { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`; } function getNextDayReset() { const next = new Date(); next.setDate(next.getDate() + 1); next.setHours(0, 0, 0, 0); return next.toLocaleString("en-US", { weekday: "long", month: "long", day: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "short" }); } function estimateTokens(text) { return Math.ceil((text || "").length / 4); } // ─── Free Session Limit Popup ───────────────────────────────────────────────── function ModuleCopyButton({ text }) { const [copied, setCopied] = React.useState(false); return ( ); } function FreeSessionLimitModal({ onUpgrade, onClose, userId }) { const resetTime = getNextDayReset(); return (
{match[5]});
last = match.index + match[0].length;
}
if (last < text.length) parts.push({text.slice(last)});
return parts.length ? parts : text;
}
function Message({ msg }) {
const isUser = msg.role === "user";
return (
| REP | {skills.map(s =>{s} | )}
|---|---|
| {rep.name} | {rep.scores.map((sc, si) => { const c = scoreColor(sc); return (
{sc}
|
);
})}
| TEAM AVG | {[0,1,2,3,4].map(si => { const avg = Math.round(demoData.reduce((a,r) => a + r.scores[si], 0) / demoData.length); const c = scoreColor(avg); return (
{avg}
|
);
})}
{BRAND_CONFIG.appName} was created out of frustration with sales training that doesn't work — generic scripts, outdated playbooks, and coaches who've never sat across from a real objection.
Give every sales rep — whether they're selling kitchen remodels in living rooms or SaaS deals over Zoom — the same elite coaching that was previously only available to reps at the largest companies in the world.
Join 2,400+ reps already using {BRAND_CONFIG.appName} to close more deals.
Everything you need to know about {BRAND_CONFIG.appName}.
Last updated: {updated}
SalesLab AI ("we," "our," or "us") is committed to protecting your privacy. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you visit thesaleslabai.com and use our AI-powered sales training platform.
We do not sell your personal information. We may share data with trusted third parties solely to operate our service, including:
We use cookies and similar tracking technologies to enhance your experience, analyze usage, and support authentication. You can control cookies through your browser settings, though disabling them may affect functionality.
We retain your personal information for as long as your account is active or as needed to provide services. You may request deletion of your data at any time by contacting support@thesaleslabai.com.
We implement industry-standard security measures including SSL encryption, secure data storage, and access controls. However, no method of transmission over the internet is 100% secure.
SalesLab AI is not directed to individuals under the age of 16. We do not knowingly collect personal information from children.
Depending on your location, you may have rights including access, correction, deletion, and data portability. Contact us at support@thesaleslabai.com to exercise these rights.
We may update this Privacy Policy from time to time. Continued use of the platform after changes constitutes acceptance of the revised policy.
By accessing or using SalesLab AI ("Service"), you agree to be bound by these Terms of Service ("Terms"). If you do not agree, please do not use our Service. These Terms apply to all visitors, users, and others who access the Service.
SalesLab AI provides an AI-powered sales training platform including roleplay simulations, objection drills, close intelligence scoring, voice input training, and team dashboards. Features may change over time at our discretion.
You must be at least 16 years old to use this Service. By agreeing to these Terms, you represent that you are of legal age to form a binding contract.
To access certain features, you must create an account. You agree to:
Some features require a paid subscription. By subscribing, you agree to pay all applicable fees. Subscriptions automatically renew unless cancelled before the renewal date. All payments are processed securely through Stripe.
We reserve the right to modify pricing with reasonable notice. Continued use after a price change constitutes acceptance of the new pricing.
You agree not to:
All content, features, and functionality of SalesLab AI, including AI models, training methodologies, UI design, and branding, are owned by SalesLab AI and protected by applicable intellectual property laws. You may not reproduce, distribute, or create derivative works without our express written permission.
By using the Service, you grant SalesLab AI a non-exclusive, royalty-free license to use your anonymized training data to improve our AI models and platform. We will not share personally identifiable content with third parties without your consent.
The Service is provided "as is" without warranties of any kind. SalesLab AI does not guarantee specific sales results or outcomes from using the platform. AI-generated feedback is for training purposes only.
To the maximum extent permitted by law, SalesLab AI shall not be liable for any indirect, incidental, special, or consequential damages arising from your use of the Service, even if we have been advised of the possibility of such damages.
We reserve the right to suspend or terminate your account at our discretion if you violate these Terms. Upon termination, your right to use the Service will immediately cease.
These Terms are governed by the laws of the United States. Any disputes shall be resolved through binding arbitration in accordance with applicable law.
We may update these Terms at any time. We will provide notice of significant changes. Continued use of the Service after changes take effect constitutes your acceptance.
SalesLab AI offers subscription-based access to our AI sales training platform. This policy outlines when and how refunds are issued for charges made through our platform via Stripe.
We offer a free tier so you can explore SalesLab AI before committing to a paid subscription. We encourage all users to try the free plan before upgrading.
Monthly Subscriptions: You may request a full refund within 7 days of your initial purchase if you are not satisfied. After 7 days, no refunds will be issued for the current billing period, but you may cancel to prevent future charges.
Annual Subscriptions: You may request a full refund within 14 days of your initial annual purchase. After 14 days, we may issue a prorated refund at our discretion based on usage.
Renewal Charges: If you are charged for a renewal you did not intend, please contact us within 48 hours of the charge and we will review your request promptly.
To request a refund, follow these steps:
If you initiate a chargeback through your bank without first contacting us, we reserve the right to suspend your account pending investigation. We always encourage you to reach out directly. We want to resolve issues quickly and fairly.
You may cancel your subscription at any time from your account settings. Cancellation stops future billing but does not automatically trigger a refund. Your access will remain active through the end of the current billing period.
We're launching an affiliate program for sales professionals, coaches, and creators who want to earn recurring commissions by sharing a tool they actually believe in.
SalesLab AI helps reps practice objections, discovery, and closing in realistic AI conversations. Managers get a clear view of readiness, risk, and where coaching is needed most.