| <!DOCTYPE html> |
| <html lang="tr"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>BF Chatbot Yöneticisi - Trek Bisiklet</title> |
| <style> |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| height: 100vh; |
| display: flex; |
| overflow: hidden; |
| } |
| |
| .sidebar { |
| width: 350px; |
| background: white; |
| border-right: 1px solid #e4e6ea; |
| display: flex; |
| flex-direction: column; |
| box-shadow: 2px 0 10px rgba(0,0,0,0.1); |
| } |
| |
| .header { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| color: white; |
| padding: 20px; |
| text-align: center; |
| position: relative; |
| } |
| |
| .header h1 { |
| font-size: 20px; |
| margin-bottom: 5px; |
| } |
| |
| .header .subtitle { |
| font-size: 12px; |
| opacity: 0.9; |
| } |
| |
| .refresh-indicator { |
| position: absolute; |
| top: 10px; |
| right: 15px; |
| width: 12px; |
| height: 12px; |
| border-radius: 50%; |
| background: #4CAF50; |
| animation: pulse 2s infinite; |
| } |
| |
| .refresh-indicator.loading { |
| background: #ff9800; |
| animation: spin 1s linear infinite; |
| } |
| |
| @keyframes pulse { |
| 0% { opacity: 1; } |
| 50% { opacity: 0.5; } |
| 100% { opacity: 1; } |
| } |
| |
| @keyframes spin { |
| 0% { transform: rotate(0deg); } |
| 100% { transform: rotate(360deg); } |
| } |
| |
| .stats-bar { |
| background: #f8f9fa; |
| padding: 15px; |
| border-bottom: 1px solid #e4e6ea; |
| } |
| |
| .stats { |
| display: flex; |
| justify-content: space-around; |
| } |
| |
| .stat-item { |
| text-align: center; |
| } |
| |
| .stat-value { |
| font-size: 24px; |
| font-weight: bold; |
| color: #667eea; |
| } |
| |
| .stat-label { |
| font-size: 11px; |
| color: #666; |
| margin-top: 2px; |
| } |
| |
| .search-bar { |
| padding: 15px; |
| border-bottom: 1px solid #e4e6ea; |
| background: white; |
| } |
| |
| .search-input { |
| width: 100%; |
| padding: 10px 15px; |
| border: 1px solid #ddd; |
| border-radius: 20px; |
| outline: none; |
| font-size: 14px; |
| } |
| |
| .search-input:focus { |
| border-color: #667eea; |
| box-shadow: 0 0 5px rgba(102, 126, 234, 0.3); |
| } |
| |
| .api-config { |
| padding: 15px; |
| border-bottom: 1px solid #e4e6ea; |
| background: #f0f2f5; |
| } |
| |
| .api-config input { |
| width: 100%; |
| padding: 8px; |
| margin: 5px 0; |
| border: 1px solid #ddd; |
| border-radius: 4px; |
| font-size: 12px; |
| } |
| |
| .customer-list { |
| flex: 1; |
| overflow-y: auto; |
| background: white; |
| } |
| |
| .customer-item { |
| padding: 15px 20px; |
| border-bottom: 1px solid #f0f2f5; |
| cursor: pointer; |
| transition: background 0.2s; |
| position: relative; |
| } |
| |
| .customer-item:hover { |
| background: #f8f9fa; |
| } |
| |
| .customer-item.active { |
| background: #e3f2fd; |
| } |
| |
| .customer-name { |
| font-size: 15px; |
| font-weight: 600; |
| margin-bottom: 4px; |
| color: #333; |
| } |
| |
| .customer-message { |
| font-size: 13px; |
| color: #666; |
| overflow: hidden; |
| white-space: nowrap; |
| text-overflow: ellipsis; |
| } |
| |
| .customer-time { |
| position: absolute; |
| top: 15px; |
| right: 20px; |
| font-size: 11px; |
| color: #999; |
| } |
| |
| .unread-badge { |
| position: absolute; |
| bottom: 15px; |
| right: 20px; |
| background: #667eea; |
| color: white; |
| font-size: 11px; |
| padding: 2px 6px; |
| border-radius: 10px; |
| font-weight: bold; |
| } |
| |
| .chat-area { |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| background: #e5ddd5; |
| } |
| |
| .chat-header { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| color: white; |
| padding: 15px 20px; |
| display: flex; |
| align-items: center; |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); |
| } |
| |
| .back-button { |
| display: none; |
| background: none; |
| border: none; |
| color: white; |
| font-size: 24px; |
| cursor: pointer; |
| margin-right: 15px; |
| padding: 5px; |
| border-radius: 50%; |
| transition: background 0.2s; |
| } |
| |
| .back-button:hover { |
| background: rgba(255,255,255,0.1); |
| } |
| |
| .chat-header-info { |
| flex: 1; |
| } |
| |
| .chat-header .customer-name { |
| font-size: 16px; |
| font-weight: 600; |
| margin-bottom: 2px; |
| } |
| |
| .chat-header .customer-phone { |
| font-size: 13px; |
| opacity: 0.8; |
| } |
| |
| .chat-messages { |
| flex: 1; |
| padding: 20px; |
| overflow-y: auto; |
| background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><rect width="100" height="100" fill="%23e5ddd5"/><circle cx="50" cy="50" r="1" fill="%23d4cfc7" opacity="0.5"/></svg>'); |
| } |
| |
| .message { |
| margin-bottom: 10px; |
| display: flex; |
| align-items: flex-end; |
| animation: fadeIn 0.3s ease-in; |
| } |
| |
| @keyframes fadeIn { |
| from { opacity: 0; transform: translateY(10px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| |
| .message.sent { |
| justify-content: flex-end; |
| } |
| |
| .message.received { |
| justify-content: flex-start; |
| } |
| |
| .message-bubble { |
| max-width: 65%; |
| padding: 8px 12px; |
| border-radius: 18px; |
| box-shadow: 0 1px 0.5px rgba(0,0,0,0.13); |
| } |
| |
| .message.sent .message-bubble { |
| background: white; |
| margin-left: auto; |
| border-bottom-right-radius: 4px; |
| } |
| |
| .message.received .message-bubble { |
| background: #d9fdd3; |
| margin-right: auto; |
| border-bottom-left-radius: 4px; |
| } |
| |
| .message-text { |
| margin-bottom: 4px; |
| line-height: 1.4; |
| white-space: pre-wrap; |
| } |
| |
| .message-time { |
| font-size: 11px; |
| color: #667781; |
| text-align: right; |
| } |
| |
| .empty-state { |
| flex: 1; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| flex-direction: column; |
| color: #999; |
| } |
| |
| .empty-state svg { |
| width: 100px; |
| height: 100px; |
| margin-bottom: 20px; |
| opacity: 0.5; |
| } |
| |
| |
| .product-mention { |
| color: #667eea; |
| font-weight: bold; |
| } |
| |
| |
| @media (max-width: 768px) { |
| .sidebar { |
| width: 100%; |
| position: absolute; |
| z-index: 10; |
| transition: transform 0.3s; |
| } |
| |
| .sidebar.hidden { |
| transform: translateX(-100%); |
| } |
| |
| .chat-area { |
| width: 100%; |
| } |
| |
| .back-button { |
| display: block !important; |
| } |
| } |
| |
| |
| ::-webkit-scrollbar { |
| width: 6px; |
| height: 6px; |
| } |
| |
| ::-webkit-scrollbar-track { |
| background: #f1f1f1; |
| } |
| |
| ::-webkit-scrollbar-thumb { |
| background: #888; |
| border-radius: 3px; |
| } |
| |
| ::-webkit-scrollbar-thumb:hover { |
| background: #555; |
| } |
| |
| .connection-status { |
| position: fixed; |
| bottom: 20px; |
| right: 20px; |
| background: white; |
| padding: 10px 20px; |
| border-radius: 20px; |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| font-size: 12px; |
| z-index: 1000; |
| } |
| |
| .connection-status.connected { |
| background: #4CAF50; |
| color: white; |
| } |
| |
| .connection-status.disconnected { |
| background: #f44336; |
| color: white; |
| } |
| |
| .connection-status.connecting { |
| background: #ff9800; |
| color: white; |
| } |
| </style> |
| </head> |
| <body> |
| |
| <div class="sidebar" id="sidebar"> |
| <div class="header"> |
| <div class="refresh-indicator" id="refreshIndicator"></div> |
| <h1>🚴 Trek BF Chatbot</h1> |
| <div class="subtitle">Konuşma Yöneticisi</div> |
| </div> |
|
|
| <div class="stats-bar"> |
| <div class="stats"> |
| <div class="stat-item"> |
| <div class="stat-value" id="totalConversations">0</div> |
| <div class="stat-label">Toplam</div> |
| </div> |
| <div class="stat-item"> |
| <div class="stat-value" id="todayConversations">0</div> |
| <div class="stat-label">Bugün</div> |
| </div> |
| <div class="stat-item"> |
| <div class="stat-value" id="activeConversations">0</div> |
| <div class="stat-label">Aktif</div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="search-bar"> |
| <input type="text" class="search-input" placeholder="Konuşmalarda ara..." id="searchInput"> |
| </div> |
|
|
| <div class="api-config"> |
| <input type="text" id="apiEndpoint" placeholder="API Endpoint (örn: https://your-space.hf.space)" value=""> |
| <small style="color: #666;">Hugging Face Space URL'nizi girin</small> |
| </div> |
|
|
| <div class="customer-list" id="customerList"> |
| |
| </div> |
| </div> |
|
|
| |
| <div class="chat-area"> |
| <div class="chat-header" id="chatHeader" style="display: none;"> |
| <button class="back-button" onclick="showSidebar()">←</button> |
| <div class="chat-header-info"> |
| <div class="customer-name" id="chatCustomerName">Müşteri</div> |
| <div class="customer-phone" id="chatCustomerPhone">Oturum</div> |
| </div> |
| </div> |
|
|
| <div class="chat-messages" id="chatMessages"> |
| <div class="empty-state"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor"> |
| <path d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| </svg> |
| <div>Bir konuşma seçin</div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="connection-status" id="connectionStatus"> |
| <span id="statusText">Bağlanıyor...</span> |
| </div> |
|
|
| <script> |
| |
| let API_ENDPOINT = localStorage.getItem('bf_api_endpoint') || ''; |
| |
| let conversations = {}; |
| let selectedCustomer = null; |
| let refreshInterval; |
| let filteredConversations = {}; |
| |
| |
| document.getElementById('apiEndpoint').value = API_ENDPOINT; |
| document.getElementById('apiEndpoint').addEventListener('change', function() { |
| API_ENDPOINT = this.value.trim(); |
| if (!API_ENDPOINT.endsWith('/')) { |
| API_ENDPOINT += '/'; |
| } |
| localStorage.setItem('bf_api_endpoint', API_ENDPOINT); |
| loadConversations(); |
| }); |
| |
| function updateConnectionStatus(status) { |
| const statusEl = document.getElementById('connectionStatus'); |
| const statusText = document.getElementById('statusText'); |
| |
| statusEl.className = 'connection-status'; |
| |
| switch(status) { |
| case 'connected': |
| statusEl.classList.add('connected'); |
| statusText.textContent = '✓ Bağlandı'; |
| setTimeout(() => statusEl.style.display = 'none', 3000); |
| break; |
| case 'disconnected': |
| statusEl.classList.add('disconnected'); |
| statusText.textContent = '✗ Bağlantı koptu'; |
| statusEl.style.display = 'flex'; |
| break; |
| case 'connecting': |
| statusEl.classList.add('connecting'); |
| statusText.textContent = '⟳ Bağlanıyor...'; |
| statusEl.style.display = 'flex'; |
| break; |
| } |
| } |
| |
| async function loadConversations() { |
| if (!API_ENDPOINT) { |
| updateConnectionStatus('disconnected'); |
| return; |
| } |
| |
| const indicator = document.getElementById('refreshIndicator'); |
| indicator.classList.add('loading'); |
| updateConnectionStatus('connecting'); |
| |
| try { |
| const response = await fetch(API_ENDPOINT + 'api/conversations'); |
| if (!response.ok) throw new Error('API yanıt vermedi'); |
| |
| conversations = await response.json(); |
| filteredConversations = {...conversations}; |
| |
| |
| const statsResponse = await fetch(API_ENDPOINT + 'api/stats'); |
| if (statsResponse.ok) { |
| const stats = await statsResponse.json(); |
| document.getElementById('totalConversations').textContent = stats.total || 0; |
| document.getElementById('todayConversations').textContent = stats.today || 0; |
| document.getElementById('activeConversations').textContent = Object.keys(conversations).length; |
| } |
| |
| renderCustomerList(); |
| updateConnectionStatus('connected'); |
| } catch (error) { |
| console.error('API Hatası:', error); |
| updateConnectionStatus('disconnected'); |
| } finally { |
| indicator.classList.remove('loading'); |
| } |
| } |
| |
| function formatTime(dateString) { |
| const date = new Date(dateString); |
| const now = new Date(); |
| const diff = now - date; |
| |
| if (diff < 60000) return 'şimdi'; |
| if (diff < 3600000) return Math.floor(diff / 60000) + ' dk'; |
| if (diff < 86400000) return Math.floor(diff / 3600000) + ' sa'; |
| |
| return date.toLocaleDateString('tr-TR'); |
| } |
| |
| function highlightProducts(text) { |
| const products = ['MADONE', 'MARLIN', 'DOMANE', 'CHECKPOINT', 'EMONDA', 'FX', 'RAIL', 'POWERFLY']; |
| let result = text; |
| products.forEach(product => { |
| const regex = new RegExp(`(${product}[^\\s]*)`, 'gi'); |
| result = result.replace(regex, '<span class="product-mention">$1</span>'); |
| }); |
| return result; |
| } |
| |
| function renderCustomerList() { |
| const listEl = document.getElementById('customerList'); |
| listEl.innerHTML = ''; |
| |
| Object.entries(filteredConversations).forEach(([phone, data]) => { |
| const lastMessage = data.messages[data.messages.length - 1]; |
| if (!lastMessage) return; |
| |
| const item = document.createElement('div'); |
| item.className = 'customer-item'; |
| if (phone === selectedCustomer) { |
| item.classList.add('active'); |
| } |
| |
| item.innerHTML = ` |
| <div class="customer-name">Kullanıcı ${phone.substr(-4)}</div> |
| <div class="customer-message">${lastMessage.text.substring(0, 50)}...</div> |
| <div class="customer-time">${formatTime(lastMessage.time)}</div> |
| `; |
| |
| item.onclick = () => selectCustomer(phone); |
| listEl.appendChild(item); |
| }); |
| } |
| |
| function selectCustomer(phone) { |
| selectedCustomer = phone; |
| const customerData = conversations[phone]; |
| |
| |
| document.getElementById('chatHeader').style.display = 'flex'; |
| document.getElementById('chatCustomerName').textContent = `Kullanıcı ${phone.substr(-4)}`; |
| document.getElementById('chatCustomerPhone').textContent = phone; |
| |
| |
| const messagesEl = document.getElementById('chatMessages'); |
| messagesEl.innerHTML = ''; |
| |
| customerData.messages.forEach(msg => { |
| const messageDiv = document.createElement('div'); |
| messageDiv.className = `message ${msg.type}`; |
| |
| messageDiv.innerHTML = ` |
| <div class="message-bubble"> |
| <div class="message-text">${highlightProducts(msg.text)}</div> |
| <div class="message-time">${formatTime(msg.time)}</div> |
| </div> |
| `; |
| |
| messagesEl.appendChild(messageDiv); |
| }); |
| |
| |
| messagesEl.scrollTop = messagesEl.scrollHeight; |
| |
| |
| renderCustomerList(); |
| |
| |
| if (window.innerWidth <= 768) { |
| document.getElementById('sidebar').classList.add('hidden'); |
| } |
| } |
| |
| function showSidebar() { |
| document.getElementById('sidebar').classList.remove('hidden'); |
| } |
| |
| |
| document.getElementById('searchInput').addEventListener('input', function() { |
| const searchTerm = this.value.toLowerCase(); |
| |
| if (!searchTerm) { |
| filteredConversations = {...conversations}; |
| } else { |
| filteredConversations = {}; |
| Object.entries(conversations).forEach(([phone, data]) => { |
| const hasMatch = data.messages.some(msg => |
| msg.text.toLowerCase().includes(searchTerm) |
| ); |
| if (hasMatch) { |
| filteredConversations[phone] = data; |
| } |
| }); |
| } |
| |
| renderCustomerList(); |
| }); |
| |
| |
| function startAutoRefresh() { |
| refreshInterval = setInterval(loadConversations, 10000); |
| } |
| |
| function stopAutoRefresh() { |
| if (refreshInterval) { |
| clearInterval(refreshInterval); |
| } |
| } |
| |
| |
| if (API_ENDPOINT) { |
| loadConversations(); |
| startAutoRefresh(); |
| } else { |
| updateConnectionStatus('disconnected'); |
| } |
| |
| |
| document.addEventListener('visibilitychange', function() { |
| if (document.hidden) { |
| stopAutoRefresh(); |
| } else { |
| loadConversations(); |
| startAutoRefresh(); |
| } |
| }); |
| </script> |
| </body> |
| </html> |