BF / bf_dashboard.html
SamiKoen's picture
Add real-time conversation dashboard for BF
1c57887
raw
history blame
20.7 kB
<!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 highlighting */
.product-mention {
color: #667eea;
font-weight: bold;
}
/* Mobile responsive */
@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;
}
}
/* Scrollbar styling */
::-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>
<!-- Sidebar -->
<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">
<!-- Customer items will be added here -->
</div>
</div>
<!-- Chat Area -->
<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>
<!-- Connection Status -->
<div class="connection-status" id="connectionStatus">
<span id="statusText">Bağlanıyor...</span>
</div>
<script>
// Configuration
let API_ENDPOINT = localStorage.getItem('bf_api_endpoint') || '';
let conversations = {};
let selectedCustomer = null;
let refreshInterval;
let filteredConversations = {};
// Load API endpoint from input
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};
// Load stats
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];
// Update header
document.getElementById('chatHeader').style.display = 'flex';
document.getElementById('chatCustomerName').textContent = `Kullanıcı ${phone.substr(-4)}`;
document.getElementById('chatCustomerPhone').textContent = phone;
// Render messages
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);
});
// Scroll to bottom
messagesEl.scrollTop = messagesEl.scrollHeight;
// Update active state
renderCustomerList();
// Mobile: hide sidebar
if (window.innerWidth <= 768) {
document.getElementById('sidebar').classList.add('hidden');
}
}
function showSidebar() {
document.getElementById('sidebar').classList.remove('hidden');
}
// Search functionality
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();
});
// Auto refresh
function startAutoRefresh() {
refreshInterval = setInterval(loadConversations, 10000); // Her 10 saniyede bir
}
function stopAutoRefresh() {
if (refreshInterval) {
clearInterval(refreshInterval);
}
}
// Initial load
if (API_ENDPOINT) {
loadConversations();
startAutoRefresh();
} else {
updateConnectionStatus('disconnected');
}
// Page visibility API
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
stopAutoRefresh();
} else {
loadConversations();
startAutoRefresh();
}
});
</script>
</body>
</html>