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.