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 = `
+
+
+ `;
+
+ 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']]
+);
+
+?>
+
+
+
Gérez vos clés API et choisissez les modèles pour vos évaluations
+Aucune clé configurée pour = $provider_name ?>
+ +| Mois | +Provider | +Modèle | +Conversations | +Messages | +
|---|---|---|---|---|
| = date('M Y', strtotime($stat['month'] . '-01')) ?> | ++ + = $provider_icons[$stat['provider']] ?? '🤖' ?> + = ucfirst($stat['provider']) ?> + + | += htmlspecialchars($stat['model_used']) ?> | += number_format($stat['conversations']) ?> | += number_format($stat['messages']) ?> | +