1// Modal Assistant Chat System 2(function() { 3 // Configuration 4 const APP_ID = 'airaintelligence'; 5 6 // Current conversation ID (session state only, not persisted locally) 7 let currentConversationId = null; 8 9 // Modal Assistant Variables 10 let modalCurrentMode = 'platform'; 11 const modalOverlay = document.getElementById('modalAssistantOverlay'); 12 const modalTitle = document.getElementById('modalAssistantTitle'); 13 const modalMessages = document.querySelector('.modal-chat-messages'); 14 const modalEmptyState = document.querySelector('.modal-chat-empty-state'); 15 const modalMessageInput = document.getElementById('modalMessageInput'); 16 const modalChatForm = document.getElementById('modalChatForm'); 17 const modalSendButton = document.getElementById('modalSendButton'); 18 const modalSuggestions = document.getElementById('modalSuggestions'); 19 const modalVoiceCircle = document.getElementById('voiceCircle'); 20 const modalVoiceButton = document.getElementById('modalVoiceButton'); 21 const modalEmptyLogo = document.getElementById('modalEmptyLogo'); 22 23 // Modal Sidebar Variables 24 const modalSidebar = document.getElementById('modalAssistantSidebar'); 25 const modalSidebarToggle = document.getElementById('modalSidebarToggle'); 26 const modalSidebarCloseBtn = document.getElementById('modalSidebarCloseBtn'); 27 28 // Document suggestion variables 29 let modalSearchTimer; 30 31 // Voice Canvas Animator 32 let voiceAnimator = null; 33 const voiceCanvas = document.getElementById('voiceCanvas'); 34 if (voiceCanvas) { 35 // Load voice-canvas.js dynamically 36 const script = document.createElement('script'); 37 script.src = "https://static.airaintelligence.ai/apps/airaintelligence/js/voice-canvas.js"; 38 script.onload = function() { 39 voiceAnimator = new VoiceCanvasAnimator(voiceCanvas); 40 }; 41 document.head.appendChild(script); 42 } 43 44 // Update modal logo based on theme 45 function updateModalLogo() { 46 if (!modalEmptyLogo) return; 47 48 const isDark = document.body.classList.contains('dark-mode'); 49 const logoSrc = isDark 50 ? 'https://static.airaintelligence.ai/shared/assets/images/logos/logobig_dark.png' 51 : 'https://static.airaintelligence.ai/shared/assets/images/logos/logobig.png'; 52 modalEmptyLogo.src = logoSrc; 53 } 54 55 // Modal Sidebar Toggle Functions - Base.html style 56 function toggleModalSidebar() { 57 if (!modalSidebar) return; 58 59 if (modalSidebar.classList.contains('show')) { 60 hideModalSidebar(); 61 } else { 62 showModalSidebar(); 63 } 64 } 65 66 function showModalSidebar() { 67 if (!modalSidebar) return; 68 modalSidebar.classList.add('show'); 69 70 // Mark overlay state (CSS can also hide the toggle) 71 if (modalOverlay) { 72 modalOverlay.classList.add('sidebar-open'); 73 } 74 75 // Hide toggle button when sidebar is open 76 if (modalSidebarToggle) { 77 modalSidebarToggle.style.setProperty('display', 'none', 'important'); 78 } 79 } 80 81 function hideModalSidebar() { 82 if (!modalSidebar) return; 83 modalSidebar.classList.remove('show'); 84 85 if (modalOverlay) { 86 modalOverlay.classList.remove('sidebar-open'); 87 } 88 89 // Show toggle button when sidebar is closed 90 if (modalSidebarToggle) { 91 modalSidebarToggle.style.display = 'flex'; 92 const icon = modalSidebarToggle.querySelector('i'); 93 if (icon) icon.className = 'bi bi-chevron-bar-right'; 94 } 95 } 96 97 // Modal Sidebar Navigation 98 function handleModalNavigation(tab) { 99 // Remove active class from all nav items 100 const navItems = document.querySelectorAll('.modal-nav-item'); 101 navItems.forEach(item => item.classList.remove('active')); 102 103 // Add active class to clicked item 104 const activeItem = document.querySelector(`[data-tab="${tab}"]`); 105 if (activeItem) { 106 activeItem.classList.add('active'); 107 } 108 109 // Handle tab content switching here 110 console.log('Switched to tab:', tab); 111 // TODO: Implement tab content switching 112 } 113 114 // Voice Circle State Manager 115 function setVoiceCircleState(state) { 116 if (!modalVoiceCircle) return; 117 118 modalVoiceCircle.className = 'voice-circle ' + state; 119 120 // Update canvas animation 121 if (voiceAnimator) { 122 voiceAnimator.setState(state); 123 } 124 } 125 126 // Modal Open/Close Functions 127 function openModalAssistant(mode = 'platform') { 128 modalCurrentMode = mode; 129 130 // Update logo based on current theme 131 updateModalLogo(); 132 133 // Initialize sidebar as hidden 134 if (modalSidebar) { 135 modalSidebar.classList.remove('show'); // Ensure it's hidden 136 if (modalOverlay) { 137 modalOverlay.classList.remove('sidebar-open'); 138 } 139 // Set toggle button icon to chevron-bar-right (sidebar hidden) and ensure it's visible 140 if (modalSidebarToggle) { 141 modalSidebarToggle.style.display = 'flex'; // CRITICAL: Ensure button is visible 142 const icon = modalSidebarToggle.querySelector('i'); 143 if (icon) icon.className = 'bi bi-chevron-bar-right'; 144 } 145 } 146 147 // Show modal with animation 148 modalOverlay.style.display = 'flex'; 149 setTimeout(() => { 150 modalOverlay.classList.add('show'); 151 setVoiceCircleState('idle');
151 // Start with idle animation 152 153 // Start canvas animation 154 if (voiceAnimator) { 155 voiceAnimator.start(); 156 } 157 }, 10); 158 159 // Load conversations when modal opens 160 loadChats(); 161 162 // Focus input 163 setTimeout(() => { 164 modalMessageInput.focus(); 165 }, 300); 166 } 167 168 function closeModalAssistant() { 169 // Zoom + Blur closing animation 170 modalOverlay.classList.add('closing'); 171 modalOverlay.classList.remove('show'); 172 173 // Stop canvas animation 174 if (voiceAnimator) { 175 voiceAnimator.stop(); 176 } 177 178 // Keep flex layout 179 modalOverlay.style.display = 'flex'; 180 modalOverlay.style.alignItems = 'center'; 181 modalOverlay.style.justifyContent = 'center'; 182 183 setTimeout(() => { 184 modalOverlay.style.display = 'none'; 185 modalOverlay.classList.remove('closing'); 186 // Clean style 187 modalOverlay.style.alignItems = ''; 188 modalOverlay.style.justifyContent = ''; 189 }, 400); 190 } 191 192 // Document suggestion functions 193 function modalGetFileIcon(fileType) { 194 if (modalCurrentMode === 'platform') { 195 return '<i class="bi bi-question-circle"></i>'; 196 } else { 197 const icons = { 198 'pdf': '<i class="bi bi-file-earmark-pdf"></i>', 199 'docx': '<i class="bi bi-file-earmark-word"></i>', 200 'markdown': '<i class="bi bi-file-earmark-text"></i>', 201 'xlsx': '<i class="bi bi-file-earmark-excel"></i>', 202 'pptx': '<i class="bi bi-file-earmark-ppt"></i>', 203 'default': '<i class="bi bi-file-earmark"></i>' 204 }; 205 return icons[fileType] || icons.default; 206 } 207 } 208 209 function modalHideDocumentSuggestions() { 210 if (modalSuggestions) { 211 modalSuggestions.style.display = 'none'; 212 } 213 // Show empty state if no messages and input is empty 214 if (modalMessageInput.value.trim() === '' && modalMessages.querySelectorAll('.chat-message').length === 0) { 215 if (modalEmptyState) { 216 modalEmptyState.classList.remove('hidden'); 217 } 218 } 219 } 220 221 function modalSelectDocument(source, title, preview) { 222 let question; 223 224 if (modalCurrentMode === 'platform') { 225 question = title; 226 } else { 227 if (preview && preview.length > 10) { 228 const content = preview.toLowerCase(); 229 230 if (content.includes('nasıl') || content.includes('iÅlem')) { 231 question = `${title} - bu iÅlem nasıl yapılır?`; 232 } else if (content.includes('rapor') || content.includes('basım')) { 233 question = `${title} raporu nasıl alınır?`; 234 } else if (content.includes('kurulum') || content.includes('ayar')) { 235 question = `${title} kurulumu nasıl yapılır?`; 236 } else if (content.includes('entegrasyon') || content.includes('baÄlantı')) { 237 question = `${title} entegrasyonu hakkında bilgi ver`; 238 } else { 239 const cleanTitle = title.replace(/\.(pdf|docx|md|txt|xlsx|pptx)$/i, ''); 240 question = `${cleanTitle} nedir`; 241 } 242 } else { 243 const cleanName = source.replace(/\.(pdf|docx|md|txt|xlsx|pptx)$/i, ''); 244 question = `${cleanName} nedir`; 245 } 246 } 247 248 modalMessageInput.value = question; 249 modalHideDocumentSuggestions(); 250 modalChatForm.dispatchEvent(new Event('submit')); 251 } 252 253 function modalDisplayDocumentSuggestions(results) { 254 if (!results.suggestions || results.suggestions.length === 0) { 255 modalHideDocumentSuggestions(); 256 return; 257 } 258 259 // Hide empty state when showing suggestions 260 if (modalEmptyState) { 261 modalEmptyState.classList.add('hidden'); 262 } 263 264 const headerText = modalCurrentMode === 'platform' ? 'Platform Help' : 'Suggested Documents'; 265 const headerIcon = modalCurrentMode === 'platform' ? '<i class="bi bi-question-circle me-1"></i>' : '<i class="bi bi-lightbulb me-1"></i>'; 266 let html = `<div class="modal-suggestions-header">${headerIcon}${headerText}</div>`; 267 268 results.suggestions.forEach((suggestion, index) => { 269 const icon = modalGetFileIcon(suggestion.file_type); 270 html += ` 271 <div class="modal-document-suggestion" data-source="${suggestion.source}" data-title="${suggestion.title}" data-content="${suggestion.content.replace(/"/g, '"').replace(/'/g, ''')}" onclick="modalSelectDocumentFromData(this)"> 272 <div class="modal-suggestion-main">
273 <span class="modal-suggestion-icon">${icon}</span> 274 <span class="modal-suggestion-title">${suggestion.title}</span> 275 <span class="modal-match-percentage">${suggestion.match_percentage}% Match</span> 276 </div> 277 <div class="modal-suggestion-preview">${suggestion.content}</div> 278 </div> 279 `; 280 }); 281 282 modalSuggestions.innerHTML = html; 283 modalSuggestions.style.display = 'block'; 284 } 285 286 async function modalSearchDocuments(query) { 287 try { 288 // Note: Update these endpoints when integrating 289 const endpoint = modalCurrentMode === 'platform' ? '/search-platform-help' : '/search-documents'; 290 291 const response = await fetch(endpoint, { 292 method: 'POST', 293 headers: { 'Content-Type': 'application/json' }, 294 body: JSON.stringify({ query: query }) 295 }); 296 297 const results = await response.json(); 298 modalDisplayDocumentSuggestions(results); 299 } catch (error) { 300 console.error('Modal document search error:', error); 301 modalHideDocumentSuggestions(); 302 } 303 } 304 305 function modalSelectDocumentFromData(element) { 306 const source = element.getAttribute('data-source'); 307 const title = element.getAttribute('data-title'); 308 const content = element.getAttribute('data-content') 309 .replace(/"/g, '"') 310 .replace(/'/g, "'"); 311 312 modalSelectDocument(source, title, content); 313 } 314 315 // Modal Event Listeners 316 document.addEventListener('DOMContentLoaded', function() { 317 // Assistant toggle button (header icon) 318 const assistantToggle = document.getElementById('assistantToggle'); 319 if (assistantToggle) { 320 assistantToggle.addEventListener('click', function(e) { 321 e.preventDefault(); 322 openModalAssistant('course'); 323 }); 324 } 325 326 // Sidebar assistant button 327 const sidebarAssistantToggle = document.getElementById('sidebarAssistantToggle'); 328 if (sidebarAssistantToggle) { 329 sidebarAssistantToggle.addEventListener('click', function(e) { 330 e.preventDefault(); 331 openModalAssistant('course'); 332 }); 333 } 334 335 // Assistant toggle button (login page header icon) 336 const loginAssistantToggle = document.getElementById('loginAssistantToggle'); 337 if (loginAssistantToggle) { 338 loginAssistantToggle.addEventListener('click', function(e) { 339 e.preventDefault(); 340 openModalAssistant('platform'); 341 }); 342 } 343 344 // Close button 345 const closeButton = document.getElementById('modalAssistantClose'); 346 if (closeButton) { 347 closeButton.addEventListener('click', closeModalAssistant); 348 } 349 350 // Click outside to close 351 if (modalOverlay) { 352 modalOverlay.addEventListener('click', function(e) { 353 if (e.target === modalOverlay) { 354 closeModalAssistant(); 355 } 356 }); 357 } 358 359 // ESC key to close 360 document.addEventListener('keydown', function(e) { 361 if (e.key === 'Escape' && modalOverlay && modalOverlay.classList.contains('show')) { 362 closeModalAssistant(); 363 } 364 }); 365 366 367 // Real-time document search 368 if (modalMessageInput) { 369 modalMessageInput.addEventListener('input', function() { 370 // Auto-resize 371 this.style.height = 'auto'; 372 this.style.height = (this.scrollHeight) + 'px'; 373 374 // Update voice circle state 375 const query = this.value.trim(); 376 if (query.length > 0) { 377 setVoiceCircleState('listening'); 378 } else { 379 setVoiceCircleState('idle'); 380 } 381 382 // Document search 383 if (query.length > 2) { 384 clearTimeout(modalSearchTimer); 385 modalSearchTimer = setTimeout(() => { 386 modalSearchDocuments(query); 387 }, 300); 388 } else { 389 modalHideDocumentSuggestions(); 390 if (query.length === 0 && modalEmptyState && !modalEmptyState.classList.contains('hidden')) { 391 modalEmptyState.style.display = 'block'; 392 } 393 } 394 }); 395 396 // Enter key handler 397 modalMessageInput.addEventListener('keydown', function(event) { 398 if (event.key === 'Enter' && !event.shiftKey) { 399 event.preventDefault(); 400 modalSendButton.click(); 401 } 402 }); 403 } 404 405 // Hide suggestions when clicking outside 406 document.addEventListener('click', function(event) { 407 if (modalSuggestions && !modalSuggestions.contains(event.target) && event.target !== modalMessageInput) { 408 modalHideDocumentSuggestions(); 409 } 410 }); 411 412 // Voice button (placeholder for future voice feature) 413 if (modalVoiceButton) { 414 modalVoiceButton.addEventListener('click', function() { 415 // TODO: Implement voice recording 416 alert('Voice input feature coming soon!'); 417 }); 418 } 419 420 // Form submit 421 if (modalChatForm) { 422 modalChatForm.addEventListener('submit', function(e) { 423 e.preventDefault(); 424 const messageText = modalMessageInput.value.trim(); 425 if (messageText === '') return; 426 427 if (modalEmptyState) { 428 modalEmptyState.classList.add('hidden'); 429 } 430 431 const userMessage = document.createElement('div'); 432 userMessage.classList.add('chat-message', 'user-message'); 433 userMessage.innerHTML = `<div class="message-bubble"><div class="message-content"></div></div>`; 434 userMessage.querySelector('.message-content').textContent = messageText; 435 modalMessages.appendChild(userMessage); 436 437 const typingIndicator = document.createElement('div'); 438 typingIndicator.classList.add('chat-message', 'assistant-message', 'typing-indicator'); 439 typingIndicator.innerHTML = `<div class="message-bubble"><div class="dot-flashing"></div></div>`; 440 modalMessages.appendChild(typingIndicator); 441 442 modalMessages.scrollTop = modalMessages.scrollHeight; 443 444 modalMessageInput.value = ''; 445 modalMessageInput.style.height = 'auto'; 446 modalHideDocumentSuggestions(); 447 448 // Set voice circle to thinking state 449 setVoiceCircleState('thinking'); 450 451 // Disable send button during request 452 if (modalSendButton) { 453 modalSendButton.disabled = true; 454 } 455 456 // Call DAG chat API with conversation_id 457 fetch('/api/ai/chat', { 458 method: 'POST', 459 headers: { 460 'Content-Type': 'application/json', 461 'X-Requested-With': 'XMLHttpRequest' 462 }, 463 body: JSON.stringify({ 464 message: messageText, 465 mode: modalCurrentMode, 466 conversation_id: currentConversationId // NEW: conversation_id support 467 }) 468 }) 469 .then(async response => { 470 console.log('Modal response status:', response.status); 471 const contentType = response.headers.get('content-type'); 472 473 // Check if response is JSON 474 if (!contentType || !contentType.includes('application/json')) { 475 const text = await response.text(); 476 console.error('Non-JSON response:', text.substring(0, 200)); 477 throw new Error(`Server returned non-JSON response: ${contentType}`); 478 } 479 480 if (!response.ok) { 481 return response.json().then(err => { 482 throw new Error(err.error || `HTTP error! status: ${response.status}`); 483 }); 484 } 485 return response.json(); 486 }) 487 .then(data => { 488 console.log('Modal response data:', data); 489 typingIndicator.remove(); 490
491 // NEW: Update currentConversationId from response 492 // Check both meta.conversation_id and root level conversation_id 493 if (data.meta && data.meta.conversation_id) { 494 currentConversationId = data.meta.conversation_id; 495 } else if (data.conversation_id) { 496 currentConversationId = data.conversation_id; 497 } 498 499 // Reload chat list if new conversation was created 500 if (currentConversationId) { 501 loadChats(); 502 } 503 504 // Set voice circle to speaking state 505 setVoiceCircleState('speaking'); 506 507 const assistantMessage = document.createElement('div'); 508 assistantMessage.classList.add('chat-message', 'assistant-message'); 509 510 // Extract result from DAG response 511 const result = data.result || {}; 512 const reply = result.reply || result.answer || 'No response generated'; 513 const confidence = result.confidence || 'medium'; 514 515 // Markdown parsing (if marked.js is available) 516 let parsedContent = reply; 517 if (typeof marked !== 'undefined') { 518 if (typeof marked.parse === 'function') { 519 parsedContent = marked.parse(parsedContent); 520 } else if (typeof marked === 'function') { 521 parsedContent = marked(parsedContent); 522 } 523 } 524 525 let messageContent = `<div class="message-content">${parsedContent}</div>`; 526 527 // Add structured data (tables, charts) if available 528 if (result.structured_data) { 529 const structured = result.structured_data; 530 531 // Render tables 532 if (structured.tables && structured.tables.length > 0) { 533 structured.tables.forEach(table => { 534 let tableHtml = '<div class="message-table mt-3"><h6 class="mb-2">' + (table.title || 'Tablo') + '</h6>'; 535 tableHtml += '<div class="table-responsive"><table class="table table-sm table-bordered">'; 536 537 if (table.headers && table.headers.length > 0) { 538 tableHtml += '<thead><tr>'; 539 table.headers.forEach(header => { 540 tableHtml += `<th>${header}</th>`; 541 }); 542 tableHtml += '</tr></thead>'; 543 } 544 545 if (table.rows && table.rows.length > 0) { 546 tableHtml += '<tbody>'; 547 table.rows.forEach(row => { 548 tableHtml += '<tr>'; 549 row.forEach(cell => { 550 tableHtml += `<td>${cell}</td>`; 551 }); 552 tableHtml += '</tr>'; 553 }); 554 tableHtml += '</tbody>'; 555 } 556 557 tableHtml += '</table></div></div>'; 558 messageContent += tableHtml; 559 }); 560 } 561 562 // Render metrics cards 563 if (structured.metrics && Object.keys(structured.metrics).length > 0) { 564 let metricsHtml = '<div class="message-metrics mt-3 row g-2">';
565 Object.entries(structured.metrics).forEach(([key, value]) => { 566 metricsHtml += `<div class="col-md-3"><div class="card bg-light"><div class="card-body p-2"><small class="text-secondary d-block">${key}</small><strong>${value}</strong></div></div></div>`; 567 }); 568 metricsHtml += '</div>'; 569 messageContent += metricsHtml; 570 } 571 572 // Charts with Chart.js 573 if (structured.charts && structured.charts.length > 0) { 574 structured.charts.forEach((chart, chartIndex) => { 575 const chartId = `chart-${Date.now()}-${chartIndex}`; 576 const chartContainerId = `chart-container-${chartId}`; 577 578 messageContent += `<div class="message-chart mt-3"><h6 class="mb-2"><i class="bi bi-bar-chart me-1"></i>${chart.title || 'Chart'}</h6><div id="${chartContainerId}" class="chart-container" style="position: relative; height: 300px;"><canvas id="${chartId}"></canvas></div></div>`; 579 580 // Render chart after message is added to DOM 581 setTimeout(() => { 582 const canvas = document.getElementById(chartId); 583 if (!canvas) return; 584 585 const ctx = canvas.getContext('2d'); 586 const chartData = chart.data || {}; 587 588 let chartConfig = {}; 589 590 if (chart.type === 'bar') { 591 chartConfig = { 592 type: 'bar', 593 data: { 594 labels: chartData.labels || [], 595 datasets: chartData.datasets || [{ 596 label: chartData.label || 'Data', 597 data: chartData.values || [], 598 backgroundColor: 'rgba(54, 162, 235, 0.5)', 599 borderColor: 'rgba(54, 162, 235, 1)', 600 borderWidth: 1 601 }] 602 }, 603 options: { 604 responsive: true, 605 maintainAspectRatio: false, 606 plugins: { 607 legend: { 608 display: chartData.datasets && chartData.datasets.length > 1 609 } 610 }, 611 scales: { 612 y: { 613 beginAtZero: true 614 } 615 } 616 } 617 }; 618 } else if (chart.type === 'line') { 619 chartConfig = { 620 type: 'line', 621 data: { 622 labels: chartData.labels || [], 623 datasets: chartData.datasets || [{ 624 label: chartData.label || 'Data', 625 data: chartData.values || [], 626 borderColor: 'rgba(75, 192, 192, 1)', 627 backgroundColor: 'rgba(75, 192, 192, 0.2)', 628 tension: 0.1 629 }] 630 }, 631 options: { 632 responsive: true, 633 maintainAspectRatio: false, 634 plugins: { 635 legend: { 636 display: chartData.datasets && chartData.datasets.length > 1 637 } 638 }, 639 scales: { 640 y: { 641 beginAtZero: true 642 } 643 } 644 } 645 }; 646 } else if (chart.type === 'pie') { 647 chartConfig = { 648 type: 'pie', 649 data: { 650 labels: chartData.labels || [], 651 datasets: [{ 652 data: chartData.values || [], 653 backgroundColor: [ 654 'rgba(255, 99, 132, 0.5)', 655 'rgba(54, 162, 235, 0.5)', 656 'rgba(255, 206, 86, 0.5)', 657 'rgba(75, 192, 192, 0.5)', 658 'rgba(153, 102, 255, 0.5)', 659 'rgba(255, 159, 64, 0.5)' 660 ], 661 borderColor: [ 662 'rgba(255, 99, 132, 1)', 663 'rgba(54, 162, 235, 1)', 664 'rgba(255, 206, 86, 1)', 665 'rgba(75, 192, 192, 1)', 666 'rgba(153, 102, 255, 1)', 667 'rgba(255, 159, 64, 1)' 668 ], 669 borderWidth: 1 670 }] 671 }, 672 options: { 673 responsive: true, 674 maintainAspectRatio: false, 675 plugins: { 676 legend: { 677 position: 'right' 678 } 679 } 680 } 681 }; 682 } 683 684 if (typeof Chart !== 'undefined' && chartConfig.type) { 685 new Chart(ctx, chartConfig); 686 } 687 }, 100); 688 }); 689 } 690 } 691 692 // Add summary if available 693 if (result.summary) { 694 messageContent += `<div class="message-summary mt-2 pt-2 border-top"><small class="text-secondary"><strong>Ãzet:</strong> ${result.summary}</small></div>`; 695 } 696 697 // Add suggested actions if available 698 if (result.suggested_actions && result.suggested_actions.length > 0) { 699 let actionsHtml = '<div class="message-actions mt-2 pt-2 border-top"><small class="text-secondary fw-bold"><i class="bi bi-lightbulb me-1"></i>Ãnerilen İÅlemler</small><ul class="mb-0 mt-1">'; 700 result.suggested_actions.forEach(action => { 701 actionsHtml += `<li class="text-secondary"><small>${action}</small></li>`; 702 }); 703 actionsHtml += '</ul></div>'; 704 messageContent += actionsHtml; 705 } 706 707 // Add insights if available 708 if (result.insights && result.insights.used_tools && result.insights.used_tools.length > 0) { 709 let toolsHtml = '<div class="message-tools mt-2 pt-2 border-top"><small class="text-secondary"><i class="bi bi-tools me-1"></i>Kullanılan Araçlar: ';
710 toolsHtml += result.insights.used_tools.join(', '); 711 toolsHtml += '</small></div>'; 712 messageContent += toolsHtml; 713 } 714 715 // Add confidence badge 716 const confidenceColors = { 717 'high': 'success', 718 'medium': 'warning', 719 'low': 'danger' 720 }; 721 const confidenceColor = confidenceColors[confidence] || 'secondary'; 722 messageContent += `<div class="message-confidence mt-2"><span class="badge bg-${confidenceColor}">Güven: ${confidence}</span></div>`; 723 724 assistantMessage.innerHTML = `<div class="message-bubble">${messageContent}</div>`; 725 modalMessages.appendChild(assistantMessage); 726 727 // Code highlighting (if hljs is available) 728 if (typeof hljs !== 'undefined') { 729 assistantMessage.querySelectorAll('pre code').forEach((block) => { 730 hljs.highlightElement(block); 731 }); 732 } 733 734 modalMessages.scrollTop = modalMessages.scrollHeight; 735 736 // Return to idle state after response 737 setTimeout(() => { 738 setVoiceCircleState('idle'); 739 }, 2000); 740 }) 741 .catch(error => { 742 console.error('Modal fetch error:', error); 743 typingIndicator.remove(); 744 745 // Error message 746 const errorMessage = document.createElement('div'); 747 errorMessage.classList.add('chat-message', 'assistant-message'); 748 const errorText = error.message || 'Bir hata oluÅtu. Lütfen tekrar deneyin.'; 749 errorMessage.innerHTML = `<div class="message-bubble"><div class="message-content text-danger"><i class="bi bi-exclamation-triangle me-1"></i>${errorText}</div></div>`; 750 modalMessages.appendChild(errorMessage); 751 modalMessages.scrollTop = modalMessages.scrollHeight; 752 753 // Return to idle state on error 754 setVoiceCircleState('idle'); 755 }) 756 .finally(() => { 757 // Re-enable send button 758 if (modalSendButton) { 759 modalSendButton.disabled = false; 760 } 761 }); 762 }); 763 } 764 }); 765 766 // Modal Sidebar Event Listeners 767 function initModalSidebarEvents() { 768 // Sidebar toggle button 769 if (modalSidebarToggle) { 770 modalSidebarToggle.addEventListener('click', function(e) { 771 e.preventDefault(); 772 toggleModalSidebar(); 773 }); 774 } 775 776 // Sidebar close button 777 if (modalSidebarCloseBtn) { 778 modalSidebarCloseBtn.addEventListener('click', function(e) { 779 e.preventDefault(); 780 e.stopPropagation(); // Prevent nav item click 781 hideModalSidebar(); 782 }); 783 } 784 785 // Navigation items 786 const navItems = document.querySelectorAll('.modal-nav-item'); 787 navItems.forEach(item => { 788 item.addEventListener('click', function(e) { 789 e.preventDefault(); 790 const tab = this.getAttribute('data-tab'); 791 if (tab) { 792 handleModalNavigation(tab); 793 } 794 }); 795 }); 796 } 797 798 // Global functions for onclick events 799 window.modalSelectDocument = modalSelectDocument; 800 window.modalSelectDocumentFromData = modalSelectDocumentFromData; 801 window.openModalAssistant = openModalAssistant; 802 window.closeModalAssistant = closeModalAssistant; 803 804 // Initialize modal sidebar 805 initModalSidebarEvents(); 806 807 // Initialize logo on page load 808 updateModalLogo(); 809 810 // Load conversations from API 811 async function loadChats() {
812 console.log(`[DEBUG] Loading chats with APP_ID: ${APP_ID}`); 813 console.log(`[DEBUG] Full URL: /api/chat/conversations?app_id=${APP_ID}`); 814 try { 815 const response = await fetch( 816 `/api/chat/conversations?app_id=${APP_ID}`, 817 { 818 credentials: 'include', 819 headers: { 820 'Content-Type': 'application/json' 821 } 822 } 823 ); 824 825 if (!response.ok) { 826 const errorText = await response.text(); 827 console.error(`[DEBUG] Chat API HTTP ${response.status} error:`, errorText); 828 throw new Error(`HTTP ${response.status}`); 829 } 830 831 const data = await response.json(); 832 const conversations = data.conversations || []; 833 834 // Convert API format to UI format 835 allChats = conversations.map(conv => ({ 836 id: conv.id, 837 title: conv.title || 'Yeni Sohbet', 838 preview: '', // Will be populated from first message if needed 839 date: conv.updated_at ? conv.updated_at.split('T')[0] : conv.created_at.split('T')[0], 840 timestamp: conv.updated_at ? new Date(conv.updated_at).toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' }) : '', 841 content: '' // Not needed for list view 842 })); 843 844 filteredChats = [...allChats]; 845 renderChatList(filteredChats); 846 } catch (error) { 847 console.error('Error loading chats:', error); 848 // Fallback to empty state 849 allChats = []; 850 filteredChats = []; 851 renderChatList([]); 852 } 853 } 854 855 // Load messages for a conversation 856 async function loadMessages(conversationId) { 857 try { 858 const response = await fetch( 859 `/api/chat/conversation/${conversationId}/messages`, 860 { 861 credentials: 'include', 862 headers: { 863 'Content-Type': 'application/json' 864 } 865 } 866 ); 867 868 if (!response.ok) { 869 throw new Error(`HTTP ${response.status}`); 870 } 871 872 const data = await response.json(); 873 const messages = data.messages || []; 874 875 // Clear current messages 876 if (modalMessages) { 877 modalMessages.innerHTML = ''; 878 } 879 880 // Hide empty state 881 if (modalEmptyState) { 882 modalEmptyState.classList.add('hidden'); 883 } 884 885 // Render messages 886 messages.forEach(msg => { 887 addMessageToChat(msg.message, msg.role); 888 }); 889 890 currentConversationId = conversationId; 891 } catch (error) { 892 console.error('Error loading messages:', error); 893 } 894 } 895 896 // Search conversations 897 async function searchChats(query) { 898 if (!query || query.trim().length === 0) { 899 loadChats(); // Load all chats if query is empty 900 return; 901 } 902 903 try { 904 const response = await fetch( 905 `/api/chat/conversations/search?q=${encodeURIComponent(query)}&app_id=${APP_ID}`, 906 { 907 credentials: 'include', 908 headers: { 909 'Content-Type': 'application/json' 910 } 911 } 912 ); 913 914 if (!response.ok) { 915 throw new Error(`HTTP ${response.status}`); 916 } 917 918 const data = await response.json(); 919 const conversations = data.conversations || []; 920 921 // Convert API format to UI format 922 filteredChats = conversations.map(conv => ({ 923 id: conv.id, 924 title: conv.title || 'Yeni Sohbet', 925 preview: '', 926 date: conv.updated_at ? conv.updated_at.split('T')[0] : conv.created_at.split('T')[0], 927 timestamp: conv.updated_at ? new Date(conv.updated_at).toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' }) : '', 928 content: '' 929 })); 930 931 renderChatList(filteredChats); 932 } catch (error) { 933 console.error('Error searching chats:', error); 934 renderChatList([]); 935 } 936 } 937 938 // Render chat list 939 function renderChatList(chats) { 940 const chatList = document.getElementById('modalChatList'); 941 if (!chatList) return; 942 943 if (!chats.length) { 944 chatList.innerHTML = '<div class="modal-chat-empty"><i class="bi bi-chat-text me-2"></i>Sonuç bulunamadı</div>'; 945 return; 946 } 947 948 chatList.innerHTML = chats.map(chat => ` 949 <div class="modal-chat-item" data-chat-id="${chat.id}"> 950 <div class="modal-chat-item-title" onclick="selectChat(${chat.id})">${chat.title}</div> 951 <button class="modal-chat-delete-btn" onclick="deleteChat(${chat.id}); event.stopPropagation();"> 952 <i class="bi bi-x"></i> 953 </button> 954 </div> 955 `).join(''); 956 } 957 958 // Format date for display 959 function formatDate(dateString) { 960 const date = new Date(dateString + 'T00:00:00'); 961 const today = new Date(); 962 const yesterday = new Date(today); 963 yesterday.setDate(yesterday.getDate() - 1); 964 965 if (date.toDateString() === today.toDateString()) { 966 return 'Bugün'; 967 } else if (date.toDateString() === yesterday.toDateString()) { 968 return 'Dün'; 969 } else { 970 return date.toLocaleDateString('tr-TR', { day: 'numeric', month: 'short' }); 971 } 972 } 973 974 // Initialize chat search functionality 975 function initializeChatSearch() { 976 const searchInput = document.getElementById('modalChatSearch'); 977 const clearBtn = document.getElementById('modalSearchClearBtn'); 978 979 if (searchInput) { 980 searchInput.addEventListener('input', function() { 981 filterChats(); 982 toggleClearButton(); 983 }); 984 } 985 986 if (clearBtn) { 987 clearBtn.addEventListener('click', function() { 988 clearSearch(); 989 }); 990 } 991 } 992 993 // Toggle clear button visibility 994 function toggleClearButton() { 995 const searchInput = document.getElementById('modalChatSearch'); 996 const clearBtn = document.getElementById('modalSearchClearBtn'); 997 998 if (searchInput && clearBtn) { 999 if (searchInput.value.trim().length > 0) { 1000 clearBtn.style.display = 'flex'; 1001 } else { 1002 clearBtn.style.display = 'none'; 1003 } 1004 } 1005 } 1006 1007 // Clear search input and results 1008 function clearSearch() { 1009 const searchInput = document.getElementById('modalChatSearch'); 1010 const clearBtn = document.getElementById('modalSearchClearBtn'); 1011 1012 if (searchInput) { 1013 searchInput.value = ''; 1014 searchInput.focus(); 1015 } 1016 1017 if (clearBtn) { 1018 clearBtn.style.display = 'none'; 1019 } 1020 1021 // Load all chats from API 1022 loadChats(); 1023 } 1024 1025 // Filter chats based on search term (now uses API search) 1026 function filterChats() { 1027 const searchInput = document.getElementById('modalChatSearch'); 1028 if (!searchInput) return; 1029 1030 const searchTerm = searchInput.value.trim(); 1031 1032 if (!searchTerm) { 1033 // Load all chats if search is empty 1034 loadChats(); 1035 } else { 1036 // Use API search 1037 searchChats(searchTerm); 1038 } 1039 } 1040 1041 // Select and load a chat 1042 async function selectChat(chatId) { 1043 currentConversationId = chatId; 1044 1045 // Load messages for this conversation 1046 await loadMessages(chatId); 1047 1048 // Close sidebar to show main chat area 1049 hideModalSidebar(); 1050 } 1051 1052 // Delete (archive) a chat 1053 async function deleteChat(chatId) { 1054 if (confirm('Bu sohbeti silmek istediÄinizden emin misiniz?')) { 1055 try { 1056 const response = await fetch( 1057 `/api/chat/conversation/${chatId}/archive`, 1058 { 1059 method: 'POST', 1060 credentials: 'include', 1061 headers: { 1062 'Content-Type': 'application/json' 1063 } 1064 } 1065 ); 1066 1067 if (!response.ok) { 1068 throw new Error(`HTTP ${response.status}`); 1069 } 1070 1071 // If deleted conversation was current, reset 1072 if (currentConversationId === chatId) { 1073 currentConversationId = null; 1074 if (modalMessages) { 1075 modalMessages.innerHTML = ''; 1076 } 1077 if (modalEmptyState) { 1078 modalEmptyState.classList.remove('hidden'); 1079 } 1080 } 1081 1082 // Reload chat list 1083 loadChats(); 1084 } catch (error) { 1085 console.error('Error archiving chat:', error); 1086 alert('Sohbet silinirken bir hata oluÅtu.'); 1087 } 1088 } 1089 } 1090 1091 // Add message to main chat area 1092 function addMessageToChat(message, sender) { 1093 const messagesContainer = document.querySelector('.modal-chat-messages'); 1094 const emptyState = messagesContainer.querySelector('.modal-chat-empty-state'); 1095 1096 // Hide empty state if it exists 1097 if (emptyState) { 1098 emptyState.style.display = 'none'; 1099 } 1100 1101 // Create message element 1102 const messageDiv = document.createElement('div'); 1103 messageDiv.className = `chat-message ${sender}-message`; 1104 messageDiv.innerHTML = ` 1105 <div class="message-content"> 1106 ${message} 1107 </div> 1108 <div class="message-time"> 1109 ${new Date().toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' })} 1110 </div> 1111 `; 1112 1113 messagesContainer.appendChild(messageDiv); 1114 messagesContainer.scrollTop = messagesContainer.scrollHeight; 1115 } 1116 1117 // Initialize chat functionality when modal opens 1118 function initializeModalChat() { 1119 loadChats(); // Load from API instead of mock 1120 initializeChatSearch(); 1121 } 1122 1123 // Global functions for chat 1124 window.selectChat = selectChat; 1125 window.deleteChat = deleteChat; 1126 1127 // Initialize chat system 1128 initializeModalChat(); 1129 1130 // Listen for theme changes (observe body class changes)
1131 const themeObserver = new MutationObserver(function(mutations) { 1132 mutations.forEach(function(mutation) { 1133 if (mutation.attributeName === 'class') { 1134 updateModalLogo(); 1135 } 1136 }); 1137 }); 1138 1139 if (document.body) { 1140 themeObserver.observe(document.body, { attributes: true }); 1141 } 1142 1143 // AIRA MODAL ENHANCEMENTS (Minimize/Restore, Drag, Resize) 1144 // Only affects #modalAssistantOverlay - AiraCoder modal untouched 1145 // =================================================================== 1146 1147 // Aira Modal Enhancement State 1148 let airaModalState = { 1149 isDragging: false, 1150 dragOffset: { x: 0, y: 0 }, 1151 isResizing: false, 1152 resizeDirection: null, 1153 isMinimized: false, 1154 minWidth: 400, 1155 minHeight: 300, 1156 maxWidth: window.innerWidth * 0.9, 1157 maxHeight: window.innerHeight * 0.9 1158 }; 1159 1160 // State persistence functions 1161 function saveAiraModalState(container) { 1162 const rect = container.getBoundingClientRect(); 1163 const state = { 1164 x: rect.left, 1165 y: rect.top, 1166 width: rect.width, 1167 height: rect.height, 1168 minimized: airaModalState.isMinimized 1169 }; 1170 localStorage.setItem('airaModalState', JSON.stringify(state)); 1171 } 1172 1173 function loadAiraModalState(container) { 1174 try { 1175 const saved = localStorage.getItem('airaModalState'); 1176 if (!saved) return; 1177 1178 const state = JSON.parse(saved); 1179 1180 // Validate bounds 1181 const maxX = window.innerWidth - state.width; 1182 const maxY = window.innerHeight - state.height; 1183 1184 const x = Math.max(0, Math.min(state.x, maxX)); 1185 const y = Math.max(0, Math.min(state.y, maxY)); 1186 const width = Math.max(airaModalState.minWidth, Math.min(state.width, airaModalState.maxWidth)); 1187 const height = Math.max(airaModalState.minHeight, Math.min(state.height, airaModalState.maxHeight)); 1188 1189 // Apply saved state 1190 container.style.left = x + 'px'; 1191 container.style.top = y + 'px'; 1192 container.style.width = width + 'px'; 1193 container.style.height = height + 'px'; 1194 container.style.right = 'auto'; 1195 1196 // Apply minimized state 1197 if (state.minimized) { 1198 airaModalState.isMinimized = true; 1199 container.classList.add('minimized'); 1200 const minimizeBtn = document.getElementById('airaModalMinimizeBtn'); 1201 if (minimizeBtn) { 1202 minimizeBtn.innerHTML = '<i class="bi bi-plus-lg"></i>'; 1203 minimizeBtn.title = 'Büyüt'; 1204 } 1205 } 1206 } catch (e) { 1207 console.warn('Failed to load Aira modal state:', e); 1208 } 1209 } 1210 1211 // Check if mobile device 1212 function isMobileDevice() { 1213 return window.innerWidth <= 768 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); 1214 } 1215 1216 // Drag functionality 1217 function initializeAiraModalDrag(airaContainer, header) { 1218 // Disable drag on mobile 1219 if (isMobileDevice()) return; 1220 1221 // Remove existing drag listeners first 1222 const existingHandler = header._airaModalDragHandler; 1223 if (existingHandler) { 1224 header.removeEventListener('mousedown', existingHandler); 1225 } 1226 1227 let isDragging = false; 1228 let dragOffset = { x: 0, y: 0 }; 1229 1230 function dragHandler(e) { 1231 // Don't drag if clicking on buttons 1232 if (e.target.closest('button')) return; 1233 1234 isDragging = true; 1235 const rect = airaContainer.getBoundingClientRect(); 1236 dragOffset.x = e.clientX - rect.left; 1237 dragOffset.y = e.clientY - rect.top; 1238 1239 // Disable transitions during drag for smoothness 1240 airaContainer.style.transition = 'none'; 1241 1242 document.addEventListener('mousemove', handleDrag); 1243 document.addEventListener('mouseup', handleDragEnd); 1244 1245 // Prevent text selection 1246 e.preventDefault(); 1247 } 1248 1249 let dragAnimationId = null; 1250 1251 function handleDrag(e) { 1252 if (!isDragging) return; 1253 1254 // Cancel previous animation frame for smooth movement 1255 if (dragAnimationId) { 1256 cancelAnimationFrame(dragAnimationId); 1257 } 1258 1259 dragAnimationId = requestAnimationFrame(() => { 1260 let newX = e.clientX - dragOffset.x; 1261 let newY = e.clientY - dragOffset.y; 1262 1263 // Boundary checks 1264 const maxX = window.innerWidth - airaContainer.offsetWidth; 1265 const maxY = window.innerHeight - airaContainer.offsetHeight; 1266 1267 newX = Math.max(0, Math.min(newX, maxX)); 1268 newY = Math.max(0, Math.min(newY, maxY)); 1269 1270 airaContainer.style.setProperty('position', 'fixed', 'important'); 1271 airaContainer.style.setProperty('left', newX + 'px', 'important'); 1272 airaContainer.style.setProperty('top', newY + 'px', 'important'); 1273 airaContainer.style.setProperty('right', 'auto', 'important'); 1274 airaContainer.style.setProperty('transform', 'none', 'important'); 1275 airaContainer.style.setProperty('margin', '0', 'important'); 1276 }); 1277 } 1278 1279 function handleDragEnd() { 1280 isDragging = false;
1281 document.removeEventListener('mousemove', handleDrag); 1282 document.removeEventListener('mouseup', handleDragEnd); 1283 1284 // Cancel any pending animation frame 1285 if (dragAnimationId) { 1286 cancelAnimationFrame(dragAnimationId); 1287 } 1288 1289 // Re-enable transitions after drag for normal animations 1290 airaContainer.style.transition = ''; 1291 1292 // Save state after drag 1293 saveAiraModalState(airaContainer); 1294 } 1295 1296 // Store handler reference for cleanup 1297 header._airaModalDragHandler = dragHandler; 1298 header.addEventListener('mousedown', dragHandler); 1299 } 1300 1301 // Resize functionality 1302 function initializeAiraModalResize(airaContainer) { 1303 // Disable resize on mobile 1304 if (isMobileDevice()) return; 1305 1306 const resizeHandles = airaContainer.querySelectorAll('.aira-resize-handle'); 1307 1308 // Clean up existing listeners 1309 resizeHandles.forEach(handle => { 1310 const existingHandler = handle._airaModalResizeHandler; 1311 if (existingHandler) { 1312 handle.removeEventListener('mousedown', existingHandler); 1313 } 1314 }); 1315 1316 resizeHandles.forEach(handle => { 1317 function resizeHandler(e) { 1318 e.preventDefault(); 1319 e.stopPropagation(); 1320 1321 airaModalState.isResizing = true; 1322 airaModalState.resizeDirection = handle.dataset.direction; 1323 1324 const startX = e.clientX; 1325 const startY = e.clientY; 1326 const startRect = airaContainer.getBoundingClientRect(); 1327 1328 // Disable transitions during resize for smoothness 1329 airaContainer.style.transition = 'none'; 1330 1331 document.addEventListener('mousemove', handleResize); 1332 document.addEventListener('mouseup', handleResizeEnd); 1333 1334 let rafId = null; 1335 1336 function handleResize(e) { 1337 if (!airaModalState.isResizing) return; 1338 1339 // Cancel previous frame for smooth animation 1340 if (rafId) { 1341 cancelAnimationFrame(rafId); 1342 } 1343 1344 rafId = requestAnimationFrame(() => { 1345 const deltaX = e.clientX - startX; 1346 const deltaY = e.clientY - startY; 1347 const direction = airaModalState.resizeDirection; 1348 1349 let newWidth = startRect.width; 1350 let newHeight = startRect.height; 1351 let newLeft = startRect.left; 1352 let newTop = startRect.top; 1353 1354 // Handle different resize directions 1355 if (direction.includes('e')) { 1356 newWidth = Math.max(airaModalState.minWidth, 1357 Math.min(airaModalState.maxWidth, startRect.width + deltaX)); 1358 } 1359 if (direction.includes('w')) { 1360 newWidth = Math.max(airaModalState.minWidth, 1361 Math.min(airaModalState.maxWidth, startRect.width - deltaX)); 1362 newLeft = startRect.left + (startRect.width - newWidth); 1363 } 1364 if (direction.includes('s')) { 1365 newHeight = Math.max(airaModalState.minHeight, 1366 Math.min(airaModalState.maxHeight, startRect.height + deltaY)); 1367 } 1368 if (direction.includes('n')) { 1369 newHeight = Math.max(airaModalState.minHeight, 1370 Math.min(airaModalState.maxHeight, startRect.height - deltaY)); 1371 newTop = startRect.top + (startRect.height - newHeight); 1372 } 1373 1374 // Apply new dimensions and position with !important 1375 airaContainer.style.setProperty('width', newWidth + 'px', 'important'); 1376 airaContainer.style.setProperty('height', newHeight + 'px', 'important'); 1377 airaContainer.style.setProperty('left', newLeft + 'px', 'important'); 1378 airaContainer.style.setProperty('top', newTop + 'px', 'important'); 1379 airaContainer.style.setProperty('right', 'auto', 'important'); 1380 }); 1381 } 1382 1383 function handleResizeEnd() { 1384 airaModalState.isResizing = false;
1385 airaModalState.resizeDirection = null; 1386 document.removeEventListener('mousemove', handleResize); 1387 document.removeEventListener('mouseup', handleResizeEnd); 1388 1389 // Cancel any pending animation frame 1390 if (rafId) { 1391 cancelAnimationFrame(rafId); 1392 } 1393 1394 // Re-enable transitions after resize for normal animations 1395 airaContainer.style.transition = ''; 1396 1397 // Save state after resize 1398 saveAiraModalState(airaContainer); 1399 } 1400 } 1401 1402 // Store handler reference for cleanup 1403 handle._airaModalResizeHandler = resizeHandler; 1404 handle.addEventListener('mousedown', resizeHandler); 1405 }); 1406 } 1407 1408 // Enhanced modal functionality - Direct drag/resize 1409 function initializeAiraModalEnhancements() { 1410 const airaModal = document.getElementById('modalAssistantOverlay'); 1411 const airaContainer = airaModal?.querySelector('.modal-assistant-container'); 1412 const minimizeBtn = document.getElementById('airaModalMinimizeBtn'); 1413 const glowToggleBtn = document.getElementById('airaModalGlowToggleBtn'); 1414 const header = airaModal?.querySelector('.modal-assistant-header'); 1415 1416 if (!airaModal || !airaContainer || !minimizeBtn || !glowToggleBtn || !header) return; 1417 1418 // Convert to fixed positioning for drag/resize (keep current position) 1419 const rect = airaContainer.getBoundingClientRect(); 1420 airaContainer.style.setProperty('position', 'fixed', 'important'); 1421 airaContainer.style.setProperty('top', rect.top + 'px', 'important'); 1422 airaContainer.style.setProperty('left', rect.left + 'px', 'important'); 1423 airaContainer.style.setProperty('width', rect.width + 'px', 'important'); 1424 airaContainer.style.setProperty('height', rect.height + 'px', 'important'); 1425 airaContainer.style.setProperty('transform', 'none', 'important'); 1426 airaContainer.style.setProperty('margin', '0', 'important'); 1427 airaContainer.classList.add('aira-draggable'); 1428 // Glow effect CSS'te otomatik uygulanıyor 1429 1430 // Initialize drag, resize, and glow toggle functionality 1431 initializeAiraModalDrag(airaContainer, header); 1432 initializeAiraModalResize(airaContainer); 1433 initializeAiraModalGlowToggle(airaContainer, glowToggleBtn); 1434 1435 // Store original position/size before minimize 1436 let originalState = null; 1437 1438 // Minimize/Restore toggle with smooth animation 1439 minimizeBtn.addEventListener('click', function(e) { 1440 e.stopPropagation(); // Prevent header drag 1441 1442 airaModalState.isMinimized = !airaModalState.isMinimized; 1443 1444 if (airaModalState.isMinimized) { 1445 // Save current position/size before minimizing 1446 const rect = airaContainer.getBoundingClientRect(); 1447 originalState = { 1448 width: rect.width, 1449 height: rect.height, 1450 top: rect.top, 1451 left: rect.left 1452 }; 1453 1454 // Smooth minimize animation 1455 animateMinimize(airaContainer, originalState); 1456 1457 minimizeBtn.innerHTML = '<i class="bi bi-plus-lg"></i>'; 1458 minimizeBtn.title = ''; // Remove tooltip when minimized 1459 } else { 1460 // Smooth restore animation 1461 if (originalState) { 1462 animateRestore(airaContainer, originalState); 1463 } 1464 1465 minimizeBtn.innerHTML = '<i class="bi bi-dash-lg"></i>'; 1466 minimizeBtn.title = 'Küçült'; 1467 } 1468 1469 // Save state after minimize/restore 1470 saveAiraModalState(airaContainer); 1471 }); 1472 1473 // Add click event to modal container for restore when minimized 1474 airaContainer.addEventListener('click', function(e) { 1475 // Only restore if minimized (header is hidden anyway) 1476 if (airaModalState.isMinimized) { 1477 // Trigger restore by simulating minimize button click 1478 minimizeBtn.click(); 1479 } 1480 }); 1481 } 1482 1483 // Smooth minimize animation 1484 function animateMinimize(container, fromState) { 1485 // Add transition temporarily 1486 container.style.transition = 'all 0.5s cubic-bezier(0.25, 0.8, 0.25, 1)'; 1487 1488 // Apply minimized styles 1489 requestAnimationFrame(() => { 1490 container.style.setProperty('width', '100px', 'important'); 1491 container.style.setProperty('height', '100px', 'important'); 1492 container.style.setProperty('bottom', '20px', 'important'); 1493 container.style.setProperty('right', '20px', 'important'); 1494 container.style.setProperty('top', 'auto', 'important'); 1495 container.style.setProperty('left', 'auto', 'important'); 1496 container.style.setProperty('overflow', 'hidden', 'important'); 1497 1498 container.classList.add('minimized'); 1499 }); 1500 1501 // Remove transition after animation 1502 setTimeout(() => { 1503 container.style.transition = ''; 1504 }, 500); 1505 } 1506 1507 // Smooth restore animation 1508 function animateRestore(container, toState) { 1509 // Add transition temporarily 1510 container.style.transition = 'all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)'; 1511 1512 // Apply restored styles 1513 requestAnimationFrame(() => { 1514 container.style.setProperty('width', toState.width + 'px', 'important'); 1515 container.style.setProperty('height', toState.height + 'px', 'important'); 1516 container.style.setProperty('top', toState.top + 'px', 'important'); 1517 container.style.setProperty('left', toState.left + 'px', 'important'); 1518 container.style.setProperty('bottom', 'auto', 'important'); 1519 container.style.setProperty('right', 'auto', 'important'); 1520 container.style.setProperty('overflow', 'visible', 'important'); 1521 1522 container.classList.remove('minimized'); 1523 }); 1524 1525 // Remove transition after animation 1526 setTimeout(() => { 1527 container.style.transition = ''; 1528 }, 400); 1529 } 1530 1531 // Glow toggle functionality 1532 function initializeAiraModalGlowToggle(container, glowToggleBtn) { 1533 let isGlowEnabled = true; // Default: glow is enabled 1534 1535 // Set initial button state 1536 updateGlowToggleButton(glowToggleBtn, isGlowEnabled); 1537 1538 glowToggleBtn.addEventListener('click', function(e) { 1539 e.stopPropagation(); // Prevent header drag 1540 1541 isGlowEnabled = !isGlowEnabled; 1542 1543 if (isGlowEnabled) { 1544 // Enable glow effect 1545 container.style.filter = ''; 1546 container.style.animation = ''; 1547 container.classList.remove('glow-disabled'); 1548 } else { 1549 // Disable glow effect 1550 container.style.filter = 'none';
1551 container.style.animation = 'none'; 1552 container.classList.add('glow-disabled'); 1553 } 1554 1555 // Update button appearance 1556 updateGlowToggleButton(glowToggleBtn, isGlowEnabled); 1557 1558 // Save state to localStorage 1559 localStorage.setItem('airaModalGlowEnabled', isGlowEnabled.toString()); 1560 }); 1561 1562 // Load saved state 1563 const savedGlowState = localStorage.getItem('airaModalGlowEnabled'); 1564 if (savedGlowState !== null) { 1565 isGlowEnabled = savedGlowState === 'true'; 1566 1567 if (!isGlowEnabled) { 1568 container.style.filter = 'none'; 1569 container.style.animation = 'none'; 1570 container.classList.add('glow-disabled'); 1571 } 1572 1573 updateGlowToggleButton(glowToggleBtn, isGlowEnabled); 1574 } 1575 } 1576 1577 // Update glow toggle button appearance 1578 function updateGlowToggleButton(button, isEnabled) { 1579 const icon = button.querySelector('i'); 1580 if (isEnabled) { 1581 icon.className = 'bi bi-lightning-charge'; // Glow açık = normal ikon 1582 button.classList.remove('active'); 1583 button.title = ''; // Remove tooltip 1584 } else { 1585 icon.className = 'bi bi-lightning-charge-fill'; // Glow kapalı = fill ikon + glow 1586 button.classList.add('active'); 1587 button.title = ''; // Remove tooltip 1588 } 1589 } 1590 1591 // Initialize enhancements when modal is opened 1592 // Use MutationObserver to detect when modal is shown 1593 function observeModalOpen() { 1594 const airaModal = document.getElementById('modalAssistantOverlay'); 1595 if (!airaModal) return; 1596 1597 const observer = new MutationObserver(function(mutations) { 1598 mutations.forEach(function(mutation) { 1599 if (mutation.type === 'attributes' && mutation.attributeName === 'class') { 1600 const target = mutation.target; 1601 if (target.classList.contains('show')) { 1602 // Modal opened, initialize enhancements 1603 setTimeout(initializeAiraModalEnhancements, 100); 1604 } 1605 } 1606 }); 1607 }); 1608 1609 observer.observe(airaModal, { 1610 attributes: true, 1611 attributeFilter: ['class'] 1612 }); 1613 } 1614 1615 // Initialize observer when DOM is ready 1616 if (document.readyState === 'loading') { 1617 document.addEventListener('DOMContentLoaded', observeModalOpen); 1618 } else { 1619 observeModalOpen(); 1620 } 1621})(); 1622
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.