PageSourceSearch

https://quorumapp.com/ts/LLMLandingPage.js

js quorumapp.com collected 2026-09-26 04:40:05 UTC 44,083 bytes, 823 lines download raw bytes

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.