PageSourceSearch

https://budget-balancer.vercel.app/scripts.js

js budget-balancer.vercel.app collected 2026-10-03 06:19:12 UTC 15,633 bytes, 451 lines download raw bytes

1import { startRealtimeCfoCall, hangUpCfoCall } from './realtime.js';
2import { logGenerateEvent } from './logging.js';
3
4document.addEventListener('DOMContentLoaded', function() {
5  window.scrollTo(0, 0);
6});
7
8// Attach to window so inline HTML onclick can find these
9window.selectOption = selectOption;
10window.toggleOption = toggleOption;
11window.calculateRecommendation = calculateRecommendation;
12window.getSelectedValue = getSelectedValue;
13window.initializeSlideToAnswer = initializeSlideToAnswer;
14
15// Button selection functions
16function selectOption(groupId, button, value) {
17  console.log(`[UI] selectOption() called: groupId=${groupId}, value=${value}`);
18  
19  // Get all buttons in the group
20  const buttonGroup = document.getElementById(groupId);
21  const buttons = buttonGroup.getElementsByTagName('button');
22  
23  // Clear all selected buttons first
24  for (let btn of buttons) {
25    btn.classList.remove('selected');
26  }
27  
28  // Select the clicked button
29  button.classList.add('selected');
30}
31
32function toggleOption(groupId, button, value) {
33  console.log(`[UI] toggleOption() called: groupId=${groupId}, value=${value}`);
34  // Allows multiple to be selected
35  button.classList.toggle('selected');
36}
37
38function getSelectedValue(groupId) {
39  console.log(`[UI] getSelectedValue() called: groupId=${groupId}`);
40  const buttons = document.getElementById(groupId).getElementsByTagName('button');
41  const selected = [];
42  
43  for (let btn of buttons) {
44    if (btn.classList.contains('selected')) {
45      // For category, only return the last selected value
46      if (groupId === 'category' && selected.length > 0) {
47        continue;
48      }
49      
50      selected.push({
51        text: btn.innerText,
52        brand: btn.getAttribute('data-brand') || null,
53        performance: btn.getAttribute('data-performance') || null,
54        offset: btn.getAttribute('data-offset') || null
55      });
56    }
57  }
58  
59  if (selected.length === 0) return null;
60  if (selected.length === 1) return selected[0];
61  return selected;
62}
63
64async function calculateRecommendation() {
65  // Make category mandatory
66  const chosenCategory = getSelectedValue('category');
67  if (!chosenCategory) {
68    alert("Please select a Category before generating a recommendation.");
69    return;
70  }
71
72  // Show the loading overlay
73  const loadingText = document.querySelector("#loadingOverlay p");
74  document.getElementById("loadingOverlay").style.display = "flex";
75  loadingText.innerHTML = "Analyzing your inputs and finding the best way to balance your marketing efforts";
76
77  // After 4 seconds, show a new loading message
78  setTimeout(() => {
79    loadingText.innerHTML = `
80      Analyzing your inputs and finding the best way to balance your marketing efforts.
81      <br><br>This will take a minute. Relax.
82    `;
83  }, 4000);
84
85  // After 10 seconds, show yet another message
86  setTimeout(() => {
87    loadingText.innerHTML = "Using data from effective campaigns to detect patterns that fit your goals.";
88  }, 10000);
89
90  // Format the data according to the assistant's expected structure
91  const data = {
92    brandName: document.getElementById('brandName').value,
93    category: chosenCategory.text,  // Just send the category name
94    baselineAllocation: `${chosenCategory.brand}/${chosenCategory.performance}`,
95    brandPercentage: parseInt(chosenCategory.brand),
96    performancePercentage: parseInt(chosenCategory.performance),
97    offsetDetails: {
98      purchaseMethod: getSelectedValue('purchaseMethod')?.offset ? parseInt(getSelectedValue('purchaseMethod').offset) : 0,
99      pricingStrategy: getSelectedValue('pricingStrategy')?.offset ? parseInt(getSelectedValue('pricingStrategy').offset) : 0,
100      innovationLevel: getSelectedValue('innovationLevel')?.offset ? parseInt(getSelectedValue('innovationLevel').offset) : 0,
101      categoryStage: getSelectedValue('categoryStage')?.offset ? parseInt(getSelectedValue('categoryStage').offset) : 0,
102      brandSize: getSelectedValue('brandSize')?.offset ? parseInt(getSelectedValue('brandSize').offset) : 0,
103      additionalContext: 0
104    },
105    additionalContext: document.getElementById('additionalContext').value,
106    adjustmentsSummary: `Starting from a ${chosenCategory.brand}/${chosenCategory.performance} split for ${chosenCategory.text}`
107  };
108
109  console.log("📥 Inputs sent to server:", {
110    timestamp: new Date().toISOString(),
111    data
112  });
113
114  try {
115    // SINGLE API CALL
116    const response = await axios.post('/api/generate', data, { timeout: 180000 });
117    loadingText.innerHTML = "Shaping a recommendation to strengthen your case...";
118    
119    console.log("Server API Response:", response.data);
120    
121    // Log the generation event
122    await logGenerateEvent(data, response.data);
123    
124    displayRecommendation(response.data);
125  } catch (error) {
126    console.error("Error during API call:", error);
127    
128    // Log the error event
129    await logGenerateEvent(data, null, error);
130    
131    if (error.response) {
132      console.error("❌ API Error Response:", error.response.data);
133      showCustomError(`Error: ${error.response.data.message || "Unknown error."}`);
134    } else {
135      console.error("Unexpected error occurred:", error.message);
136      showCustomError(`Unexpected error occurred. Please try again. Error: ${error.message}`);
137    }
138  } finally {
139    document.getElementById('loadingOverlay').style.display = 'none';
140  }
141}
142
143function displayRecommendation(responseData) {
144  console.log("[UI] displayRecommendation() called with responseData:", responseData);
145
146  let content = {};
147
148  // Parse the assistant's message
149  if (responseData.messages && Array.isArray(responseData.messages)) {
150    const assistantMessages = responseData.messages
151      .filter(msg => msg.role === "assistant")
152      .sort((a, b) => b.created_at - a.created_at);
153
154    if (assistantMessages.length > 0) {
155      try {
156        let rawText = assistantMessages[0].content[0].text.value;
157        
158        // Remove code block markers if present
159        rawText = rawText.replace(/```json\n/, '').replace(/```\n?$/, '');
160        
161        try {
162          content = JSON.parse(rawText);
163        } catch (e) {
164          console.log("Assistant response is not JSON; attempting to parse structured text");
165          
166          // Parse structured text response
167          const parts = rawText.split(/["'](\w+)["']:\s*["']/);
168          if (parts.length > 1) {
169            content = {};
170            for (let i = 1; i < parts.length; i += 2) {
171              const key = parts[i];
172              let value = parts[i + 1] || '';
173              value = value.replace(/["'],?\s*$/, ''); // Remove trailing quotes and commas
174              value = value.replace(/\\n/g, '\n'); // Handle newlines
175              content[key] = value;
176            }
177          } else {
178            content = {
179              recommendation: rawText,
180              rationale: "Unable to parse structured response."
181            };
182          }
183        }
184      } catch (e) {
185        console.error("Error processing message content:", e);
186        content = {
187          recommendation: "Error processing recommendation",
188          rationale: "Unable to parse the assistant's response."
189        };
190      }
191    }
192  }
193
194  // Provide defaults for all expected fields
195  content.recommendation = content.recommendation || "No recommendation provided.";
196  content.rationale = content.rationale || "No rationale provided.";
197  content.dataMethodology = content.dataMethodology || "No data methodology provided.";
198  content.argument = content.argument || "No argument provided.";
199  content.pros = Array.isArray(content.pros) ? content.pros : 
200                 content.pros ? content.pros.split(/["\[\],]/).filter(Boolean) : [];
201  content.cons = Array.isArray(content.cons) ? content.cons : 
202                 content.cons ? content.cons.split(/["\[\],]/).filter(Boolean) : [];
203  content.alternative = content.alternative || "No alternative approach provided.";
204  content.cfoMemo = content.cfoMemo || "";
205
206  // Store the latest recommendation for use in the CFO call
207  window.latestRecommendationText = content.recommendation;
208
209  // Show the result card
210  const resultCard = document.getElementById('resultCard');
211  resultCard.style.display = 'block';
212
213  // Update recommendation content
214  const recommendationContent = document.getElementById('recommendationContent');
215  const htmlContent = `
216    <article class="recommendation-content">
217      <header>
218        <h1>Recommendation</h1>
219        <p class="recommendation-main">${content.recommendation}</p>
220      </header>
221
222      <section class="recommendation-details">
223        <h2>Rationale</h2>
224        <p>${content.rationale}</p>
225      </section>
226
227      <section class="recommendation-details">
228        <h2>Data Methodology</h2>
229        <p>${content.dataMethodology}</p>
230      </section>
231
232      <section class="recommendation-details">
233        <h2>The Argument</h2>
234        <p>${content.argument}</p>
235      </section>
236
237      <div class="pros-cons-container">
238        <section class="recommendation-details">
239          <h2>Pros</h2>
240          <ul class="benefits-list">
241            ${content.pros ? content.pros.map(pro => `<li>${pro}</li>`).join('') : "<li>No pros provided.</li>"}
242          </ul>
243        </section>
244
245        <section class="recommendation-details">
246          <h2>Cons</h2>
247          <ul class="drawbacks-list">
248            ${content.cons ? content.cons.map(con => `<li>${con}</li>`).join('') : "<li>No cons provided.</li>"}
249          </ul>
250        </section>
251      </div>
252
253      <section class="recommendation-details">
254        <h2>Alternative Approach</h2>
255        <p>${content.alternative}</p>
256      </section>
257    </article>
258  `;
259  recommendationContent.innerHTML = htmlContent;
260
261  // Show memo section
262  document.getElementById('memoSection').style.display = 'block';
263  
264  // Store the memo text for later use
265  window.latestMemoText = content.cfoMemo;
266
267  // Set up event listeners for the new UX flow
268  setupMemoFunctionality();
269
270  // Smooth scroll to the result card
271  requestAnimationFrame(() => {
272    document.getElementById('resultCard').scrollIntoView({
273      behavior: 'smooth',
274      block: 'start'
275    });
276  });
277
278  console.log("[UI] Final content object:", content);
279  console.log("[UI] Recommendation displayed, revealing result card.");
280}
281
282function setupMemoFunctionality() {
283  const cfoMemoButton = document.getElementById('cfoMemoButton');
284  const rolePlayButton = document.getElementById('rolePlayButton');
285  const slideToAnswerWrapper = document.querySelector('.slide-to-answer-wrapper');
286  const memoContent = document.getElementById('memoContent');
287  const memoText = document.getElementById('memoText');
288  const hangUpButton = document.getElementById('hangUpButton');
289
290  // CFO Memo button click handler
291  cfoMemoButton.addEventListener('click', () => {
292    memoContent.style.display = 'block';
293    memoText.style.display = 'block';
294    memoText.innerHTML = window.latestMemoText;
295    cfoMemoButton.style.display = 'none';
296    rolePlayButton.style.display = 'block';
297  });
298
299  // Role Play button click handler
300  rolePlayButton.addEventListener('click', () => {
301    rolePlayButton.style.display = 'none';
302    slideToAnswerWrapper.style.display = 'block';
303    initializeSlideToAnswer();
304  });
305
306  // Hang Up button click handler
307  hangUpButton.addEventListener('click', () => {
308    hangUpButton.style.display = 'none';
309    rolePlayButton.style.display = 'block';
310    slideToAnswerWrapper.style.display = 'none';
311    const thumb = document.querySelector('.slide-thumb');
312    thumb.style.left = '2px'; // Reset slider position
313    hangUpCfoCall();
314  });
315}
316
317function initializeSlideToAnswer() {
318  const container = document.getElementById('slideToAnswerContainer');
319  const thumb = container.querySelector('.slide-thumb');
320  const hangUpButton = document.getElementById('hangUpButton');
321  const slideWrapper = document.querySelector('.slide-to-answer-wrapper');
322  
323  let isDragging = false;
324  let startX;
325  let thumbLeft;
326
327  function handleStart(e) {
328    e.preventDefault(); // Prevent default touch/mouse events
329    isDragging = true;
330    startX = e.type === 'mousedown' ? e.clientX : e.touches[0].clientX;
331    thumbLeft = thumb.offsetLeft;
332    
333    // Add dragging class to body
334    document.body.classList.add('sliding');
335  }
336
337  function handleMove(e) {
338    if (!isDragging) return;
339    
340    e.preventDefault(); // Prevent default touch/mouse events
341    
342    const x = e.type === 'mousemove' ? e.clientX : e.touches[0].clientX;
343    const walk = x - startX;
344    let newLeft = thumbLeft + walk;
345
346    // Constrain movement
347    const maxLeft = container.offsetWidth - thumb.offsetWidth;
348    newLeft = Math.max(0, Math.min(newLeft, maxLeft));
349    thumb.style.left = `${newLeft}px`;
350
351    // If dragged more than 80% of the way
352    if (newLeft > maxLeft * 0.8) {
353      handleAnswer();
354    }
355  }
356
357  function handleEnd(e) {
358    if (!isDragging) return;
359    
360    e.preventDefault(); // Prevent default touch/mouse events
361    isDragging = false;
362    
363    // Remove dragging class from body
364    document.body.classList.remove('sliding');
365    
366    // Reset position if not answered
367    const maxLeft = container.offsetWidth - thumb.offsetWidth;
368    if (parseInt(thumb.style.left) < maxLeft * 0.8) {
369      thumb.style.left = '0px';
370    }
371  }
372
373  function handleAnswer() {
374    // Clean up event listeners
375    cleanup();
376    
377    // Show hang up button and start call
378    slideWrapper.style.display = 'none';
379    hangUpButton.style.display = 'block';
380    
381    // Start the CFO call
382    startRealtimeCfoCall(window.latestMemoText);
383  }
384
385  function cleanup() {
386    container.removeEventListener('mousedown', handleStart);
387    container.removeEventListener('touchstart', handleStart);
388    window.removeEventListener('mousemove', handleMove);
389    window.removeEventListener('touchmove', handleMove);
390    window.removeEventListener('mouseup', handleEnd);
391    window.removeEventListener('touchend', handleEnd);
392    document.body.classList.remove('sliding');
393  }
394
395  // Add event listeners
396  container.addEventListener('mousedown', handleStart);
397  container.addEventListener('touchstart', handleStart);
398  window.addEventListener('mousemove', handleMove);
399  window.addEventListener('touchmove', handleMove);
400  window.addEventListener('mouseup', handleEnd);
401  window.addEventListener('touchend', handleEnd);
402}
403
404function showCustomSuccess(message) {
405  const successContainer = document.createElement('div');
406  successContainer.style.cssText = `
407    position: fixed;
408    top: 20px;
409    right: 20px;
410    background: rgba(0, 255, 0, 0.1);
411    backdrop-filter: blur(10px);
412    padding: 15px;
413    border-radius: 8px;
414    color: white;
415    z-index: 1000;
416    max-width: 500px;
417  `;
418  successContainer.innerHTML = `<p style="margin: 0;">${message}</p>`;
419  document.body.appendChild(successContainer);
420
421  setTimeout(() => {
422    successContainer.remove();
423  }, 3000);
424}
425
426function showCustomError(message) {
427  const errorContainer = document.createElement('div');
428  errorContainer.style.cssText = `
429    position: fixed;
430    top: 20px;
431    right: 20px;
432    background: rgba(255, 0, 0, 0.1);
433    backdrop-filter: blur(10px);
434    padding: 15px;
435    border-radius: 8px;
436    color: white;
437    z-index: 1000;
438    max-width: 500px;
439  `;
440  errorContainer.innerHTML = `
441    <p style="margin: 0; white-space: pre-wrap;">${message}</p>
442    <button
443      onclick="this.parentElement.remove()"
444      style="background: rgba(255,255,255,0.2); border: none;
445             color: white; padding: 5px 10px; margin-top: 10px;
446             border-radius: 4px; cursor: pointer;">
447      Close
448    </button>
449  `;
450  document.body.appendChild(errorContainer);
451}

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.