Chatm2 / templates /admin.html
kuro223's picture
p
6e3cc2c
raw
history blame
22.5 kB
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin - Conversations</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
dark: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
}
}
}
}
}
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body { font-family: 'Inter', sans-serif; }
.conversation-card {
transition: all 0.3s ease;
}
.conversation-card:hover {
transform: translateY(-2px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.message-bubble {
animation: fadeInUp 0.3s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.loading-dots {
display: inline-block;
}
.loading-dots::after {
content: '';
animation: dots 2s infinite;
}
@keyframes dots {
0%, 20% { content: '.'; }
40% { content: '..'; }
60%, 100% { content: '...'; }
}
</style>
</head>
<body class="bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 min-h-screen text-white">
<!-- Header -->
<header class="bg-slate-800/50 backdrop-blur-sm border-b border-slate-700/50 sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.031 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
</div>
<h1 class="text-xl font-bold text-white">Admin Dashboard</h1>
</div>
<div class="flex items-center space-x-4">
<button id="refreshBtn" class="px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors duration-200 flex items-center space-x-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
<span>Actualiser</span>
</button>
</div>
</div>
</div>
</header>
<!-- Stats Bar -->
<div class="bg-slate-800/30 border-b border-slate-700/30">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
<div class="bg-gradient-to-r from-blue-500/20 to-blue-600/20 rounded-lg p-6 border border-blue-500/30">
<div class="flex items-center">
<div class="p-2 bg-blue-500/30 rounded-lg">
<svg class="w-6 h-6 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
</svg>
</div>
<div class="ml-4">
<p class="text-sm text-slate-400">Conversations</p>
<p class="text-2xl font-bold text-white" id="totalConversations">0</p>
</div>
</div>
</div>
<div class="bg-gradient-to-r from-green-500/20 to-green-600/20 rounded-lg p-6 border border-green-500/30">
<div class="flex items-center">
<div class="p-2 bg-green-500/30 rounded-lg">
<svg class="w-6 h-6 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z"/>
</svg>
</div>
<div class="ml-4">
<p class="text-sm text-slate-400">Messages</p>
<p class="text-2xl font-bold text-white" id="totalMessages">0</p>
</div>
</div>
</div>
<div class="bg-gradient-to-r from-purple-500/20 to-purple-600/20 rounded-lg p-6 border border-purple-500/30">
<div class="flex items-center">
<div class="p-2 bg-purple-500/30 rounded-lg">
<svg class="w-6 h-6 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div class="ml-4">
<p class="text-sm text-slate-400">Actives</p>
<p class="text-2xl font-bold text-white" id="activeConversations">0</p>
</div>
</div>
</div>
<div class="bg-gradient-to-r from-orange-500/20 to-orange-600/20 rounded-lg p-6 border border-orange-500/30">
<div class="flex items-center">
<div class="p-2 bg-orange-500/30 rounded-lg">
<svg class="w-6 h-6 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 00-2-2z"/>
</svg>
</div>
<div class="ml-4">
<p class="text-sm text-slate-400">Avec fichiers</p>
<p class="text-2xl font-bold text-white" id="fileConversations">0</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 sm:py-16">
<!-- Search & Filter -->
<div class="mb-8 sm:mb-10">
<div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
<div class="relative flex-1 max-w-md">
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<input
type="text"
id="searchInput"
placeholder="Rechercher dans les conversations..."
class="w-full pl-10 pr-4 py-2 bg-slate-800/50 border border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-slate-400"
>
</div>
<div class="flex gap-2">
<select id="sortSelect" class="px-4 py-2 bg-slate-800/50 border border-slate-600 rounded-lg text-white focus:ring-2 focus:ring-blue-500">
<option value="newest">Plus récentes</option>
<option value="oldest">Plus anciennes</option>
<option value="messages">Plus de messages</option>
</select>
</div>
</div>
</div>
<!-- Loading State -->
<div id="loadingState" class="text-center py-12">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
<p class="mt-4 text-slate-400">Chargement des conversations<span class="loading-dots"></span></p>
</div>
<!-- Empty State -->
<div id="emptyState" class="text-center py-12 hidden">
<div class="w-16 h-16 bg-slate-700 rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
</svg>
</div>
<h3 class="text-lg font-medium text-white mb-2">Aucune conversation</h3>
<p class="text-slate-400">Aucune conversation n'a été trouvée.</p>
</div>
<!-- Conversations Grid -->
<div id="conversationsGrid" class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8 sm:gap-10 hidden">
<!-- Les conversations seront ajoutées ici dynamiquement -->
</div>
<!-- Modal pour voir une conversation -->
<div id="conversationModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 hidden">
<div class="flex items-center justify-center min-h-screen p-6 sm:p-4">
<div class="bg-slate-800 rounded-2xl max-w-4xl w-full max-h-[85vh] overflow-hidden">
<div class="flex items-center justify-between p-8 border-b border-slate-700">
<h2 class="text-xl font-bold text-white" id="modalTitle">Conversation</h2>
<button id="closeModal" class="text-slate-400 hover:text-white transition-colors p-2">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="p-8 overflow-y-auto max-h-[60vh]" id="modalContent">
<!-- Contenu de la conversation -->
</div>
</div>
</div>
</div>
</main>
<script>
let conversations = [];
let filteredConversations = [];
// Initialisation
document.addEventListener('DOMContentLoaded', function() {
loadConversations();
setupEventListeners();
});
function setupEventListeners() {
document.getElementById('refreshBtn').addEventListener('click', loadConversations);
document.getElementById('searchInput').addEventListener('input', filterConversations);
document.getElementById('sortSelect').addEventListener('change', sortConversations);
document.getElementById('closeModal').addEventListener('click', closeModal);
// Fermer modal avec Escape
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeModal();
});
}
async function loadConversations() {
showLoading();
try {
const response = await fetch('/admin/conversations');
const data = await response.json();
conversations = data.conversations || [];
filteredConversations = [...conversations];
updateStats(data.stats || {});
displayConversations();
} catch (error) {
console.error('Erreur lors du chargement:', error);
showError('Erreur lors du chargement des conversations');
}
}
function updateStats(stats) {
document.getElementById('totalConversations').textContent = stats.total || 0;
document.getElementById('totalMessages').textContent = stats.totalMessages || 0;
document.getElementById('activeConversations').textContent = stats.active || 0;
document.getElementById('fileConversations').textContent = stats.withFiles || 0;
}
function displayConversations() {
const grid = document.getElementById('conversationsGrid');
const loading = document.getElementById('loadingState');
const empty = document.getElementById('emptyState');
loading.classList.add('hidden');
if (filteredConversations.length === 0) {
grid.classList.add('hidden');
empty.classList.remove('hidden');
return;
}
empty.classList.add('hidden');
grid.classList.remove('hidden');
grid.innerHTML = filteredConversations.map(conv => createConversationCard(conv)).join('');
}
function createConversationCard(conv) {
const lastMessage = conv.messages[conv.messages.length - 1] || {};
const messageCount = conv.messages.length;
const hasFiles = conv.messages.some(m => m.hasFile);
const lastActivity = conv.lastActivity ?
new Date(conv.lastActivity).toLocaleString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
}) : 'Inconnue';
return `
<div class="conversation-card bg-slate-800/50 rounded-xl border border-slate-700/50 p-8 cursor-pointer hover:border-slate-600"
onclick="openConversation('${conv.id}')">
<div class="flex items-start justify-between mb-6">
<div class="flex items-center space-x-4">
<div class="w-12 h-12 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</div>
<div>
<h3 class="font-semibold text-white">${conv.id}</h3>
<p class="text-sm text-slate-400">${messageCount} message${messageCount > 1 ? 's' : ''}</p>
</div>
</div>
<div class="flex flex-col items-end space-y-2">
${hasFiles ? `
<span class="px-2 py-1 bg-orange-500/20 text-orange-400 text-xs rounded-full border border-orange-500/30">
📎 Fichiers
</span>
` : ''}
<span class="px-2 py-1 bg-blue-500/20 text-blue-400 text-xs rounded-full border border-blue-500/30">
${conv.status || 'Active'}
</span>
</div>
</div>
<div class="mb-6">
<p class="text-sm text-slate-300 line-clamp-2">
${lastMessage.content ?
(lastMessage.content.length > 100 ?
lastMessage.content.substring(0, 100) + '...' :
lastMessage.content
) : 'Conversation vide'
}
</p>
</div>
<div class="flex items-center justify-between text-xs text-slate-500">
<span>Dernière activité</span>
<span>${lastActivity}</span>
</div>
</div>
`;
}
function openConversation(conversationId) {
const conv = conversations.find(c => c.id === conversationId);
if (!conv) return;
document.getElementById('modalTitle').textContent = `Conversation: ${conversationId}`;
const modalContent = document.getElementById('modalContent');
modalContent.innerHTML = conv.messages.map(msg => `
<div class="message-bubble mb-4 ${msg.role === 'user' ? 'ml-8' : 'mr-8'}">
<div class="flex items-start space-x-3">
<div class="flex-shrink-0">
<div class="w-8 h-8 rounded-full flex items-center justify-center ${
msg.role === 'user'
? 'bg-blue-500/20 text-blue-400'
: 'bg-green-500/20 text-green-400'
}">
${msg.role === 'user' ? '👤' : '🤖'}
</div>
</div>
<div class="flex-1">
<div class="flex items-center space-x-2 mb-1">
<span class="text-sm font-medium ${msg.role === 'user' ? 'text-blue-400' : 'text-green-400'}">
${msg.role === 'user' ? 'Utilisateur' : 'Assistant'}
</span>
${msg.timestamp ? `
<span class="text-xs text-slate-500">
${new Date(msg.timestamp).toLocaleString('fr-FR')}
</span>
` : ''}
${msg.hasFile ? '<span class="text-xs bg-orange-500/20 text-orange-400 px-2 py-1 rounded">📎 Fichier</span>' : ''}
</div>
<div class="bg-slate-700/50 rounded-lg p-3 text-sm text-slate-200">
${msg.content || 'Message vide'}
</div>
</div>
</div>
</div>
`).join('');
document.getElementById('conversationModal').classList.remove('hidden');
}
function closeModal() {
document.getElementById('conversationModal').classList.add('hidden');
}
function filterConversations() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
filteredConversations = conversations.filter(conv => {
return conv.id.toLowerCase().includes(searchTerm) ||
conv.messages.some(msg =>
msg.content && msg.content.toLowerCase().includes(searchTerm)
);
});
displayConversations();
}
function sortConversations() {
const sortBy = document.getElementById('sortSelect').value;
filteredConversations.sort((a, b) => {
switch(sortBy) {
case 'newest':
return new Date(b.lastActivity || 0) - new Date(a.lastActivity || 0);
case 'oldest':
return new Date(a.lastActivity || 0) - new Date(b.lastActivity || 0);
case 'messages':
return b.messages.length - a.messages.length;
default:
return 0;
}
});
displayConversations();
}
function showLoading() {
document.getElementById('loadingState').classList.remove('hidden');
document.getElementById('conversationsGrid').classList.add('hidden');
document.getElementById('emptyState').classList.add('hidden');
}
function showError(message) {
// Vous pouvez implémenter une notification d'erreur ici
console.error(message);
}
</script>
</body>
</html>