1/* @license GPL-2.0-or-later https://www.drupal.org/licensing/faq */ 2window.googletag=window.googletag||{};window.googletag.cmd=window.googletag.cmd||[];(function(Drupal,once){'use strict';Drupal.behaviors.loadGoogleAds={attach:function(context,settings){const elements=once('loadGoogleAdsSetup','html',context);elements.forEach(function(element){window.googletag.cmd.push(function(){var mapping1=window.googletag.sizeMapping().addSize([0,0],[[300,250]]).addSize([748,200],[[728,90]]).addSize([990,200],[[970,90],[728,90]]).build();var mapping2=googletag.sizeMapping().addSize([0,0],[[160,600]]).addSize([1400,600],[[300,600]]).build();window.googletag.defineSlot('/23301409767/ieee_multisize_content_1',[[970,90],[728,90],[300,250]],'div-gpt-ad-4507714-1').defineSizeMapping(mapping1).addService(window.googletag.pubads());window.googletag.defineSlot('/23301409767/ieee_multisize_content_homepage',[[970,90],[728,90],[300,250]],'div-gpt-ad-4507714-3').defineSizeMapping(mapping1).addService(window.googletag.pubads());window.googletag.defineSlot('/23301409767/ieee_multisize_sidebar_1',[[300,600],[160,600]],'div-gpt-ad-4507714-2').defineSizeMapping(mapping2).addService(window.googletag.pubads());window.googletag.pubads().enableSingleRequest();window.googletag.pubads().collapseEmptyDivs();window.googletag.enableServices();if(document.getElementById('div-gpt-ad-4507714-1'))window.googletag.cmd.push(function(){googletag.display('div-gpt-ad-4507714-1');});if(document.getElementById('div-gpt-ad-4507714-2'))window.googletag.cmd.push(function(){googletag.display('div-gpt-ad-4507714-2');});if(document.getElementById('div-gpt-ad-4507714-3'))window.googletag.cmd.push(function(){googletag.display('div-gpt-ad-4507714-3');});});});}};})(Drupal,once);; 3(function(Drupal){'use strict';window.dataLayer=window.dataLayer||[];const tabMap={report:{event:'ai_report_view',category:'report'},visualizations:{event:'ai_viz_view',category:'viz'},code:{event:'ai_code_view',category:'code'},about:{event:'ai_about_view',category:'about'},chat:{event:'ai_chat_open',category:'chat'}};Drupal.aiTracking={track:function(eventName,params){const data={event:eventName,ai_category:params.ai_category||'',ai_action:params.ai_action||'',ai_entity_id:params.ai_entity_id||'',ai_node_id:params.ai_node_id||'',ai_detail:params.ai_detail||''};window.dataLayer.push(data);},getContext:function(el){const node=(el instanceof jQuery||(el&&el.jquery))?el[0]:el;let entityId='';let nodeId='';let current=node;while(current&¤t!==document){if(!entityId&¤t.dataset&¤t.dataset.entityId)entityId=current.dataset.entityId;if(!nodeId&¤t.dataset&¤t.dataset.nodeId)nodeId=current.dataset.nodeId;if(entityId&&nodeId)break;current=current.parentElement;}return {ai_entity_id:entityId,ai_node_id:nodeId};},getTabEvent:function(tab){return tabMap[tab]||null;},getTabNameFromPaneId:function(targetId){if(!targetId)return '';if(targetId.includes('report-pane'))return 'report';if(targetId.includes('visualizations-pane'))return 'visualizations';if(targetId.includes('code-pane'))return 'code';if(targetId.includes('about-pane'))return 'about';if(targetId.includes('chat-pane'))return 'chat';return '';}};})(Drupal);; 4(function(Drupal){'use strict';window.dataLayer=window.dataLayer||[];Drupal.notebookTracking={track:function(eventName,params){const detail=(params&¶ms.nb_detail)||'';const data={event:eventName,nb_category:(params&¶ms.nb_category)||'',nb_action:(params&¶ms.nb_action)||'',nb_node_id:(params&¶ms.nb_node_id)||'',nb_detail:String(detail).slice(0,100)};window.dataLayer.push(data);}};})(Drupal);; 5(function(Drupal,$,once){'use strict';let _notebookSessionChannel=null;Drupal.behaviors.sps_bootstrap={attach:function(context,settings){$(".region-primary-menu .nav-item.dropdown > .nav-link[data-bs-toggle='dropdown']",context).on("click",function(e){if(isLargeScreen()){e.preventDefault();e.stopPropagation();const $parent=$(this).parent();$('.region-primary-menu .nav-item.dropdown').not($parent).find('.dropdown-toggle').removeClass('show');$('.region-primary-menu .nav-item.dropdown').not($parent).find('.dropdown-menu').removeClass('show');$parent.find('.dropdown-menu').addClass('show');}});$(window).on('resize',function(){if(!isLargeScreen())$('.region-primary-menu .nav-item.dropdown .dropdown-menu').removeClass('show');});$(".region-primary-menu .dropdown > a").on("click",function(e){e.stopPropagation();if($(this).hasClass("trigger"))if($(this).find(".caret").hasClass("right-caret")){$(this).find(".caret").removeClass("right-caret");$(this).find(".caret").addClass("down-caret");}else{$(this).find(".caret").removeClass("down-caret");$(this).find(".caret").addClass("right-caret");}});$(".region-primary-menu .dropdownSubmenu > ul > li > a.trigger").on("click",function(e){e.stopPropagation();if($(this).find(".caret").hasClass("right-caret")){$(this).find(".caret").removeClass("right-caret");$(this).find(".caret").addClass("down-caret");}else{$(this).find(".caret").removeClass("down-caret");$(this).find(".caret").addClass("right-caret");}});function isLargeScreen(){return window.innerWidth>992;}$(once('btn-popovers','[data-bs-toggle="popover"]:not([data-bs-html])',context)).each(function(){new bootstrap.Popover(this,{container:'body',sanitize:false});});$(once('btn-popovers-html','[data-bs-toggle="popover"][data-bs-html]',context)).each(function(){var el=this;var hideTimeout;var popover=new bootstrap.Popover(el,{container:'body',sanitize:false,trigger:'manual',html:true});function scheduleHide(){clearTimeout(hideTimeout);hideTimeout=setTimeout(function(){popover.hide();},250);}function cancelHide(){clearTimeout(hideTimeout);}$(el).on('mouseenter focusin',function(){cancelHide();popover.show();}).on('mouseleave focusout',scheduleHide);$(document).on('mouseenter','.popover',function(){cancelHide();}).on('mouseleave','.popover',scheduleHide);});$(once('popover-modal-trigger','[data-modal-target]',context)).on('click',function(e){e.preventDefault();var $btn=$(this);var popoverInstance=bootstrap.Popover.getInstance(this);if(popoverInstance)popoverInstance.hide();var modalTarget=$btn.data('modal-target');if(modalTarget){var $modal=$(modalTarget);if($modal.length){var modalInstance=bootstrap.Modal.getInstance($modal[0]);if(!modalInstance)modalInstance=new bootstrap.Modal($modal[0]);modalInstance.show();}}});$(document).on('click','.zipContent',function(){const $button=$(this);const url=$button.data("zip-url");const filename=$button.data("filename");
5const $modalLabel=$('#zipContentModalLabel');const $treeContainer=$('#tree-container');const $modal=$('#zipContentModal');if(typeof bootstrap!=='undefined'&&bootstrap.Modal&&$modal.length){let modalInstance=bootstrap.Modal.getInstance($modal[0]);if(!modalInstance)modalInstance=new bootstrap.Modal($modal[0]);modalInstance.show();}$button.prop('disabled',true).html('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Loading...');$modalLabel.text(filename);$treeContainer.html('<div class="text-center py-4"><div class="spinner-border text-primary" role="status"></div><p class="mt-2">Loading file structure...</p></div>');if(window.zipContentAjax)window.zipContentAjax.abort();window.zipContentAjax=$.ajax({type:"GET",url,dataType:'json',timeout:15000,success:function(res){if(res&&res.data)try{const treeHTML=createTreeHTML(res.data);$treeContainer.html(treeHTML);$treeContainer.find('.caret').on('click',function(){$(this).toggleClass('caret-down').parent().find('> ul').toggleClass('active');});}catch(e){showError("Error processing file structure");console.error("Error generating tree:",e);}else showError("No content available");},error:function(xhr,status,error){if(status!=="abort"){const errorMsg=xhr.responseJSON&&xhr.responseJSON.message?xhr.responseJSON.message:"Apologies, the file listing could not be loaded.";showError(errorMsg);console.error("AJAX error:",error);}},complete:function(){$button.prop('disabled',false).text('Show Zip Contents');window.zipContentAjax=null;}});function showError(message){$treeContainer.html(`<div class="alert alert-warning text-center m-3 py-4" role="alert"> 6 <i class="bi bi-exclamation-triangle-fill fs-3 d-block mb-2"></i> 7 <span class="fs-6">${message}</span> 8 </div>`);}});function formatFileSize(bytes){if(typeof bytes!=='number')return '0 bytes';if(bytes<1024)return bytes+' bytes';if(bytes<1024*1024)return (bytes/1024).toFixed(2)+' KB';if(bytes<1024*1024*1024)return (bytes/(1024*1024)).toFixed(2)+' MB';return (bytes/(1024*1024*1024)).toFixed(2)+' GB';}function createTreeHTML(data){let html='<ul>';for(const [key,value] of Object.entries(data))if(typeof value==='object'&&value!==null)html+=`<li class="folder open fw-normal border-0 pe-none"><span class="caret">${key}</span>${createTreeHTML(value)}</li>`;else{const ext=key.split('.').pop();const size=formatFileSize(value);html+=`<li class="d-block file border-0 ${ext}">${key} (${size})</li>`;}html+='</ul>';return html;}function handleIntersection(entries){entries.map((entry)=>{if(entry.isIntersecting)if(!entry.target.classList.contains('loaded')){entry.target.classList.add('loaded');var vidDefer=document.getElementsByTagName('iframe');for(var i=0;i<vidDefer.length;i++)if(vidDefer[i].getAttribute('data-src'))vidDefer[i].setAttribute('src',vidDefer[i].getAttribute('data-src'));}});}const youtubeElement=document.getElementById('youtube-dataPort');if(youtubeElement){const observer=new IntersectionObserver(handleIntersection);observer.observe(youtubeElement);}$(once('ai-analysis-copy','.ai-analysis-copy-btn',context)).on('click',function(){const $button=$(this);const targetId=$button.data('code-target');const $textarea=$('#'+targetId);if($textarea.length){const code=$textarea.val();const isModal=$button.hasClass('ai-code-copy-btn');const ctx=Drupal.aiTracking.getContext($button[0]);Drupal.aiTracking.track('ai_code_copy',{ai_category:'code',ai_action:isModal?'copy_modal':'copy_card',ai_entity_id:ctx.ai_entity_id,ai_node_id:ctx.ai_node_id});if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(code).then(function(){showCopySuccess($button);}).catch(function(){fallbackCopy($textarea,$button);});else fallbackCopy($textarea,$button);}});function showCopySuccess($button){const originalHtml=$button.html();$button.html('<i class="fas fa-check"></i> Copied!');setTimeout(function(){$button.html(originalHtml);},2000);}function fallbackCopy($textarea,$button){$textarea.removeClass('visually-hidden');$textarea.select();try{document.execCommand('copy');showCopySuccess($button);}catch(err){console.error('Failed to copy code:',err);}$textarea.addClass('visually-hidden');}$(once('ai-analysis-br-spacing','.ai-analysis-report-body',context)).each(function(){const $body=$(this);let html=$body.html();html=html.replace(/<br\s*\/?>\s*(?!<br)/gi,'<br><br>');$body.html(html);});const AI_DISCLAIMER_COOKIE='ai_analysis_disclaimer_accepted';const AI_DISCLAIMER_DURATION_DAYS=drupalSettings.aiAnalysis?.disclaimerCookieDays||365;function getCookie(name){const value=`; ${document.cookie}`;const parts=value.split(`; ${name}=`);if(parts.length===2)return parts.pop().split(';').shift();return null;}function setCookie(name,value,days){const date=new Date();date.setTime(date.getTime()+(days*24*60*60*1000));document.cookie=`${name}=${value}; expires=${date.toUTCString()}; path=/; SameSite=Lax`;}function hasAcceptedDisclaimer(){return getCookie(AI_DISCLAIMER_COOKIE)==='true';}function openAiAnalysisModal(modalId,tabName){const $modal=$('#'+modalId);if(!$modal.length)return;$modal.data('_skipNextTabTrack',true);const modalElement=$modal[0];let modalInstance=bootstrap.Modal.getInstance(modalElement);if(!modalInstance)modalInstance=new bootstrap.Modal(modalElement);const $tabButton=$modal.find('#'+tabName+'-tab-'+modalId.replace('ai-analysis-modal-',''));if($tabButton.length){const tabInstance=new bootstrap.Tab($tabButton[0]);tabInstance.show();}modalInstance.show();}$(once('ai-disclaimer-accept','.ai-disclaimer-accept-btn',context)).on('click',function(){const $button=$(this);const targetModalId=$button.data('target-modal');const pendingTab=$button.data('pending-tab')||'report';const ctx=Drupal.aiTracking.getContext($button[0]);Drupal.aiTracking.track('ai_disclaimer_response',{ai_category:'disclaimer',ai_action:'accept',ai_entity_id:ctx.ai_entity_id,ai_node_id:ctx.ai_no
8de_id});setCookie(AI_DISCLAIMER_COOKIE,'true',AI_DISCLAIMER_DURATION_DAYS);const $disclaimerModal=$button.closest('.modal');const disclaimerInstance=bootstrap.Modal.getInstance($disclaimerModal[0]);if(disclaimerInstance)disclaimerInstance.hide();$disclaimerModal.one('hidden.bs.modal',function(){openAiAnalysisModal(targetModalId,pendingTab);});});$(once('notebook-consent-accept','.notebook-consent-accept-btn',context)).on('click',function(){var NOTEBOOK_CONSENT_COOKIE='notebook_consent_accepted';var NOTEBOOK_CONSENT_DAYS=drupalSettings.notebookConsent?.cookieDays||365;setCookie(NOTEBOOK_CONSENT_COOKIE,'true',NOTEBOOK_CONSENT_DAYS);var $consentModal=$(this).closest('.modal');var $pendingButton=$consentModal.data('pending-button');Drupal.notebookTracking.track('notebook_consent_accept',{nb_category:'consent',nb_action:'accept',nb_node_id:($pendingButton&&$pendingButton.data('node-id'))||''});var consentInstance=bootstrap.Modal.getInstance($consentModal[0]);if(consentInstance)consentInstance.hide();$consentModal.one('hidden.bs.modal',function(){if($pendingButton&&$pendingButton.length)$pendingButton.trigger('click');});});$(once('ai-analysis-trigger','.ai-analysis-trigger',context)).on('click',function(e){e.preventDefault();const $trigger=$(this);const modalId=$trigger.data('modal-id');const tabName=$trigger.data('tab');if(!modalId)return;const disclaimerEnabled=drupalSettings?.aiAnalysis?.disclaimerEnabled;if(disclaimerEnabled&&!hasAcceptedDisclaimer()){const entityId=modalId.replace('ai-analysis-modal-','');const $disclaimerModal=$('#ai-disclaimer-modal-'+entityId);if($disclaimerModal.length){$disclaimerModal.find('.ai-disclaimer-accept-btn').data('pending-tab',tabName);let disclaimerInstance=bootstrap.Modal.getInstance($disclaimerModal[0]);if(!disclaimerInstance)disclaimerInstance=new bootstrap.Modal($disclaimerModal[0]);disclaimerInstance.show();return;}}const ctx=Drupal.aiTracking.getContext($trigger[0]);const tabEvent=Drupal.aiTracking.getTabEvent(tabName);if(tabEvent)Drupal.aiTracking.track(tabEvent.event,{ai_category:tabEvent.category,ai_action:'open',ai_entity_id:ctx.ai_entity_id,ai_node_id:ctx.ai_node_id});openAiAnalysisModal(modalId,tabName);});$(once('ai-analysis-carousel','.ai-analysis-display .carousel',context)).on('slid.bs.carousel',function(e){const $carousel=$(this);const $tabPane=$carousel.closest('.tab-pane');const $activeItem=$carousel.find('.carousel-item.active');const caption=$activeItem.data('caption');const downloadUrl=$activeItem.data('download-url');const downloadName=$activeItem.data('download-name');$tabPane.find('.ai-analysis-caption').text(caption);const $downloadBtn=$tabPane.find('.ai-analysis-download-btn');$downloadBtn.attr('href',downloadUrl);$downloadBtn.attr('download',downloadName);const $remixBtn=$tabPane.find('.ai-analysis-viz-remix');if($remixBtn.length)$remixBtn.attr('data-filename',downloadName);const slideIndex=e.to;const $indicators=$tabPane.find('.carousel-indicators button');$indicators.removeClass('active').removeAttr('aria-current');$indicators.eq(slideIndex).addClass('active').attr('aria-current','true');});const AI_VIZ_LAYOUTS={CAROUSEL:'carousel',GRID:'grid',LIST:'list'};const AI_VIZ_LAYOUT_COOKIE='ai_viz_layout_preference';const AI_VIZ_LAYOUT_COOKIE_DAYS=7;function setVisualizationLayout($container,layout,savePreference=false){if(!$container.length)return;const $tabPane=$container.closest('.tab-pane');if(layout!==AI_VIZ_LAYOUTS.CAROUSEL){const $slideshowBtn=$tabPane.find('.ai-analysis-slideshow-btn.playing');if($slideshowBtn.length)stopSlideshow($slideshowBtn);}$container.attr('data-layout',layout);const $toggleBtns=$tabPane.find('.ai-viz-layout-toggle .btn');$toggleBtns.removeClass('active');$toggleBtns.filter(`[data-layout="${layout}"]`).addClass('active');if(savePreference)setCookie(AI_VIZ_LAYOUT_COOKIE,layout,AI_VIZ_LAYOUT_COOKIE_DAYS);}$(document).on('shown.bs.tab','[id^="visualizations-tab-"]',function(){const savedLayout=getCookie(AI_VIZ_LAYOUT_COOKIE)||AI_VIZ_LAYOUTS.CAROUSEL;const targetSelector=$(this).attr('data-bs-target');const $container=$(targetSelector).find('.ai-viz-container');setVisualizationLayout($container,savedLayout);});$(once('ai-viz-layout-toggle','.ai-viz-layout-toggle .btn',context)).on('click',function(){const $btn=$(this);const layout=$btn.data('layout');const $container=$btn.closest('.tab-pane').find('.ai-viz-container');const ctx=Drupal.aiTracking.getContext($btn[0]);Drupal.aiTracking.track('ai_viz_layout_change',{ai_category:'viz',ai_action:layout,ai_entity_id:ctx.ai_entity_id,ai_node_id:ctx.ai_no
8de_id});setVisualizationLayout($container,layout,true);});$(once('ai-viz-item-click','.ai-viz-grid-item, .ai-viz-list-item',context)).on('click',function(){const $item=$(this);const index=parseInt($item.data('index'),10);const $container=$item.closest('.ai-viz-container');setVisualizationLayout($container,AI_VIZ_LAYOUTS.CAROUSEL,true);const carouselEl=$container.find('.carousel')[0];if(carouselEl&&!isNaN(index)){const carousel=bootstrap.Carousel.getOrCreateInstance(carouselEl);carousel.to(index);}});if(!_notebookSessionChannel&&'BroadcastChannel' in window){_notebookSessionChannel=new BroadcastChannel('dataport-notebook-session');_notebookSessionChannel.addEventListener('message',function(event){if(event.data&&event.data.type==='session-invalidated')$('.jupyterhub-provision-btn').trigger('jupyterhub:session-invalidated');});}$(once('jupyterhub-provision-btn','.jupyterhub-provision-btn',context)).each(function(){const $button=$(this);const nodeId=$button.data('node-id');const provisionUrl=$button.data('provision-url');function setReconnectMode(jupyterUrl){$button.prop('disabled',false).removeClass('btn-primary').addClass('btn-success').data('jupyter-url',jupyterUrl).html('<i class="fas fa-plug me-1"></i>Reconnect to Notebook');if(!$button.next('.jupyterhub-session-info').length)$button.after('<div class="jupyterhub-session-info text-muted small mt-1">'+'Active session — '+'<a href="#" class="jupyterhub-new-session-link">Launch new instead</a>'+'</div>');}function setProvisionMode(){$button.removeClass('btn-success').addClass('btn-primary').removeData('jupyter-url').prop('disabled',false).html('<i class="fas fa-book me-1"></i>Launch Notebook');$button.next('.jupyterhub-session-info').remove();$button.siblings('.jupyterhub-popup-blocked, .jupyterhub-already-launching').remove();}const INFLIGHT_KEY='notebookLaunchInflight:'+nodeId;const INFLIGHT_TTL_MS=300000;function isLaunchInflight(){try{const raw=sessionStorage.getItem(INFLIGHT_KEY);if(!raw)return false;const ts=parseInt(raw,10);if(isNaN(ts)||(Date.now()-ts)>INFLIGHT_TTL_MS){sessionStorage.removeItem(INFLIGHT_KEY);return false;}return true;}catch(e){return false;}}function markLaunchInflight(){try{sessionStorage.setItem(INFLIGHT_KEY,String(Date.now()));}catch(e){}}function clearLaunchInflight(){try{sessionStorage.removeItem(INFLIGHT_KEY);}catch(e){}}function showAlreadyLaunchingNotice(){$button.siblings('.jupyterhub-already-launching').remove();const note=$('<div class="alert alert-info alert-dismissible fade show mt-2 jupyterhub-already-launching">').html('<i class="fas fa-info-circle me-2"></i>'+'A notebook is already opening for this dataset. Check your other browser tabs, or dismiss the existing notice to try again.'+'<button type="button" class="btn-close" data-bs-dismiss="alert"></button>');$button.after(note);}function showPopupBlockedAlert(url){$button.siblings('.jupyterhub-popup-blocked').remove();const alert=$('<div class="alert alert-warning alert-dismissible fade show mt-2 jupyterhub-popup-blocked">').html('<i class="fas fa-exclamation-triangle me-2"></i>'+'Your browser blocked the notebook from opening. '+'Please allow pop-ups for this site, or '+'<a target="_blank" class="alert-link">click here to open your notebook</a>.'+'<button type="button" class="btn-close" data-bs-dismiss="alert"></button>');const $link=alert.find('a').attr('href',url);$link.on('click',function(e){if($link.data('clicked')){e.preventDefault();return;}$link.data('clicked',true).css('pointer-events','none').addClass('disabled');});alert.on('closed.bs.alert',function(){clearLaunchInflight();setProvisionMode();});$button.after(alert);}$button.on('jupyterhub:session-invalidated',function(){clearLaunchInflight();$button.siblings('.jupyterhub-popup-blocked, .jupyterhub-already-launching').remove();setProvisionMode();});$button.on('click',function(e){e.preventDefault();if(isLaunchInflight()){showAlreadyLaunchingNotice();return;}const jupyterUrl=$button.data('jupyter-url');if(jupyterUrl){const newWindow=window.open(jupyterUrl,'_blank');if(!newWindow||newWindow.closed||typeof newWindow.closed==='undefined'){markLaunchInflight();showPopupBlockedAlert(jupyterUrl);$button.prop('disabled',true).html('<i class="fas fa-external-link-alt me-1"></i>Notebook ready — click the link above');}return;}var NOTEBOOK_CONSENT_COOKIE='notebook_consent_accepted';var $consentModal=$('#notebookConsentModal');if($consentModal.length&&getCookie(NOTEBOOK_CONSENT_COOKIE)!=='true'){$consentModal.data('pending-button',$button);var consentInstance=bootstrap.Modal.getInstance($consentModal[0]);if(!consentInstance)consentInstance=new bootstrap.Modal($consentModal[0]);consentInstance.show();return;}if(!provisionUrl){console.error('Provision URL not found');return;}Drupal.notebookTracking.track('notebook_launch_click',{nb_category:'launch',nb_action:'click',nb_node_id:nodeId});markLaunchInflight();const notebookWindow=window.open('about:blank','_blank');const preOpenSucceeded=!!(notebookWindow&&!notebookWindow.closed);
8if(preOpenSucceeded){notebookWindow.document.write('<!DOCTYPE html><html><head><title>IEEE DataPort</title></head>'+'<body style="margin:0;display:flex;align-items:center;justify-content:center;'+'min-height:100vh;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif;'+'background:#f8f9fa;color:#333">'+'<div style="text-align:center">'+'<div style="width:40px;height:40px;border:4px solid #e9ecef;border-top-color:#0d6efd;'+'border-radius:50%;animation:spin 1s linear infinite;margin:0 auto 16px"></div>'+'<p style="font-size:16px;margin:0">Provisioning your notebook…</p>'+'</div>'+'<style>@keyframes spin{to{transform:rotate(360deg)}}</style>'+'</body></html>');notebookWindow.document.close();}$button.prop('disabled',true).html('<i class="fas fa-spinner fa-spin me-1"></i>Provisioning...');function doProvision(){$.ajax({url:provisionUrl,type:'POST',dataType:'json',headers:{'X-Requested-With':'XMLHttpRequest'},success:function(response){if(response.success){Drupal.notebookTracking.track('notebook_provision_success',{nb_category:'provision',nb_action:'success',nb_node_id:nodeId});const message=$('<div class="alert alert-success alert-dismissible fade show mt-2">').html('<i class="fas fa-check-circle me-2"></i>'+response.message+'<button type="button" class="btn-close" data-bs-dismiss="alert"></button>');$button.after(message);if(response.notebook_url)if(preOpenSucceeded&&!notebookWindow.closed){notebookWindow.location.href=response.notebook_url;clearLaunchInflight();setProvisionMode();}else{showPopupBlockedAlert(response.notebook_url);$button.prop('disabled',true).html('<i class="fas fa-external-link-alt me-1"></i>Notebook ready — click the link above');}else{if(preOpenSucceeded&&!notebookWindow.closed)notebookWindow.close();clearLaunchInflight();setProvisionMode();}}else{Drupal.notebookTracking.track('notebook_provision_error',{nb_category:'provision',nb_action:'error',nb_node_id:nodeId,nb_detail:'app:'+(response.message||'unknown')});if(preOpenSucceeded&&!notebookWindow.closed)notebookWindow.close();const message=$('<div class="alert alert-danger alert-dismissible fade show mt-2">').html('<i class="fas fa-exclamation-triangle me-2"></i>'+(response.message||'Failed to provision notebook.')+'<button type="button" class="btn-close" data-bs-dismiss="alert"></button>');$button.after(message);clearLaunchInflight();setProvisionMode();}},error:function(xhr){if(preOpenSucceeded&&!notebookWindow.closed)notebookWindow.close();let errorMsg='An error occurred while provisioning the notebook.';if(xhr.responseJSON&&xhr.responseJSON.message)errorMsg=xhr.responseJSON.message;const detailMsg=(xhr.responseJSON&&xhr.responseJSON.message)||xhr.statusText||'network_error';Drupal.notebookTracking.track('notebook_provision_error',{nb_category:'provision',nb_action:'error',nb_node_id:nodeId,nb_detail:xhr.status+':'+detailMsg});const message=$('<div class="alert alert-danger alert-dismissible fade show mt-2">').html('<i class="fas fa-exclamation-triangle me-2"></i>'+errorMsg+'<button type="button" class="btn-close" data-bs-dismiss="alert"></button>');$button.after(message);clearLaunchInflight();setProvisionMode();}});}if(nodeId)$.ajax({url:'/api/jupyterhub/status/'+nodeId,method:'GET',dataType:'json',timeout:5000,success:function(statusData){if(statusData.active&&statusData.status==='ACTIVE')if(preOpenSucceeded&&!notebookWindow.closed){notebookWindow.location.href=statusData.jupyter_url;setReconnectMode(statusData.jupyter_url);clearLaunchInflight();}else if(!preOpenSucceeded){showPopupBlockedAlert(statusData.jupyter_url);setReconnectMode(statusData.jupyter_url);$button.prop('disabled',true).html('<i class="fas fa-external-link-alt me-1"></i>Notebook ready — click the link above');}else{setReconnectMode(statusData.jupyter_url);clearLaunchInflight();}else doProvision();},error:function(){doProvision();}});else doProvision();});$button.parent().on('click','.jupyterhub-new-session-link',function(e){e.preventDefault();clearLaunchInflight();$button.siblings('.jupyterhub-popup-blocked, .jupyterhub-already-launching').remove();setProvisionMode();});if(nodeId)$.ajax({url:'/api/jupyterhub/status/'+nodeId,method:'GET',dataType:'json',timeout:5000,success:function(data){if(data.active&&data.status==='ACTIVE')setReconnectMode(data.jupyter_url);else{if(data.active&&data.status==='PROVISIONING'){$button.prop('disabled',true).html('<i class="fas fa-spinner fa-spin me-1"></i>Notebook Starting...');var MAX_POLL_COUNT=24;var pollCount=0;var poll=setInterval(function(){pollCount++;
8if(pollCount>=MAX_POLL_COUNT){clearInterval(poll);setProvisionMode();return;}$.ajax({url:'/api/jupyterhub/status/'+nodeId,method:'GET',dataType:'json',timeout:5000,success:function(pollData){if(pollData.active&&pollData.status==='ACTIVE'){clearInterval(poll);setReconnectMode(pollData.jupyter_url);}else{if(!pollData.active){clearInterval(poll);setProvisionMode();}}}});},5000);}}}});});$(once('notebook-logout-broadcast','a[href*="/user/logout"]',context)).on('click',function(){if('BroadcastChannel' in window){const ch=new BroadcastChannel('dataport-notebook-session');ch.postMessage({type:'session-invalidated'});ch.close();}});$(once('ai-generate-btn','.ai-analysis-generate-btn',context)).on('click',function(e){e.preventDefault();const $button=$(this);const generateUrl=$button.data('generate-url');if(!generateUrl){console.error('Generate URL not found');return;}const ctx=Drupal.aiTracking.getContext($button[0]);Drupal.aiTracking.track('ai_admin_action',{ai_category:'admin',ai_action:'generate',ai_entity_id:ctx.ai_entity_id,ai_node_id:ctx.ai_node_id});$button.prop('disabled',true).html('<i class="fas fa-spinner fa-spin me-1"></i>Generating...');$.ajax({url:generateUrl,type:'POST',dataType:'json',headers:{'X-Requested-With':'XMLHttpRequest'},success:function(response){if(response.success){const message=$('<div class="alert alert-success alert-dismissible fade show mt-2">').html('<i class="fas fa-check-circle me-2"></i>'+response.message+'<button type="button" class="btn-close" data-bs-dismiss="alert"></button>');$button.closest('.ai-analysis-section').after(message);setTimeout(function(){location.reload();},2000);}else{const message=$('<div class="alert alert-danger alert-dismissible fade show mt-2">').html('<i class="fas fa-exclamation-triangle me-2"></i>'+(response.message||'Failed to initiate AI content generation.')+'<button type="button" class="btn-close" data-bs-dismiss="alert"></button>');$button.closest('.ai-analysis-section').after(message);$button.prop('disabled',false).html('<i class="fas fa-magic me-1"></i>Generate AI Content');}},error:function(xhr){let errorMsg='An error occurred while generating AI content.';if(xhr.responseJSON&&xhr.responseJSON.message)errorMsg=xhr.responseJSON.message;const message=$('<div class="alert alert-danger alert-dismissible fade show mt-2">').html('<i class="fas fa-exclamation-triangle me-2"></i>'+errorMsg+'<button type="button" class="btn-close" data-bs-dismiss="alert"></button>');$button.closest('.ai-analysis-section').after(message);$button.prop('disabled',false).html('<i class="fas fa-magic me-1"></i>Generate AI Content');}});});$(once('ai-report-dashboard-toc','.ai-report-dashboard',context)).each(function(){const $dashboard=$(this);const entityId=$dashboard.data('entity-id');const $content=$dashboard.find('.ai-report-content');const $tocNav=$dashboard.find('.ai-toc-nav');if(!$content.length||!$tocNav.length)return;const $headers=$content.find('h2');if($headers.length===0){$dashboard.find('.ai-toc-wrapper').hide();$dashboard.find('.ai-report-scroll-area').removeClass('col-md-10').addClass('col-md-12');return;}$headers.each(function(index){const $header=$(this);const headerId='ai-section-'+entityId+'-'+index;let headerText=$header.text().trim();headerText=headerText.replace(/^\d+(\.\d+)*\.?\s*/,'');if(headerText.length>35)headerText=headerText.substring(0,32)+'...';$header.attr('id',headerId);const $tocItem=$('<li class="nav-item"></li>');const $tocLink=$('<a class="nav-link py-1" href="#'+headerId+'"></a>').text(headerText);$tocItem.append($tocLink);$tocNav.append($tocItem);});const $scrollArea=$dashboard.find('.ai-report-scroll-area');$tocNav.on('click','a.nav-link',function(e){e.preventDefault();const targetId=$(this).attr('href');const $target=$(targetId);if($target.length&&$scrollArea.length){const targetOffset=$target.offset().top;const scrollAreaOffset=$scrollArea.offset().top;const currentScroll=$scrollArea.scrollTop();const scrollTo=currentScroll+(targetOffset-scrollAreaOffset)-20;$scrollArea.animate({scrollTop:scrollTo},300);}});});$(once('ai-about-dashboard-toc','.ai-about-dashboard',context)).each(function(){const $dashboard=$(this);const entityId=$dashboard.data('entity-id');const $content=$dashboard.find('.ai-about-content');const $tocNav=$dashboard.find('.ai-about-toc-nav');
8if(!$content.length||!$tocNav.length)return;const $headers=$content.find('h2');if($headers.length===0){$dashboard.find('.ai-about-toc-wrapper').hide();$dashboard.find('.ai-about-scroll-area').removeClass('col-md-10').addClass('col-md-12');return;}$headers.each(function(index){const $header=$(this);const headerId=$header.attr('id')||'ai-about-section-'+entityId+'-'+index;let headerText=$header.text().trim();headerText=headerText.replace(/^\d+(\.\d+)*\.?\s*/,'');if(headerText.length>35)headerText=headerText.substring(0,32)+'...';if(!$header.attr('id'))$header.attr('id',headerId);const $tocItem=$('<li class="nav-item"></li>');const $tocLink=$('<a class="nav-link py-1" href="#'+headerId+'"></a>').text(headerText);$tocItem.append($tocLink);$tocNav.append($tocItem);});const $scrollArea=$dashboard.find('.ai-about-scroll-area');$tocNav.on('click','a.nav-link',function(e){e.preventDefault();const targetId=$(this).attr('href');const $target=$(targetId);if($target.length&&$scrollArea.length){const targetOffset=$target.offset().top;const scrollAreaOffset=$scrollArea.offset().top;const currentScroll=$scrollArea.scrollTop();const scrollTo=currentScroll+(targetOffset-scrollAreaOffset)-20;$scrollArea.animate({scrollTop:scrollTo},300);}});});$(once('ai-code-dashboard-toc','.ai-code-dashboard',context)).each(function(){const $dashboard=$(this);const entityId=$dashboard.data('entity-id');const $codeBlock=$dashboard.find('pre.ai-code-content code');const $tocNav=$dashboard.find('.ai-code-toc-nav');const $scrollArea=$dashboard.find('.ai-code-scroll-area');if(!$codeBlock.length||!$tocNav.length)return;const codeText=$codeBlock.text();const codeLines=codeText.split('\n');let visualizations=[];try{const vizDataAttr=$dashboard.data('visualizations');if(vizDataAttr&&typeof vizDataAttr==='string')visualizations=JSON.parse(vizDataAttr);else{if(vizDataAttr)visualizations=vizDataAttr;}}catch(e){console.warn('Failed to parse visualization data:',e);}let tocItems=[];tocItems.push({label:'Configuration & Imports',lineIndex:0,type:'config'});codeLines.forEach((line,index)=>{const trimmedLine=line.trim();if(trimmedLine.toUpperCase()==='# QUALITY CONTROL UTILITIES'||trimmedLine.toUpperCase()==='# ============================================================================'||trimmedLine.match(/^#\s*QUALITY\s*CONTROL\s*UTILITIES/i))if(trimmedLine.toUpperCase().includes('QUALITY CONTROL'))tocItems.push({label:'Quality Control Utilities',lineIndex:index,type:'qc'});});const vizPattern=/^#\s*Visualization\s+(\d+)\s*:/i;codeLines.forEach((line,index)=>{const match=line.trim().match(vizPattern);if(match){const vizNumber=match[1];tocItems.push({label:'Visualization '+vizNumber,lineIndex:index,type:'visualization',vizNumber});}});if(tocItems.length<=1){$dashboard.find('.ai-code-toc-wrapper').hide();$scrollArea.removeClass('col-md-10').addClass('col-md-12');return;}tocItems.forEach((item,idx)=>{const $tocItem=$('<li class="nav-item"></li>');const searchText=codeLines[item.lineIndex]||'';const $tocLink=$('<a class="nav-link py-1" href="#"></a>').attr('data-search-text',searchText.trim()).attr('data-line-index',item.lineIndex);if(item.type==='visualization'){$tocLink.text(item.label);const $icons=$('<small class="pe-1 float-end"></small>');$icons.append('<i class="fas fa-code me-2" title="Jump to code"></i>');$icons.append('<i class="fas fa-search-plus ai-viz-zoom-icon" title="View full size"></i>');$tocLink.append($icons);$tocItem.append($tocLink);const vizMatch=visualizations.find((v)=>parseInt(v.number,10)===parseInt(item.vizNumber,10));if(vizMatch){const $vizPreview=$('<img loading="lazy" />').addClass('mt-3 mb-1 img-fluid ai-viz-preview-img').attr('src',vizMatch.preview_url).attr('alt',vizMatch.alt||vizMatch.name).attr('data-full-url',vizMatch.full_url).attr('data-viz-name',vizMatch.name);const $imgWrapper=$('<div class="px-2"></div>').append($vizPreview);$tocItem.prepend($imgWrapper);}}else{$tocLink.text(item.label);$tocItem.append($tocLink);}$tocNav.append($tocItem);});$tocNav.on('click','a.nav-link',function(e){e.preventDefault();const searchText=$(this).data('search-text');const lineIndex=parseInt($(this).data('line-index'),10);if($scrollArea.length){if(lineIndex===0)$scrollArea.stop().animate({scrollTop:0},300);else{if(searchText){const codeElement=$codeBlock[0];const fullText=codeElement.textContent;const textIndex=fullText.indexOf(searchText);if(textIndex!==-1){const range=document.createRange();const treeWalker=document.createTreeWalker(codeElement,NodeFilter.SHOW_TEXT,null,fal
8se);let currentIndex=0;let foundNode=null;let foundOffset=0;while(treeWalker.nextNode()){const node=treeWalker.currentNode;const nodeLength=node.textContent.length;if(currentIndex+nodeLength>textIndex){foundNode=node;foundOffset=textIndex-currentIndex;break;}currentIndex+=nodeLength;}if(foundNode){range.setStart(foundNode,foundOffset);range.setEnd(foundNode,Math.min(foundOffset+searchText.length,foundNode.textContent.length));const rect=range.getBoundingClientRect();const scrollAreaRect=$scrollArea[0].getBoundingClientRect();const currentScroll=$scrollArea.scrollTop();const scrollTo=currentScroll+(rect.top-scrollAreaRect.top)-20;$scrollArea.stop().animate({scrollTop:Math.max(0,scrollTo)},300);}}}}$tocNav.find('.nav-link').removeClass('active');$(this).addClass('active');}});if(!$('.ai-viz-zoom-overlay').length){const $overlay=$('<div class="ai-viz-zoom-overlay"></div>');const $overlayImg=$('<img class="ai-viz-zoom-img" />');const $closeBtn=$('<button class="ai-viz-zoom-close" title="Close">×</button>');$overlay.append($overlayImg).append($closeBtn);$('body').append($overlay);$overlay.on('click',function(){$(this).removeClass('active');});}$tocNav.on('click','.ai-viz-zoom-icon',function(e){e.preventDefault();e.stopPropagation();const $img=$(this).closest('.nav-item').find('.ai-viz-preview-img');const fullUrl=$img.data('full-url');const vizName=$img.data('viz-name');if(fullUrl){$('.ai-viz-zoom-overlay img').attr('src',fullUrl).attr('alt',vizName||'');$('.ai-viz-zoom-overlay').addClass('active');}});$tocNav.on('click','.ai-viz-preview-img',function(e){e.preventDefault();$(this).closest('.nav-item').find('a.nav-link').trigger('click');});});$(once('ai-tab-footer-toggle','button[data-bs-toggle="tab"]',context)).on('shown.bs.tab',function(e){const $tab=$(this);const targetId=$tab.data('bs-target');const $modal=$tab.closest('.modal');const $copyBtn=$modal.find('.ai-code-copy-btn');const $downloadBtn=$modal.find('.ai-analysis-download-btn');if(targetId&&targetId.includes('code-pane'))$copyBtn.removeClass('d-none');else $copyBtn.addClass('d-none');if(targetId&&targetId.includes('chat-pane'))$downloadBtn.addClass('d-none');else $downloadBtn.removeClass('d-none');});$(once('ai-publish-btn','.ai-analysis-publish-btn',context)).on('click',function(e){e.preventDefault();const $button=$(this);const entityId=$button.data('entity-id');const publishUrl=$button.data('publish-url');if(!confirm('Are you sure you want to publish this AI analysis?'))return;const ctx=Drupal.aiTracking.getContext($button[0]);Drupal.aiTracking.track('ai_admin_action',{ai_category:'admin',ai_action:'publish',ai_entity_id:entityId,ai_node_id:ctx.ai_node_id});$button.prop('disabled',true).html('<span class="spinner-border spinner-border-sm me-1"></span>Publishing...');$.ajax({url:publishUrl,type:'POST',dataType:'json',headers:{'X-Requested-With':'XMLHttpRequest'},success:function(response){if(response.status==='success'){const $bubble=$button.closest('.speech-bubble-up');$bubble.prepend('<div class="alert alert-success alert-dismissible fade show mb-2" role="alert">'+'<i class="fas fa-check-circle me-1"></i>'+response.message+'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>'+'</div>');setTimeout(function(){window.location.reload();},1500);}else showManagementError($button,response.message||'Failed to publish.');},error:function(xhr){let errorMsg='Failed to publish AI analysis.';if(xhr.responseJSON&&xhr.responseJSON.message)errorMsg=xhr.responseJSON.message;showManagementError($button,errorMsg);}});});$(once('ai-unpublish-btn','.ai-analysis-unpublish-btn',context)).on('click',function(e){e.preventDefault();const $button=$(this);const entityId=$button.data('entity-id');const requiresComment=$button.data('requires-comment')==='true'||$button.data('requires-comment')===true;const $modal=$('#ai-unpublish-modal-'+entityId);if(!$modal.length)return;$modal.data('unpublish-url',$button.data('unpublish-url'));$modal.data('entity-id',entityId);$modal.data('requires-comment',requiresComment);const $requiredIndicator=$modal.find('.ai-required-indicator');const $charRequirement=$modal.find('.ai-char-requirement');if(requiresComment){$requiredIndicator.show();$charRequirement.show();}else{$requiredIndicator.hide();$charRequirement.hide();}$modal.find('.ai-unpublish-feedback').val('').removeClass('is-invalid');$modal.find('.ai-char-counter').text('0');const modalInstance=new bootstrap.Modal($modal[0]);modalInstance.show();});$(once('ai-unpublish-feedback','.ai-unpublish-feedback',context)).on('input',function(){const $textarea=$(this);const charCount=$textarea.val().length;const $modal=$textarea.closest('.modal');const $charCounter=$modal.find('.ai-char-counter');$charCounter.text(charCount);if($textarea.hasClass('is-invalid'))$textarea.removeClass('is-invalid');});$(once('ai-unpublish-submit','.ai-unpublish-submit-btn',context)).on('click',function(e){e.preventDefault();const $submitBtn=$(this);const $modal=$submitBtn.closest('.modal');const $textarea=$modal.find('.ai-unpublish-feedback');const feedback=$textarea.val().trim();const requiresComment=$modal.data('requires-comment');const unpublishUrl=$modal.data('unpublish-url');const entityId=$modal.data('entity-id');const minFeedbackLength=drupalSettings.aiAnalysis?.minFeedbackLength||10;if(requiresComment&&feedback.length<minFeedbackLength){$textarea.addClass('is-invalid');return;}Drupal.aiTracking.track('ai_admin_action',{ai_category:'admin',ai_action:'unpublish',ai_entity_id:entityId,ai_node_id:Drupal.aiTracking.getContext($modal[0]).ai_node_id});$submitBtn.prop('disabled',true).html('<span class="spinner-border spinner-border-sm me-1"></span>Unpublishing...');$.ajax({url:unpublishUrl,type:'POST',data:{feedback},dataType:'json',headers:{'X-Requested-With':'XMLHttpRequest'},success:function(response){if(response.status==='success'){const modalInstance=bootstrap.Modal.getInstance($modal[0]);if(modalInstance)modalInstance.hide();const $bubble=$('.speech-bubble-up');$bubble.prepend('<div class="alert alert-success alert-dismissible fade show mb-2" role="alert">'+'<i class="fas fa-check-circle me-1"></i>'+response.message+'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>'+'</div>');setTimeout(function(){window.location.reload();},1500);}else{showModalError($modal,response.message||'Failed to unpublish.');$submitBtn.prop('disabled',false).html('<i class="fas fa-eye-slash me-1"></i>Unpublish');}},error:function(xhr){let errorMsg='Failed to unpublish AI analysis.';if(xhr.responseJSON&&xhr.responseJSON.message)errorMsg=xhr.responseJSON.message;showModalError($modal,errorMsg);$submitBtn.prop('disabled',false).html('<i class="fas fa-eye-slash me-1"></i>Unpublish');}});});$(once('ai-regenerate-btn','.ai-analysis-regenerate-btn',context)).on('click',function(e){e.preventDefault();const $button=$(this);const entityId=$button.data('entity-id');const regenerateUrl=$button.data('regenerate-url');if(!confirm('Are you sure you want to regenerate this AI analysis? This may take 5-15 minutes.'))return;const ctx=Drupal.aiTracking.getContext($button[0]);Drupal.aiTracking.track('ai_admin_action',{ai_category:'admin',ai_action:'regenerate',ai_entity_id:entityId,ai_node_id:ctx.ai_no
8de_id});$button.prop('disabled',true);$button.find('.ai-regenerate-spinner').removeClass('d-none');const originalHtml=$button.html();$.ajax({url:regenerateUrl,type:'POST',dataType:'json',headers:{'X-Requested-With':'XMLHttpRequest'},success:function(response){if(response.status==='success'){const $bubble=$button.closest('.speech-bubble-up');$bubble.prepend('<div class="alert alert-info alert-dismissible fade show mb-2" role="alert">'+'<i class="fas fa-info-circle me-1"></i>'+response.message+'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>'+'</div>');$button.html('<i class="fas fa-clock me-1"></i>Regeneration Pending...');}else{showManagementError($button,response.message||'Failed to trigger regeneration.');$button.prop('disabled',false).html(originalHtml);$button.find('.ai-regenerate-spinner').addClass('d-none');}},error:function(xhr){let errorMsg='Failed to trigger regeneration.';if(xhr.responseJSON&&xhr.responseJSON.message)errorMsg=xhr.responseJSON.message;showManagementError($button,errorMsg);$button.prop('disabled',false).html(originalHtml);$button.find('.ai-regenerate-spinner').addClass('d-none');}});});function showDownloadError($button,message){const $container=$button.closest('.card-body, .modal-footer, div');const $existingAlert=$container.find('.alert-danger.ai-download-error');if($existingAlert.length){$existingAlert.find('.ai-error-message').text(message);return;}const $alert=$(`<div class="alert alert-danger alert-dismissible fade show ai-download-error mb-2" role="alert"> 9 <i class="fas fa-exclamation-circle me-1"></i><span class="ai-error-message">${message}</span> 10 <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> 11 </div>`);$button.before($alert);setTimeout(function(){$alert.alert('close');},5000);}$(once('ai-download-btn','.ai-analysis-download-btn',context)).on('click',function(e){const $button=$(this);const downloadUrl=$button.data('download-url');const ctx=Drupal.aiTracking.getContext($button[0]);if(!downloadUrl){Drupal.aiTracking.track('ai_viz_download',{ai_category:'viz',ai_action:'download',ai_entity_id:ctx.ai_entity_id,ai_node_id:ctx.ai_node_id});return;}Drupal.aiTracking.track('ai_code_download',{ai_category:'code',ai_action:'download_package',ai_entity_id:ctx.ai_entity_id,ai_node_id:ctx.ai_node_id});e.preventDefault();const BUTTON_RESET_DELAY_MS=1500;const originalHtml=$button.html();const originalWidth=$button.outerWidth();const resetButton=()=>{$button.prop('disabled',false).css('min-width','').html(originalHtml);};$button.css('min-width',originalWidth+'px');$button.prop('disabled',true).html('<span class="spinner-border spinner-border-sm me-1"></span>Preparing...');$.ajax({url:downloadUrl,type:'GET',dataType:'json',headers:{'X-Requested-With':'XMLHttpRequest'},success:function(response){if(response.status==='success'&&response.download_url){window.location.href=response.download_url;setTimeout(resetButton,BUTTON_RESET_DELAY_MS);}else{showDownloadError($button,response.message||'Failed to prepare download.');resetButton();}},error:function(xhr){let errorMsg='Failed to prepare download.';if(xhr.status===404)errorMsg='Download package not available.';else{if(xhr.responseJSON&&xhr.responseJSON.message)errorMsg=xhr.responseJSON.message;}showDownloadError($button,errorMsg);resetButton();}});});function showManagementError($button,message){const $bubble=$button.closest('.speech-bubble-up');const $existingAlert=$bubble.find('.alert-danger');if($existingAlert.length)$existingAlert.remove();$bubble.prepend('<div class="alert alert-danger alert-dismissible fade show mb-2" role="alert">'+'<i class="fas fa-exclamation-triangle me-1"></i>'+message+'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>'+'</div>');$button.prop('disabled',false);}function showModalError($modal,message){const $modalBody=$modal.find('.modal-body');const $existingAlert=$modalBody.find('.alert-danger');if($existingAlert.length)$existingAlert.remove();$modalBody.prepend('<div class="alert alert-danger alert-dismissible fade show" role="alert">'+'<i class="fas fa-exclamation-triangle me-1"></i>'+message+'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>'+'</div>');}}};Drupal.behaviors.categoriesToggle={attach:function(context){$(once('categories-toggle','.view-display-id-block_2 ul.list-group',context)).each(function(){var $list=$(this);var $items=$list.children('li');var limit=10;if($items.length<=limit)return;$items.slice(limit).hide();var $btn=$('<a href="#" class="see-all-categories pager__item">See All Categories</a>');$list.after($btn);$btn.on('click',function(e){e.preventDefault();$items.slice(limit).show();$btn.remove();});});}};$(document).on('shown.bs.tab','.ai-analysis-modal-wide button[data-bs-toggle="tab"]',function(){const $tab=$(this);const $modal=$tab.closest('.modal');if($modal.data('_skipNextTabTrack')){$modal.removeData('_skipNextTabTrack');return;}const tabName=Drupal.aiTracking.getTabNameFromPaneId($tab.data('bs-target')||'');if(tabName){const tabEvent=Drupal.aiTracking.getTabEvent(tabName);if(tabEvent){const ctx=Drupal.aiTracking.getContext($tab[0]);Drupal.aiTracking.track(tabEvent.event,{ai_category:tabEvent.category,ai_action:'tab_switch',ai_entity_id:ctx.ai_entity_id,ai_node_id:ctx.ai_no
11de_id});}}});$(document).on('click','[id^="ai-disclaimer-modal-"] .btn-secondary[data-bs-dismiss="modal"]',function(){const ctx=Drupal.aiTracking.getContext(this);Drupal.aiTracking.track('ai_disclaimer_response',{ai_category:'disclaimer',ai_action:'decline',ai_entity_id:ctx.ai_entity_id,ai_node_id:ctx.ai_node_id});});function exitFullscreenMode($modal){$modal.removeClass('ai-analysis-fullscreen-mode');$(document).off('keydown.aiFullscreen');const $fullscreenBtn=$modal.find('.ai-analysis-fullscreen-btn');$fullscreenBtn.find('i').removeClass('fa-compress').addClass('fa-expand');$fullscreenBtn.attr({'title':'Fullscreen','aria-label':'Enter fullscreen','aria-pressed':'false'});}$(document).on('click','.ai-analysis-slideshow-btn',function(e){e.preventDefault();e.stopPropagation();const $btn=$(this);const carouselId=$btn.data('carousel-id');const $carousel=$('#'+carouselId);if(!$carousel.length||typeof bootstrap==='undefined')return;let carouselInstance=bootstrap.Carousel.getInstance($carousel[0]);if(!carouselInstance)carouselInstance=new bootstrap.Carousel($carousel[0],{interval:false,ride:false,wrap:true});const isPlaying=$btn.hasClass('playing');const ctx=Drupal.aiTracking.getContext($btn[0]);Drupal.aiTracking.track('ai_viz_slideshow',{ai_category:'viz',ai_action:isPlaying?'pause':'play',ai_entity_id:ctx.ai_entity_id,ai_node_id:ctx.ai_node_id});if(isPlaying){const intervalId=$btn.data('slideshow-interval');if(intervalId){clearInterval(intervalId);$btn.removeData('slideshow-interval');}$btn.removeClass('playing');$btn.find('i').removeClass('fa-pause').addClass('fa-play');$btn.attr({'title':'Play slideshow','aria-label':'Play slideshow','aria-pressed':'false'});}else{const intervalId=setInterval(function(){carouselInstance.next();},4000);$btn.data('slideshow-interval',intervalId);$btn.addClass('playing');$btn.find('i').removeClass('fa-play').addClass('fa-pause');$btn.attr({'title':'Pause slideshow','aria-label':'Pause slideshow','aria-pressed':'true'});}});$(document).on('click','.ai-analysis-fullscreen-btn',function(e){e.preventDefault();e.stopPropagation();const $btn=$(this);const modalId=$btn.data('modal-id');const $modal=$('#'+modalId);if(!$modal.length)return;const isFullscreen=$modal.hasClass('ai-analysis-fullscreen-mode');const ctx=Drupal.aiTracking.getContext($btn[0]);Drupal.aiTracking.track('ai_viz_fullscreen',{ai_category:'viz',ai_action:isFullscreen?'exit':'enter',ai_entity_id:ctx.ai_entity_id,ai_node_id:ctx.ai_node_id});if(isFullscreen)exitFullscreenMode($modal);else{$modal.addClass('ai-analysis-fullscreen-mode');$btn.find('i').removeClass('fa-expand').addClass('fa-compress');$btn.attr({'title':'Exit fullscreen','aria-label':'Exit fullscreen','aria-pressed':'true'});$(document).on('keydown.aiFullscreen',function(e){if(e.key==='Escape'){e.preventDefault();e.stopPropagation();exitFullscreenMode($modal);}});}});$(document).on('click','.ai-analysis-fullscreen-exit-btn',function(e){e.preventDefault();const $modal=$(this).closest('.modal');exitFullscreenMode($modal);});function stopSlideshow($slideshowBtn){if(!$slideshowBtn||!$slideshowBtn.length)return;const intervalId=$slideshowBtn.data('slideshow-interval');if(intervalId){clearInterval(intervalId);$slideshowBtn.removeData('slideshow-interval');}$slideshowBtn.removeClass('playing');$slideshowBtn.find('i').removeClass('fa-pause').addClass('fa-play');$slideshowBtn.attr({'title':'Play slideshow','aria-label':'Play slideshow','aria-pressed':'false'});}$(document).on('hidden.bs.modal','.modal',function(){const $modal=$(this);stopSlideshow($modal.find('.ai-analysis-slideshow-btn.playing'));if($modal.hasClass('ai-analysis-fullscreen-mode'))exitFullscreenMode($modal);});$(document).on('hide.bs.tab','[id^="visualizations-tab-"]',function(){const $modal=$(this).closest('.modal');stopSlideshow($modal.find('.ai-analysis-slideshow-btn.playing'));});function initializeScrollSpy($dashboard,scrollAreaClass,tocNavClass){if(!$dashboard.length||typeof bootstrap==='undefined'||!bootstrap.ScrollSpy)return;const scrollSpyEl=$dashboard.find(scrollAreaClass)[0];const tocNavEl=$dashboard.find(tocNavClass)[0];if(scrollSpyEl&&tocNavEl){let scrollSpyInstance=bootstrap.ScrollSpy.getInstance(scrollSpyEl);if(scrollSpyInstance)scrollSpyInstance.dispose();new bootstrap.ScrollSpy(scrollSpyEl,{target:tocNavEl,offset:20,smoothScroll:true});console.log('ScrollSpy initialized for',scrollAreaClass);}}$(document).on('shown.bs.tab','[id^="report-tab-"]',function(){const $modal=$(this).closest('.modal');const $dashboard=$modal.find('.ai-report-dashboard');initializeScrollSpy($dashboard,'.ai-report-scroll-area','.ai-toc-nav');});$(document).on('shown.bs.tab','[id^="about-tab-"]',function(){const $modal=$(this).closest('.modal');const $dashboard=$modal.find('.ai-about-dashboard');initializeScrollSpy($dashboard,'.ai-about-scroll-area','.ai-about-toc-nav');});$(document).on('shown.bs.modal','.modal',function(){const $modal=$(this);if($modal.attr('id')&&$modal.attr('id').indexOf('ai-analysis-modal-')===0){const $activeTabBtn=$modal.find('.nav-link.active[data-bs-toggle="tab"]');const activeTab=$activeTabBtn.length?Drupal.aiTracking.getTabNameFromPaneId($activeTabBtn.data('bs-target')||''):'';const ctx=Drupal.aiTracking.getContext($modal[0]);Drupal.aiTracking.track('ai_modal_open',{ai_category:'modal',ai_action:activeTab||'report',ai_entity_id:ctx.ai_entity_id,ai_node_id:ctx.ai_no
11de_id});}const $activeReportTab=$modal.find('[id^="report-pane-"].active');if($activeReportTab.length){const $dashboard=$activeReportTab.find('.ai-report-dashboard');initializeScrollSpy($dashboard,'.ai-report-scroll-area','.ai-toc-nav');}const $activeAboutTab=$modal.find('[id^="about-pane-"].active');if($activeAboutTab.length){const $dashboard=$activeAboutTab.find('.ai-about-dashboard');initializeScrollSpy($dashboard,'.ai-about-scroll-area','.ai-about-toc-nav');}});})(Drupal,jQuery,once);; 12(function($,Drupal,once){'use strict';const markdownToHtml=function(text){if(!text)return '';let html=text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');html=html.replace(/```(\w*)\n([\s\S]*?)```/g,function(match,lang,code){return '<pre class="ai-chat-code-block"><code class="language-'+(lang||'text')+'">'+code.trim()+'</code></pre>';});html=html.replace(/`([^`]+)`/g,'<code class="ai-chat-inline-code">$1</code>');html=html.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>');html=html.replace(/__([^_]+)__/g,'<strong>$1</strong>');html=html.replace(/\*([^*]+)\*/g,'<em>$1</em>');html=html.replace(/_([^_]+)_/g,'<em>$1</em>');html=html.replace(/^### (.+)$/gm,'<h5 class="ai-chat-heading">$1</h5>');html=html.replace(/^## (.+)$/gm,'<h4 class="ai-chat-heading">$1</h4>');html=html.replace(/^# (.+)$/gm,'<h3 class="ai-chat-heading">$1</h3>');html=html.replace(/(?:^\s*[-*+]\s+.+\n?)+/gm,(listBlock)=>{const items=listBlock.replace(/^\s*[-*+]\s+(.+)/gm,'<li>$1</li>');return `<ul class="ai-chat-list">${items}</ul>`;});html=html.replace(/(?:^\s*\d+\.\s+.+\n?)+/gm,(listBlock)=>{const items=listBlock.replace(/^\s*\d+\.\s+(.+)/gm,'<li>$1</li>');return `<ol class="ai-chat-list">${items}</ol>`;});html=html.replace(/\[([^\]]+)\]\(([^)]+)\)/g,'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');html=html.replace(/(?:^|\n)(\|[^\n]+\|\s*\n)(\|[\s:|-]+\|\s*\n)((?:\|[^\n]+\|\s*\n?)+)/gm,(match,headerRow,separatorRow,bodyRows)=>{const separatorCells=separatorRow.replace(/^\||\|$/g,'').split('|');const alignments=separatorCells.map((cell)=>{const trimmedCell=cell.trim();if(trimmedCell.startsWith(':')&&trimmedCell.endsWith(':'))return 'text-center';if(trimmedCell.endsWith(':'))return 'text-right';return 'text-left';});const headerCells=headerRow.replace(/^\||\|$/g,'').split('|');const thead=`<thead><tr>${headerCells.map((cell,i)=>`<th class="${alignments[i]||'text-left'}">${cell.trim()}</th>`).join('')}</tr></thead>`;const tbodyRows=bodyRows.trim().split('\n').map((row)=>{if(!row.trim())return '';const cells=row.replace(/^\||\|$/g,'').split('|');return `<tr>${cells.map((cell,i)=>`<td class="${alignments[i]||'text-left'}">${cell.trim()}</td>`).join('')}</tr>`;}).join('');const tbody=`<tbody>${tbodyRows}</tbody>`;return `<table class="ai-chat-table">${thead}${tbody}</table>`;});html=html.replace(/\n\n/g,'</p><p>');html='<p>'+html+'</p>';html=html.replace(/<p>\s*<\/p>/g,'');html=html.replace(/<p>(<h[3-5])/g,'$1');html=html.replace(/(<\/h[3-5]>)<\/p>/g,'$1');html=html.replace(/<p>(<pre)/g,'$1');html=html.replace(/(<\/pre>)<\/p>/g,'$1');html=html.replace(/<p>(<ul)/g,'$1');html=html.replace(/(<\/ul>)<\/p>/g,'$1');html=html.replace(/<p>(<ol)/g,'$1');html=html.replace(/(<\/ol>)<\/p>/g,'$1');html=html.replace(/<p>(<table)/g,'$1');html=html.replace(/(<\/table>)<\/p>/g,'$1');return html;};const loadingMessages=['Parsing natural language intent...','Analyzing your question...','Mapping query to data schema...','Searching through dataset documentation...','Reviewing metadata and file structures...','Processing relevant information...','Resolving ambiguous timestamp formats...','Looking for null values and NaNs...','Scanning for invisible UTF-8 characters...','Aggregating metrics by category...','Detecting outliers in the distribution...','Checking floating-points...','Synthesizing analytical insights...','Drafting executable logic...','Thinking of descriptive variable names...','Ensuring zero-based indexing...','Constructing regular expressions...','Handling edge cases...','Balancing opening and closing brackets...','Trimming trailing whitespace...','Formatting code snippets...','Finally Finalizing...','Formulating a comprehensive response...','Cross-referencing data sources...','Generating insights...'];const loadingMessagesFinal=['Almost there...','Staying on target...','Just hang on...'];const streamingLoadingMessages=['Initializing...','Connecting to AI...','Reviewing reports...','Analyzing your question...','Parsing natural language intent...','Mapping query to data schema...','Searching through dataset documentation...','Reviewing metadata and file structures...','Processing relevant information...','Formulating approach...','Synthesizing analytical insights...','Drafting executable logic...','Handling edge cases...','Formatting response...','Cross-referencing data sources...','Generating insights...','Revising first draft...','Almost there...','Finalizing...','Just a moment longer...'];const toolExecutionMessages={'data_query':['Executing data query...','Scanning dataset columns...','Calculating statistics...','Aggregating results...','Finalizing calculations...'],'visualization':['Generating visualization...','Rendering chart elements...','Applying styles and colors...','Preparing image output...','Finalizing chart...'],'widget':['Building interactive widget...','Configuring Plotly components...','Setting up interactivity...','Rendering widget HTML...'],'simulation':['Running simulation...','Applying hypothetical changes...','Calculating projected values...','Generating comparison chart...'],'audit':['Auditing data quality...','Scanning for anomalies...','Checking for missing values...','Analyzing distributions...','Compiling audit report...'],'default':['Executing code...','Processing data...','Running analysis...','Computing results...']};const toolDisplayInfo={'data_query':{name:'Data Query',icon:'fa-calculator'},'visualization':{name:'Visualization',icon:'fa-chart-bar'},'widget':{name:'Interactive Widget',icon:'fa-cube'},'simulation':{name:'Simulation',icon:'fa-flask'},'audit':{name:'Data Audit',icon:'fa-search'},'default':{name:'Code Execution',icon:'fa-code'}};class DataPortChat{constructor(container){this.$container=$(container);this.nodeId=this.$container.data('node-id');this.sessionUuid=null;this.isLoading=false;this.initialized=false;this.abortController=null;this._currentPartialResponse='';this._streamCompletedProperly=false;this.inactivityTimeout=null;this.maxResponseTimeout=null;this.INACTIVITY_TIMEOUT_MS=30000;this.MAX_RESPONSE_TIMEOUT_MS=300000;this.selectedToolChoice='auto';this.selectedPurpose=null;this.codeContext=null;this.remixFilenameHint=null;this.history=[];this.rateLimits=null;this.dailyMessageCount=0;this._pruningNoticeShown=false;this.$messagesWrapper=this.$container.find('.ai-chat-messages-wrapper');this.$messages=this.$container.find('.ai-chat-messages');this.$loading=this.$container.find('.ai-chat-loading');this.$welcome=this.$container.find('.ai-chat-welcome');this.$form=this.$container.find('.ai-chat-form');
12this.$input=this.$container.find('.ai-chat-input');this.$sendBtn=this.$container.find('.ai-chat-send-btn');this.$tocPanel=this.$container.find('.ai-chat-toc-panel');this.$tocToggle=this.$tocPanel.find('.ai-chat-toc-toggle');this.$tocCollapsedToggle=this.$container.find('.ai-chat-toc-collapsed-toggle');this.$tocContent=this.$tocPanel.find('.ai-chat-toc-content');this.$tocNav=this.$tocPanel.find('.ai-chat-toc-nav');this.$clearBtn=this.$tocPanel.find('.ai-chat-clear-btn');this.$rateLimitInfo=this.$container.find('.ai-chat-rate-limit-info');this.scrollSpyObserver=null;this.bindEvents();}bindEvents(){const self=this;this.$form.on('submit',function(e){e.preventDefault();self.sendMessage();});this.$input.on('input',function(){self.updateSendButton();});this.$input.on('keydown',function(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();self.sendMessage();}});this.$clearBtn.on('click',function(){Drupal.aiTracking.track('ai_chat_clear',{ai_category:'chat',ai_action:'clear',ai_node_id:self.nodeId});self.clearHistory();});this.$sendBtn.on('click',function(e){if(self.isLoading&&self.abortController){e.preventDefault();e.stopPropagation();Drupal.aiTracking.track('ai_chat_stop',{ai_category:'chat',ai_action:'stop',ai_node_id:self.nodeId});self.stopResponse();}});this.$tocToggle.on('click',function(){self.$tocPanel.removeClass('expanded');$(this).attr('aria-expanded',false);});this.$tocCollapsedToggle.on('click',function(){self.$tocPanel.addClass('expanded');self.$tocToggle.attr('aria-expanded',true);});this.$container.on('click','.ai-chat-tool-dropdown .dropdown-item',function(e){e.preventDefault();const $item=$(this);const toolChoice=$item.data('tool-choice');const purpose=$item.data('purpose')||null;Drupal.aiTracking.track('ai_chat_tool_select',{ai_category:'chat',ai_action:toolChoice,ai_node_id:self.nodeId,ai_detail:purpose||''});self.selectedToolChoice=toolChoice;self.selectedPurpose=purpose;self.$container.find('.ai-chat-tool-dropdown .dropdown-item').removeClass('active');$item.addClass('active');self.updateToolToggleIcon(purpose);});this.bindTocClickHandler();}bindTocClickHandler(){const self=this;this.$tocNav.off('click.aiChatToc').on('click.aiChatToc','a.nav-link',function(e){e.preventDefault();const targetId=$(this).attr('href');const $target=$(targetId);if($target.length){Drupal.aiTracking.track('ai_chat_toc_click',{ai_category:'chat',ai_action:'toc_nav',ai_node_id:self.nodeId});self.scrollToMessage($target,true);self.$tocNav.find('.nav-link').removeClass('active');$(this).addClass('active');}});}updateToolToggleIcon(purpose){const $toggle=this.$container.find('.ai-chat-tool-toggle');const iconMap={'auto':'fa-magic','data_query':'fa-calculator','visualization':'fa-chart-bar','widget':'fa-cube','simulation':'fa-flask','audit':'fa-search'};const icon=purpose?iconMap[purpose]:iconMap['auto'];$toggle.find('.ai-chat-tool-icon').attr('class','fas '+icon+' ai-chat-tool-icon');}triggerVisualizationRemix(sourceCode,filename=null){this.codeContext=sourceCode;this.remixFilenameHint=filename;this.selectedToolChoice='execute_python_code';this.selectedPurpose='visualization';this.updateToolToggleIcon('visualization');this.$container.find('.ai-chat-tool-toggle').addClass('remix-active');this.$input.attr('placeholder',Drupal.t('Describe how to modify this chart...'));this.$input.focus();this.showRemixIndicator();}showRemixIndicator(){this.$container.find('.ai-chat-remix-indicator').remove();const self=this;const $indicator=$('<div>').addClass('ai-chat-remix-indicator badge bg-primary mb-2').html('<i class="fas fa-magic me-1"></i>'+Drupal.t('Remix Mode: Modifying previous chart')+'<button type="button" class="btn-close btn-close-white ms-2" aria-label="Cancel"></button>');$indicator.find('.btn-close').on('click',function(){self.clearRemixContext();});this.$form.before($indicator);}clearRemixContext(){this.codeContext=null;this.remixFilenameHint=null;this.selectedToolChoice='auto';this.selectedPurpose=null;this.updateToolToggleIcon(null);this.$container.find('.ai-chat-tool-toggle').removeClass('remix-active');this.$container.find('.ai-chat-remix-indicator').remove();this.$input.attr('placeholder',Drupal.t('Ask about this dataset...'));this.$container.find('.ai-chat-tool-dropdown .dropdown-item').removeClass('active');this.$container.find('.ai-chat-tool-dropdown .dropdown-item[data-tool-choice="auto"]').addClass('active');}renderImageWithActions($container,imageData,sourceCode,filename){const self=this;const $wrapper=$('<div>').addClass('ai-chat-image-wrapper position-relative');const $img=$('<img>').attr('src','data:image/png;base64,'+imageData).addClass('ai-chat-image img-fluid rounded').attr('alt',Drupal.t('Generated visualization')).attr('data-code-context',sourceCode||'').attr('data-filename',filename||'chart.png');const $actions=$('<div>').addClass('ai-chat-image-actions');const $downloadBtn=$('<button>').addClass('btn btn-sm btn-light ai-chat-image-download').attr('title',Drupal.t('Download image')).html('<i class="fas fa-download"></i>').on('click',function(){Drupal.aiTracking.track('ai_chat_output_action',{ai_category:'chat',ai_action:'image_download',ai_
12node_id:self.nodeId});self.downloadImage(imageData,filename||'chart.png');});const $fullscreenBtn=$('<button>').addClass('btn btn-sm btn-light ai-chat-image-fullscreen').attr('title',Drupal.t('View fullscreen')).html('<i class="fas fa-expand"></i>').on('click',function(){Drupal.aiTracking.track('ai_chat_output_action',{ai_category:'chat',ai_action:'image_fullscreen',ai_node_id:self.nodeId});self.showFullscreenImage(imageData,filename||'chart.png');});const $remixBtn=$('<button>').addClass('btn btn-sm btn-outline-primary ai-chat-image-remix').attr('title',Drupal.t('Modify this chart')).html('<i class="fas fa-magic me-1"></i>'+Drupal.t('Remix'));$remixBtn.on('click',function(){Drupal.aiTracking.track('ai_chat_output_action',{ai_category:'chat',ai_action:'image_remix',ai_node_id:self.nodeId});const code=$img.attr('data-code-context');self.triggerVisualizationRemix(code);});$actions.append($downloadBtn,$fullscreenBtn,$remixBtn);$wrapper.append($img,$actions);$container.append($wrapper);$container.addClass('has-rich-content');}downloadImage(base64Data,filename){const link=document.createElement('a');link.href='data:image/png;base64,'+base64Data;link.download=filename;link.click();}renderWidget($container,html,_sourceCode){if(!html){console.error('[AI Chat] renderWidget: html is empty or undefined!');return;}const self=this;const $wrapper=$('<div>').addClass('ai-chat-widget-wrapper');const $iframe=$('<iframe>').addClass('ai-chat-widget-iframe').attr('sandbox','allow-scripts allow-same-origin');const iframe=$iframe[0];iframe.onload=function(){const doc=iframe.contentDocument||iframe.contentWindow.document;doc.open();doc.write(` 13 <!DOCTYPE html> 14 <html> 15 <head> 16 <script src="https://cdn.plot.ly/plotly-2.27.0.min.js"><\/script> 17 <style>body { margin: 0; overflow: hidden; }</style> 18 </head> 19 <body>${html}</body> 20 </html> 21 `);doc.close();setTimeout(function(){const height=doc.body.scrollHeight||400;iframe.style.height=Math.max(height,400)+'px';},500);};$wrapper.append($iframe);$container.append($wrapper);$container.addClass('has-rich-content');const $actions=$('<div>').addClass('ai-chat-widget-actions mt-2');const $copyBtn=$('<button>').addClass('btn btn-sm btn-outline-secondary').html('<i class="fas fa-copy me-1"></i>'+Drupal.t('Copy HTML')).on('click',function(){Drupal.aiTracking.track('ai_chat_output_action',{ai_category:'chat',ai_action:'widget_copy',ai_node_id:self.nodeId});navigator.clipboard.writeText(html);$copyBtn.html('<i class="fas fa-check me-1"></i>'+Drupal.t('Copied!'));setTimeout(function(){$copyBtn.html('<i class="fas fa-copy me-1"></i>'+Drupal.t('Copy HTML'));},2000);});const $downloadBtn=$('<button>').addClass('btn btn-sm btn-outline-secondary ms-2').html('<i class="fas fa-download me-1"></i>'+Drupal.t('Download')).on('click',function(){Drupal.aiTracking.track('ai_chat_output_action',{ai_category:'chat',ai_action:'widget_download',ai_node_id:self.nodeId});self.downloadHtml(html,'widget.html');});const $fullscreenBtn=$('<button>').addClass('btn btn-sm btn-outline-secondary ms-2').html('<i class="fas fa-expand me-1"></i>'+Drupal.t('Fullscreen')).on('click',function(){Drupal.aiTracking.track('ai_chat_output_action',{ai_category:'chat',ai_action:'widget_fullscreen',ai_node_id:self.nodeId});self.showFullscreenWidget(html);});$actions.append($copyBtn,$downloadBtn,$fullscreenBtn);$wrapper.append($actions);}downloadHtml(html,filename){const fullHtml=`<!DOCTYPE html> 22<html> 23<head> 24 <meta charset="UTF-8"> 25 <title>Interactive Widget</title> 26 <script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script> 27</head> 28<body> 29${html} 30</body> 31</html>`;const blob=new Blob([fullHtml],{type:'text/html'});const url=URL.createObjectURL(blob);const link=document.createElement('a');link.href=url;link.download=filename;link.click();URL.revokeObjectURL(url);}showFullscreenImage(imageData,filename){$('.ai-chat-fullscreen-overlay').remove();const $overlay=$('<div>').addClass('ai-chat-fullscreen-overlay').html(` 32 <div class="ai-chat-fullscreen-content"> 33 <img src="data:image/png;base64,${imageData}" alt="${filename||'Visualization'}" /> 34 </div> 35 <button class="ai-chat-fullscreen-close" title="${Drupal.t('Close (Esc)')}"> 36 <i class="fas fa-times"></i> 37 </button> 38 `);$('body').append($overlay);const closeOverlay=function(){$overlay.fadeOut(150,function(){$(this).remove();});$(document).off('keydown.aiChatFullscreen');};$overlay.find('.ai-chat-fullscreen-close').on('click',closeOverlay);$overlay.on('click',function(e){if(e.target===this)closeOverlay();});$(document).on('keydown.aiChatFullscreen',function(e){if(e.key==='Escape'){e.preventDefault();closeOverlay();}});$overlay.hide().fadeIn(150);}showFullscreenWidget(html){$('.ai-chat-fullscreen-overlay').remove();const $overlay=$('<div>').addClass('ai-chat-fullscreen-overlay ai-chat-fullscreen-widget').html(` 39 <div class="ai-chat-fullscreen-content"> 40 <iframe class="ai-chat-fullscreen-iframe" sandbox="allow-scripts allow-same-origin"></iframe> 41 </div> 42 <button class="ai-chat-fullscreen-close" title="${Drupal.t('Close (Esc)')}"> 43 <i class="fas fa-times"></i> 44 </button> 45 `);$('body').append($overlay);const iframe=$overlay.find('.ai-chat-fullscreen-iframe')[0];iframe.onload=function(){const doc=iframe.contentDocument||iframe.contentWindow.document;doc.open();doc.write(` 46 <!DOCTYPE html> 47 <html> 48 <head> 49 <script src="https://cdn.plot.ly/plotly-2.27.0.min.js"><\/script> 50 <style> 51 body { margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #fff; } 52 .plotly-graph-div { width: 90vw !important; height: 85vh !important; } 53 </style> 54 </head> 55 <body>${html}</body> 56 </html> 57 `);
57doc.close();};iframe.src='about:blank';const closeOverlay=function(){$overlay.fadeOut(150,function(){$(this).remove();});$(document).off('keydown.aiChatFullscreen');};$overlay.find('.ai-chat-fullscreen-close').on('click',closeOverlay);$overlay.on('click',function(e){if(e.target===this)closeOverlay();});$(document).on('keydown.aiChatFullscreen',function(e){if(e.key==='Escape'){e.preventDefault();closeOverlay();}});$overlay.hide().fadeIn(150);}renderAuditReport($container,json,summary){const $wrapper=$('<div>').addClass('ai-chat-audit-report card');const status=json.anomalies?.length>0?'warning':'success';const statusIcon=status==='success'?'check-circle':'exclamation-triangle';const statusText=status==='success'?Drupal.t('Clean'):Drupal.t('Issues Found');const $header=$('<div>').addClass('card-header d-flex align-items-center').html(`<i class="fas fa-clipboard-check me-2"></i> 58 <strong>${Drupal.t('Data Quality Report')}</strong> 59 <span class="badge bg-${status} ms-auto"> 60 <i class="fas fa-${statusIcon} me-1"></i>${statusText} 61 </span>`);const $body=$('<div>').addClass('card-body');if(summary)$body.append($('<p>').text(summary));if(json.anomalies&&json.anomalies.length>0){const $list=$('<ul>').addClass('list-group list-group-flush');json.anomalies.forEach((anomaly)=>{const $item=$('<li>').addClass('list-group-item');let itemHtml=`<strong>${this.escapeHtml(anomaly.column||'')}</strong>: ${this.escapeHtml(anomaly.issue||'')}`;if(anomaly.count)itemHtml+=` <span class="badge bg-secondary ms-2">${anomaly.count} instances</span>`;if(anomaly.example_values&&anomaly.example_values.length>0)itemHtml+=`<br><small class="text-muted">Examples: ${anomaly.example_values.slice(0,3).map((v)=>this.escapeHtml(String(v))).join(', ')}</small>`;$item.html(itemHtml);$list.append($item);});$body.append($list);}const $actions=$('<div>').addClass('card-footer');const auditNodeId=this.nodeId;const $copyBtn=$('<button>').addClass('btn btn-sm btn-outline-secondary').html('<i class="fas fa-copy me-1"></i>'+Drupal.t('Copy JSON')).on('click',function(){Drupal.aiTracking.track('ai_chat_output_action',{ai_category:'chat',ai_action:'audit_copy',ai_node_id:auditNodeId});navigator.clipboard.writeText(JSON.stringify(json,null,2));$copyBtn.html('<i class="fas fa-check me-1"></i>'+Drupal.t('Copied!'));setTimeout(function(){$copyBtn.html('<i class="fas fa-copy me-1"></i>'+Drupal.t('Copy JSON'));},2000);});const $downloadBtn=$('<button>').addClass('btn btn-sm btn-outline-secondary ms-2').html('<i class="fas fa-download me-1"></i>'+Drupal.t('Download JSON')).on('click',function(){Drupal.aiTracking.track('ai_chat_output_action',{ai_category:'chat',ai_action:'audit_download',ai_node_id:auditNodeId});const blob=new Blob([JSON.stringify(json,null,2)],{type:'application/json'});const url=URL.createObjectURL(blob);const link=document.createElement('a');link.href=url;link.download='audit_report.json';link.click();URL.revokeObjectURL(url);});$actions.append($copyBtn,$downloadBtn);$wrapper.append($header,$body,$actions);$container.append($wrapper);$container.addClass('has-rich-content');}renderSimulationResult($container,imageData,summary,sourceCode){const $badge=$('<div>').addClass('ai-chat-simulation-badge badge bg-info mb-2').html('<i class="fas fa-flask me-1"></i>'+Drupal.t('Simulation - Original data unchanged'));$container.append($badge);this.renderImageWithActions($container,imageData,sourceCode,'simulation.png');if(summary){const $summary=$('<div>').addClass('ai-chat-simulation-summary mt-2 p-2 bg-light rounded').html('<strong>'+Drupal.t('Result:')+'</strong> '+this.escapeHtml(summary));$container.append($summary);}}renderToolOutputsOnBubble($bubble,toolOutput){if(!toolOutput)return;if(toolOutput.images&&toolOutput.images.length>0)toolOutput.images.forEach((img)=>{if(img.simulation_summary)this.renderSimulationResult($bubble,img.data,img.simulation_summary,img.executed_code);else this.renderImageWithActions($bubble,img.data,img.executed_code,img.filename);});if(toolOutput.widgets&&toolOutput.widgets.length>
610)toolOutput.widgets.forEach((widget)=>{this.renderWidget($bubble,widget.html,widget.executed_code);});if(toolOutput.audit_report)this.renderAuditReport($bubble,toolOutput.audit_report.json,toolOutput.audit_report.summary);}escapeHtml(text){const div=document.createElement('div');div.textContent=text;return div.innerHTML;}showToolExecutionIndicator($bubble,tool,purpose,explanation){this.hideToolExecutionIndicator($bubble,true);const skeletonInterval=$bubble.data('skeleton-interval');if(skeletonInterval){clearInterval(skeletonInterval);$bubble.removeData('skeleton-interval');}const $skeleton=$bubble.find('.streaming-skeleton');$skeleton.remove();$bubble.removeClass('has-skeleton');const purposeKey=purpose||'default';const info=toolDisplayInfo[purposeKey]||toolDisplayInfo['default'];const messages=toolExecutionMessages[purposeKey]||toolExecutionMessages['default'];const initialStatus=explanation||messages[0];const $indicator=$('<div>').addClass('ai-chat-tool-indicator').html(` 62 <div class="tool-indicator-content"> 63 <div class="tool-indicator-spinner"> 64 <span class="spinner-border spinner-border-sm"></span> 65 </div> 66 <div class="tool-indicator-info"> 67 <span class="tool-indicator-name"> 68 <i class="fas ${info.icon} me-1"></i>${info.name} 69 </span> 70 <span class="tool-indicator-status">${this.escapeHtml(initialStatus)}</span> 71 </div> 72 </div> 73 `);$bubble.append($indicator);let messageIndex=0;const $status=$indicator.find('.tool-indicator-status');const statusInterval=setInterval(()=>{messageIndex=(messageIndex+1)%messages.length;$status.css('opacity',0);setTimeout(()=>{$status.text(messages[messageIndex]);$status.css('opacity',1);},150);},2500);$indicator.data('status-interval',statusInterval);$bubble.data('tool-indicator',$indicator);$bubble.data('tool-indicator-shown-at',Date.now());const toolTimeout=setTimeout(()=>{this.hideToolExecutionIndicator($bubble,true);const $notice=$('<div>').addClass('ai-chat-tool-error text-muted small mt-2').html('<i class="fas fa-exclamation-circle me-1"></i>'+Drupal.t('Code execution is taking longer than expected. Results may still arrive.'));$bubble.append($notice);},90000);$bubble.data('tool-timeout',toolTimeout);this.scrollToKeepAnswerVisible($bubble);}hideToolExecutionIndicator($bubble,force=false){const $indicator=$bubble.data('tool-indicator');if(!$indicator)return;const MIN_DISPLAY_MS=1500;const shownAt=$bubble.data('tool-indicator-shown-at')||0;const elapsed=Date.now()-shownAt;if(!force&&elapsed<MIN_DISPLAY_MS){const remainingTime=MIN_DISPLAY_MS-elapsed;const existingTimeout=$bubble.data('tool-indicator-hide-timeout');if(existingTimeout)clearTimeout(existingTimeout);const hideTimeout=setTimeout(()=>{this.hideToolExecutionIndicator($bubble,true);},remainingTime);$bubble.data('tool-indicator-hide-timeout',hideTimeout);return;}const hideTimeout=$bubble.data('tool-indicator-hide-timeout');if(hideTimeout){clearTimeout(hideTimeout);$bubble.removeData('tool-indicator-hide-timeout');}const interval=$indicator.data('status-interval');if(interval)clearInterval(interval);const toolTimeout=$bubble.data('tool-timeout');if(toolTimeout){clearTimeout(toolTimeout);$bubble.removeData('tool-timeout');}$indicator.fadeOut(200,function(){$(this).remove();});$bubble.removeData('tool-indicator');$bubble.removeData('tool-indicator-shown-at');}extractRelevantCodeForRemix(fullCode,filename){if(!fullCode||!filename)return fullCode;const vizNumber=this.extractVisualizationNumber(filename);if(vizNumber!==null){const extracted=this.extractVisualizationBlock(fullCode,vizNumber);if(extracted)return extracted;}const baseFilename=filename.replace(/\.[^.]+$/,'');const escapedFilename=baseFilename.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');const patterns=[new RegExp(`([\\s\\S]{0,2000}(?:savefig|to_html|write_html)\\s*\\([^)]*${escapedFilename}[^)]*\\)[\\s\\S]{0,200})`,'i'),new RegExp(`(def\\s+(?:create|generate|plot|make|build|draw)[^(]*\\([\\s\\S]{0,3000}${escapedFilename}[\\s\\S]{0,500})`,'i')];for(const pattern of patterns){const match=fullCode.match(pattern);if(match&&match[1]&&match[1].length>100){const imports=this.extractImports(fullCode);return imports+'\n\n# ... (context extracted for remix of '+filename+') ...\n\n'+match[1].trim();}}if(fullCode.length>10000){console.warn('[AI Chat] Code context is very large ('+fullCode.length+' chars), truncating for remix');return fullCode.substring(0,8000)+'\n\n# ... (code truncated for remix, target: '+filename+') ...\n\n'+fullCode.substring(fullCode.length-2000);}return fullCode;}extractVisualizationNumber(filename){if(!filename)return null;const patterns=[/visualization[_-]?(\d+)/i,/viz[_-]?(\d+)/i,/figure[_-]?(\d+)/i,/chart[_-]?(\d+)/i,/plot[_-]?(\d+)/i];for(const pattern of patterns){const match=filename.match(pattern);if(match)return parseInt(match[1],10);}return null;}extractVisualizationBlock(fullCode,vizNumber){if(!fullCode||vizNumber===null)return null;const lines=fullCode.split('\n');const vizPattern=/^#\s*Visualization\s+(\d+)\s*:/i;let startLine=-1;let endLine=lines.length;for(let i=0;i<lines.length;i++){const match=lines[i].trim().match(vizPattern);if(match){const currentVizNum=parseInt(match[1],10);if(currentVizNum===vizNumber)startLine=i;else{if(startLine!==-1&¤tVizNum>vizNumber){endLine=i;break;}}}}if(startLine===-1)return null;const vizBlock=lines.slice(startLine,endLine).join('\n').trim();const imports=this.extractImports(fullCode);return imports+'\n\n# === Code block for Visualization '+vizNumber+' ===\n\n'+vizBlock;}extractImports(code){const importLines=code.match(/^(?:import|from)\s+.+$/gm);return importLines?importLines.join('\n'):'';}initScrollSpy(){this.destroyScrollSpy();const self=this;
73const $userMessages=this.$messages.find('.ai-chat-bubble-user[id]');if($userMessages.length===0)return;this.scrollSpyObserver=new IntersectionObserver((entries)=>{entries.forEach((entry)=>{if(entry.isIntersecting){const msgId=entry.target.id;const $tocLink=self.$tocNav.find(`a[href="#${msgId}"]`);if($tocLink.length){self.$tocNav.find('.nav-link').removeClass('active');$tocLink.addClass('active');}}});},{root:this.$messagesWrapper[0],rootMargin:'0px 0px -70% 0px',threshold:0});$userMessages.each(function(){self.scrollSpyObserver.observe(this);});}destroyScrollSpy(){if(this.scrollSpyObserver){this.scrollSpyObserver.disconnect();this.scrollSpyObserver=null;}}buildToc(){if(!this.$tocNav||!this.$tocNav.length)return;this.destroyScrollSpy();this.$tocNav.empty();const $userMessages=this.$messages.find('.ai-chat-bubble-user');const self=this;if($userMessages.length===0){this.$tocPanel.removeClass('expanded');this.$tocToggle.attr('aria-expanded',false);return;}this.$tocPanel.addClass('expanded');this.$tocToggle.attr('aria-expanded',true);this.addSuggestedQuestionsLink();$userMessages.each(function(index){const $msg=$(this);const msgId='ai-chat-q-'+self.nodeId+'-'+index;let questionText=$msg.text().trim();if(questionText.length>80)questionText=questionText.substring(0,77)+'...';$msg.attr('id',msgId);const $tocItem=$('<li class="nav-item"></li>');const $tocLink=$('<a class="nav-link py-1" href="#'+msgId+'"></a>').text(questionText).attr('title',$msg.text().trim());$tocItem.append($tocLink);self.$tocNav.append($tocItem);});this.$tocNav.find('.nav-item:not(.ai-chat-toc-suggested) .nav-link').last().addClass('active');}addSuggestedQuestionsLink(){const self=this;const $tocItem=$('<li class="nav-item ai-chat-toc-suggested"></li>');const $tocLink=$('<a class="nav-link py-1" href="#"></a>').html('<em>'+Drupal.t('Welcome Screen')+'</em>').attr('title',Drupal.t('View welcome screen with suggested questions'));$tocLink.on('click',function(e){e.preventDefault();e.stopPropagation();self.showWelcomeScreen();self.$tocNav.find('.nav-link').removeClass('active');$(this).addClass('active');});$tocItem.append($tocLink);this.$tocNav.append($tocItem);}showWelcomeScreen(){this.$welcome.removeClass('d-none');const wrapper=this.$messagesWrapper[0];if(wrapper)wrapper.scrollTop=0;}addTocEntry($bubble,content){if(!this.$tocNav||!this.$tocNav.length)return;if(!this.$tocPanel.hasClass('expanded')){this.$tocPanel.addClass('expanded');this.$tocToggle.attr('aria-expanded',true);this.addSuggestedQuestionsLink();}const index=this.$messages.find('.ai-chat-bubble-user').length-1;const msgId='ai-chat-q-'+this.nodeId+'-'+index;let questionText=content.trim();if(questionText.length>80)questionText=questionText.substring(0,77)+'...';$bubble.attr('id',msgId);this.$tocNav.find('.nav-link').removeClass('active');const $tocItem=$('<li class="nav-item"></li>');const $tocLink=$('<a class="nav-link py-1 active" href="#'+msgId+'"></a>').text(questionText).attr('title',content.trim());$tocItem.append($tocLink);this.$tocNav.append($tocItem);if(this.scrollSpyObserver)this.scrollSpyObserver.observe($bubble[0]);else this.initScrollSpy();}init(){if(this.initialized)return;this.initialized=true;this.loadHistory();}async loadHistory(){if(!this.nodeId){console.error('AI Chat: No node ID provided');return;}this.showLoading(true);try{const response=await fetch('/api/v1/ai/session/'+this.nodeId,{method:'GET',headers:{'Content-Type':'application/json'},credentials:'same-origin'});const data=await response.json();if(data.success){this.sessionUuid=data.session_uuid;if(data.messages&&data.messages.length>0){this.history=data.messages.map((msg)=>({role:msg.role,content:msg.content}));data.messages.forEach((msg)=>{const $bubble=this.renderMessage(msg.role,msg.content,false);if(msg.role==='assistant'&&msg.tool_output)this.renderToolOutputsOnBubble($bubble,msg.tool_output);});this.$clearBtn.prop('disabled',false);this.$welcome.addClass('d-none');this.buildToc();}else{this.history=[];this.$welcome.removeClass('d-none');}if(data.exploration_paths&&data.exploration_paths.length>0)this.renderSuggestedPrompts(data.exploration_paths);if(data.messages&&data.messages.length>0){const SECONDS_IN_A_DAY=86400;const twentyFourHoursAgo=Math.floor(Date.now()/1000)-SECONDS_IN_A_DAY;this.dailyMessageCount=data.messages.filter((m)=>m.role==='user'&&m.timestamp>=twentyFourHoursAgo).length;}if(data.limits){this.rateLimits=data.limits;this.updateRateLimitIndicator();}}else this.showError('Failed to load chat history.');}catch(error){console.error('AI Chat: Error loading history',error);this.showError('Error loading chat history.');}finally{this.showLoading(false);this.updateSendButton();const hasMessages=this.$messages.children('.ai-chat-bubble').length>0;if(hasMessages){const self=this;setTimeout(function(){self.scrollToBottom();self.initScrollSpy();},50);}}}async sendMessage(){const message=this.$input.val().trim();if(!message||this.isLoading)return;Drupal.aiTracking.track('ai_chat_message_send',{ai_category:'chat',ai_action:this.codeContext?'remix':'send',ai_node_id:this.nodeId,ai_detail
73:this.selectedToolChoice||'auto'});this.isLoading=true;this.abortController=new AbortController();this.updateSendButton();this.$welcome.addClass('d-none');this.renderMessage('user',message,true);this.history.push({role:'user',content:message});this.dailyMessageCount++;this.updateRateLimitIndicator();this.$input.val('').trigger('input');this.scrollToBottom();const $typingIndicator=this.renderTypingIndicator();const aiChatConfig=drupalSettings.aiChat||{};if(aiChatConfig.streamingEnabled&&aiChatConfig.streamingUrl)await this.sendMessageStreaming(message,$typingIndicator);else await this.sendMessageSync(message,$typingIndicator);}async sendMessageSync(message,$typingIndicator,isRetry=false){try{const response=await fetch('/api/v1/ai/chat',{method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',signal:this.abortController?this.abortController.signal:undefined,body:JSON.stringify({node_id:this.nodeId,message,session_uuid:this.sessionUuid,tool_choice:this.selectedToolChoice!=='auto'?this.selectedToolChoice:null,purpose_hint:this.selectedPurpose,code_context:this.codeContext,code_context_hint:this.remixFilenameHint})});this.removeTypingIndicator($typingIndicator);if(!response.ok){if(response.status===429){const rateLimitData=await response.json().catch(()=>({}));this.showRateLimitMessage(rateLimitData.message);return;}let errorMessage='Failed to get response.';if(response.status===504||response.status===408)errorMessage='The request timed out. Your question may require more processing time. Please try again or simplify your question.';else if(response.status===503)errorMessage='The AI service is temporarily unavailable. Please try again in a moment.';else{if(response.status>=500)errorMessage='A server error occurred. Please try again later.';}this.showError(errorMessage);return;}const data=await response.json();if(data.success){this.sessionUuid=data.session_uuid;const $newMessage=this.renderMessage('assistant',data.response,true);this.$clearBtn.prop('disabled',false);if(data.response)this.history.push({role:'assistant',content:data.response});this.clearRemixContext();if(data.images&&data.images.length>0)data.images.forEach((img)=>{this.renderImageWithActions($newMessage,img.data,img.executed_code,img.filename);});if(data.widgets&&data.widgets.length>0)data.widgets.forEach((widget)=>{this.renderWidget($newMessage,widget.html,widget.executed_code);});if(data.audit_report)this.renderAuditReport($newMessage,data.audit_report.json||data.audit_report,data.audit_report.summary);setTimeout(()=>{const $userMessage=$newMessage.prev('.ai-chat-bubble-user');if($userMessage.length)this.scrollToMessage($userMessage,true);else this.scrollToMessage($newMessage,true);},50);if(data.context_stats&&data.context_stats.pruning&&data.context_stats.pruning.was_pruned&&!this._pruningNoticeShown)this.showPruningNotice(data.context_stats.pruning);}else if(data.error==='throttled'){const retryable=data.token_usage&&data.token_usage.retryable;const retrySeconds=(data.token_usage&&data.token_usage.retry_after_seconds)||5;if(retryable&&!isRetry){const $notice=this.showThrottleRetryNotice(retrySeconds);await this.sleep(retrySeconds*1000);if($notice){const intervalId=$notice.data('countdown-interval');if(intervalId)clearInterval(intervalId);$notice.remove();}const $retryTyping=this.renderTypingIndicator();return this.sendMessageSync(message,$retryTyping,true);}this.showThrottleMessage(message);}else this.showError(data.error||'Failed to get response.');}catch(error){if(error.name==='AbortError'){this.removeTypingIndicator($typingIndicator);return;}console.error('AI Chat: Error sending message',error);this.removeTypingIndicator($typingIndicator);this.showError('Error communicating with AI service. Please check your connection and try again.');}finally{this.abortController=null;this.isLoading=false;this.updateSendButton();}}async sendMessageStreaming(message,$typingIndicator,isRetry=false){const aiChatConfig=drupalSettings.aiChat;let partialResponse='';let $responseBubble=null;this._streamCompletedProperly=false;this.startMaxResponseTimeout();this.resetInactivityTimer();this.showGeneratingIndicator();this.updateSendButton();try{let auth=null;
73if(!aiChatConfig.skipStreamingAuth)auth=await this.getStreamingAuth();this.removeTypingIndicator($typingIndicator);$responseBubble=this.createEmptyResponseBubble();const headers={'Content-Type':'application/json'};if(auth){headers['X-Auth-Token']=auth.auth;headers['X-Auth-Signature']=auth.signature;}if(!this.abortController){this.abortController=new AbortController();this.isLoading=true;this.updateSendButton();}const response=await fetch(new URL('/api/v1/ai/chat/stream',aiChatConfig.streamingUrl),{method:'POST',headers,signal:this.abortController.signal,body:JSON.stringify({node_id:String(this.nodeId),user_message:message,session_uuid:this.sessionUuid,bucket:drupalSettings.aiChat.bucket||'ieee-dataport-ai-analysis',history:this.history,tool_choice:this.selectedToolChoice!=='auto'?this.selectedToolChoice:null,purpose_hint:this.selectedPurpose,code_context:this.codeContext,code_context_hint:this.remixFilenameHint})});if(!response.ok)throw new Error(`HTTP ${response.status}: ${response.statusText}`);const {text,metadata,toolOutput,completedProperly}=await this.processStream(response,$responseBubble);partialResponse=text;this.finalizeResponseBubble($responseBubble,text);await this.persistStreamedMessage(message,text,metadata,completedProperly,toolOutput);if(text)this.history.push({role:'assistant',content:text});this.$clearBtn.prop('disabled',false);this.clearRemixContext();setTimeout(()=>{const $userMessage=$responseBubble.prev('.ai-chat-bubble-user');if($userMessage.length)this.scrollToMessage($userMessage,true);else this.scrollToMessage($responseBubble,true);},50);}catch(error){if(error.name==='AbortError'){const stoppedPartialResponse=this._currentPartialResponse||'';if($responseBubble&&stoppedPartialResponse){this.finalizeResponseBubble($responseBubble,stoppedPartialResponse);const $stoppedBadge=$('<span>').addClass('badge bg-secondary ms-2 ai-chat-interrupted-badge').html('<i class="fas fa-stop-circle me-1"></i>'+Drupal.t('Stopped'));$responseBubble.find('.ai-chat-bubble-content').append($stoppedBadge);try{await this.persistStreamedMessage(message,stoppedPartialResponse,{},false);}catch(e){console.error('Failed to persist partial response:',e);}this.$clearBtn.prop('disabled',false);}else{if($responseBubble)$responseBubble.remove();}return;}if(error.isContextOverflow){if($responseBubble)$responseBubble.remove();this.showContextOverflowMessage();return;}if(error.isRateLimit){if($responseBubble)$responseBubble.remove();this.showRateLimitMessage(error.rateLimitMessage);return;}if(error.isThrottle){if($responseBubble)$responseBubble.remove();if(error.retryable&&!isRetry){const retrySeconds=error.retryAfterSeconds||5;const $notice=this.showThrottleRetryNotice(retrySeconds);await this.sleep(retrySeconds*1000);if($notice){const intervalId=$notice.data('countdown-interval');if(intervalId)clearInterval(intervalId);$notice.remove();}const $retryTyping=this.renderTypingIndicator();this.isLoading=true;this.abortController=new AbortController();this.updateSendButton();return this.sendMessageStreaming(message,$retryTyping,true);}this.showThrottleMessage(message);return;}console.warn('Streaming failed, falling back to sync:',error);if($responseBubble)$responseBubble.remove();if(partialResponse)try{await this.persistStreamedMessage(message,partialResponse,{},false);}catch(e){console.error('Failed to persist partial response:',e);}this.showNotice('Switching to standard mode...');const $newTypingIndicator=this.renderTypingIndicator();await this.sendMessageSync(message,$newTypingIndicator);return;}finally{this.clearAllTimeouts();this.abortController=null;this._currentPartialResponse='';this.isLoading=false;this.hideGeneratingIndicator();this.updateSendButton();}}stopResponse(){if(this.abortController)this.abortController.abort();}resetInactivityTimer(){if(this.inactivityTimeout)clearTimeout(this.inactivityTimeout);this.inactivityTimeout=setTimeout(()=>{this.handleInactivityTimeout();},this.INACTIVITY_TIMEOUT_MS);}handleInactivityTimeout(){if(this.abortController&&this.isLoading){console.warn('AI Chat: Inactivity timeout - no events received for 30 seconds');this.showTimeoutWarning(Drupal.t('No response received. The connection may be stalled.'));}}showTimeoutWarning(message){this.$messages.find('.ai-chat-timeout-warning').remove();const $warning=$('<div>').addClass('ai-chat-timeout-warning alert alert-warning mb-2').html(` 74 <i class="fas fa-exclamation-triangle me-1"></i>
75 <span>${message}</span> 76 <button type="button" class="btn btn-sm btn-outline-secondary ms-2 ai-chat-cancel-btn"> 77 ${Drupal.t('Cancel request')} 78 </button> 79 `);$warning.find('.ai-chat-cancel-btn').on('click',()=>{this.stopResponse();$warning.remove();});this.$messages.append($warning);this.scrollToBottom();}showContextOverflowMessage(){const self=this;const $msg=$('<div>').addClass('ai-chat-context-overflow alert alert-warning mb-2').html('<i class="fas fa-exclamation-triangle me-2"></i>'+'<span>'+Drupal.t('This conversation has grown too long. Please start a new chat session.')+'</span>'+'<button type="button" class="btn btn-sm btn-primary ms-2 ai-chat-new-session-btn">'+Drupal.t('Start New Chat')+'</button>');$msg.find('.ai-chat-new-session-btn').on('click',function(){self.clearHistory();$msg.remove();});this.$messages.append($msg);this.scrollToBottom();}showPruningNotice(pruning){this._pruningNoticeShown=true;const included=pruning.included_messages||0;const total=pruning.total_history_messages||0;const $notice=$('<div>').addClass('ai-chat-pruning-notice alert alert-info mb-0').attr('role','status');const $icon=$('<i>').addClass('fas fa-info-circle me-2');const $text=$('<span>').text(Drupal.t('This conversation is long. The AI is using the most recent messages (@included of @total) for context, but can still search older messages if needed.',{'@included':included,'@total':total}));const $dismiss=$('<button>').attr('type','button').addClass('btn-close ms-auto').attr('aria-label',Drupal.t('Dismiss')).on('click',()=>{$notice.fadeOut(200,()=>$notice.remove());});$notice.append($icon).append($text).append($dismiss);this.$messagesWrapper.prepend($notice);}showThrottleRetryNotice(seconds){this.$messages.find('.ai-chat-throttle-notice').remove();const $notice=$('<div>').addClass('ai-chat-throttle-notice alert alert-warning mb-2').attr('role','status');const $icon=$('<i>').addClass('fas fa-clock me-2');const $text=$('<span>');$text.text(Drupal.t('The AI service is busy. Retrying in @seconds seconds...',{'@seconds':seconds}));$notice.append($icon).append($text);this.$messages.append($notice);this.scrollToBottom();let remaining=seconds;const countdownInterval=setInterval(()=>{remaining--;if(remaining>0)$text.text(Drupal.t('The AI service is busy. Retrying in @seconds seconds...',{'@seconds':remaining}));else{$text.text(Drupal.t('Retrying now...'));clearInterval(countdownInterval);}},1000);$notice.data('countdown-interval',countdownInterval);return $notice;}showThrottleMessage(lastMessage){const self=this;this.$messages.find('.ai-chat-throttle-message').remove();const $msg=$('<div>').addClass('ai-chat-throttle-message alert alert-warning mb-2').html('<i class="fas fa-hourglass-half me-2"></i>'+'<span>'+Drupal.t('The AI service is experiencing high demand. Please try again in a moment.')+'</span>'+'<button type="button" class="btn btn-sm btn-outline-warning ms-2 ai-chat-throttle-retry-btn">'+'<i class="fas fa-redo me-1"></i>'+Drupal.t('Try Again')+'</button>');$msg.find('.ai-chat-throttle-retry-btn').on('click',function(){$msg.remove();self.isLoading=true;self.abortController=new AbortController();self.updateSendButton();const $typingIndicator=self.renderTypingIndicator();const aiChatConfig=drupalSettings.aiChat||{};if(aiChatConfig.streamingEnabled&&aiChatConfig.streamingUrl)self.sendMessageStreaming(lastMessage,$typingIndicator,false);else self.sendMessageSync(lastMessage,$typingIndicator,false);});this.$messages.append($msg);this.scrollToBottom();}sleep(ms){return new Promise((resolve)=>setTimeout(resolve,ms));}startMaxResponseTimeout(){this.maxResponseTimeout=setTimeout(()=>{this.handleMaxResponseTimeout();},this.MAX_RESPONSE_TIMEOUT_MS);}handleMaxResponseTimeout(){if(this.abortController&&this.isLoading){console.error('AI Chat: Maximum response timeout exceeded (5 minutes)');this.showError(Drupal.t('Request timed out after 5 minutes. Please try again with a simpler question.'));this.stopResponse();}}clearAllTimeouts(){if(this.inactivityTimeout){clearTimeout(this.inactivityTimeout);this.inactivityTimeout=null;}if(this.maxResponseTimeout){clearTimeout(this.maxResponseTimeout);this.maxResponseTimeout=null;}this.$messages.find('.ai-chat-timeout-warning').remove();}updateProgressStatus($bubble,stage){const stageMessages={'loading_manifest':Drupal.t('Loading dataset manifest...'),'building_context':Drupal.t('Building context...'),'querying_model':Drupal.t('Querying AI model...'),'executing_tool':Drupal.t('Executing analysis tool...'),'processing_results':Drupal.t('Processing results...')};const message=stageMessages[stage]||stage;const $status=$bubble.find('.skeleton-status');if($status.length){$status.css('opacity',0);setTimeout(()=>{$status.text(message);$status.css('opacity',1);},150);}}async getStreamingAuth(){const aiChatConfig=drupalSettings.aiChat;const response=await fetch(aiChatConfig.streamAuthUrl,{method:'GET',credentials:'same-origin'});if(!response.ok){if(response.status===429){const rateLimitData=await response.json().catch(()=>({}));const err=new Error(rateLimitData.message||'Rate limit reached');err.isRateLimit=true;err.rateLimitMessage=rateLimitData.message;throw err;}throw new Error('Auth failed: '+response.status);}return response.json();}async processStream(response,$bubble){const reader=response.body.getReader();const decoder=new TextDecoder();let fullText='';let metadata={};let buffer='';let toolOutput={images:[],widgets:[],audit_report:null};this._currentPartialResponse='';const activeTools=new Set();while(true){const {done,value}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:true});const lines=buffer.split('\n');buffer=lines.pop();for(const line of lines)if(line.startsWith('data: ')){this.resetInactivityTimer();try{const data=JSON.parse(line.slice(6));switch(data.type){case 'session':if(!this.sessionUuid)console.error('Streaming started without a valid session UUID from Drupal.');break;case 'text':fullText+=data.content;this._currentPartialResponse=fullText;this.updateResponseBubble($bubble,fullText);break;case 'tool_use_start':if(data.tool_use_id)activeTools.add(data.tool_use_id);break;case 'tool_executing':this.showToolExecutionIndicator($bubble,data.tool,data.purpose,data.explanation);break;case 'tool_result':{if(data.result&&data.result.images&&data.result.images.length>
790)data.result.images.forEach((img)=>{this.renderImageWithActions($bubble,img.data,data.result.executed_code,img.filename);toolOutput.images.push({data:img.data,filename:img.filename,executed_code:data.result.executed_code});});const widgetsList=data.result?.widgets||data.result?.html_widgets||[];if(widgetsList.length>0)widgetsList.forEach((widget)=>{this.renderWidget($bubble,widget.html,data.result.executed_code);toolOutput.widgets.push({html:widget.html,executed_code:data.result.executed_code});});if(data.result&&data.result.audit_report){this.renderAuditReport($bubble,data.result.audit_report.json||data.result.audit_report,data.result.audit_report.summary);toolOutput.audit_report={json:data.result.audit_report.json||data.result.audit_report,summary:data.result.audit_report.summary};}break;}case 'tool_complete':if(data.tool_use_id)activeTools.delete(data.tool_use_id);if(activeTools.size===0)this.hideToolExecutionIndicator($bubble,true);break;case 'image':this.renderImageWithActions($bubble,data.data,data.executed_code,data.filename);toolOutput.images.push({data:data.data,filename:data.filename,executed_code:data.executed_code});break;case 'widget':this.renderWidget($bubble,data.html,data.executed_code);toolOutput.widgets.push({html:data.html,executed_code:data.executed_code});break;case 'audit_report':this.renderAuditReport($bubble,data.json,data.summary);toolOutput.audit_report={json:data.json,summary:data.summary};break;case 'simulation':this.renderSimulationResult($bubble,data.image,data.summary,data.executed_code);toolOutput.images.push({data:data.image,filename:'simulation.png',executed_code:data.executed_code,simulation_summary:data.summary});break;case 'metadata':metadata=data.metadata||{};break;case 'context_stats':if(data.pruning&&data.pruning.was_pruned&&!this._pruningNoticeShown)this.showPruningNotice(data.pruning);break;case 'heartbeat':break;case 'progress':this.updateProgressStatus($bubble,data.stage);break;case 'cancelled':break;case 'error':this._streamCompletedProperly=true;if(data.error==='ContextOverflow'){const err=new Error(data.message||'Conversation too long. Please start a new chat session.');err.isContextOverflow=true;throw err;}if(data.error==='throttled'){const err=new Error(data.message||'The AI service is temporarily busy. Please wait a moment.');err.isThrottle=true;err.retryAfterSeconds=data.retry_after_seconds||5;err.retryable=data.retryable!==false;throw err;}throw new Error(data.message||'Stream error');case 'done':this._streamCompletedProperly=true;activeTools.clear();this.hideToolExecutionIndicator($bubble,true);break;}}catch(e){if(e instanceof SyntaxError)console.warn('Invalid JSON in SSE:',line);else throw e;}}}if(fullText&&!this._streamCompletedProperly)console.warn('AI Chat: Stream closed without done event, treating as success');const cleanToolOutput={};if(toolOutput.images.length>0)cleanToolOutput.images=toolOutput.images;if(toolOutput.widgets.length>0)cleanToolOutput.widgets=toolOutput.widgets;if(toolOutput.audit_report)cleanToolOutput.audit_report=toolOutput.audit_report;return {text:fullText,metadata,toolOutput:cleanToolOutput,completedProperly:this._streamCompletedProperly};}createEmptyResponseBubble(){const bubbleId='bubble-'+Date.now();const $bubble=$('<div>').addClass('ai-chat-bubble ai-chat-bubble-assistant ai-chat-bubble-streaming has-skeleton').attr('data-bubble-id',bubbleId);$bubble.html(` 80 <div class="streaming-skeleton"> 81 <div class="skeleton-lines"> 82 <div class="skeleton-line"></div> 83 <div class="skeleton-line"></div> 84 <div class="skeleton-line"></div> 85 <div class="skeleton-line"></div> 86 </div> 87 <div class="skeleton-status">${streamingLoadingMessages[0]}</div> 88 </div> 89 `);let messageIndex=0;const $status=$bubble.find('.skeleton-status');const statusInterval=setInterval(()=>{messageIndex=(messageIndex+1)%streamingLoadingMessages.length;const nextMessage=streamingLoadingMessages[messageIndex];$status.css('opacity',0);setTimeout(()=>{$status.text(nextMessage);$status.css('opacity',1);},150);},2000);$bubble.data('skeleton-interval',statusInterval);this.$messages.append($bubble);this.scrollToBottom();return $bubble;}updateResponseBubble($bubble,text){const skeletonInterval=$bubble.data('skeleton-interval');if(skeletonInterval){clearInterval(skeletonInterval);$bubble.removeData('skeleton-interval');$bubble.removeClass('has-skeleton');}let $textContent=$bubble.find('.ai-chat-text-content');if($textContent.length===0){$bubble.find('.streaming-skeleton').remove();$textContent=$('<div>').addClass('ai-chat-text-content');$bubble.prepend($textContent);}$textContent.html(markdownToHtml(text));this.scrollToKeepAnswerVisible($bubble);}scrollToKeepAnswerVisible($bubble){const wrapper=this.$messagesWrapper[0];if(!wrapper||!$bubble||!$bubble.length)return;const bubbleTop=$bubble[0].offsetTop;const bubbleHeight=$bubble[0].offsetHeight;const wrapperHeight=wrapper.clientHeight;const currentScroll=wrapper.scrollTop;const maxScrollToKeepTopVisible=bubbleTop-140;const scrollToShowBottom=bubbleTop+bubbleHeight-wrapperHeight+76;if(scrollToShowBottom>currentScroll){const newScroll=Math.min(scrollToShowBottom,maxScrollToKeepTopVisible);if(newScroll>currentScroll)wrapper.scrollTop=newScroll;}}finalizeResponseBubble($bubble,content){const skeletonInterval=$bubble.data('skeleton-interval');if(skeletonInterval){clearInterval(skeletonInterval);$bubble.removeData('skeleton-interval');}$bubble.removeClass('ai-chat-bubble-streaming');
89if(content&&content.trim().length>0){const $copyBtn=$('<button>').attr('type','button').addClass('ai-chat-copy-btn').attr('title',Drupal.t('Copy to clipboard')).html('<i class="fas fa-copy"></i>').on('click',function(e){e.stopPropagation();const $btn=$(this);if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(content).then(function(){const $icon=$btn.find('i');$icon.removeClass('fa-copy').addClass('fa-check');$btn.addClass('copied');setTimeout(function(){$icon.removeClass('fa-check').addClass('fa-copy');$btn.removeClass('copied');},2000);}).catch(function(err){console.error('Failed to copy:',err);});});$bubble.append($copyBtn);}if(window.Prism)$bubble.find('pre code').each(function(){Prism.highlightElement(this);});}async persistStreamedMessage(userMessage,assistantResponse,metadata,isComplete,toolOutput=null){const aiChatConfig=drupalSettings.aiChat;try{const payload={session_uuid:this.sessionUuid,node_id:String(this.nodeId),user_message:userMessage,assistant_response:assistantResponse,token_usage:metadata,is_complete:isComplete};if(toolOutput&&Object.keys(toolOutput).length>0)payload.tool_output=toolOutput;await fetch(aiChatConfig.persistUrl,{method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',body:JSON.stringify(payload)});}catch(error){console.error('Failed to persist streamed message:',error);}}showNotice(message){const $notice=$('<div>').addClass('ai-chat-notice alert alert-info mb-2');const $icon=$('<i>').addClass('fas fa-info-circle me-1');$notice.append($icon).append(document.createTextNode(' '+message));this.$messages.append($notice);this.scrollToBottom();setTimeout(function(){$notice.fadeOut(300,function(){$(this).remove();});},3000);}async clearHistory(){if(!this.sessionUuid||this.isLoading)return;if(!confirm(Drupal.t('Are you sure you want to clear the chat history?')))return;this.isLoading=true;try{const response=await fetch('/api/v1/ai/session/'+this.sessionUuid+'/clear',{method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin'});const data=await response.json();if(data.success){this.$messages.empty();this.$welcome.removeClass('d-none');this.$clearBtn.prop('disabled',true);this.history=[];this._pruningNoticeShown=false;this.$messagesWrapper.find('.ai-chat-pruning-notice').remove();this.buildToc();}else this.showError(data.error||'Failed to clear history.');}catch(error){console.error('AI Chat: Error clearing history',error);this.showError('Error clearing chat history.');}finally{this.isLoading=false;this.updateSendButton();}}renderMessage(role,content,animate){const isUser=role==='user';const $bubble=$('<div>').addClass('ai-chat-bubble').addClass(isUser?'ai-chat-bubble-user':'ai-chat-bubble-assistant');
89if(isUser)$bubble.text(content);else{$bubble.html(markdownToHtml(content));const $copyBtn=$('<button>').attr('type','button').addClass('ai-chat-copy-btn').attr('title',Drupal.t('Copy to clipboard')).html('<i class="fas fa-copy"></i>').on('click',function(e){e.stopPropagation();const $btn=$(this);if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(content).then(function(){const $icon=$btn.find('i');$icon.removeClass('fa-copy').addClass('fa-check');$btn.addClass('copied');setTimeout(function(){$icon.removeClass('fa-check').addClass('fa-copy');$btn.removeClass('copied');},2000);}).catch(function(err){console.error('Failed to copy:',err);});});$bubble.append($copyBtn);}if(animate){$bubble.hide();this.$messages.append($bubble);$bubble.fadeIn(200);if(isUser)this.addTocEntry($bubble,content);}else this.$messages.append($bubble);if(window.Prism)$bubble.find('pre code').each(function(){Prism.highlightElement(this);});return $bubble;}renderTypingIndicator(){const self=this;const $indicator=$('<div>').addClass('ai-chat-bubble ai-chat-bubble-assistant ai-chat-typing').html('<span></span><span></span><span></span>');this.$messages.append($indicator);this.scrollToBottom();const transitionTimer=setTimeout(()=>{self.transformToSkeletonLoader($indicator);},5000);$indicator.data('transition-timer',transitionTimer);return $indicator;}transformToSkeletonLoader($indicator){$indicator.removeClass('ai-chat-typing').addClass('ai-chat-typing-skeleton').html(` 90 <div class="skeleton-lines"> 91 <div class="skeleton-line"></div> 92 <div class="skeleton-line"></div> 93 <div class="skeleton-line"></div> 94 <div class="skeleton-line"></div> 95 </div> 96 <div class="skeleton-status">${loadingMessages[0]}</div> 97 `);let messageIndex=0;let inFinalPhase=false;let finalIndex=0;const $status=$indicator.find('.skeleton-status');const statusInterval=setInterval(()=>{let nextMessage;if(!inFinalPhase){messageIndex++;if(messageIndex>=loadingMessages.length){inFinalPhase=true;nextMessage=loadingMessagesFinal[0];finalIndex=0;}else nextMessage=loadingMessages[messageIndex];}else{finalIndex=(finalIndex+1)%loadingMessagesFinal.length;nextMessage=loadingMessagesFinal[finalIndex];}$status.css('opacity',0);setTimeout(()=>{$status.text(nextMessage);$status.css('opacity',1);},150);},5000);$indicator.data('status-interval',statusInterval);this.scrollToBottom();}removeTypingIndicator($indicator){if(!$indicator||!$indicator.length)return;const transitionTimer=$indicator.data('transition-timer');if(transitionTimer)clearTimeout(transitionTimer);const statusInterval=$indicator.data('status-interval');if(statusInterval)clearInterval(statusInterval);$indicator.remove();}showError(message){const $error=$('<div>').addClass('ai-chat-error alert alert-danger mb-2');const $icon=$('<i>').addClass('fas fa-exclamation-circle me-1');$error.append($icon).append(document.createTextNode(' '+message));this.$messages.append($error);this.scrollToBottom();setTimeout(function(){$error.fadeOut(300,function(){$(this).remove();});},10000);}showLoading(show){if(show){this.$loading.removeClass('d-none');this.$messages.addClass('d-none');}else{this.$loading.addClass('d-none');this.$messages.removeClass('d-none');}}updateCharCount(){const length=this.$input.val().length;const max=65000;this.$charCount.text(length.toLocaleString()+' / '+max.toLocaleString());if(length>max*0.9)this.$charCount.addClass('text-warning');else this.$charCount.removeClass('text-warning');if(length>=max)this.$charCount.addClass('text-danger').removeClass('text-warning');else this.$charCount.removeClass('text-danger');}updateRateLimitIndicator(){const dailyLimit=this.rateLimits&&this.rateLimits.daily_limit;if(!dailyLimit){this.$rateLimitInfo.addClass('d-none');return;}this.$rateLimitInfo.html('<i class="fas fa-info-circle me-1"></i>'+Drupal.t('@count / @limit prompts per day',{'@count':this.dailyMessageCount,'@limit':dailyLimit})).removeClass('d-none');}showRateLimitMessage(message){const $msg=$('<div>').addClass('ai-chat-rate-limit alert alert-warning mb-2').html('<i class="fas fa-hourglass-half me-2"></i><span>'+this.escapeHtml(message||Drupal.t('Message limit reached. Please try again later.'))+'</span>');this.$messages.append($msg);this.scrollToBottom();this.$input.prop('disabled',true).attr('placeholder',Drupal.t('Message limit reached'));this.updateSendButton();}updateSendButton(){const hasContent=this.$input.val().trim().length>0;const canSend=hasContent&&!this.isLoading&&this.initialized;if(this.isLoading&&this.abortController)this.$sendBtn.prop('disabled',false).addClass('ai-chat-stop-btn').html('<i class="fas fa-stop"></i>').attr('title',Drupal.t('Stop response'));else if(this.isLoading)this.$sendBtn.prop('disabled',true).removeClass('ai-chat-stop-btn').html('<span class="spinner-border spinner-border-sm" role="status"></span>').removeAttr('title');else this.$sendBtn.prop('disabled',!canSend).removeClass('ai-chat-stop-btn').html('<i class="fas fa-paper-plane"></i>').removeAttr('title');}showGeneratingIndicator(){if(!this.$generatingOverlay){this.$generatingOverlay=$('<div>').addClass('ai-chat-generating-overlay').html(`
98 <span class="spinner-border spinner-border-sm me-2" role="status"></span> 99 <span>${Drupal.t('Generating Response...')}</span> 100 `);this.$form.find('.input-group').append(this.$generatingOverlay);}this.$generatingOverlay.addClass('active');this.$input.addClass('generating');}hideGeneratingIndicator(){if(this.$generatingOverlay)this.$generatingOverlay.removeClass('active');this.$input.removeClass('generating');}scrollToBottom(){const wrapper=this.$messagesWrapper[0];if(wrapper)wrapper.scrollTop=wrapper.scrollHeight;}scrollToMessage($message,smooth=false){if(!$message||!$message.length)return;const wrapper=this.$messagesWrapper[0];if(wrapper){const messageTop=$message[0].offsetTop;const targetPosition=Math.max(0,messageTop-75);if(smooth)this.animateScroll(wrapper,targetPosition,800);else wrapper.scrollTop=targetPosition;}}animateScroll(element,targetPosition,duration){const startPosition=element.scrollTop;const distance=targetPosition-startPosition;const startTime=performance.now();const easeOutCubic=(t)=>1-Math.pow(1-t,3);const step=(currentTime)=>{const elapsed=currentTime-startTime;const progress=Math.min(elapsed/duration,1);const easedProgress=easeOutCubic(progress);element.scrollTop=startPosition+(distance*easedProgress);if(progress<1)requestAnimationFrame(step);};requestAnimationFrame(step);}renderSuggestedPrompts(explorationPaths){if(!explorationPaths||!explorationPaths.length)return;const $container=this.$container.find('.ai-chat-suggested-prompts');if(!$container.length)return;const self=this;const $wrapper=$('<div>').addClass('ai-chat-prompts-list');$wrapper.append($('<div>').addClass('ai-chat-prompts-header small text-muted mb-2').html('<i class="fas fa-lightbulb me-1"></i>'+Drupal.t('Suggested questions to explore:')));explorationPaths.forEach(function(path){const $card=$('<div>').addClass('ai-chat-prompt-card').attr('role','button').attr('tabindex','0').attr('data-question',path.user_question);const $topic=$('<span>').addClass('ai-chat-prompt-topic badge bg-primary-subtle text-primary mb-1').text(path.topic);const $question=$('<div>').addClass('ai-chat-prompt-question').text(path.user_question);$card.append($topic).append($question);$wrapper.append($card);});$container.html($wrapper);$container.off('click.promptCard').on('click.promptCard','.ai-chat-prompt-card',function(e){e.preventDefault();e.stopPropagation();const question=$(this).attr('data-question');if(question){Drupal.aiTracking.track('ai_chat_prompt_click',{ai_category:'chat',ai_action:'prompt',ai_node_id:self.nodeId});self.$input.val(question).trigger('input');self.$input.focus();self.$input[0].scrollIntoView({behavior:'smooth',block:'center'});}});$container.off('keydown.promptCard').on('keydown.promptCard','.ai-chat-prompt-card',function(e){if(e.key==='Enter'||e.key===' '){e.preventDefault();$(this).trigger('click');}});}}Drupal.behaviors.aiChat={attach:function(context,settings){$(once('ai-chat','.ai-chat-container',context)).each(function(){const chat=new DataPortChat(this);$(this).data('aiChat',chat);});$(once('ai-chat-tab','[data-bs-toggle="tab"][data-bs-target*="chat-pane"]',context)).on('shown.bs.tab',function(){const targetId=$(this).data('bs-target');const $pane=$(targetId);const $container=$pane.find('.ai-chat-container');const chat=$container.data('aiChat');if(chat)chat.init();});$(once('ai-chat-modal-cleanup','.modal',context)).on('hidden.bs.modal',function(){const $chatContainer=$(this).find('.ai-chat-container');const chat=$chatContainer.data('aiChat');if(chat&&chat.isLoading&&chat.abortController)chat.abortController.abort();});$(once('ai-chat-trigger','.ai-analysis-trigger[data-tab="chat"]',context)).on('click',function(){const modalId=$(this).data('modal-id');const $modal=$('#'+modalId);$modal.one('shown.bs.modal',function(){const $chatTab=$modal.find('[data-bs-target*="chat-pane"]');if($chatTab.length){const bsTab=new bootstrap.Tab($chatTab[0]);bsTab.show();}});});$(once('ai-viz-remix','.ai-analysis-viz-remix',context)).on('click',function(){const $btn=$(this);const filename=$btn.data('filename');const fullCode=drupalSettings.aiChat?.fullCodeContext||'';const ctx=Drupal.aiTracking.getContext($btn[0]);Drupal.aiTracking.track('ai_viz_remix',{ai_category:'viz',ai_action:'remix',ai_entity_id:ctx.ai_entity_id,ai_node_id:ctx.ai_no
100de_id,ai_detail:filename||''});const $modal=$btn.closest('.modal');const $chatContainer=$modal.find('.ai-chat-container');if($chatContainer.length&&$chatContainer.data('aiChat')){const chat=$chatContainer.data('aiChat');if(!chat.initialized)chat.init();const relevantCode=chat.extractRelevantCodeForRemix(fullCode,filename);chat.codeContext=relevantCode;chat.remixFilenameHint=filename;chat.selectedToolChoice='execute_python_code';chat.selectedPurpose='visualization';chat.updateToolToggleIcon('visualization');chat.showRemixIndicator();chat.$input.val(Drupal.t('Modify the chart from @filename: ',{'@filename':filename}));chat.$input.focus();const $chatTab=$modal.find('[data-bs-target*="chat-pane"]');if($chatTab.length){const bsTab=new bootstrap.Tab($chatTab[0]);bsTab.show();}}});}};})(jQuery,Drupal,once);;
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.