(function() { // Конфігурація агента const config = { agentId: 5, apiKey: 'nav_gksn-EWBLeDSuwL3MI2qo7lD5jX6tSOAq6UN1fS4It0', apiEndpoint: 'https://api.eon.plus/chat/navigation', websocketEndpoint: 'wss://api.eon.plus/chat/navigation/ws', agentName: 'EON+ Assistant', language: 'uk', theme: 'light', position: 'bottom-left' }; // Перевірка чи скрипт вже завантажено if (window.NavigationAssistant) { console.log('Navigation Assistant already loaded'); return; } // Додаємо CSS стилі const style = document.createElement('style'); style.textContent = `/* Сучасні стилі для навігаційного асистента */ .nav-assistant-container { position: fixed !important; bottom: 24px !important; left: 24px !important; z-index: 999999 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Inter', sans-serif; animation: slideInUp 0.4s cubic-bezier(0.16, 1, 0.3, 1); max-width: calc(100vw - 48px); max-height: calc(100vh - 48px); pointer-events: auto !important; } @keyframes slideInUp { from { transform: translateY(100px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } .nav-assistant-toggle { width: 64px; height: 64px; border-radius: 50%; background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #ec4899 100%); border: none; color: white; font-size: 28px; cursor: pointer; box-shadow: 0 4px 14px 0 rgba(99, 102, 241, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.05); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); position: relative; overflow: hidden; backdrop-filter: blur(10px); display: flex; align-items: center; justify-content: center; } .nav-assistant-toggle::before { content: ''; position: absolute; top: 50%; left: 50%; width: 0; height: 0; background: rgba(255, 255, 255, 0.3); border-radius: 50%; transform: translate(-50%, -50%); transition: all 0.3s ease; } .nav-assistant-toggle:hover { transform: scale(1.05) translateY(-2px); box-shadow: 0 8px 25px 0 rgba(99, 102, 241, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.1); background: linear-gradient(135deg, #7c3aed 0%, #a855f7 50%, #f472b6 100%); } .nav-assistant-toggle:hover::before { width: 100%; height: 100%; background: rgba(255, 255, 255, 0.1); } .nav-assistant-toggle:active { transform: scale(0.98) translateY(0px); transition: all 0.1s ease; } .nav-assistant-chat { position: absolute; bottom: 84px; left: 0; width: min(400px, calc(100vw - 48px)); height: min(600px, calc(100vh - 120px)); background: rgba(255, 255, 255, 0.95); border-radius: 20px; box-shadow: 0 32px 64px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(255, 255, 255, 0.8), inset 0 1px 0 rgba(255, 255, 255, 0.9); display: none; flex-direction: column; overflow: hidden; backdrop-filter: blur(20px) saturate(180%); transform: scale(0.85) translateY(24px); opacity: 0; transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1); } .nav-assistant-chat.open { display: flex; transform: scale(1) translateY(0); opacity: 1; } .nav-assistant-header { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #ec4899 100%); color: white; padding: 20px 24px; display: flex; justify-content: space-between; align-items: center; position: relative; overflow: hidden; } .nav-assistant-header::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, transparent 50%); pointer-events: none; } .nav-assistant-header h3 { margin: 0; font-size: 18px; font-weight: 700; position: relative; z-index: 1; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } .nav-assistant-close { background: rgba(255, 255, 255, 0.1); border: none; color: white; font-size: 20px; cursor: pointer; padding: 0; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); position: relative; z-index: 1; backdrop-filter: blur(10px); } .nav-assistant-close:hover { background: rgba(255, 255, 255, 0.2); transform: scale(1.1); } .nav-assistant-close:active { transform: scale(0.95); } .nav-assistant-messages { flex: 1; padding: 24px 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; scroll-behavior: smooth; } .nav-assistant-messages::-webkit-scrollbar { width: 6px; } .nav-assistant-messages::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.05); border-radius: 3px; } .nav-assistant-messages::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.2); border-radius: 3px; transition: background 0.2s ease; } .nav-assistant-messages::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.3); } .nav-assistant-message { max-width: 85%; padding: 14px 18px; font-size: 15px; line-height: 1.5; word-wrap: break-word; animation: messageSlideIn 0.4s cubic-bezier(0.16, 1, 0.3, 1); position: relative; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } .nav-assistant-message.user { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); color: white; align-self: flex-end; border-radius: 20px 20px 6px 20px; margin-left: auto; box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); } .nav-assistant-message.bot { background: rgba(248, 250, 252, 0.9); color: #1e293b; align-self: flex-start; border-radius: 20px 20px 20px 6px; border: 1px solid rgba(226, 232, 240, 0.8); backdrop-filter: blur(10px); } .nav-assistant-input { padding: 20px 24px; border-top: 1px solid rgba(226, 232, 240, 0.6); background: rgba(248, 250, 252, 0.8); backdrop-filter: blur(10px); display: flex; gap: 12px; align-items: center; } .nav-assistant-input input { flex: 1; padding: 14px 18px; border: 2px solid rgba(226, 232, 240, 0.6); border-radius: 25px; font-size: 15px; font-family: inherit; outline: none; background: rgba(255, 255, 255, 0.7); backdrop-filter: blur(10px); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); color: rgba(30, 41, 59, 0.8); } .nav-assistant-input input:focus { border-color: #6366f1; box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); background: rgba(255, 255, 255, 1); color: #1e293b; } .nav-assistant-input input::placeholder { color: rgba(100, 116, 139, 0.7); font-weight: 400; transition: color 0.3s ease; } .nav-assistant-input input:focus::placeholder { color: #94a3b8; } .nav-assistant-send { padding: 14px 24px; background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); color: white; border: none; border-radius: 25px; cursor: pointer; font-size: 15px; font-weight: 600; font-family: inherit; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); position: relative; overflow: hidden; } .nav-assistant-send::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); transition: left 0.5s; } .nav-assistant-send:hover { background: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%); transform: translateY(-1px); box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4); } .nav-assistant-send:hover::before { left: 100%; } .nav-assistant-send:active { transform: translateY(0); } .nav-assistant-action { padding: 0; border: none; border-radius: 50%; cursor: pointer; font-size: 18px; width: 48px; height: 48px; display: flex; align-items: center; justify-content: center; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); position: relative; overflow: hidden; color: white; } .nav-assistant-action.voice-mode { background: linear-gradient(135deg, #10b981 0%, #059669 100%); box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3); } .nav-assistant-action.send-mode { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); } .nav-assistant-action::before { content: ''; position: absolute; top: 50%; left: 50%; width: 0; height: 0; background: rgba(255, 255, 255, 0.2); border-radius: 50%; transform: translate(-50%, -50%); transition: all 0.3s ease; } .nav-assistant-action.voice-mode:hover { background: linear-gradient(135deg, #059669 0%, #047857 100%); transform: scale(1.05) translateY(-1px); box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4); } .nav-assistant-action.send-mode:hover { background: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%); transform: scale(1.05) translateY(-1px); box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4); } .nav-assistant-action:hover::before { width: 100%; height: 100%; } .nav-assistant-action:active { transform: scale(0.98) translateY(0); } /* Анімації для іконок */ .nav-assistant-action svg { transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); } .nav-assistant-action.send-mode .send-icon { animation: iconFadeIn 0.2s ease-out; } .nav-assistant-action.voice-mode .microphone-icon { animation: iconFadeIn 0.2s ease-out; } @keyframes iconFadeIn { from { opacity: 0; transform: scale(0.8); } to { opacity: 1; transform: scale(1); } } /* Швидкі кнопки */ .nav-assistant-quick-buttons { padding: 12px 16px; border-top: 1px solid #f0f0f0; display: flex; flex-wrap: wrap; gap: 8px; animation: slideInUp 0.3s ease-out; } .nav-assistant-quick-button { padding: 8px 12px; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border: 1px solid #dee2e6; border-radius: 20px; font-size: 12px; color: #495057; cursor: pointer; transition: all 0.2s ease; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; } .nav-assistant-quick-button:hover { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); } .nav-assistant-quick-button:active { transform: translateY(0); } /* Покращені повідомлення */ .nav-assistant-message { margin: 8px 16px; padding: 12px 16px; border-radius: 18px; max-width: 80%; word-wrap: break-word; animation: messageSlideIn 0.3s ease-out; position: relative; } @keyframes messageSlideIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .nav-assistant-message.user { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; align-self: flex-end; border-bottom-right-radius: 4px; } .nav-assistant-message.bot { background: #f8f9fa; color: #333; align-self: flex-start; border-bottom-left-radius: 4px; border: 1px solid #e9ecef; } .nav-assistant-message.system { background: #fff3cd; color: #856404; align-self: center; border: 1px solid #ffeaa7; font-size: 12px; text-align: center; max-width: 90%; } /* Анімація друкування */ .nav-assistant-message.typing { position: relative; } .nav-assistant-message.typing::after { content: '...'; animation: typing 1.5s infinite; } @keyframes typing { 0%, 60%, 100% { opacity: 0; } 30% { opacity: 1; } } /* Адаптивність для мобільних пристроїв */ @media (max-width: 480px) { .nav-assistant-container { bottom: 16px; left: 16px; right: 16px; max-width: none; } .nav-assistant-chat { width: 100%; height: min(80vh, 500px); left: 0; bottom: 80px; } .nav-assistant-toggle { width: 56px; height: 56px; font-size: 24px; } .nav-assistant-header { padding: 16px 20px; } .nav-assistant-header h3 { font-size: 16px; } .nav-assistant-messages { padding: 16px; gap: 12px; } .nav-assistant-message { max-width: 90%; padding: 12px 16px; font-size: 14px; } .nav-assistant-input { padding: 16px; gap: 8px; } .nav-assistant-input input { padding: 12px 16px; font-size: 14px; background: rgba(255, 255, 255, 0.9); color: rgba(30, 41, 59, 0.9); } .nav-assistant-input input:focus { background: rgba(255, 255, 255, 1); color: #1e293b; } .nav-assistant-send { padding: 12px 20px; font-size: 14px; } .nav-assistant-action { width: 44px; height: 44px; font-size: 16px; } } /* Темна тема (автоматично визначається) */ @media (prefers-color-scheme: dark) { .nav-assistant-chat { background: rgba(30, 41, 59, 0.95); box-shadow: 0 32px 64px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.05); } .nav-assistant-message.bot { background: rgba(51, 65, 85, 0.9); color: #e2e8f0; border: 1px solid rgba(71, 85, 105, 0.8); } .nav-assistant-input { background: rgba(51, 65, 85, 0.8); border-top: 1px solid rgba(71, 85, 105, 0.6); } .nav-assistant-input input { background: rgba(30, 41, 59, 0.8); border: 2px solid rgba(71, 85, 105, 0.6); color: rgba(226, 232, 240, 0.9); } .nav-assistant-input input:focus { background: rgba(30, 41, 59, 1); border-color: #8b5cf6; color: #e2e8f0; } .nav-assistant-input input::placeholder { color: rgba(148, 163, 184, 0.7); } .nav-assistant-input input:focus::placeholder { color: #94a3b8; } .nav-assistant-messages::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.1); } .nav-assistant-messages::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); } } `; document.head.appendChild(style); // Вбудовуємо скрипти /** * Система збереження контексту чату між сторінками * Використовує LocalStorage як основне сховище та IndexedDB як резерв */ class NavigationChatStorage { constructor(agentId) { this.agentId = agentId; this.storageKey = `nav-chat-${agentId}`; this.maxMessages = 100; // Максимум повідомлень в історії this.maxStorageSize = 5 * 1024 * 1024; // 5MB максимум this.initIndexedDB(); } /** * Ініціалізація IndexedDB як резервного сховища */ async initIndexedDB() { try { this.db = await new Promise((resolve, reject) => { const request = indexedDB.open('NavigationChat', 1); request.onerror = () => reject(request.error); request.onsuccess = () => resolve(request.result); request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains('chats')) { const store = db.createObjectStore('chats', { keyPath: 'agentId' }); store.createIndex('timestamp', 'timestamp', { unique: false }); } }; }); console.log('[ChatStorage] IndexedDB ініціалізовано'); } catch (error) { console.warn('[ChatStorage] IndexedDB недоступна:', error); this.db = null; } } /** * Зберегти історію чату */ async saveChat(messages, metadata = {}) { const chatData = { agentId: this.agentId, messages: messages.slice(-this.maxMessages), // Обмежуємо кількість metadata: { ...metadata, lastUpdated: Date.now(), domain: window.location.hostname, userAgent: navigator.userAgent }, timestamp: Date.now() }; try { // Спочатку пробуємо LocalStorage const dataString = JSON.stringify(chatData); // Перевіряємо розмір if (dataString.length > this.maxStorageSize) { console.warn('[ChatStorage] Дані занадто великі, очищуємо старі повідомлення'); chatData.messages = chatData.messages.slice(-50); // Залишаємо тільки 50 останніх } localStorage.setItem(this.storageKey, JSON.stringify(chatData)); console.log(`[ChatStorage] Збережено ${chatData.messages.length} повідомлень в LocalStorage`); // Дублюємо в IndexedDB якщо доступна if (this.db) { await this.saveToIndexedDB(chatData); } return true; } catch (error) { console.error('[ChatStorage] Помилка збереження в LocalStorage:', error); // Пробуємо IndexedDB як резерв if (this.db) { try { await this.saveToIndexedDB(chatData); console.log('[ChatStorage] Збережено в IndexedDB як резерв'); return true; } catch (dbError) { console.error('[ChatStorage] Помилка збереження в IndexedDB:', dbError); } } return false; } } /** * Завантажити історію чату */ async loadChat() { try { // Спочатку пробуємо LocalStorage const data = localStorage.getItem(this.storageKey); if (data) { const chatData = JSON.parse(data); console.log(`[ChatStorage] Завантажено ${chatData.messages.length} повідомлень з LocalStorage`); return chatData; } } catch (error) { console.error('[ChatStorage] Помилка читання з LocalStorage:', error); } // Пробуємо IndexedDB як резерв if (this.db) { try { const chatData = await this.loadFromIndexedDB(); if (chatData) { console.log(`[ChatStorage] Завантажено ${chatData.messages.length} повідомлень з IndexedDB`); return chatData; } } catch (error) { console.error('[ChatStorage] Помилка читання з IndexedDB:', error); } } console.log('[ChatStorage] Історія чату не знайдена'); return null; } /** * Очистити історію чату */ async clearChat() { try { localStorage.removeItem(this.storageKey); if (this.db) { const transaction = this.db.transaction(['chats'], 'readwrite'); const store = transaction.objectStore('chats'); await store.delete(this.agentId); } console.log('[ChatStorage] Історію чату очищено'); return true; } catch (error) { console.error('[ChatStorage] Помилка очищення:', error); return false; } } /** * Отримати статистику сховища */ getStorageStats() { try { const data = localStorage.getItem(this.storageKey); const size = data ? new Blob([data]).size : 0; const sizeKB = Math.round(size / 1024); return { hasData: !!data, sizeBytes: size, sizeKB: sizeKB, maxSizeKB: Math.round(this.maxStorageSize / 1024), usagePercent: Math.round((size / this.maxStorageSize) * 100) }; } catch (error) { return { hasData: false, error: error.message }; } } /** * Збереження в IndexedDB */ async saveToIndexedDB(chatData) { if (!this.db) return; return new Promise((resolve, reject) => { const transaction = this.db.transaction(['chats'], 'readwrite'); const store = transaction.objectStore('chats'); const request = store.put(chatData); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } /** * Завантаження з IndexedDB */ async loadFromIndexedDB() { if (!this.db) return null; return new Promise((resolve, reject) => { const transaction = this.db.transaction(['chats'], 'readonly'); const store = transaction.objectStore('chats'); const request = store.get(this.agentId); request.onsuccess = () => resolve(request.result || null); request.onerror = () => reject(request.error); }); } /** * Автоматичне очищення старих даних */ async cleanup() { const WEEK_MS = 7 * 24 * 60 * 60 * 1000; // Тиждень в мілісекундах try { const chatData = await this.loadChat(); if (chatData && (Date.now() - chatData.timestamp > WEEK_MS)) { console.log('[ChatStorage] Очищення старих даних (> 7 днів)'); await this.clearChat(); } } catch (error) { console.error('[ChatStorage] Помилка очищення:', error); } } /** * Експорт історії чату */ async exportChat() { try { const chatData = await this.loadChat(); if (!chatData) return null; const exportData = { agentId: chatData.agentId, messages: chatData.messages, exportDate: new Date().toISOString(), domain: chatData.metadata.domain, messageCount: chatData.messages.length }; return JSON.stringify(exportData, null, 2); } catch (error) { console.error('[ChatStorage] Помилка експорту:', error); return null; } } } /** * Спрощений клієнтський сканер сайту */ class ClientSiteScanner { constructor(config) { this.config = config; this.apiEndpoint = config.apiEndpoint; this.apiKey = config.apiKey; this.agentId = config.agentId; this.isScanning = false; } async startScanning() { if (this.isScanning) { console.log('[ClientSiteScanner] Сканування вже запущено'); return; } console.log('[ClientSiteScanner] Перевірка наявності аналізу сайту...'); this.isScanning = true; try { // Спочатку перевіряємо чи є вже аналіз const existingAnalysis = await this.checkExistingAnalysis(); if (existingAnalysis) { console.log('[ClientSiteScanner] Знайдено існуючий аналіз, використовуємо його'); return; } console.log('[ClientSiteScanner] Аналізу не знайдено, починаємо сканування...'); const siteData = await this.scanSite(); console.log('[ClientSiteScanner] Сканування завершено:', siteData); // Відправляємо дані на сервер для аналізу await this.sendSiteDataToServer(siteData); } catch (error) { console.error('[ClientSiteScanner] Помилка сканування:', error); } finally { this.isScanning = false; } } async checkExistingAnalysis() { try { console.log('[ClientSiteScanner] Перевірка існуючого аналізу...'); const response = await fetch(`${this.apiEndpoint}/site-map?domain=${encodeURIComponent(this.getCurrentDomain())}`, { headers: { 'X-API-Key': this.apiKey } }); if (response.ok) { const analysis = await response.json(); if (analysis && analysis.pages_map && Object.keys(analysis.pages_map).length > 0) { console.log('[ClientSiteScanner] Знайдено існуючий аналіз сайту'); return analysis; } } console.log('[ClientSiteScanner] Аналіз не знайдено або порожній'); return null; } catch (error) { console.warn('[ClientSiteScanner] Помилка перевірки існуючого аналізу:', error); return null; } } getCurrentDomain() { const hostname = window.location.hostname; const port = window.location.port; return port ? `${hostname}:${port}` : hostname; } async scanSite() { console.log('[ClientSiteScanner] Сканування поточної сторінки...'); const siteData = { domain: window.location.hostname + (window.location.port ? ':' + window.location.port : ''), pages_data: {} }; // Скануємо поточну сторінку const currentPage = await this.scanCurrentPage(); siteData.pages_data[window.location.href] = currentPage; return siteData; } async scanCurrentPage() { const pageData = { url: window.location.href, title: document.title, html_content: document.documentElement.outerHTML, elements: [] }; console.log(`[ClientSiteScanner] Проскановано сторінку: ${pageData.title}`); return pageData; } async sendSiteDataToServer(siteData) { try { console.log('[ClientSiteScanner] Запуск асинхронного аналізу HTML...'); // Створюємо асинхронну задачу аналізу const response = await fetch(`${this.apiEndpoint}/sites/analyze-async`, { method: 'POST', headers: { 'X-API-Key': this.apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ domain: siteData.domain, site_data: siteData }) }); if (response.ok) { const taskData = await response.json(); console.log('[ClientSiteScanner] Задача аналізу створена:', taskData.id); // Моніторимо виконання задачі await this.monitorAnalysisTask(taskData.id); } else { console.error('[ClientSiteScanner] Помилка створення задачі аналізу:', response.status, await response.text()); } } catch (error) { console.error('[ClientSiteScanner] Помилка мережі при відправці даних:', error); } } async monitorAnalysisTask(taskId) { console.log(`[ClientSiteScanner] Моніторинг задачі аналізу: ${taskId}`); const maxAttempts = 60; // 5 хвилин максимум (60 * 5 сек) let attempts = 0; while (attempts < maxAttempts) { try { await new Promise(resolve => setTimeout(resolve, 5000)); // Очікуємо 5 секунд attempts++; const response = await fetch(`${this.apiEndpoint}/analysis-tasks/${taskId}`, { headers: { 'X-API-Key': this.apiKey } }); if (response.ok) { const taskStatus = await response.json(); console.log(`[ClientSiteScanner] Статус задачі ${taskId}: ${taskStatus.status} (спроба ${attempts}/${maxAttempts})`); if (taskStatus.status === 'completed') { console.log('[ClientSiteScanner] Аналіз завершено успішно!'); if (taskStatus.result_data && taskStatus.result_data.pages_map) { const pagesCount = Object.keys(taskStatus.result_data.pages_map).length; console.log(`[ClientSiteScanner] Проаналізовано ${pagesCount} сторінок`); // Показуємо кількість елементів Object.keys(taskStatus.result_data.pages_map).forEach(url => { const page = taskStatus.result_data.pages_map[url]; const elementsCount = page.elements ? page.elements.length : 0; console.log(`[ClientSiteScanner] ${url}: ${elementsCount} елементів`); }); } return; } else if (taskStatus.status === 'failed') { console.error(`[ClientSiteScanner] Аналіз не вдався: ${taskStatus.error_message}`); return; } // Якщо статус 'pending' або 'processing', продовжуємо очікування } else { console.warn(`[ClientSiteScanner] Помилка перевірки статусу задачі: ${response.status}`); } } catch (error) { console.warn(`[ClientSiteScanner] Помилка моніторингу задачі: ${error.message}`); } } console.warn(`[ClientSiteScanner] Тайм-аут очікування аналізу (${maxAttempts * 5} секунд)`); } } // Експортуємо для використання window.ClientSiteScanner = ClientSiteScanner; /** * Двигун навігації для виконання дій на сторінці */ class NavigationEngine { constructor() { this.executionHistory = []; this.isExecuting = false; } /** * Виконання навігаційної дії */ async executeAction(action, context = {}) { if (this.isExecuting) { console.warn('[NavigationEngine] Already executing an action'); return { success: false, error: 'Already executing' }; } this.isExecuting = true; const startTime = Date.now(); try { console.log(`[NavigationEngine] Executing action: ${action.action_type}`); let result; switch (action.action_type) { case 'click': result = await this.executeClick(action, context); break; case 'fill_form': result = await this.executeFillForm(action, context); break; case 'navigate': result = await this.executeNavigate(action, context); break; case 'sequence': result = await this.executeSequence(action, context); break; default: result = { success: false, error: `Unknown action type: ${action.action_type}` }; } const executionTime = Date.now() - startTime; // Додаємо до історії this.executionHistory.push({ action, result, timestamp: Date.now(), executionTime }); console.log(`[NavigationEngine] Action completed in ${executionTime}ms:`, result); return { ...result, executionTime }; } catch (error) { console.error('[NavigationEngine] Error executing action:', error); return { success: false, error: error.message }; } finally { this.isExecuting = false; } } /** * Виконання кліку */ async executeClick(action, context) { const element = await this.findElement(action.element_selector); if (!element) { return { success: false, error: 'Element not found' }; } try { // Скролл до елемента element.scrollIntoView({ behavior: 'smooth', block: 'center' }); await this.wait(500); // Фокус на елемент if (element.focus) { element.focus(); } await this.wait(100); // Спробуємо різні способи кліку let clickSuccess = false; // 1. Звичайний клік try { element.click(); clickSuccess = true; console.log('[NavigationEngine] Regular click successful'); } catch (e) { console.warn('[NavigationEngine] Regular click failed, trying alternatives'); } // 2. Якщо звичайний клік не спрацював, спробуємо dispatchEvent if (!clickSuccess) { try { const clickEvent = new MouseEvent('click', { view: window, bubbles: true, cancelable: true }); element.dispatchEvent(clickEvent); clickSuccess = true; console.log('[NavigationEngine] MouseEvent click successful'); } catch (e) { console.warn('[NavigationEngine] MouseEvent click failed'); } } // 3. Для посилань спробуємо перехід через href if (!clickSuccess && element.tagName === 'A' && element.href) { try { window.location.href = element.href; clickSuccess = true; console.log('[NavigationEngine] Navigation via href successful'); } catch (e) { console.warn('[NavigationEngine] Navigation via href failed'); } } // 4. Для кнопок спробуємо submit якщо це форма if (!clickSuccess && element.tagName === 'BUTTON') { const form = element.closest('form'); if (form) { try { form.submit(); clickSuccess = true; console.log('[NavigationEngine] Form submit successful'); } catch (e) { console.warn('[NavigationEngine] Form submit failed'); } } } // Очікування після кліку if (action.wait_time) { await this.wait(action.wait_time * 1000); } return { success: clickSuccess, element: element.tagName, text: element.textContent?.trim(), href: element.href || null, method: clickSuccess ? 'click' : 'failed' }; } catch (error) { return { success: false, error: `Click failed: ${error.message}` }; } } /** * Заповнення форми */ async executeFillForm(action, context) { const element = await this.findElement(action.element_selector); if (!element) { return { success: false, error: 'Input field not found' }; } try { // Скролл до елемента element.scrollIntoView({ behavior: 'smooth', block: 'center' }); await this.wait(500); // Фокус на поле element.focus(); await this.wait(100); // Очищення поля element.value = ''; // Введення тексту element.value = action.text; // Тригеримо події element.dispatchEvent(new Event('input', { bubbles: true })); element.dispatchEvent(new Event('change', { bubbles: true })); // Очікування if (action.wait_time) { await this.wait(action.wait_time * 1000); } return { success: true, field: element.name || element.id, value: action.text }; } catch (error) { return { success: false, error: `Fill form failed: ${error.message}` }; } } /** * Навігація */ async executeNavigate(action, context) { try { if (action.url) { window.location.href = action.url; return { success: true, url: action.url }; } else { return { success: false, error: 'No URL provided' }; } } catch (error) { return { success: false, error: `Navigation failed: ${error.message}` }; } } /** * Виконання послідовності дій */ async executeSequence(sequence, context) { console.log(`[NavigationEngine] Executing sequence: ${sequence.name}`); const results = []; let currentContext = { ...context }; for (let i = 0; i < sequence.steps.length; i++) { const step = sequence.steps[i]; console.log(`[NavigationEngine] Step ${i + 1}/${sequence.steps.length}: ${step.description || step.action_type}`); const result = await this.executeAction(step, currentContext); results.push(result); if (!result.success && sequence.stop_on_error) { return { success: false, error: `Step ${i + 1} failed: ${result.error}`, completedSteps: i + 1, totalSteps: sequence.steps.length, results }; } // Оновлюємо контекст if (result.url) { currentContext.currentUrl = result.url; } } return { success: true, completedSteps: sequence.steps.length, totalSteps: sequence.steps.length, results }; } /** * Пошук елемента на сторінці */ async findElement(selector) { if (!selector) { console.warn('[NavigationEngine] No selector provided'); return null; } console.log(`[NavigationEngine] Searching for element with selector: ${selector.selector}`); // Спробуємо основний селектор let element = this.trySelector(selector.selector); if (element) { console.log(`[NavigationEngine] Found element with main selector: ${selector.selector}`); return element; } // Спробуємо альтернативні селектори for (const altSelector of selector.alternative_selectors || []) { element = this.trySelector(altSelector); if (element) { console.log(`[NavigationEngine] Found element with alternative selector: ${altSelector}`); return element; } } // Пошук за текстом (пріоритетний для навігації) if (selector.text) { element = this.findByText(selector.text); if (element) { console.log(`[NavigationEngine] Found element by text: "${selector.text}"`); return element; } } // Пошук за placeholder if (selector.placeholder) { element = this.findByPlaceholder(selector.placeholder); if (element) { console.log(`[NavigationEngine] Found element by placeholder: "${selector.placeholder}"`); return element; } } // Пошук за атрибутами if (selector.attributes) { element = this.findByAttributes(selector.attributes); if (element) { console.log(`[NavigationEngine] Found element by attributes:`, selector.attributes); return element; } } console.warn(`[NavigationEngine] Element not found with any method`); return null; } /** * Спробувати знайти елемент за селектором */ trySelector(selector) { try { return document.querySelector(selector); } catch (error) { console.warn(`[NavigationEngine] Invalid selector: ${selector}`); return null; } } /** * Пошук за текстом */ findByText(text) { const elements = document.querySelectorAll('button, a, span, div, input[type="button"], input[type="submit"], p, h1, h2, h3, h4, h5, h6'); console.log(`[NavigationEngine] Searching for text: "${text}"`); // Створюємо варіанти пошуку для української мови const searchVariants = this.getTextVariants(text); console.log(`[NavigationEngine] Search variants:`, searchVariants); // Спочатку шукаємо точний збіг for (const element of elements) { const elementText = element.textContent?.trim().toLowerCase(); for (const variant of searchVariants) { if (elementText === variant) { console.log(`[NavigationEngine] Found exact text match: "${elementText}"`); return element; } } } // Потім шукаємо частковий збіг for (const element of elements) { const elementText = element.textContent?.trim().toLowerCase(); for (const variant of searchVariants) { if (elementText && elementText.includes(variant)) { console.log(`[NavigationEngine] Found partial text match: "${elementText}" contains "${variant}"`); return element; } } } console.log(`[NavigationEngine] No text match found for: "${text}"`); return null; } /** * Генерація варіантів тексту для пошуку */ getTextVariants(text) { const variants = new Set(); const lowerText = text.toLowerCase(); // Додаємо оригінальний текст variants.add(lowerText); // Словник синонімів та варіантів const synonyms = { 'вхід': ['увійти', 'вхід', 'логін', 'login', 'sign in', 'ввійти'], 'увійти': ['вхід', 'увійти', 'логін', 'login', 'sign in', 'ввійти'], 'логін': ['вхід', 'увійти', 'логін', 'login', 'sign in'], 'замовити': ['замовити', 'купити', 'придбати', 'order', 'buy'], 'контакти': ['контакти', 'зв\'язок', 'contact', 'contacts'], 'про нас': ['про нас', 'about', 'about us'], 'головна': ['головна', 'home', 'головна сторінка'], 'послуги': ['послуги', 'services', 'сервіси'], 'демо': ['демо', 'demo', 'демонстрація'], 'консультація': ['консультація', 'consultation', 'консультація'] }; // Додаємо синоніми if (synonyms[lowerText]) { synonyms[lowerText].forEach(synonym => variants.add(synonym.toLowerCase())); } // Додаємо варіанти без пробілів та з різними регістрами variants.add(lowerText.replace(/\s+/g, '')); variants.add(lowerText.toUpperCase()); return Array.from(variants); } /** * Пошук за placeholder */ findByPlaceholder(placeholder) { return document.querySelector(`input[placeholder*="${placeholder}"], textarea[placeholder*="${placeholder}"]`); } /** * Пошук за атрибутами */ findByAttributes(attributes) { for (const [key, value] of Object.entries(attributes)) { const element = document.querySelector(`[${key}="${value}"]`); if (element) return element; } return null; } /** * Очікування */ wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Отримання історії виконання */ getExecutionHistory() { return [...this.executionHistory]; } /** * Очищення історії */ clearHistory() { this.executionHistory = []; } } // Експортуємо для використання window.NavigationEngine = NavigationEngine; class NavigationAssistant { constructor(config) { this.config = config; this.apiEndpoint = config.apiEndpoint; this.apiKey = config.apiKey; this.agentId = config.agentId; this.isInitialized = false; this.navigationEngine = new NavigationEngine(); // Ініціалізуємо систему збереження чату (якщо доступна) if (typeof NavigationChatStorage !== 'undefined') { this.chatStorage = new NavigationChatStorage(config.agentId); console.log('[NavigationAssistant] Chat storage ініціалізовано'); } else { console.warn('[NavigationAssistant] NavigationChatStorage недоступний, працюємо без збереження'); this.chatStorage = null; } this.messages = []; // Локальний масив повідомлень // Налаштування автоочищення this.maxChatAge = 60 * 60 * 1000; // 1 година в мілісекундах this.maxMessages = 50; // Максимум повідомлень в сесії this.cleanupInterval = 10 * 60 * 1000; // Перевірка кожні 10 хвилин this.isListening = false; this.recognition = null; this.initSpeechRecognition(); } async initialize() { if (this.isInitialized) { console.log('[NavigationAssistant] Вже ініціалізовано'); return; } console.log('[NavigationAssistant] Ініціалізація...'); this.createUI(); // Завантажуємо історію чату (якщо доступне збереження) if (this.chatStorage) { await this.loadChatHistory(); // Запускаємо автоматичне очищення this.startAutoCleanup(); } else { console.log('[NavigationAssistant] Працюємо без збереження історії'); } this.isInitialized = true; console.log('[NavigationAssistant] Ініціалізація завершена'); } createUI() { console.log('[NavigationAssistant] Створення UI...'); // Перевіряємо чи вже існує контейнер if (document.querySelector('.nav-assistant-container')) { console.log('[NavigationAssistant] UI вже існує'); return; } const container = document.createElement('div'); container.className = 'nav-assistant-container'; container.innerHTML = ` `; document.body.appendChild(container); console.log('[NavigationAssistant] UI додано в DOM'); this.setupEventListeners(container); console.log('[NavigationAssistant] Event listeners налаштовано'); } setupEventListeners(container) { const toggle = container.querySelector('.nav-assistant-toggle'); const chat = container.querySelector('.nav-assistant-chat'); const close = container.querySelector('.nav-assistant-close'); const actionBtn = container.querySelector('.nav-assistant-action'); const input = container.querySelector('input'); // Зберігаємо посилання на елементи this.actionBtn = actionBtn; this.input = input; this.microphoneIcon = actionBtn.querySelector('.microphone-icon'); this.sendIcon = actionBtn.querySelector('.send-icon'); toggle.addEventListener('click', () => { chat.classList.toggle('open'); }); close.addEventListener('click', () => { chat.classList.remove('open'); }); // Універсальна кнопка (мікрофон/відправка) actionBtn.addEventListener('click', () => { if (input.value.trim()) { // Якщо є текст - відправляємо this.sendMessage(input.value); input.value = ''; this.updateActionButton(); } else { // Якщо пусто - голосовий ввід this.toggleVoiceInput(); } }); // Відстежуємо зміни в полі вводу input.addEventListener('input', () => { this.updateActionButton(); }); input.addEventListener('keypress', (e) => { if (e.key === 'Enter') { this.sendMessage(input.value); input.value = ''; this.updateActionButton(); } }); // Ініціалізуємо початковий стан this.updateActionButton(); } updateActionButton() { const hasText = this.input.value.trim().length > 0; if (hasText) { // Показуємо іконку відправки this.microphoneIcon.style.display = 'none'; this.sendIcon.style.display = 'block'; this.actionBtn.className = 'nav-assistant-action send-mode'; this.actionBtn.title = 'Відправити повідомлення'; } else { // Показуємо іконку мікрофона this.microphoneIcon.style.display = 'block'; this.sendIcon.style.display = 'none'; this.actionBtn.className = 'nav-assistant-action voice-mode'; this.actionBtn.title = 'Голосовий ввід'; } } async sendMessage(message) { if (!message.trim()) return; this.addMessage(message, 'user'); this.hideQuickButtons(); this.showTypingIndicator(); try { const response = await fetch(`${this.apiEndpoint}/chat`, { method: 'POST', headers: { 'X-API-Key': this.apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ message: message, current_url: window.location.href, context: { page_title: document.title, user_agent: navigator.userAgent } }) }); if (response.ok) { const result = await response.json(); this.hideTypingIndicator(); this.addMessage(result.message, 'bot'); // Показуємо швидкі кнопки якщо є if (result.quick_buttons && result.quick_buttons.length > 0) { this.showQuickButtons(result.quick_buttons); } // Якщо є навігаційна дія, виконуємо її (крім answer - це тільки текст) if (result.action && result.action_type !== 'none' && result.action_type !== 'answer') { await this.executeNavigationAction(result.action, result.action_type); } } else { this.hideTypingIndicator(); this.addMessage('Вибачте, сталася помилка при обробці запиту.', 'bot'); } } catch (error) { console.error('[NavigationAssistant] Помилка відправки повідомлення:', error); this.hideTypingIndicator(); this.addMessage('Вибачте, сталася помилка мережі.', 'bot'); } } async executeNavigationAction(action, actionType) { try { console.log(`[NavigationAssistant] Executing navigation action: ${actionType}`); const context = { currentUrl: window.location.href, pageTitle: document.title }; let result; if (actionType === 'sequence') { result = await this.navigationEngine.executeSequence(action, context); } else { result = await this.navigationEngine.executeAction(action, context); } if (result.success) { console.log('[NavigationAssistant] Navigation action completed successfully'); // Додаємо повідомлення про успішне виконання if (actionType === 'sequence') { this.addMessage(`✅ Виконано послідовність: ${action.name} (${result.completedSteps}/${result.totalSteps} кроків)`, 'system'); } else { this.addMessage(`✅ Виконано: ${action.description || actionType}`, 'system'); } } else { console.error('[NavigationAssistant] Navigation action failed:', result.error); this.addMessage(`❌ Помилка: ${result.error}`, 'system'); } } catch (error) { console.error('[NavigationAssistant] Error executing navigation action:', error); this.addMessage(`❌ Помилка виконання: ${error.message}`, 'system'); } } addMessage(text, sender) { const messagesContainer = document.querySelector('.nav-assistant-messages'); const messageDiv = document.createElement('div'); messageDiv.className = `nav-assistant-message ${sender}`; messageDiv.textContent = text; messagesContainer.appendChild(messageDiv); messagesContainer.scrollTop = messagesContainer.scrollHeight; // Додаємо до локального масиву const messageData = { id: Date.now() + Math.random(), text: text, type: sender, timestamp: Date.now(), url: window.location.href }; this.messages.push(messageData); // Зберігаємо в сховище (асинхронно, якщо доступне) if (this.chatStorage) { this.saveChatHistory(); } } /** * Завантажити історію чату */ async loadChatHistory() { if (!this.chatStorage) return; try { const chatData = await this.chatStorage.loadChat(); if (chatData && chatData.messages && chatData.messages.length > 0) { console.log(`[NavigationAssistant] Завантажено ${chatData.messages.length} повідомлень`); // Відновлюємо повідомлення в UI const messagesContainer = document.querySelector('.nav-assistant-messages'); if (messagesContainer) { messagesContainer.innerHTML = ''; // Очищуємо chatData.messages.forEach(msg => { const messageDiv = document.createElement('div'); messageDiv.className = `nav-assistant-message ${msg.type}`; messageDiv.textContent = msg.text; messagesContainer.appendChild(messageDiv); }); messagesContainer.scrollTop = messagesContainer.scrollHeight; } // Зберігаємо в локальний масив this.messages = chatData.messages; // Показуємо повідомлення про відновлення if (chatData.messages.length > 0) { const lastMessage = chatData.messages[chatData.messages.length - 1]; const timeDiff = Date.now() - lastMessage.timestamp; const hoursAgo = Math.floor(timeDiff / (1000 * 60 * 60)); if (hoursAgo < 24) { this.addSystemMessage(`📋 Відновлено історію чату (${hoursAgo}г тому)`); } } } } catch (error) { console.error('[NavigationAssistant] Помилка завантаження історії:', error); } } /** * Зберегти історію чату */ async saveChatHistory() { if (!this.chatStorage) return; try { // Обмежуємо частоту збереження if (this.saveTimeout) { clearTimeout(this.saveTimeout); } this.saveTimeout = setTimeout(async () => { const metadata = { currentUrl: window.location.href, pageTitle: document.title, userAgent: navigator.userAgent, sessionStart: this.sessionStart || Date.now() }; const success = await this.chatStorage.saveChat(this.messages, metadata); if (success) { console.log('[NavigationAssistant] Історію збережено'); } }, 1000); // Затримка 1 секунда } catch (error) { console.error('[NavigationAssistant] Помилка збереження історії:', error); } } /** * Додати системне повідомлення (без збереження) */ addSystemMessage(text) { const messagesContainer = document.querySelector('.nav-assistant-messages'); const messageDiv = document.createElement('div'); messageDiv.className = 'nav-assistant-message system'; messageDiv.textContent = text; messageDiv.style.opacity = '0.7'; messageDiv.style.fontSize = '13px'; messagesContainer.appendChild(messageDiv); messagesContainer.scrollTop = messagesContainer.scrollHeight; } /** * Запустити автоматичне очищення */ startAutoCleanup() { // Очищення при запуску this.cleanupOldMessages(); // Періодичне очищення this.cleanupTimer = setInterval(() => { this.cleanupOldMessages(); }, this.cleanupInterval); console.log(`[NavigationAssistant] Автоочищення запущено (кожні ${this.cleanupInterval/60000} хв)`); } /** * Очистити старі повідомлення */ async cleanupOldMessages() { try { const now = Date.now(); let cleanupCount = 0; // Видаляємо старі повідомлення (старше 1 години) const filteredMessages = this.messages.filter(msg => { const isOld = (now - msg.timestamp) > this.maxChatAge; if (isOld) cleanupCount++; return !isOld; }); // Обмежуємо кількість повідомлень if (filteredMessages.length > this.maxMessages) { const excess = filteredMessages.length - this.maxMessages; filteredMessages.splice(0, excess); cleanupCount += excess; } if (cleanupCount > 0) { this.messages = filteredMessages; await this.saveChatHistory(); // Оновлюємо UI await this.refreshChatUI(); console.log(`[NavigationAssistant] Очищено ${cleanupCount} старих повідомлень`); } } catch (error) { console.error('[NavigationAssistant] Помилка автоочищення:', error); } } /** * Оновити UI чату */ async refreshChatUI() { const messagesContainer = document.querySelector('.nav-assistant-messages'); if (messagesContainer && this.messages.length > 0) { messagesContainer.innerHTML = ''; this.messages.forEach(msg => { const messageDiv = document.createElement('div'); messageDiv.className = `nav-assistant-message ${msg.type}`; messageDiv.textContent = msg.text; messagesContainer.appendChild(messageDiv); }); messagesContainer.scrollTop = messagesContainer.scrollHeight; } } /** * Зупинити автоочищення */ stopAutoCleanup() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = null; console.log('[NavigationAssistant] Автоочищення зупинено'); } } initSpeechRecognition() { if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) { console.warn('[NavigationAssistant] Speech recognition not supported'); return; } const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; this.recognition = new SpeechRecognition(); this.recognition.continuous = true; // Не зупиняти після пауз this.recognition.interimResults = false; this.recognition.lang = 'uk-UA'; // Українська мова this.recognition.onstart = () => { console.log('[NavigationAssistant] Голосове розпізнавання запущено'); this.updateVoiceButton(true); // Таймер на 1 хвилину this.recordingTimeout = setTimeout(() => { console.log('[NavigationAssistant] Автоматична зупинка після 1 хвилини'); if (this.isListening) { this.recognition.stop(); this.addMessage('⏰ Запис автоматично зупинено через 1 хвилину', 'system'); } }, 60000); // 60 секунд }; this.recognition.onresult = (event) => { const transcript = event.results[0][0].transcript; console.log('[NavigationAssistant] Розпізнано:', transcript); // Вставляємо текст в поле вводу this.input.value = transcript; this.updateActionButton(); // Оновлюємо кнопку після вставки тексту // НЕ відправляємо автоматично - користувач сам натисне кнопку // this.sendMessage(transcript); // Показуємо підказку console.log('[NavigationAssistant] Текст вставлено, натисніть кнопку відправки'); }; this.recognition.onerror = (event) => { console.error('[NavigationAssistant] Помилка розпізнавання:', event.error); this.addMessage(`❌ Помилка розпізнавання: ${event.error}`, 'system'); this.updateVoiceButton(false); }; this.recognition.onend = () => { console.log('[NavigationAssistant] Голосове розпізнавання завершено'); this.isListening = false; this.updateVoiceButton(false); // Очищуємо таймер if (this.recordingTimeout) { clearTimeout(this.recordingTimeout); this.recordingTimeout = null; } }; } toggleVoiceInput() { if (!this.recognition) { this.addMessage('❌ Голосовий ввід не підтримується в цьому браузері', 'system'); return; } if (this.isListening) { // Ручна зупинка this.recognition.stop(); this.isListening = false; // Очищуємо таймер при ручній зупинці if (this.recordingTimeout) { clearTimeout(this.recordingTimeout); this.recordingTimeout = null; } } else { // Запуск запису this.recognition.start(); this.isListening = true; } } updateVoiceButton(isListening) { if (this.actionBtn) { if (isListening) { // Під час запису показуємо червону іконку this.microphoneIcon.innerHTML = ` `; this.actionBtn.style.background = 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)'; this.actionBtn.style.boxShadow = '0 4px 12px rgba(239, 68, 68, 0.3)'; this.actionBtn.title = 'Зупинити запис'; } else { // Повертаємо звичайну іконку мікрофона this.microphoneIcon.innerHTML = ` `; this.actionBtn.style.background = ''; this.actionBtn.style.boxShadow = ''; this.updateActionButton(); // Оновлюємо стан кнопки } } } showQuickButtons(buttons) { const quickButtonsContainer = document.querySelector('.nav-assistant-quick-buttons'); if (!quickButtonsContainer || !buttons || buttons.length === 0) { quickButtonsContainer.style.display = 'none'; return; } quickButtonsContainer.innerHTML = ''; buttons.forEach(button => { const btn = document.createElement('button'); btn.className = 'nav-assistant-quick-button'; btn.textContent = button.text; btn.title = button.description || button.text; btn.addEventListener('click', () => { this.sendMessage(button.text); this.hideQuickButtons(); }); quickButtonsContainer.appendChild(btn); }); quickButtonsContainer.style.display = 'flex'; } hideQuickButtons() { const quickButtonsContainer = document.querySelector('.nav-assistant-quick-buttons'); if (quickButtonsContainer) { quickButtonsContainer.style.display = 'none'; } } showTypingIndicator() { const messagesContainer = document.querySelector('.nav-assistant-messages'); const typingDiv = document.createElement('div'); typingDiv.className = 'nav-assistant-message bot typing'; typingDiv.textContent = 'Думаю'; typingDiv.id = 'typing-indicator'; messagesContainer.appendChild(typingDiv); messagesContainer.scrollTop = messagesContainer.scrollHeight; } hideTypingIndicator() { const typingIndicator = document.getElementById('typing-indicator'); if (typingIndicator) { typingIndicator.remove(); } } } window.NavigationAssistant = NavigationAssistant; // Ініціалізація після завантаження DOM if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeAssistant); } else { initializeAssistant(); } function initializeAssistant() { try { // Ініціалізуємо сканер сайту if (window.ClientSiteScanner) { const scanner = new ClientSiteScanner(config); scanner.startScanning(); } // Ініціалізуємо асистента (включає двигун навігації) if (window.NavigationAssistant && window.NavigationEngine) { const assistant = new NavigationAssistant(config); assistant.initialize(); } console.log('Navigation Assistant with Engine initialized successfully'); // Налаштовуємо глобальний моніторинг навігації setupGlobalNavigation(); } catch (error) { console.error('Error initializing Navigation Assistant:', error); } } // Функція для глобального моніторингу навігації (замість Service Worker) function setupGlobalNavigation() { // Зберігаємо поточний URL let currentUrl = window.location.href; // Функція для перевірки та додавання асистента на нові сторінки function checkAndInjectAssistant() { const newUrl = window.location.href; if (newUrl !== currentUrl) { currentUrl = newUrl; console.log('[NavigationAssistant] URL змінився:', newUrl); // Перевіряємо чи це домен eon.plus if (window.location.hostname === 'eon.plus' || window.location.hostname.endsWith('.eon.plus')) { // Затримка для завантаження нової сторінки setTimeout(() => { if (!document.querySelector('.nav-assistant-container')) { console.log('[NavigationAssistant] Додаємо асистента на нову сторінку'); loadAssistantOnNewPage(); } }, 1000); } } } // Моніторинг змін URL через MutationObserver const observer = new MutationObserver(() => { checkAndInjectAssistant(); }); observer.observe(document, { subtree: true, childList: true }); // Моніторинг подій навігації window.addEventListener('popstate', checkAndInjectAssistant); window.addEventListener('pushstate', checkAndInjectAssistant); window.addEventListener('replacestate', checkAndInjectAssistant); // Перехоплення History API const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function(...args) { originalPushState.apply(this, args); checkAndInjectAssistant(); }; history.replaceState = function(...args) { originalReplaceState.apply(this, args); checkAndInjectAssistant(); }; console.log('[NavigationAssistant] Глобальний моніторинг навігації активовано'); } // Функція для завантаження асистента на нову сторінку function loadAssistantOnNewPage() { try { // Додаємо CSS якщо його немає if (!document.querySelector('style[data-nav-assistant]')) { const style = document.createElement('style'); style.setAttribute('data-nav-assistant', 'true'); style.textContent = `/* Сучасні стилі для навігаційного асистента */ .nav-assistant-container { position: fixed !important; bottom: 24px !important; left: 24px !important; z-index: 999999 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Inter', sans-serif; animation: slideInUp 0.4s cubic-bezier(0.16, 1, 0.3, 1); max-width: calc(100vw - 48px); max-height: calc(100vh - 48px); pointer-events: auto !important; } @keyframes slideInUp { from { transform: translateY(100px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } .nav-assistant-toggle { width: 64px; height: 64px; border-radius: 50%; background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #ec4899 100%); border: none; color: white; font-size: 28px; cursor: pointer; box-shadow: 0 4px 14px 0 rgba(99, 102, 241, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.05); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); position: relative; overflow: hidden; backdrop-filter: blur(10px); display: flex; align-items: center; justify-content: center; } .nav-assistant-toggle::before { content: ''; position: absolute; top: 50%; left: 50%; width: 0; height: 0; background: rgba(255, 255, 255, 0.3); border-radius: 50%; transform: translate(-50%, -50%); transition: all 0.3s ease; } .nav-assistant-toggle:hover { transform: scale(1.05) translateY(-2px); box-shadow: 0 8px 25px 0 rgba(99, 102, 241, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.1); background: linear-gradient(135deg, #7c3aed 0%, #a855f7 50%, #f472b6 100%); } .nav-assistant-toggle:hover::before { width: 100%; height: 100%; background: rgba(255, 255, 255, 0.1); } .nav-assistant-toggle:active { transform: scale(0.98) translateY(0px); transition: all 0.1s ease; } .nav-assistant-chat { position: absolute; bottom: 84px; left: 0; width: min(400px, calc(100vw - 48px)); height: min(600px, calc(100vh - 120px)); background: rgba(255, 255, 255, 0.95); border-radius: 20px; box-shadow: 0 32px 64px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(255, 255, 255, 0.8), inset 0 1px 0 rgba(255, 255, 255, 0.9); display: none; flex-direction: column; overflow: hidden; backdrop-filter: blur(20px) saturate(180%); transform: scale(0.85) translateY(24px); opacity: 0; transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1); } .nav-assistant-chat.open { display: flex; transform: scale(1) translateY(0); opacity: 1; } .nav-assistant-header { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #ec4899 100%); color: white; padding: 20px 24px; display: flex; justify-content: space-between; align-items: center; position: relative; overflow: hidden; } .nav-assistant-header::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, transparent 50%); pointer-events: none; } .nav-assistant-header h3 { margin: 0; font-size: 18px; font-weight: 700; position: relative; z-index: 1; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } .nav-assistant-close { background: rgba(255, 255, 255, 0.1); border: none; color: white; font-size: 20px; cursor: pointer; padding: 0; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); position: relative; z-index: 1; backdrop-filter: blur(10px); } .nav-assistant-close:hover { background: rgba(255, 255, 255, 0.2); transform: scale(1.1); } .nav-assistant-close:active { transform: scale(0.95); } .nav-assistant-messages { flex: 1; padding: 24px 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; scroll-behavior: smooth; } .nav-assistant-messages::-webkit-scrollbar { width: 6px; } .nav-assistant-messages::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.05); border-radius: 3px; } .nav-assistant-messages::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.2); border-radius: 3px; transition: background 0.2s ease; } .nav-assistant-messages::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.3); } .nav-assistant-message { max-width: 85%; padding: 14px 18px; font-size: 15px; line-height: 1.5; word-wrap: break-word; animation: messageSlideIn 0.4s cubic-bezier(0.16, 1, 0.3, 1); position: relative; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } .nav-assistant-message.user { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); color: white; align-self: flex-end; border-radius: 20px 20px 6px 20px; margin-left: auto; box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); } .nav-assistant-message.bot { background: rgba(248, 250, 252, 0.9); color: #1e293b; align-self: flex-start; border-radius: 20px 20px 20px 6px; border: 1px solid rgba(226, 232, 240, 0.8); backdrop-filter: blur(10px); } .nav-assistant-input { padding: 20px 24px; border-top: 1px solid rgba(226, 232, 240, 0.6); background: rgba(248, 250, 252, 0.8); backdrop-filter: blur(10px); display: flex; gap: 12px; align-items: center; } .nav-assistant-input input { flex: 1; padding: 14px 18px; border: 2px solid rgba(226, 232, 240, 0.6); border-radius: 25px; font-size: 15px; font-family: inherit; outline: none; background: rgba(255, 255, 255, 0.7); backdrop-filter: blur(10px); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); color: rgba(30, 41, 59, 0.8); } .nav-assistant-input input:focus { border-color: #6366f1; box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); background: rgba(255, 255, 255, 1); color: #1e293b; } .nav-assistant-input input::placeholder { color: rgba(100, 116, 139, 0.7); font-weight: 400; transition: color 0.3s ease; } .nav-assistant-input input:focus::placeholder { color: #94a3b8; } .nav-assistant-send { padding: 14px 24px; background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); color: white; border: none; border-radius: 25px; cursor: pointer; font-size: 15px; font-weight: 600; font-family: inherit; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); position: relative; overflow: hidden; } .nav-assistant-send::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); transition: left 0.5s; } .nav-assistant-send:hover { background: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%); transform: translateY(-1px); box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4); } .nav-assistant-send:hover::before { left: 100%; } .nav-assistant-send:active { transform: translateY(0); } .nav-assistant-action { padding: 0; border: none; border-radius: 50%; cursor: pointer; font-size: 18px; width: 48px; height: 48px; display: flex; align-items: center; justify-content: center; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); position: relative; overflow: hidden; color: white; } .nav-assistant-action.voice-mode { background: linear-gradient(135deg, #10b981 0%, #059669 100%); box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3); } .nav-assistant-action.send-mode { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); } .nav-assistant-action::before { content: ''; position: absolute; top: 50%; left: 50%; width: 0; height: 0; background: rgba(255, 255, 255, 0.2); border-radius: 50%; transform: translate(-50%, -50%); transition: all 0.3s ease; } .nav-assistant-action.voice-mode:hover { background: linear-gradient(135deg, #059669 0%, #047857 100%); transform: scale(1.05) translateY(-1px); box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4); } .nav-assistant-action.send-mode:hover { background: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%); transform: scale(1.05) translateY(-1px); box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4); } .nav-assistant-action:hover::before { width: 100%; height: 100%; } .nav-assistant-action:active { transform: scale(0.98) translateY(0); } /* Анімації для іконок */ .nav-assistant-action svg { transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); } .nav-assistant-action.send-mode .send-icon { animation: iconFadeIn 0.2s ease-out; } .nav-assistant-action.voice-mode .microphone-icon { animation: iconFadeIn 0.2s ease-out; } @keyframes iconFadeIn { from { opacity: 0; transform: scale(0.8); } to { opacity: 1; transform: scale(1); } } /* Швидкі кнопки */ .nav-assistant-quick-buttons { padding: 12px 16px; border-top: 1px solid #f0f0f0; display: flex; flex-wrap: wrap; gap: 8px; animation: slideInUp 0.3s ease-out; } .nav-assistant-quick-button { padding: 8px 12px; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border: 1px solid #dee2e6; border-radius: 20px; font-size: 12px; color: #495057; cursor: pointer; transition: all 0.2s ease; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; } .nav-assistant-quick-button:hover { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); } .nav-assistant-quick-button:active { transform: translateY(0); } /* Покращені повідомлення */ .nav-assistant-message { margin: 8px 16px; padding: 12px 16px; border-radius: 18px; max-width: 80%; word-wrap: break-word; animation: messageSlideIn 0.3s ease-out; position: relative; } @keyframes messageSlideIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .nav-assistant-message.user { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; align-self: flex-end; border-bottom-right-radius: 4px; } .nav-assistant-message.bot { background: #f8f9fa; color: #333; align-self: flex-start; border-bottom-left-radius: 4px; border: 1px solid #e9ecef; } .nav-assistant-message.system { background: #fff3cd; color: #856404; align-self: center; border: 1px solid #ffeaa7; font-size: 12px; text-align: center; max-width: 90%; } /* Анімація друкування */ .nav-assistant-message.typing { position: relative; } .nav-assistant-message.typing::after { content: '...'; animation: typing 1.5s infinite; } @keyframes typing { 0%, 60%, 100% { opacity: 0; } 30% { opacity: 1; } } /* Адаптивність для мобільних пристроїв */ @media (max-width: 480px) { .nav-assistant-container { bottom: 16px; left: 16px; right: 16px; max-width: none; } .nav-assistant-chat { width: 100%; height: min(80vh, 500px); left: 0; bottom: 80px; } .nav-assistant-toggle { width: 56px; height: 56px; font-size: 24px; } .nav-assistant-header { padding: 16px 20px; } .nav-assistant-header h3 { font-size: 16px; } .nav-assistant-messages { padding: 16px; gap: 12px; } .nav-assistant-message { max-width: 90%; padding: 12px 16px; font-size: 14px; } .nav-assistant-input { padding: 16px; gap: 8px; } .nav-assistant-input input { padding: 12px 16px; font-size: 14px; background: rgba(255, 255, 255, 0.9); color: rgba(30, 41, 59, 0.9); } .nav-assistant-input input:focus { background: rgba(255, 255, 255, 1); color: #1e293b; } .nav-assistant-send { padding: 12px 20px; font-size: 14px; } .nav-assistant-action { width: 44px; height: 44px; font-size: 16px; } } /* Темна тема (автоматично визначається) */ @media (prefers-color-scheme: dark) { .nav-assistant-chat { background: rgba(30, 41, 59, 0.95); box-shadow: 0 32px 64px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.05); } .nav-assistant-message.bot { background: rgba(51, 65, 85, 0.9); color: #e2e8f0; border: 1px solid rgba(71, 85, 105, 0.8); } .nav-assistant-input { background: rgba(51, 65, 85, 0.8); border-top: 1px solid rgba(71, 85, 105, 0.6); } .nav-assistant-input input { background: rgba(30, 41, 59, 0.8); border: 2px solid rgba(71, 85, 105, 0.6); color: rgba(226, 232, 240, 0.9); } .nav-assistant-input input:focus { background: rgba(30, 41, 59, 1); border-color: #8b5cf6; color: #e2e8f0; } .nav-assistant-input input::placeholder { color: rgba(148, 163, 184, 0.7); } .nav-assistant-input input:focus::placeholder { color: #94a3b8; } .nav-assistant-messages::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.1); } .nav-assistant-messages::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); } } `; document.head.appendChild(style); } // Ініціалізуємо асистента if (window.NavigationAssistant && window.NavigationChatStorage) { const assistant = new NavigationAssistant(config); assistant.initialize().then(() => { console.log('[NavigationAssistant] Асистент ініціалізовано на новій сторінці'); }); } } catch (error) { console.error('[NavigationAssistant] Помилка ініціалізації на новій сторінці:', error); } } })();