1/// <reference path="references.d.ts" /> 2var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 return new (P || (P = Promise))(function (resolve, reject) { 5 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 }); 10}; 11var __generator = (this && this.__generator) || function (thisArg, body) { 12 var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 function verb(n) { return function (v) { return step([n, v]); }; } 15 function step(op) { 16 if (f) throw new TypeError("Generator is already executing."); 17 while (g && (g = 0, op[0] && (_ = 0)), _) try { 18 if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 if (y = 0, t) op = [op[0] & 2, t.value]; 20 switch (op[0]) { 21 case 0: case 1: t = op; break; 22 case 4: _.label++; return { value: op[1], done: false }; 23 case 5: _.label++; y = op[1]; op = [0]; continue; 24 case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 default: 26 if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 if (t[2]) _.ops.pop(); 31 _.trys.pop(); continue; 32 } 33 op = body.call(thisArg, _); 34 } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 } 37}; 38import { apiEndpoint, getTimezoneOffset, httpPostJsonAsync, httpSendAsync, makeCode } from './LLMSallyUtils.js'; 39/** False on localhost / loopback so GTM, Clarity, and Meta pixel stay off during local dev (see sally-index.html). */ 40export function allowSallyMarketingScripts() { 41 if (typeof window === 'undefined' || !window.location) { 42 return true; 43 } 44 var h = window.location.hostname.toLowerCase(); 45 return h !== 'localhost' && h !== '127.0.0.1' && !h.endsWith('.localhost'); 46} 47var LLMLandingPage = /** @class */ (function () { 48 function LLMLandingPage(params) { 49 var _this = this; 50 this.ONBOARDING_QUESTIONS = ko.observableArray([ 51 { 52 type: 'multipleChoice', 53 name: 'gender', 54 text: "Select your gender", 55 answers: ko.observableArray([ 56 { text: ko.observable('Male'), isSelected: ko.observable(false) }, 57 { text: ko.observable('Female'), isSelected: ko.observable(false) } 58 ]) 59 }, 60 { 61 type: 'numeric', 62 name: 'YOB', 63 text: "Enter your year of birth", 64 answers: ko.observableArray([ 65 { text: ko.observable(null), placeholder: ko.observable('e.g. 1989'), submission: ko.observable(null) }, 66 ]) 67 }, 68 { 69 type: 'numeric', 70 name: 'height', 71 text: "Enter your height", 72 answers: ko.observableArray([ 73 { text: ko.observable('\''), placeholder: ko.observable('feet'), submission: ko.observable(null) }, 74 { text: ko.observable('"'), placeholder: ko.observable('inches'), submission: ko.observable(null) } 75 ]) 76 }, 77 { 78 type: 'numeric', 79 name: 'weight', 80 text: "Enter your weight", 81 answers: ko.observableArray([ 82 { text: ko.observable('lbs'), placeholder: ko.observable('Your weight'), submission: ko.observable(null) }, 83 ]) 84 }, 85 { 86 type: 'numeric', 87 name: 'baseCalBurn', 88 text: "How many calories per day do you think you burn, without exercise?", 89 isConditional: true, 90 answers: ko.observableArray([ 91 { text: ko.observable('calories/day'), placeholder: ko.observable('Base calories burned'), submission: ko.observable(null) }, 92 ]) 93 }, 94 { 95 type: 'multipleChoice', 96 name: 'motivation', 97 text: "People are interested in using our services for lots of different reasons. Please select all that apply to you: ", 98 answers: ko.observableArray([ 99 { text: ko.observable('Improving mood / energy throughout the day'), isSelected: ko.observable(false) },
100 { text: ko.observable('Living longer'), isSelected: ko.observable(false) }, 101 { text: ko.observable('Losing weight'), isSelected: ko.observable(false) }, 102 { text: ko.observable('Gaining weight'), isSelected: ko.observable(false) }, 103 { text: ko.observable('Diabetes prevention / support'), isSelected: ko.observable(false) }, 104 { text: ko.observable('Maternal health (prenatal / postnatal)'), isSelected: ko.observable(false) }, 105 { text: ko.observable('Staying healthy on GLP-1s (e.g. Ozempic and related medications)'), isSelected: ko.observable(false) }, 106 { text: ko.observable('Improving athletic performance'), isSelected: ko.observable(false) }, 107 { text: ko.observable('None of the above'), isSelected: ko.observable(false) }, 108 ]) 109 }, 110 { 111 type: 'numeric', 112 name: 'weightGoal', 113 text: ko.observable("How much weight would you like to lose?"), 114 isConditional: true, 115 answers: ko.observableArray([ 116 { text: ko.observable('lbs'), placeholder: ko.observable('Weight goal'), submission: ko.observable(null) }, 117 ]) 118 }, 119 { 120 type: 'numeric', 121 name: 'baseCalOffset', 122 text: ko.observable(""), 123 isConditional: true, 124 isCalorieOffset: true, 125 answers: ko.observableArray([ 126 { text: ko.observable('calories/day'), placeholder: ko.observable('Calorie offset'), submission: ko.observable('200') }, 127 ]), 128 daysLabel: ko.observable("") 129 }, 130 { 131 type: 'openResponse', 132 name: 'eatingHabits', 133 text: "Let's start with what a typical day looks like for you in terms of your food consumption. Describe your go-to breakfasts, lunches and dinners below -- plus any other eating habits that characterize a typical day (snacks, etc). We will generate an initial report on your likely nutritional deficits and excessess based on your answer to this question.", 134 openResponse: ko.observable('') 135 }, 136 { 137 type: 'openResponse', 138 name: 'anythingElse', 139 text: "Anything else you think we should know?", 140 openResponse: ko.observable('') 141 } 142 ]); 143 this.phoneNumber = ko.observable(null); 144 this.showOnboarding = ko.observable(false); 145 this.showOnboardingFlow = ko.observable(false); 146 this.onboardingStep = ko.observable(null); // 'verify', 'questions', 'complete' 147 this.codeNumbers = ko.observableArray([ 148 { number: ko.observable(null) }, 149 { number: ko.observable(null) }, 150 { number: ko.observable(null) }, 151 { number: ko.observable(null) } 152 ]); 153 this.phoneVerificationError = ko.observable(null); 154 this.phoneNumberError = ko.observable(null); 155 this.currentQuestionIndex = ko.observable(0); 156 this.calculatedBMR = ko.observable(null); 157 this.showBMRConfirmation = ko.observable(false); 158 this.existingUserTempUserId = ko.observable(null); 159 this.learnMoreSectionHash = ko.pureComputed(function () { return '#why-sally'; }); 160 this.scrollToLearnMoreSection = function (_data, event) { 161 event.preventDefault(); 162 var id = 'why-sally'; 163 var el = document.getElementById(id); 164 if (el) { 165 el.scrollIntoView({ behavior: 'smooth', block: 'start' }); 166 } 167 var _a = window.location, pathname = _a.pathname, search = _a.search; 168 window.history.replaceState(null, '', "".concat(pathname).concat(search, "#").concat(id)); 169 }; 170 /** Controls visibility and purpose of the legal (terms/privacy) modal on Landing Page 2. */ 171 this.showLegalModal = ko.observable(false); 172 this.legalModalPurpose = ko.observable('terms'); 173 /** Mood-energy campaign: nutrient info modal (Landing Page 2). */ 174 this.showNutrientModal = ko.observable(false); 175 this.moodNutrients = ko.observableArray([ 176 'Iron', 'B12', 'Folate', 'Vitamin D', 'Magnesium', 'Zinc' 177 ]); 178 this.selectedNutrientIndex = ko.observable(0); 179 this.currentMoodNutrientLabel = ko.computed(function () { 180 var list = _this.moodNutrients(); 181 var i = _this.selectedNutrientIndex(); 182 if (i < 0 || i >= list.length) { 183 return ''; 184 } 185 return list[i]; 186 }); 187 this.canGoToPreviousMoodNutrient = ko.computed(function () { return _this.selectedNutrientIndex() > 0; }); 188 this.canGoToNextMoodNutrient = ko.computed(function () { 189 var list = _this.moodNutrients(); 190 return _this.selectedNutrientIndex() < list.length - 1; 191 }); 192 this.goToPreviousMoodNutrient = function () { 193 var i = _this.selectedNutrientIndex(); 194 if (i > 0) { 195 _this.selectedNutrientIndex(i - 1); 196 } 197 }; 198 this.goToNextMoodNutrient = function () { 199 var list = _this.moodNutrients(); 200 var i = _this.selectedNutrientIndex(); 201 if (i < list.length - 1) { 202 _this.selectedNutrientIndex(i + 1); 203 } 204 }; 205 // Computed observable that filters questions based on conditional logic 206 this.visibleQuestions = ko.computed(function () { 207 var allQuestions = _this.ONBOARDING_QUESTIONS(); 208 var visible = []; 209 // Check if all required questions for BMR calculation are answered 210 var genderQuestion = allQuestions.find(function (q) { return q.name === 'gender'; }); 211 var yobQuestion = allQuestions.find(function (q) { return q.name === 'YOB'; }); 212 var heightQuestion = allQuestions.find(function (q) { return q.name === 'height'; }); 213 var weightQuestion = allQuestions.find(function (q) { return q.name === 'weight'; }); 214 var hasGender = genderQuestion && _this.isQuestionAnswered(genderQuestion); 215 var hasYOB = yobQuestion && _this.isQuestionAnswered(yobQuestion); 216 var hasHeight = heightQuestion && _this.isQuestionAnswered(heightQuestion); 217 var hasWeight = weightQuestion && _this.isQuestionAnswered(weightQuestion); 218 var canCalculateBMR = hasGender && hasYOB && hasHeight && hasWeight; 219 var _loop_1 = function (i) { 220 var question = allQuestions[i]; 221 // Check if this is the conditional baseCalBurn question 222 if (question.name === 'baseCalBurn' && question.isConditional) { 223 // Only show if we can't calculate BMR (missing one or more required fields) 224 // AND we haven't calculated BMR yet (no confirmation to show) 225 if (!canCalculateBMR && !_this.showBMRConfirmation()) { 226 visible.push(question); 227 } 228 // If we can calculate BMR, skip this question (we'll show confirmation instead) 229 } 230 // Check if this is the conditional weight goal question 231 else if (question.name === 'weightGoal' && question.isConditional) { 232 // Find the motivation question 233 var motivationQuestion = allQuestions.find(function (q) { return q.name === 'motivation'; }); 234 if (motivationQuestion) { 235 var selectedAnswers = motivationQuestion.answers().filter(function (a) { return a.isSelected && a.isSelected(); }); 236 var hasLoseWeight = selectedAnswers.some(function (a) { return a.text && a.text() === 'Losing weight'; }); 237 var hasGainWeight = selectedAnswers.some(function (a) { return a.text && a.text() === 'Gaining weight'; }); 238 if (hasLoseWeight || hasGainWeight) { 239 // Update the question text based on selection 240 var textObservable = question.text; 241 if (hasLoseWeight && hasGainWeight) { 242 textObservable("How much weight would you like to lose or gain?"); 243 } 244 else if (hasLoseWeight) { 245 textObservable("How much weight would you like to lose?"); 246 } 247 else if (hasGainWeight) { 248 textObservable("How much weight would you like to gain?"); 249 } 250 visible.push(question); 251 } 252 // If neither is selected, skip this question 253 } 254 } 255 // Check if this is the conditional baseCalOffset question 256 else if (question.name === 'baseCalOffset' && question.isConditional) { 257 // Find the motivation question 258 var motivationQuestion = allQuestions.find(function (q) { return q.name === 'motivation'; }); 259 if (motivationQuestion) { 260 var selectedAnswers = motivationQuestion.answers().filter(function (a) { return a.isSelected && a.isSelected(); }); 261 var hasLoseWeight = selectedAnswers.some(function (a) { return a.text && a.text() === 'Losing weight'; }); 262 var hasGainWeight = selectedAnswers.some(function (a) { return a.text && a.text() === 'Gaining weight'; }); 263 if (hasLoseWeight || hasGainWeight) { 264 // Update the question text and days label based on selection 265 var textObservable = question.text; 266 var daysLabelObservable_1 = question.daysLabel; 267 var firstAnswer = question.answers()[0]; 268 var answerSubmission_1 = firstAnswer === null || firstAnswer === void 0 ? void 0 : firstAnswer.submission; 269 // Calculate days to lose/gain 1 pound (3500 calories = 1 lb) 270 var calculateDays_1 = function () { 271 var calOffset = answerSubmission_1 && answerSubmission_1() ? parseFloat(answerSubmission_1()) : 200; 272 if (calOffset && calOffset > 0) { 273 var days = Math.round(3500 / calOffset * 10) / 10; // Round to 1 decimal place 274 return days; 275 } 276 return 0; 277 }; 278 // Update text and label based on goal 279 if (hasLoseWeight && !hasGainWeight) { 280 textObservable("We suggest consuming 200 calories less per day than you burn. You can adjust this number:"); 281 // Subscribe to changes in the submission to update the label 282 if (answerSubmission_1) { 283 var updateLabel = function () { 284 var days = calculateDays_1(); 285 daysLabelObservable_1("This would mean losing 1 pound every ".concat(days, " days.")); 286 }; 287 // Initial update 288 updateLabel(); 289 // Subscribe to changes 290 if (!answerSubmission_1._calOffsetSubscribed) { 291 answerSubmission_1.subscribe(updateLabel); 292 answerSubmission_1._calOffsetSubscribed = true; 293 } 294 } 295 } 296 else if (hasGainWeight && !hasLoseWeight) { 297 textObservable("We suggest consuming 200 calories more per day than you burn. You can adjust this number:"); 298 // Subscribe to changes in the submission to update the label 299 if (answerSubmission_1) { 300 var updateLabel = function () { 301 var days = calculateDays_1(); 302 daysLabelObservable_1("This would mean gaining 1 pound every ".concat(days, " days.")); 303 }; 304 // Initial update 305 updateLabel(); 306 // Subscribe to changes 307 if (!answerSubmission_1._calOffsetSubscribed) { 308 answerSubmission_1.subscribe(updateLabel); 309 answerSubmission_1._calOffsetSubscribed = true; 310 } 311 } 312 } 313 else if (hasLoseWeight && hasGainWeight) { 314 // Both selected - default to lose weight messaging 315 textObservable("We suggest consuming 200 calories less per day than you burn. You can adjust this number:"); 316 if (answerSubmission_1) { 317 var updateLabel = function () { 318 var days = calculateDays_1(); 319 daysLabelObservable_1("This would mean losing 1 pound every ".concat(days, " days.")); 320 }; 321 updateLabel(); 322 if (!answerSubmission_1._calOffsetSubscribed) { 323 answerSubmission_1.subscribe(updateLabel); 324 answerSubmission_1._calOffsetSubscribed = true; 325 } 326 } 327 } 328 visible.push(question); 329 } 330 // If neither is selected, skip this question 331 } 332 } 333 else { 334 // Regular question, always include 335 visible.push(question); 336 } 337 }; 338 for (var i = 0; i < allQuestions.length; i++) { 339 _loop_1(i); 340 } 341 // If we calculated BMR and should show confirmation, add it as the last question 342 if (_this.showBMRConfirmation() && _this.calculatedBMR() !== null) { 343 var bmrValue = _this.calculatedBMR(); 344 // Create a confirmation question with the calculated value pre-filled 345 var bmrConfirmationQuestion = { 346 type: 'numeric', 347 name: 'baseCalBurn', 348 text: "Based on your height, weight, age, and gender, we estimate that you burn ".concat(bmrValue, " calories per day <i>without exercise</i>. Please confirm this value or adjust it if needed:"), 349 isBMRConfirmation: true, 350 answers: ko.observableArray([ 351 { 352 text: ko.observable('calories/day'), 353 placeholder: ko.observable('Base calories burned'), 354 submission: ko.observable(bmrValue.toString()) // Pre-fill with calculated value 355 }, 356 ]) 357 }; 358 visible.push(bmrConfirmationQuestion); 359 } 360 return visible; 361 }); 362 this.currentQuestion = ko.computed(function () { 363 var index = _this.currentQuestionIndex(); 364 var questions = _this.visibleQuestions(); 365 if (index >= 0 && index < questions.length) { 366 return questions[inde
366x]; 367 } 368 return null; 369 }); 370 this.selectMCAnswerOption = function (question, answerOption) { 371 answerOption.isSelected(!answerOption.isSelected()); 372 if (!question || !question.text) { 373 console.log("question: " + question); 374 return; 375 } 376 if (question.text.indexOf('all that apply') < 0) { 377 question.answers().forEach(function (option) { 378 if (option.text() !== answerOption.text()) { 379 option.isSelected(false); 380 } 381 }); 382 } 383 }; 384 this.registerPhoneNumber = function () { 385 var phoneNumber = _this.phoneNumber(); 386 // Clear previous error 387 _this.phoneNumberError(null); 388 if (!phoneNumber || phoneNumber.trim() === '') { 389 _this.phoneNumberError('Please enter a phone number'); 390 return; 391 } 392 if (!_this.isValidPhoneNumber(phoneNumber)) { 393 _this.phoneNumberError('Please enter a valid US phone number'); 394 return; 395 } 396 var cleanedPhone = _this.normalizeUSPhoneToE164(phoneNumber); 397 var userId = _this.tempUserId(); 398 var data = { 399 phoneNumber: cleanedPhone 400 }; 401 _this.phoneNumber(cleanedPhone); 402 _this.phoneVerificationError(null); 403 var self = _this; 404 httpSendAsync(apiEndpoint + "/api/registerPhoneNumberLLM/".concat(userId), JSON.stringify(data), function (response) { 405 response = JSON.parse(response); 406 if (response.err) { 407 console.log("response.err: " + response.err); 408 alert("Oops! Please make sure you\'ve entered a valid US phone number. If you have and are still getting this error, contact [email protected] to report this issue."); 409 } 410 else { 411 self.showOnboarding(true); 412 self.onboardingStep('verify'); 413 // Focus first input box 414 setTimeout(function () { 415 var firstInputBox = document.querySelector('.verification-code-input.index0'); 416 if (firstInputBox) { 417 firstInputBox.focus(); 418 } 419 }, 100); 420 var _loop_2 = function (i) { 421 var codeNumber = self.codeNumbers()[i]; 422 // Only allow digits 423 codeNumber.number.subscribe(function (number) { 424 if (number !== null && number !== undefined && number !== '') { 425 // Ensure only digits 426 var numStr = number.toString(); 427 if (!/^\d$/.test(numStr)) { 428 codeNumber.number(numStr.replace(/\D/g, '')); 429 return; 430 } 431 } 432 if (i < self.codeNumbers().length - 1 && (number || number == 0)) { 433 var selector = '.verification-code-input.index' + (i + 1); 434 var nextInputBox = document.querySelector(selector); 435 if (nextInputBox) { 436 nextInputBox.focus(); 437 } 438 } 439 else if (i == self.codeNumbers().length - 1 && (number || number == 0)) { 440 self.verifyPhoneNumber(); 441 } 442 }); 443 }; 444 // Set up auto-advance for code inputs 445 for (var i = 0; i < self.codeNumbers().length; i++) { 446 _loop_2(i); 447 } 448 } 449 }); 450 }; 451 this.handleCodeInput = function (event, index) { 452 // Only allow numbers 453 if (event.key.length === 1 && !/[0-9]/.test(event.key)) { 454 event.preventDefault(); 455 return false;
456 } 457 // Handle backspace - move to previous input if current is empty 458 if (event.key === 'Backspace' && index > 0) { 459 var currentValue = _this.codeNumbers()[index].number(); 460 if (!currentValue || currentValue === '') { 461 var prevInput = document.querySelector('.verification-code-input.index' + (index - 1)); 462 if (prevInput) { 463 prevInput.focus(); 464 prevInput.select(); 465 } 466 } 467 } 468 return true; 469 }; 470 this.handleCodePaste = function (event) { 471 var _a; 472 event.preventDefault(); 473 var pastedData = ((_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.getData('text')) || ''; 474 var digits = pastedData.replace(/\D/g, '').slice(0, 4); 475 for (var i = 0; i < digits.length && i < _this.codeNumbers().length; i++) { 476 _this.codeNumbers()[i].number(digits[i]); 477 } 478 // Focus the next empty input or the last one 479 var nextIndex = Math.min(digits.length, _this.codeNumbers().length - 1); 480 setTimeout(function () { 481 var nextInput = document.querySelector('.verification-code-input.index' + nextIndex); 482 if (nextInput) { 483 nextInput.focus(); 484 } 485 // If all 4 digits are pasted, verify 486 if (digits.length === 4) { 487 setTimeout(function () { return _this.verifyPhoneNumber(); }, 100); 488 } 489 }, 10); 490 return false; 491 }; 492 this.verifyPhoneNumber = function () { 493 var code = ''; 494 for (var i = 0; i < _this.codeNumbers().length; i++) { 495 var codeNumber = _this.codeNumbers()[i]; 496 var num = codeNumber.number(); 497 if (num !== null && num !== undefined && num !== '') { 498 code += num.toString(); 499 } 500 } 501 if (code.length !== 4) { 502 return; 503 } 504 var userId = _this.tempUserId(); 505 var data = { 506 code: code, 507 phoneNumber: _this.phoneNumber(), 508 timezoneOffset: getTimezoneOffset() 509 }; 510 var self = _this; 511 httpSendAsync(apiEndpoint + "/api/verifyPhoneNumberLLM/".concat(userId), JSON.stringify(data), function (response) { 512 response = JSON.parse(response); 513 if (response.err) { 514 self.phoneVerificationError("Oops! Wrong code. Please try again"); 515 for (var i = 0; i < self.codeNumbers().length; i++) { 516 var codeNumber = self.codeNumbers()[i]; 517 codeNumber.number(null); 518 } 519 setTimeout(function () { 520 var firstInputBox = document.querySelector('.verification-code-input.index0'); 521 if (firstInputBox) { 522 firstInputBox.focus(); 523 } 524 }, 100); 525 } 526 else if (response.existingUser) { 527 self.existingUserTempUserId(response.existingUser.tempUserId); 528 self.tempUserId(response.existingUser.tempUserId); 529 self.phoneVerificationError(null); 530 self.onboardingStep('existingUser'); 531 } 532 else { 533 self.phoneVerificationError(null); 534 self.onboardingStep('questions'); 535 self.currentQuestionIndex(0); 536 } 537 }); 538 }; 539 this.finishOnboarding = function () { 540 // TODO: Submit onboarding answers to server 541 _this.showOnboarding(false); 542 // Could redirect or show success message here 543 }; 544 this.navigateToExistingUser = function () { 545 var tempUserId = _this.existingUserTempUserId(); 546 if (tempUserId) { 547 window.location.href = "".concat(window.location.origin, "/app/").concat(tempUserId); 548 } 549 }; 550 this.startOnboarding = function () { 551 _this.closeNutrientModal(); 552 _this.showOnboardingFlow(true); 553 }; 554 /** Close onboarding overlay when the flow has no exitFunction (e.g. Nevermind on phone consent intro from LLMLandingPage.html). */
555 this.dismissOnboardingFlow = function () { 556 _this.showOnboardingFlow(false); 557 }; 558 /** Open the legal modal with a given purpose ('terms' | 'privacy'). */ 559 this.openLegalModal = function (purpose) { 560 _this.legalModalPurpose(purpose); 561 _this.showLegalModal(true); 562 }; 563 /** Close the legal modal. */ 564 this.closeLegalModal = function () { 565 _this.showLegalModal(false); 566 }; 567 this.openNutrientModal = function (nutrientName) { 568 var list = _this.moodNutrients(); 569 var idx = list.indexOf(nutrientName); 570 _this.selectedNutrientIndex(idx >= 0 ? idx : 0); 571 _this.showNutrientModal(true); 572 }; 573 this.closeNutrientModal = function () { 574 _this.showNutrientModal(false); 575 }; 576 /** Close nutrient modal and scroll to pricing (Landing Page 2). */ 577 this.closeNutrientModalAndScrollToPricing = function (_data, event) { 578 event.preventDefault(); 579 _this.closeNutrientModal(); 580 var id = 'landing2-pricing'; 581 window.setTimeout(function () { 582 var el = document.getElementById(id); 583 if (el) { 584 el.scrollIntoView({ behavior: 'smooth', block: 'start' }); 585 } 586 var _a = window.location, pathname = _a.pathname, search = _a.search; 587 window.history.replaceState(null, '', "".concat(pathname).concat(search, "#").concat(id)); 588 }, 0); 589 }; 590 /** Cached question index when user closes onboarding from LLMLandingPage2; used to resume on next open. */ 591 this.cachedOnboardingQuestionIndex = ko.observable(0); 592 /** Called by llm-onboarding-flow when user clicks X (exitFunction). Saves index and closes overlay. */ 593 this.handleOnboardingExit = function (questionIndex) { 594 _this.cachedOnboardingQuestionIndex(questionIndex); 595 _this.showOnboardingFlow(false); 596 }; 597 this.tempUserId = ko.observable(null); 598 /** Randomized contact path for skip-phone-until-end onboarding; persisted in llm_url_params as firstContactMethod. */ 599 this.firstContactMethod = ko.observable(null); 600 /** Assign or load firstContactMethod via saveUrlParamsLLM (server returns existing row if present). */ 601 this.syncFirstContactMethodWithServer = function () { return __awaiter(_this, void 0, void 0, function () { 602 var userId, candidate, payload; 603 var _this = this; 604 return __generator(this, function (_a) { 605 userId = this.tempUserId(); 606 if (!userId) { 607 return [2 /*return*/]; 608 } 609 candidate = Math.random() < 0.5 ? 'email' : 'phone'; 610 payload = [{ name: LLMLandingPage.FIRST_CONTACT_METHOD_VAR, value: candidate }]; 611 return [2 /*return*/, new Promise(function (resolve, reject) { 612 httpPostJsonAsync(apiEndpoint + "/api/saveUrlParamsLLM/".concat(userId), payload, function (response) { 613 try { 614 var parsedResponse = JSON.parse(response); 615 if (parsedResponse.err) { 616 console.error('Error syncing firstContactMethod:', parsedResponse.err); 617 reject(new Error(parsedResponse.err)); 618 return; 619 } 620 var list = parsedResponse.data && parsedResponse.data.urlParam
620s; 621 if (Array.isArray(list)) { 622 var row = list.find(function (p) { return p.name === LLMLandingPage.FIRST_CONTACT_METHOD_VAR; }); 623 if (row && row.value !== undefined && row.value !== null) { 624 _this.firstContactMethod(String(row.value)); 625 } 626 } 627 resolve(); 628 } 629 catch (error) { 630 console.error('Error parsing firstContactMethod response:', error); 631 reject(error); 632 } 633 }); 634 })]; 635 }); 636 }); }; 637 // Extract URL params - no filtering, save all params 638 this.getUrlParamsForLLM = function () { 639 var urlParams = new URLSearchParams(window.location.search); 640 var filteredParams = []; 641 var urlParamsArray = Array.from(urlParams.entries()); 642 for (var i = 0; i < urlParamsArray.length; i++) { 643 var _a = urlParamsArray[i], name_1 = _a[0], value = _a[1]; 644 if (name_1 && value) { 645 if (!isNaN(value)) { 646 value = parseFloat(value); 647 } 648 filteredParams.push({ name: name_1, value: value }); 649 } 650 } 651 return filteredParams; 652 }; 653 // Save URL params to server 654 this.saveUrlParamsLLM = function () { return __awaiter(_this, void 0, void 0, function () { 655 var userId, urlParams; 656 return __generator(this, function (_a) { 657 userId = this.tempUserId(); 658 if (!userId) { 659 console.log("No tempUserId available yet, skipping URL params save"); 660 return [2 /*return*/]; 661 } 662 urlParams = this.getUrlParamsForLLM(); 663 if (urlParams.length === 0) { 664 return [2 /*return*/]; // No params to save 665 } 666 return [2 /*return*/, new Promise(function (resolve, reject) { 667 httpPostJsonAsync(apiEndpoint + "/api/saveUrlParamsLLM/".concat(userId), urlParams, function (response) { 668 try { 669 var parsedResponse = JSON.parse(response); 670 if (parsedResponse.err) { 671 console.error("Error saving URL params:", parsedResponse.err); 672 reject(new Error(parsedResponse.err)); 673 } 674 else { 675 resolve(); 676 } 677 } 678 catch (error) { 679 console.error("Error parsing response:", error); 680 reject(error); 681 } 682 }); 683 })]; 684 }); 685 }); }; 686 // Rotating text functionality 687 var rotatingTextElement = document.getElementById('rotatingText'); 688 if (rotatingTextElement) { 689 var rotatingTexts_1 = ['Feel better.', 'Get stronger.', 'Sleep deeper.', 'Live longer.']; 690 var rotatingColors_1 = ['#4dabf7', '#ff6b9d', '#51cf66']; 691 var currentIndex_1 = 0; 692 var currentColorIndex_1 = 0; 693 // Set transition once 694 rotatingTextElement.style.transition = 'opacity 1s ease-in-out, color 1s ease-in-out'; 695 rotatingTextElement.style.opacity = '1'; 696 rotatingTextElement.style.color = rotatingColors_1[0]; 697 var isFadingOut_1 = true; 698 var rotateText = function () { 699 // Start fade out 700 rotatingTextElement.style.opacity = '0'; 701 }; 702 // Listen for the end of opacity transitions 703 rotatingTextElement.addEventListener('transitionend', function (e) { 704 if (e.propertyName !== 'opacity') 705 return; 706 if (isFadingOut_1) {
707 // Fade-out finished â swap text and color 708 currentIndex_1 = (currentIndex_1 + 1); 709 currentColorIndex_1 = (currentColorIndex_1 + 1); 710 if (currentIndex_1 >= rotatingTexts_1.length) { 711 currentIndex_1 = 0; 712 } 713 if (currentColorIndex_1 >= rotatingColors_1.length) { 714 currentColorIndex_1 = 0; 715 } 716 rotatingTextElement.textContent = rotatingTexts_1[currentIndex_1]; 717 rotatingTextElement.style.color = rotatingColors_1[currentColorIndex_1]; 718 // Start fade-in 719 rotatingTextElement.style.opacity = '1'; 720 isFadingOut_1 = false; 721 } 722 else { 723 // Fade-in finished â ready for next cycle 724 isFadingOut_1 = true; 725 } 726 }); 727 // Start rotation 728 setInterval(rotateText, 3000); 729 } 730 if (!this.tempUserId()) { 731 this.tempUserId(makeCode(10) + '-' + makeCode(10)); 732 } 733 void this.syncFirstContactMethodWithServer().catch(function (error) { 734 console.error('Failed to sync firstContactMethod:', error); 735 }); 736 // Save URL params after page loads and tempUserId is available 737 // Use a subscription to ensure we call it once tempUserId is set 738 var self = this; 739 this.tempUserId.subscribe(function (newUserId) { 740 if (newUserId) { 741 // Call after a short delay to ensure page is fully loaded 742 setTimeout(function () { 743 self.saveUrlParamsLLM().catch(function (error) { 744 console.error("Failed to save URL params:", error); 745 }); 746 }, 100); 747 } 748 }); 749 // Also call immediately if tempUserId is already set (in case subscription doesn't fire) 750 if (this.tempUserId()) { 751 setTimeout(function () { 752 _this.saveUrlParamsLLM().catch(function (error) { 753 console.error("Failed to save URL params:", error); 754 }); 755 }, 100); 756 } 757 // Clear phone number error when user starts typing 758 this.phoneNumber.subscribe(function () { 759 if (_this.phoneNumberError()) { 760 _this.phoneNumberError(null); 761 } 762 }); 763 // Handle success/cancel URLs from Stripe checkout 764 var urlParams = new URLSearchParams(window.location.search); 765 var paid = urlParams.get('paid'); 766 var canceled = urlParams.get('canceled'); 767 if (paid) { 768 // Show success message or handle successful payment 769 console.log('Payment successful for plan:', paid); 770 // You can add UI feedback here if needed 771 } 772 if (canceled) { 773 // Show cancellation message or handle canceled payment 774 console.log('Payment was canceled'); 775 // You can add UI feedback here if needed 776 } 777 } 778 // Helper function to check if a question is answered 779 LLMLandingPage.prototype.isQuestionAnswered = function (question) { 780 if (question.type === 'multipleChoice') { 781 return question.answers().some(function (a) { return a.isSelected && a.isSelected(); }); 782 } 783 else if (question.type === 'numeric') { 784 // For height, we need both feet and inches to be answered 785 if (question.name === 'height') { 786 var answers = question.answers(); 787 return answers.length >= 2 && 788 answers[0].submission && answers[0].submission() !== null && answers[0].submission() !== undefined && answers[0].submission() !== '' &&
789 answers[1].submission && answers[1].submission() !== null && answers[1].submission() !== undefined && answers[1].submission() !== ''; 790 } 791 // For other numeric questions, at least one answer needs to be filled 792 return question.answers().some(function (a) { return a.submission && a.submission() !== null && a.submission() !== undefined && a.submission() !== ''; }); 793 } 794 else if (question.type === 'openResponse') { 795 return question.openResponse && question.openResponse() && question.openResponse().trim() !== ''; 796 } 797 return false; 798 }; 799 LLMLandingPage.prototype.normalizeUSPhoneToE164 = function (phoneNumber) { 800 if (!phoneNumber || phoneNumber.trim() === '') { 801 return null; 802 } 803 var digits = phoneNumber.replace(/\D/g, ''); 804 if (!digits) { 805 return null; 806 } 807 var d = digits; 808 while (d.length > 10 && d[0] === '1') { 809 d = d.slice(1); 810 } 811 if (d.length !== 10 || !LLMLandingPage.US_NANP_REGEX.test(d)) { 812 return null; 813 } 814 return '+1' + d; 815 }; 816 LLMLandingPage.prototype.isValidPhoneNumber = function (phoneNumber) { 817 return this.normalizeUSPhoneToE164(phoneNumber) !== null; 818 }; 819 LLMLandingPage.US_NANP_REGEX = /^([2-9]\d{2})([2-9]\d{2})(\d{4})$/; 820 LLMLandingPage.FIRST_CONTACT_METHOD_VAR = 'firstContactMethod'; 821 return LLMLandingPage; 822}()); 823export default LLMLandingPage;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.