diff --git a/assets/css/chatbot_settings.css b/assets/css/chatbot_settings.css new file mode 100644 index 0000000..3ba333e --- /dev/null +++ b/assets/css/chatbot_settings.css @@ -0,0 +1,737 @@ +/** + * Styles pour la configuration API Chatbot + * Design moderne avec une palette professionnelle + */ + +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap'); + +:root { + --primary: #667eea; + --primary-dark: #5568d3; + --secondary: #48bb78; + --danger: #f56565; + --warning: #ed8936; + --bg-main: #f7fafc; + --bg-card: #ffffff; + --text-primary: #2d3748; + --text-secondary: #718096; + --border: #e2e8f0; + --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + --radius: 12px; + --transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + background: var(--bg-main); + color: var(--text-primary); + line-height: 1.6; + min-height: 100vh; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +/* Header */ +.page-header { + margin-bottom: 3rem; +} + +.header-content { + position: relative; +} + +.back-link { + display: inline-flex; + align-items: center; + gap: 0.5rem; + color: var(--text-secondary); + text-decoration: none; + font-size: 0.875rem; + margin-bottom: 1rem; + transition: var(--transition); +} + +.back-link:hover { + color: var(--primary); + transform: translateX(-4px); +} + +.page-header h1 { + font-size: 2.5rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 0.5rem; +} + +.subtitle { + font-size: 1.125rem; + color: var(--text-secondary); +} + +/* Stats Banner */ +.stats-banner { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1.5rem; + margin-bottom: 3rem; +} + +.stat-card { + background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); + border-radius: var(--radius); + padding: 1.5rem; + display: flex; + align-items: center; + gap: 1rem; + box-shadow: var(--shadow-md); + transition: var(--transition); +} + +.stat-card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-lg); +} + +.stat-icon { + font-size: 2.5rem; + filter: brightness(0) invert(1); + opacity: 0.9; +} + +.stat-content { + color: white; +} + +.stat-value { + font-size: 2rem; + font-weight: 700; + line-height: 1; + margin-bottom: 0.25rem; +} + +.stat-label { + font-size: 0.875rem; + opacity: 0.9; +} + +/* Content Grid */ +.content-grid { + display: grid; + grid-template-columns: 1fr 350px; + gap: 2rem; + margin-bottom: 3rem; +} + +@media (max-width: 1024px) { + .content-grid { + grid-template-columns: 1fr; + } +} + +/* Section Styling */ +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.section-header h2, +.section-header h3 { + font-size: 1.5rem; + font-weight: 600; +} + +/* Buttons */ +.btn-primary, +.btn-secondary, +.btn-danger, +.btn-test { + padding: 0.75rem 1.5rem; + border-radius: 8px; + border: none; + font-weight: 600; + font-size: 0.875rem; + cursor: pointer; + transition: var(--transition); + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +.btn-primary { + background: var(--primary); + color: white; +} + +.btn-primary:hover { + background: var(--primary-dark); + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.btn-secondary { + background: var(--bg-card); + color: var(--text-primary); + border: 2px solid var(--border); +} + +.btn-secondary:hover { + border-color: var(--primary); + color: var(--primary); +} + +.btn-danger { + background: var(--danger); + color: white; +} + +.btn-danger:hover { + background: #e53e3e; +} + +.btn-test { + background: var(--secondary); + color: white; +} + +.btn-test:hover { + background: #38a169; +} + +.btn-sm { + padding: 0.5rem 1rem; + font-size: 0.8125rem; +} + +/* Keys List */ +.keys-list { + display: grid; + gap: 1.5rem; +} + +.key-card { + background: var(--bg-card); + border-radius: var(--radius); + border: 2px solid var(--border); + overflow: hidden; + transition: var(--transition); +} + +.key-card:hover { + box-shadow: var(--shadow-lg); + border-color: var(--primary); +} + +.key-card.active { + border-color: var(--secondary); +} + +.key-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.25rem; + background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%); + border-bottom: 1px solid var(--border); +} + +.key-provider { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.provider-icon { + font-size: 1.5rem; +} + +.provider-name { + font-weight: 600; + font-size: 1.125rem; +} + +.status-badge { + padding: 0.375rem 0.75rem; + border-radius: 20px; + font-size: 0.8125rem; + font-weight: 600; +} + +.status-badge.active { + background: #c6f6d5; + color: #22543d; +} + +.status-badge.inactive { + background: #fed7d7; + color: #742a2a; +} + +.status-badge.none { + background: #e2e8f0; + color: #4a5568; +} + +.key-body { + padding: 1.25rem; +} + +.key-info { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-bottom: 1rem; +} + +.info-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0; +} + +.info-row .label { + color: var(--text-secondary); + font-size: 0.875rem; +} + +.info-row .value { + font-weight: 500; + font-family: 'JetBrains Mono', monospace; + font-size: 0.875rem; +} + +.quota-bar { + display: inline-block; + width: 100px; + height: 6px; + background: var(--border); + border-radius: 3px; + overflow: hidden; + margin-left: 0.5rem; + vertical-align: middle; +} + +.quota-fill { + display: block; + height: 100%; + background: var(--secondary); + transition: width 0.3s ease; +} + +.key-actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; +} + +.key-empty { + text-align: center; + padding: 2rem 1rem; + color: var(--text-secondary); +} + +.key-empty p { + margin-bottom: 1rem; +} + +/* Models Sidebar */ +.models-sidebar { + background: var(--bg-card); + border-radius: var(--radius); + padding: 1.5rem; + border: 2px solid var(--border); + position: sticky; + top: 2rem; + max-height: calc(100vh - 4rem); + overflow-y: auto; +} + +.models-list { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.provider-group h4 { + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.75rem; + color: var(--text-primary); +} + +.model-item { + padding: 0.75rem; + background: var(--bg-main); + border-radius: 8px; + border: 1px solid var(--border); + transition: var(--transition); +} + +.model-item:hover { + border-color: var(--primary); + background: white; +} + +.model-item.recommended { + border-color: var(--secondary); + background: #f0fff4; +} + +.model-name { + font-weight: 500; + font-size: 0.875rem; + margin-bottom: 0.5rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.model-meta { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.badge-recommended, +.badge-free, +.badge-paid, +.badge-speed { + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; +} + +.badge-recommended { + background: #fef5e7; +} + +.badge-free { + background: #c6f6d5; + color: #22543d; +} + +.badge-paid { + background: #fed7d7; + color: #742a2a; +} + +.help-box { + margin-top: 1.5rem; + padding: 1rem; + background: #ebf8ff; + border-radius: 8px; + border-left: 4px solid var(--primary); +} + +.help-box h4 { + font-size: 0.875rem; + margin-bottom: 0.75rem; +} + +.help-box ul { + list-style: none; + font-size: 0.8125rem; + line-height: 1.8; +} + +.help-box li { + padding-left: 1rem; + position: relative; +} + +.help-box li::before { + content: "→"; + position: absolute; + left: 0; + color: var(--primary); +} + +/* Stats Table */ +.stats-section { + background: var(--bg-card); + border-radius: var(--radius); + padding: 1.5rem; + border: 2px solid var(--border); +} + +.stats-table-wrapper { + overflow-x: auto; +} + +.stats-table { + width: 100%; + border-collapse: collapse; +} + +.stats-table thead { + background: var(--bg-main); +} + +.stats-table th { + padding: 0.75rem 1rem; + text-align: left; + font-weight: 600; + font-size: 0.875rem; + color: var(--text-secondary); + border-bottom: 2px solid var(--border); +} + +.stats-table td { + padding: 1rem; + border-bottom: 1px solid var(--border); + font-size: 0.875rem; +} + +.stats-table tbody tr:hover { + background: var(--bg-main); +} + +.provider-badge { + padding: 0.375rem 0.75rem; + background: var(--bg-main); + border-radius: 6px; + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +/* Modal */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + backdrop-filter: blur(4px); + animation: fadeIn 0.2s ease; +} + +.modal.show { + display: flex; + align-items: center; + justify-content: center; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.modal-content { + background: var(--bg-card); + border-radius: var(--radius); + max-width: 600px; + width: 90%; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); + animation: slideUp 0.3s ease; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem; + border-bottom: 2px solid var(--border); +} + +.modal-header h2 { + font-size: 1.5rem; + font-weight: 600; +} + +.modal-close { + background: none; + border: none; + font-size: 2rem; + color: var(--text-secondary); + cursor: pointer; + line-height: 1; + transition: var(--transition); +} + +.modal-close:hover { + color: var(--danger); + transform: scale(1.1); +} + +/* Form */ +#keyForm { + padding: 1.5rem; +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + font-weight: 600; + margin-bottom: 0.5rem; + color: var(--text-primary); +} + +.form-group input[type="text"], +.form-group input[type="password"], +.form-group input[type="number"], +.form-group select { + width: 100%; + padding: 0.75rem 1rem; + border: 2px solid var(--border); + border-radius: 8px; + font-size: 0.875rem; + font-family: 'JetBrains Mono', monospace; + transition: var(--transition); +} + +.form-group input:focus, +.form-group select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.input-with-button { + position: relative; +} + +.input-with-button input { + padding-right: 3rem; +} + +.btn-toggle-visibility { + position: absolute; + right: 0.5rem; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + font-size: 1.25rem; + cursor: pointer; + padding: 0.5rem; + opacity: 0.6; + transition: var(--transition); +} + +.btn-toggle-visibility:hover { + opacity: 1; +} + +.help-text { + display: block; + margin-top: 0.5rem; + font-size: 0.8125rem; + color: var(--text-secondary); +} + +.form-actions { + display: flex; + gap: 1rem; + justify-content: flex-end; + margin-top: 2rem; + padding-top: 1.5rem; + border-top: 2px solid var(--border); +} + +.test-result { + margin-top: 1rem; + padding: 1rem; + border-radius: 8px; + font-size: 0.875rem; +} + +.test-result.success { + background: #c6f6d5; + color: #22543d; + border: 2px solid #9ae6b4; +} + +.test-result.error { + background: #fed7d7; + color: #742a2a; + border: 2px solid #fc8181; +} + +/* Toast */ +#toast-container { + position: fixed; + bottom: 2rem; + right: 2rem; + z-index: 2000; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.toast { + background: white; + padding: 1rem 1.5rem; + border-radius: 8px; + box-shadow: var(--shadow-lg); + min-width: 300px; + display: flex; + align-items: center; + gap: 0.75rem; + border-left: 4px solid var(--primary); + animation: slideInRight 0.3s ease; +} + +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(100%); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.toast.success { + border-left-color: var(--secondary); +} + +.toast.error { + border-left-color: var(--danger); +} + +.toast-icon { + font-size: 1.5rem; +} + +.toast-message { + flex: 1; + font-size: 0.875rem; + font-weight: 500; +} diff --git a/assets/js/chatbot_settings.js b/assets/js/chatbot_settings.js new file mode 100644 index 0000000..3e888f8 --- /dev/null +++ b/assets/js/chatbot_settings.js @@ -0,0 +1,337 @@ +/** + * JavaScript pour la gestion des clés API du chatbot + */ + +// État global +let currentProvider = null; +let editMode = false; + +// Helper URLs +const API_HELP_URLS = { + 'gemini': 'https://aistudio.google.com/apikey', + 'mistral': 'https://console.mistral.ai/api-keys/', + 'openai': 'https://platform.openai.com/api-keys' +}; + +// ========== Modal Management ========== + +function openAddKeyModal(provider = null) { + const modal = document.getElementById('keyModal'); + const title = document.getElementById('modalTitle'); + const form = document.getElementById('keyForm'); + const providerSelect = document.getElementById('provider'); + const testResult = document.getElementById('testResult'); + + // Reset form + form.reset(); + testResult.style.display = 'none'; + document.getElementById('keyAction').value = 'add'; + editMode = false; + + // Pre-select provider if provided + if (provider) { + providerSelect.value = provider; + updateModelsList(); + providerSelect.disabled = false; + } else { + providerSelect.disabled = false; + } + + title.textContent = 'Ajouter une clé API'; + modal.classList.add('show'); +} + +function editKey(provider) { + currentProvider = provider; + editMode = true; + + // Récupérer les infos de la clé + fetch(`chatbot_api_ajax.php?action=get&provider=${provider}`) + .then(response => response.json()) + .then(data => { + if (data.success) { + const modal = document.getElementById('keyModal'); + const title = document.getElementById('modalTitle'); + const form = document.getElementById('keyForm'); + const providerSelect = document.getElementById('provider'); + const modelSelect = document.getElementById('model_default'); + const testResult = document.getElementById('testResult'); + + testResult.style.display = 'none'; + document.getElementById('keyAction').value = 'edit'; + + // Remplir le formulaire + providerSelect.value = provider; + providerSelect.disabled = true; + updateModelsList(); + + // Attendre que la liste des modèles soit chargée + setTimeout(() => { + modelSelect.value = data.data.model_default; + + if (data.data.quota_max) { + document.getElementById('set_quota').checked = true; + document.getElementById('quota_max').value = data.data.quota_max; + toggleQuotaInput(); + } + }, 100); + + title.textContent = `Modifier la clé ${provider}`; + modal.classList.add('show'); + } else { + showToast(data.error || 'Erreur lors du chargement', 'error'); + } + }) + .catch(error => { + console.error('Error:', error); + showToast('Erreur de connexion', 'error'); + }); +} + +function closeKeyModal() { + const modal = document.getElementById('keyModal'); + modal.classList.remove('show'); + currentProvider = null; + editMode = false; +} + +// ========== Form Handling ========== + +function updateModelsList() { + const provider = document.getElementById('provider').value; + const modelSelect = document.getElementById('model_default'); + const keyHelp = document.getElementById('keyHelp'); + + // Clear current options + modelSelect.innerHTML = ''; + + if (!provider) { + keyHelp.innerHTML = ''; + return; + } + + // Populate models for selected provider + if (models[provider]) { + models[provider].forEach(model => { + const option = document.createElement('option'); + option.value = model.model_id; + option.textContent = model.display_name; + if (model.recommended) { + option.textContent += ' ⭐'; + } + modelSelect.appendChild(option); + }); + + // Auto-select first recommended model + const recommended = models[provider].find(m => m.recommended); + if (recommended) { + modelSelect.value = recommended.model_id; + } + } + + // Update help text + if (API_HELP_URLS[provider]) { + keyHelp.innerHTML = `💡 Obtenir une clé : ${API_HELP_URLS[provider]}`; + } else { + keyHelp.innerHTML = ''; + } +} + +function toggleQuotaInput() { + const checkbox = document.getElementById('set_quota'); + const quotaGroup = document.getElementById('quotaGroup'); + quotaGroup.style.display = checkbox.checked ? 'block' : 'none'; +} + +function toggleKeyVisibility() { + const input = document.getElementById('api_key'); + const button = event.target; + + if (input.type === 'password') { + input.type = 'text'; + button.textContent = '🙈'; + } else { + input.type = 'password'; + button.textContent = '👁️'; + } +} + +// ========== Form Submission ========== + +document.getElementById('keyForm').addEventListener('submit', function(e) { + e.preventDefault(); + + const formData = new FormData(this); + formData.append('action', 'save'); + + // Show loading + const submitBtn = this.querySelector('button[type="submit"]'); + const originalText = submitBtn.innerHTML; + submitBtn.innerHTML = '⏳ Enregistrement...'; + submitBtn.disabled = true; + + fetch('chatbot_api_ajax.php', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast(data.message || 'Clé enregistrée avec succès', 'success'); + closeKeyModal(); + + // Reload page to show updated keys + setTimeout(() => { + window.location.reload(); + }, 1000); + } else { + showToast(data.error || 'Erreur lors de l\'enregistrement', 'error'); + } + }) + .catch(error => { + console.error('Error:', error); + showToast('Erreur de connexion', 'error'); + }) + .finally(() => { + submitBtn.innerHTML = originalText; + submitBtn.disabled = false; + }); +}); + +// ========== Test Connection ========== + +function testKeyBeforeSave() { + const provider = document.getElementById('provider').value; + const apiKey = document.getElementById('api_key').value; + const model = document.getElementById('model_default').value; + + if (!provider || !apiKey || !model) { + showToast('Veuillez remplir tous les champs obligatoires', 'error'); + return; + } + + testConnectionWithParams(provider, apiKey, model); +} + +function testConnection(provider) { + testConnectionWithParams(provider, null, null); +} + +function testConnectionWithParams(provider, apiKey = null, model = null) { + const testResult = document.getElementById('testResult'); + testResult.style.display = 'block'; + testResult.className = 'test-result'; + testResult.innerHTML = '🔄 Test en cours...'; + + const formData = new FormData(); + formData.append('action', 'test'); + formData.append('provider', provider); + if (apiKey) formData.append('api_key', apiKey); + if (model) formData.append('model', model); + + fetch('chatbot_api_ajax.php', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + testResult.className = 'test-result success'; + testResult.innerHTML = `✅ ${data.message}
Latence: ${data.latency_ms}ms`; + } else { + testResult.className = 'test-result error'; + testResult.innerHTML = `❌ ${data.message}`; + } + }) + .catch(error => { + console.error('Error:', error); + testResult.className = 'test-result error'; + testResult.innerHTML = '❌ Erreur de connexion au serveur'; + }); +} + +// ========== Delete Key ========== + +function deleteKey(provider) { + if (!confirm(`Êtes-vous sûr de vouloir supprimer la clé ${provider} ?\n\nCette action est irréversible.`)) { + return; + } + + const formData = new FormData(); + formData.append('action', 'delete'); + formData.append('provider', provider); + + fetch('chatbot_api_ajax.php', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast('Clé supprimée avec succès', 'success'); + setTimeout(() => { + window.location.reload(); + }, 1000); + } else { + showToast(data.error || 'Erreur lors de la suppression', 'error'); + } + }) + .catch(error => { + console.error('Error:', error); + showToast('Erreur de connexion', 'error'); + }); +} + +// ========== Toast Notifications ========== + +function showToast(message, type = 'info') { + const container = document.getElementById('toast-container'); + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + + const icons = { + 'success': '✅', + 'error': '❌', + 'warning': '⚠️', + 'info': 'ℹ️' + }; + + toast.innerHTML = ` + ${icons[type] || icons.info} + ${message} + `; + + container.appendChild(toast); + + // Auto-remove after 4 seconds + setTimeout(() => { + toast.style.opacity = '0'; + toast.style.transform = 'translateX(100%)'; + setTimeout(() => toast.remove(), 300); + }, 4000); +} + +// ========== Close modal on outside click ========== + +document.getElementById('keyModal').addEventListener('click', function(e) { + if (e.target === this) { + closeKeyModal(); + } +}); + +// ========== Keyboard shortcuts ========== + +document.addEventListener('keydown', function(e) { + // ESC to close modal + if (e.key === 'Escape') { + const modal = document.getElementById('keyModal'); + if (modal.classList.contains('show')) { + closeKeyModal(); + } + } +}); + +// ========== Initialize ========== + +console.log('✅ Chatbot Settings JS loaded'); +console.log('Available models:', models); diff --git a/enseignant/chatbot_api_ajax.php b/enseignant/chatbot_api_ajax.php new file mode 100644 index 0000000..d63cfad --- /dev/null +++ b/enseignant/chatbot_api_ajax.php @@ -0,0 +1,201 @@ + 'Non autorisé']); + exit; +} + +$user = SessionManager::getUser(); +$action = $_GET['action'] ?? $_POST['action'] ?? null; + +try { + switch ($action) { + + case 'save': + // Sauvegarder une clé API + $provider = $_POST['provider'] ?? null; + $api_key = $_POST['api_key'] ?? null; + $model_default = $_POST['model_default'] ?? null; + $quota_max = !empty($_POST['quota_max']) ? (int)$_POST['quota_max'] : null; + + if (!$provider || !$api_key || !$model_default) { + throw new Exception('Paramètres manquants'); + } + + // Valider le provider + $valid_providers = ['gemini', 'mistral', 'openai', 'ollama']; + if (!in_array($provider, $valid_providers)) { + throw new Exception('Provider invalide'); + } + + // Sauvegarder + $result = APIKeyManager::saveKey( + $user['id_utilisateur'], + $provider, + $api_key, + $model_default + ); + + if (!$result) { + throw new Exception('Erreur lors de la sauvegarde'); + } + + // Mettre à jour le quota si défini + if ($quota_max !== null) { + $db = Database::getInstance(); + $db->query( + "UPDATE api_keys SET quota_max = ? WHERE id_enseignant = ? AND provider = ?", + [$quota_max, $user['id_utilisateur'], $provider] + ); + } + + echo json_encode([ + 'success' => true, + 'message' => 'Clé API enregistrée avec succès' + ]); + break; + + case 'test': + // Tester une connexion API + $provider = $_POST['provider'] ?? null; + $api_key = $_POST['api_key'] ?? null; + $model = $_POST['model'] ?? null; + + if (!$provider) { + throw new Exception('Provider manquant'); + } + + // Si pas de clé fournie, récupérer celle enregistrée + if (!$api_key) { + $key_data = APIKeyManager::getKey($user['id_utilisateur'], $provider); + if (!$key_data) { + throw new Exception('Aucune clé enregistrée pour ce provider'); + } + $api_key = $key_data['api_key']; + $model = $model ?? $key_data['model_default']; + } + + // Tester la connexion + $result = APIKeyManager::testConnection($provider, $api_key, $model); + + echo json_encode($result); + break; + + case 'delete': + // Supprimer une clé API + $provider = $_POST['provider'] ?? null; + + if (!$provider) { + throw new Exception('Provider manquant'); + } + + $result = APIKeyManager::deleteKey($user['id_utilisateur'], $provider); + + if (!$result) { + throw new Exception('Erreur lors de la suppression'); + } + + echo json_encode([ + 'success' => true, + 'message' => 'Clé API supprimée' + ]); + break; + + case 'get': + // Récupérer une clé API (pour édition) + $provider = $_GET['provider'] ?? null; + + if (!$provider) { + throw new Exception('Provider manquant'); + } + + $db = Database::getInstance(); + $key_data = $db->fetchOne( + "SELECT provider, model_default, quota_max, is_active + FROM api_keys + WHERE id_enseignant = ? AND provider = ?", + [$user['id_utilisateur'], $provider] + ); + + if (!$key_data) { + throw new Exception('Clé non trouvée'); + } + + // Ne pas retourner la clé API elle-même + echo json_encode([ + 'success' => true, + 'data' => $key_data + ]); + break; + + case 'toggle': + // Activer/désactiver une clé + $provider = $_POST['provider'] ?? null; + $active = isset($_POST['active']) ? (bool)$_POST['active'] : null; + + if (!$provider || $active === null) { + throw new Exception('Paramètres manquants'); + } + + $db = Database::getInstance(); + $result = $db->query( + "UPDATE api_keys SET is_active = ? WHERE id_enseignant = ? AND provider = ?", + [$active, $user['id_utilisateur'], $provider] + ); + + echo json_encode([ + 'success' => true, + 'message' => $active ? 'Clé activée' : 'Clé désactivée' + ]); + break; + + case 'stats': + // Récupérer les statistiques d'utilisation + $db = Database::getInstance(); + + $stats = $db->fetchAll( + "SELECT + provider, + model_used, + SUM(total_conversations) as conversations, + SUM(total_messages) as messages, + AVG(avg_response_time_ms) as avg_latency, + month + FROM v_chatbot_usage_stats + WHERE id_enseignant = ? + GROUP BY provider, model_used, month + ORDER BY month DESC, conversations DESC + LIMIT 50", + [$user['id_utilisateur']] + ); + + echo json_encode([ + 'success' => true, + 'stats' => $stats + ]); + break; + + default: + throw new Exception('Action inconnue'); + } + +} catch (Exception $e) { + http_response_code(400); + echo json_encode([ + 'success' => false, + 'error' => $e->getMessage() + ]); +} diff --git a/enseignant/chatbot_api_settings.php b/enseignant/chatbot_api_settings.php new file mode 100644 index 0000000..522c260 --- /dev/null +++ b/enseignant/chatbot_api_settings.php @@ -0,0 +1,377 @@ +fetchAll( + "SELECT * FROM available_models WHERE active = TRUE ORDER BY provider, recommended DESC, display_name" +); + +foreach ($all_models as $model) { + $models_by_provider[$model['provider']][] = $model; +} + +// Récupérer les statistiques d'utilisation +$stats = $db->fetchOne( + "SELECT + COUNT(DISTINCT id_conversation) as total_conversations, + SUM(total_messages) as total_messages, + provider, + model_used + FROM v_chatbot_usage_stats + WHERE id_enseignant = ? + GROUP BY provider, model_used + ORDER BY total_conversations DESC + LIMIT 1", + [$user['id_utilisateur']] +); + +$stats_detail = $db->fetchAll( + "SELECT + provider, + model_used, + SUM(total_conversations) as conversations, + SUM(total_messages) as messages, + month + FROM v_chatbot_usage_stats + WHERE id_enseignant = ? + GROUP BY provider, model_used, month + ORDER BY month DESC, conversations DESC", + [$user['id_utilisateur']] +); + +?> + + + + + + Configuration API Chatbot - WebVal + + + +
+ + + + + +
+
+
💬
+
+
+
Conversations
+
+
+
+
📨
+
+
+
Messages
+
+
+
+
🤖
+
+
+
Provider actuel
+
+
+
+ + + +
+ + +
+
+

🔑 Mes clés API

+ +
+ +
+ 'Gemini', 'mistral' => 'Mistral', 'openai' => 'OpenAI', 'ollama' => 'Ollama']; + $provider_icons = [ + 'gemini' => '✨', + 'mistral' => '⚡', + 'openai' => '🧠', + 'ollama' => '🏠' + ]; + + foreach ($providers as $provider_id => $provider_name): + $key_info = null; + foreach ($api_keys as $key) { + if ($key['provider'] === $provider_id) { + $key_info = $key; + break; + } + } + + $is_configured = $key_info !== null; + $is_active = $is_configured && $key_info['is_active']; + ?> +
+
+
+ + +
+
+ + 🟢 Active + + 🟠 Désactivée + + ⚪ Aucune + +
+
+ +
+ +
+
+ Modèle par défaut : + +
+ +
+ Quota : + + / + + + + +
+ +
+ Dernière utilisation : + + + +
+
+
+ + + +
+ +
+

Aucune clé configurée pour

+ +
+ +
+
+ +
+
+ + + +
+ + + +
+
+

📊 Historique d'utilisation

+
+
+ + + + + + + + + + + + + + + + + + + + + +
MoisProviderModèleConversationsMessages
+ + + + +
+
+
+ +
+ + + + + +
+ + + + +