Files
wevads-platform/scripts/widget-admin.php
2026-02-26 04:53:11 +01:00

650 lines
36 KiB
PHP
Executable File

<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
$DB = ['host'=>'localhost','db'=>'adx_system','user'=>'admin','pass'=>'admin123'];
try {
$pdo = new PDO("pgsql:host={$DB['host']};dbname={$DB['db']}", $DB['user'], $DB['pass']);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (Exception $e) {
die("❌ Connexion DB: " . $e->getMessage());
}
// Upload image
$uploadMsg = '';
if (isset($_FILES['robot_image']) && $_FILES['robot_image']['error'] == 0) {
$uploadDir = '/opt/wevads/public/images/';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
$ext = pathinfo($_FILES['robot_image']['name'], PATHINFO_EXTENSION);
$filename = 'chatbot-avatar.' . $ext;
if (move_uploaded_file($_FILES['robot_image']['tmp_name'], $uploadDir . $filename)) {
$stmt = $pdo->prepare("INSERT INTO admin.chatbot_config (config_key, config_value) VALUES ('robot_image', ?)
ON CONFLICT (config_key) DO UPDATE SET config_value = EXCLUDED.config_value");
$stmt->execute(['/images/' . $filename . '?v=' . time()]);
$uploadMsg = '✅ Image uploadée!';
}
}
// Sauvegarder config
$msg = '';
if ($_POST && isset($_POST['save_config'])) {
$fields = [
'bot_name', 'bubble_text', 'welcome_message', 'color1', 'color2',
'default_provider', 'widget_size', 'glow', 'opacity', 'border',
'groq_api_key', 'deepseek_api_key', 'claude_api_key', 'gemini_api_key',
'mistral_api_key', 'ollama_url', 'vllm_url', 'system_prompt', 'fallback_enabled'
];
foreach ($fields as $key) {
$value = $_POST[$key] ?? '';
if ($key == 'fallback_enabled') $value = isset($_POST[$key]) ? '1' : '0';
$stmt = $pdo->prepare("INSERT INTO admin.chatbot_config (config_key, config_value) VALUES (?, ?)
ON CONFLICT (config_key) DO UPDATE SET config_value = EXCLUDED.config_value");
$stmt->execute([$key, $value]);
}
$msg = '✅ Configuration sauvegardée!';
}
// API Test endpoint
if (isset($_GET['test_provider'])) {
header('Content-Type: application/json');
$provider = $_GET['test_provider'];
// Recharger config fraîche
$config = [];
$stmt = $pdo->query("SELECT config_key, config_value FROM admin.chatbot_config");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $config[$row['config_key']] = $row['config_value'];
$result = ['provider' => $provider, 'status' => 'error', 'message' => 'Non testé', 'latency' => 0];
$start = microtime(true);
try {
switch ($provider) {
case 'groq':
$apiKey = $config['groq_api_key'] ?? '';
if (empty($apiKey)) { $result['message'] = 'API Key manquante'; break; }
$ch = curl_init('https://api.groq.com/openai/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $apiKey],
CURLOPT_POSTFIELDS => json_encode(['model' => 'llama-3.3-70b-versatile', 'messages' => [['role' => 'user', 'content' => 'Hi']], 'max_tokens' => 5])
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 200) { $result['status'] = 'ok'; $result['message'] = 'Groq OK - LLama 3.3 70B'; }
else { $result['message'] = "HTTP $httpCode"; }
break;
case 'deepseek':
$apiKey = $config['deepseek_api_key'] ?? '';
if (empty($apiKey)) { $result['message'] = 'API Key manquante'; break; }
$ch = curl_init('https://api.deepseek.com/chat/completions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $apiKey],
CURLOPT_POSTFIELDS => json_encode(['model' => 'deepseek-chat', 'messages' => [['role' => 'user', 'content' => 'Hi']], 'max_tokens' => 5])
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 200) { $result['status'] = 'ok'; $result['message'] = 'DeepSeek OK'; }
else { $result['message'] = "HTTP $httpCode"; }
break;
case 'claude':
$apiKey = $config['claude_api_key'] ?? '';
if (empty($apiKey)) { $result['message'] = 'API Key manquante'; break; }
$ch = curl_init('https://api.anthropic.com/v1/messages');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'x-api-key: ' . $apiKey, 'anthropic-version: 2023-06-01'],
CURLOPT_POSTFIELDS => json_encode(['model' => 'claude-3-haiku-20240307', 'messages' => [['role' => 'user', 'content' => 'Hi']], 'max_tokens' => 5])
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 200) { $result['status'] = 'ok'; $result['message'] = 'Claude OK'; }
else { $result['message'] = "HTTP $httpCode"; }
break;
case 'gemini':
$apiKey = $config['gemini_api_key'] ?? '';
if (empty($apiKey)) { $result['message'] = 'API Key manquante'; break; }
$ch = curl_init('https://generativelanguage.googleapis.com/v1/models/gemini-pro:generateContent?key=' . $apiKey);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(['contents' => [['parts' => [['text' => 'Hi']]]]])
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 200) { $result['status'] = 'ok'; $result['message'] = 'Gemini OK'; }
else { $result['message'] = "HTTP $httpCode"; }
break;
case 'mistral':
$apiKey = $config['mistral_api_key'] ?? '';
if (empty($apiKey)) { $result['message'] = 'API Key manquante'; break; }
$ch = curl_init('https://api.mistral.ai/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $apiKey],
CURLOPT_POSTFIELDS => json_encode(['model' => 'mistral-tiny', 'messages' => [['role' => 'user', 'content' => 'Hi']], 'max_tokens' => 5])
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 200) { $result['status'] = 'ok'; $result['message'] = 'Mistral OK'; }
else { $result['message'] = "HTTP $httpCode"; }
break;
case 'ollama':
$url = $config['ollama_url'] ?? 'http://localhost:11434';
$ch = curl_init($url . '/api/tags');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 200) {
$data = json_decode($response, true);
$models = isset($data['models']) ? count($data['models']) : 0;
$result['status'] = 'ok';
$result['message'] = "Ollama OK - $models modèles";
} else { $result['message'] = "Non accessible"; }
break;
case 'vllm':
$url = $config['vllm_url'] ?? 'http://localhost:8000';
$ch = curl_init($url . '/v1/models');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 200) { $result['status'] = 'ok'; $result['message'] = 'vLLM OK'; }
else { $result['message'] = "Non accessible"; }
break;
}
} catch (Exception $e) {
$result['message'] = $e->getMessage();
}
$result['latency'] = round((microtime(true) - $start) * 1000);
echo json_encode($result);
exit;
}
// Charger config
$config = [];
$stmt = $pdo->query("SELECT config_key, config_value FROM admin.chatbot_config");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $config[$row['config_key']] = $row['config_value'];
// Defaults
$d = [
'bot_name' => 'Assistant WEVAL', 'bubble_text' => 'Salam !',
'welcome_message' => 'Bonjour! 👋 Je suis votre assistant WEVAL.',
'color1' => '#06b6d4', 'color2' => '#0891b2', 'default_provider' => 'groq',
'widget_size' => '80px', 'glow' => 'Moyen', 'opacity' => '100%', 'border' => 'Moyen',
'ollama_url' => 'http://localhost:11434', 'vllm_url' => 'http://localhost:8000',
'system_prompt' => 'Tu es Hamid Esmart, assistant IA expert WEVAL.', 'fallback_enabled' => '1',
'robot_image' => '/images/chatbot-avatar.png'
];
foreach ($d as $k => $v) if (!isset($config[$k]) || $config[$k] === '') $config[$k] = $v;
$kbCount = $pdo->query("SELECT COUNT(*) FROM admin.chatbot_knowledge")->fetchColumn() ?: 0;
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🤖 Chatbot Admin</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', sans-serif; background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); min-height: 100vh; color: #e2e8f0; }
.header { background: rgba(15,23,42,0.9); padding: 12px 25px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(6,182,212,0.3); position: sticky; top: 0; z-index: 100; }
.header h1 { font-size: 18px; display: flex; align-items: center; gap: 10px; color: #06b6d4; }
.header-right { display: flex; gap: 10px; }
.header-right a { padding: 6px 14px; background: rgba(6,182,212,0.1); color: #06b6d4; text-decoration: none; border-radius: 6px; font-size: 12px; }
.header-right a:hover { background: rgba(6,182,212,0.2); }
.main-container { display: grid; grid-template-columns: 300px 1fr 280px; gap: 20px; padding: 20px; max-width: 1500px; margin: 0 auto; }
@media (max-width: 1200px) { .main-container { grid-template-columns: 1fr; } }
.panel { background: rgba(30,41,59,0.8); border-radius: 14px; padding: 18px; border: 1px solid rgba(255,255,255,0.08); }
.panel h2 { font-size: 14px; margin-bottom: 15px; display: flex; align-items: center; gap: 8px; color: #06b6d4; }
/* Robot */
.robot-preview { text-align: center; margin-bottom: 15px; }
.robot-preview img { width: 100px; height: 100px; border-radius: 16px; background: linear-gradient(135deg, #06b6d4, #0891b2); object-fit: cover; }
.upload-area { position: relative; }
.upload-btn { width: 100%; padding: 10px; background: rgba(6,182,212,0.1); border: 2px dashed rgba(6,182,212,0.4); border-radius: 8px; color: #06b6d4; cursor: pointer; font-size: 12px; }
.upload-btn:hover { background: rgba(6,182,212,0.2); }
.upload-input { position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
/* Form */
.form-group { margin-bottom: 12px; }
.form-group label { display: block; font-size: 11px; color: #94a3b8; margin-bottom: 5px; font-weight: 600; text-transform: uppercase; }
.form-group input, .form-group select, .form-group textarea {
width: 100%; padding: 9px 11px; background: rgba(15,23,42,0.6);
border: 1px solid rgba(255,255,255,0.1); border-radius: 7px; color: white; font-size: 13px;
}
.form-group input:focus, .form-group select:focus { outline: none; border-color: #06b6d4; }
.form-group textarea { resize: vertical; min-height: 50px; }
.form-group input[type="color"] { height: 36px; padding: 2px; cursor: pointer; }
.row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.checkbox-label { display: flex; align-items: center; gap: 8px; font-size: 12px; cursor: pointer; padding: 8px; background: rgba(6,182,212,0.1); border-radius: 8px; }
.checkbox-label input { width: 16px; height: 16px; }
/* Providers */
.provider-cat { font-size: 10px; color: #64748b; margin: 12px 0 6px; text-transform: uppercase; letter-spacing: 1px; display: flex; align-items: center; gap: 6px; }
.provider-cat:first-child { margin-top: 0; }
.provider-item {
display: grid; grid-template-columns: 30px 1fr auto; align-items: center; gap: 8px;
padding: 8px 10px; background: rgba(15,23,42,0.4); border-radius: 8px; margin-bottom: 6px;
border: 1px solid rgba(255,255,255,0.05);
}
.provider-item:hover { background: rgba(15,23,42,0.6); }
.provider-item.recommended { background: rgba(34,197,94,0.1); border-color: rgba(34,197,94,0.3); }
.provider-icon { font-size: 16px; text-align: center; }
.provider-info { min-width: 0; }
.provider-name { font-weight: 600; font-size: 12px; display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
.provider-input { margin-top: 4px; }
.provider-input input { padding: 5px 8px; font-size: 11px; }
.badge { padding: 2px 5px; border-radius: 3px; font-size: 8px; font-weight: 700; }
.badge-local { background: #8b5cf6; color: white; }
.badge-cloud { background: #06b6d4; color: white; }
.test-btn {
padding: 5px 10px; background: rgba(34,197,94,0.15); border: 1px solid #22c55e;
color: #22c55e; border-radius: 5px; cursor: pointer; font-size: 10px; white-space: nowrap;
transition: all 0.2s;
}
.test-btn:hover { background: rgba(34,197,94,0.25); }
.test-btn.testing { background: rgba(251,191,36,0.2); border-color: #fbbf24; color: #fbbf24; }
.test-btn.success { background: rgba(34,197,94,0.25); }
.test-btn.error { background: rgba(239,68,68,0.15); border-color: #ef4444; color: #ef4444; }
/* Preview */
.preview-box { background: rgba(251,191,36,0.08); border: 1px solid rgba(251,191,36,0.2); padding: 6px 10px; border-radius: 6px; font-size: 10px; color: #fbbf24; margin-bottom: 12px; display: flex; align-items: center; gap: 6px; }
.widget-preview {
width: 100%; max-width: 260px; margin: 0 auto;
background: linear-gradient(135deg, #0f172a, #1e293b);
border-radius: 16px; overflow: hidden; box-shadow: 0 15px 40px rgba(0,0,0,0.4);
}
.widget-header {
background: linear-gradient(135deg, var(--c1, #06b6d4), var(--c2, #0891b2));
padding: 12px; display: flex; align-items: center; gap: 10px;
}
.widget-avatar { width: 40px; height: 40px; background: rgba(255,255,255,0.2); border-radius: 10px; display: flex; align-items: center; justify-content: center; overflow: hidden; }
.widget-avatar img { width: 100%; height: 100%; object-fit: cover; }
.widget-title { font-weight: 600; font-size: 13px; }
.widget-body { padding: 12px; min-height: 80px; }
.widget-message { background: rgba(6,182,212,0.1); padding: 10px; border-radius: 10px; font-size: 12px; line-height: 1.4; }
.widget-input { display: flex; gap: 6px; padding: 10px; border-top: 1px solid rgba(255,255,255,0.1); }
.widget-input input { flex: 1; background: rgba(255,255,255,0.05); border: none; padding: 8px 12px; border-radius: 16px; color: white; font-size: 11px; }
.widget-input button { width: 32px; height: 32px; border-radius: 50%; background: linear-gradient(135deg, var(--c1), var(--c2)); border: none; color: white; cursor: pointer; }
.bubble-float { position: relative; margin-bottom: 10px; text-align: right; }
.bubble-text { display: inline-block; background: white; color: #333; padding: 6px 12px; border-radius: 12px; font-size: 11px; box-shadow: 0 3px 10px rgba(0,0,0,0.15); }
/* Status */
.status-box { margin-top: 15px; padding: 12px; background: rgba(6,182,212,0.08); border-radius: 8px; }
.status-box h3 { font-size: 12px; margin-bottom: 8px; color: #06b6d4; }
.status-item { font-size: 10px; padding: 3px 0; display: flex; align-items: center; gap: 6px; }
.dot { width: 6px; height: 6px; border-radius: 50%; }
.dot.ok { background: #22c55e; }
.dot.err { background: #ef4444; }
.dot.unk { background: #64748b; }
/* Table */
.table-section { margin-top: 20px; }
.comp-table { width: 100%; border-collapse: collapse; font-size: 10px; }
.comp-table th { background: linear-gradient(135deg, #06b6d4, #0891b2); color: white; padding: 8px 5px; text-align: left; }
.comp-table td { padding: 6px 5px; border-bottom: 1px solid rgba(255,255,255,0.05); }
.comp-table tr:hover { background: rgba(6,182,212,0.05); }
.comp-table tr.rec { background: rgba(34,197,94,0.1); }
.stars { color: #fbbf24; }
.g { color: #22c55e; }
.r { color: #ef4444; }
/* Buttons */
.save-btn {
width: 100%; padding: 11px; background: linear-gradient(135deg, #06b6d4, #0891b2);
border: none; border-radius: 8px; color: white; font-weight: 600; font-size: 13px;
cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 15px;
}
.save-btn:hover { transform: translateY(-1px); box-shadow: 0 5px 15px rgba(6,182,212,0.3); }
.alert { padding: 10px 12px; border-radius: 8px; margin-bottom: 12px; font-size: 12px; }
.alert-success { background: rgba(34,197,94,0.15); border: 1px solid #22c55e; color: #22c55e; }
</style>
</head>
<body>
<div class="header">
<h1><i class="fas fa-robot"></i> Chatbot Admin</h1>
<div class="header-right">
<a href="/hamid-fullscreen.php"><i class="fas fa-comments"></i> Chat</a>
<a href="/hamid-admin.php"><i class="fas fa-database"></i> KB (<?= $kbCount ?>)</a>
<a href="/"><i class="fas fa-home"></i> Retour</a>
</div>
</div>
<form method="POST" enctype="multipart/form-data" id="mainForm">
<div class="main-container">
<!-- LEFT -->
<div class="panel">
<?php if ($msg): ?><div class="alert alert-success"><?= $msg ?></div><?php endif; ?>
<?php if ($uploadMsg): ?><div class="alert alert-success"><?= $uploadMsg ?></div><?php endif; ?>
<h2><i class="fas fa-robot"></i> Robot</h2>
<div class="robot-preview">
<img id="robotImg" src="<?= htmlspecialchars($config['robot_image']) ?>" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2280%22>🤖</text></svg>'">
</div>
<div class="upload-area">
<button type="button" class="upload-btn"><i class="fas fa-upload"></i> Cliquer pour changer</button>
<input type="file" name="robot_image" accept="image/*" class="upload-input" onchange="previewImage(this)">
</div>
<h2 style="margin-top:20px;"><i class="fas fa-cog"></i> Paramètres</h2>
<div class="form-group">
<label>Nom du Bot</label>
<input type="text" name="bot_name" value="<?= htmlspecialchars($config['bot_name']) ?>" id="inp_name">
</div>
<div class="form-group">
<label><i class="fas fa-comment"></i> Texte Bulle</label>
<input type="text" name="bubble_text" value="<?= htmlspecialchars($config['bubble_text']) ?>" id="inp_bubble">
</div>
<div class="form-group">
<label>Message d'Accueil</label>
<textarea name="welcome_message" id="inp_welcome"><?= htmlspecialchars($config['welcome_message']) ?></textarea>
</div>
<div class="row-2">
<div class="form-group">
<label>Couleur 1</label>
<input type="color" name="color1" value="<?= htmlspecialchars($config['color1']) ?>" id="inp_c1">
</div>
<div class="form-group">
<label>Couleur 2</label>
<input type="color" name="color2" value="<?= htmlspecialchars($config['color2']) ?>" id="inp_c2">
</div>
</div>
<div class="row-2">
<div class="form-group">
<label>Provider</label>
<select name="default_provider">
<option value="groq" <?= $config['default_provider']=='groq'?'selected':'' ?>>⚡ Groq</option>
<option value="deepseek" <?= $config['default_provider']=='deepseek'?'selected':'' ?>>🌊 DeepSeek</option>
<option value="claude" <?= $config['default_provider']=='claude'?'selected':'' ?>>🤖 Claude</option>
<option value="gemini" <?= $config['default_provider']=='gemini'?'selected':'' ?>>💎 Gemini</option>
<option value="mistral" <?= $config['default_provider']=='mistral'?'selected':'' ?>>🇫🇷 Mistral</option>
<option value="ollama" <?= $config['default_provider']=='ollama'?'selected':'' ?>>🦙 Ollama</option>
</select>
</div>
<div class="form-group">
<label>Taille</label>
<select name="widget_size">
<option value="60px" <?= $config['widget_size']=='60px'?'selected':'' ?>>60px</option>
<option value="80px" <?= $config['widget_size']=='80px'?'selected':'' ?>>80px</option>
<option value="100px" <?= $config['widget_size']=='100px'?'selected':'' ?>>100px</option>
</select>
</div>
</div>
<div class="row-2">
<div class="form-group">
<label>Glow</label>
<select name="glow">
<option value="Aucun" <?= $config['glow']=='Aucun'?'selected':'' ?>>Aucun</option>
<option value="Léger" <?= $config['glow']=='Léger'?'selected':'' ?>>Léger</option>
<option value="Moyen" <?= $config['glow']=='Moyen'?'selected':'' ?>>Moyen</option>
<option value="Fort" <?= $config['glow']=='Fort'?'selected':'' ?>>Fort</option>
</select>
</div>
<div class="form-group">
<label>Opacité</label>
<select name="opacity">
<option value="100%" <?= $config['opacity']=='100%'?'selected':'' ?>>100%</option>
<option value="90%" <?= $config['opacity']=='90%'?'selected':'' ?>>90%</option>
<option value="80%" <?= $config['opacity']=='80%'?'selected':'' ?>>80%</option>
</select>
</div>
</div>
<div class="form-group">
<label>Bordure</label>
<select name="border">
<option value="Aucune" <?= $config['border']=='Aucune'?'selected':'' ?>>Aucune</option>
<option value="Fine" <?= $config['border']=='Fine'?'selected':'' ?>>Fine</option>
<option value="Moyen" <?= $config['border']=='Moyen'?'selected':'' ?>>Moyenne</option>
</select>
</div>
<button type="submit" name="save_config" value="1" class="save-btn">
<i class="fas fa-save"></i> Sauvegarder
</button>
</div>
<!-- CENTER -->
<div class="panel">
<h2><i class="fas fa-bolt"></i> Providers IA</h2>
<label class="checkbox-label">
<input type="checkbox" name="fallback_enabled" value="1" <?= $config['fallback_enabled']?'checked':'' ?>>
🔄 Fallback - Réponses KB uniquement si API échoue
</label>
<div class="provider-cat"><i class="fas fa-server"></i> LOCAL</div>
<div class="provider-item">
<div class="provider-icon">🦙</div>
<div class="provider-info">
<div class="provider-name">Ollama <span class="badge badge-local">LOCAL</span></div>
<div class="provider-input"><input type="text" name="ollama_url" value="<?= htmlspecialchars($config['ollama_url']) ?>"></div>
</div>
<button type="button" class="test-btn" onclick="testAPI('ollama',this)">▶ Test</button>
</div>
<div class="provider-item">
<div class="provider-icon">⚡</div>
<div class="provider-info">
<div class="provider-name">vLLM <span class="badge badge-local">LOCAL</span></div>
<div class="provider-input"><input type="text" name="vllm_url" value="<?= htmlspecialchars($config['vllm_url']) ?>"></div>
</div>
<button type="button" class="test-btn" onclick="testAPI('vllm',this)">▶ Test</button>
</div>
<div class="provider-cat"><i class="fas fa-cloud"></i> CLOUD</div>
<div class="provider-item recommended">
<div class="provider-icon">⚡</div>
<div class="provider-info">
<div class="provider-name">Groq <span class="badge badge-cloud">CLOUD</span> <span style="color:#22c55e;">⭐ Recommandé</span></div>
<div class="provider-input"><input type="text" name="groq_api_key" value="<?= htmlspecialchars($config['groq_api_key'] ?? '') ?>" placeholder="gsk_..."></div>
</div>
<button type="button" class="test-btn" onclick="testAPI('groq',this)">▶ Test</button>
</div>
<div class="provider-item">
<div class="provider-icon">🌊</div>
<div class="provider-info">
<div class="provider-name">DeepSeek <span class="badge badge-cloud">CLOUD</span></div>
<div class="provider-input"><input type="text" name="deepseek_api_key" value="<?= htmlspecialchars($config['deepseek_api_key'] ?? '') ?>" placeholder="sk-..."></div>
</div>
<button type="button" class="test-btn" onclick="testAPI('deepseek',this)">▶ Test</button>
</div>
<div class="provider-item">
<div class="provider-icon">🤖</div>
<div class="provider-info">
<div class="provider-name">Claude <span class="badge badge-cloud">CLOUD</span></div>
<div class="provider-input"><input type="text" name="claude_api_key" value="<?= htmlspecialchars($config['claude_api_key'] ?? '') ?>" placeholder="sk-ant-..."></div>
</div>
<button type="button" class="test-btn" onclick="testAPI('claude',this)">▶ Test</button>
</div>
<div class="provider-item">
<div class="provider-icon">💎</div>
<div class="provider-info">
<div class="provider-name">Gemini <span class="badge badge-cloud">CLOUD</span></div>
<div class="provider-input"><input type="text" name="gemini_api_key" value="<?= htmlspecialchars($config['gemini_api_key'] ?? '') ?>" placeholder="AIza..."></div>
</div>
<button type="button" class="test-btn" onclick="testAPI('gemini',this)">▶ Test</button>
</div>
<div class="provider-item">
<div class="provider-icon">🇫🇷</div>
<div class="provider-info">
<div class="provider-name">Mistral <span class="badge badge-cloud">CLOUD</span></div>
<div class="provider-input"><input type="text" name="mistral_api_key" value="<?= htmlspecialchars($config['mistral_api_key'] ?? '') ?>" placeholder="..."></div>
</div>
<button type="button" class="test-btn" onclick="testAPI('mistral',this)">▶ Test</button>
</div>
<div class="form-group" style="margin-top:15px;">
<label>System Prompt</label>
<textarea name="system_prompt" rows="2"><?= htmlspecialchars($config['system_prompt']) ?></textarea>
</div>
<!-- Table -->
<div class="table-section">
<h2><i class="fas fa-balance-scale"></i> Comparatif</h2>
<table class="comp-table">
<tr><th>Provider</th><th>Vitesse</th><th>Qualité</th><th>Coût</th><th>Notes</th></tr>
<tr class="rec"><td><b>⚡ Groq</b></td><td class="stars">★★★★★</td><td class="stars">★★★★★</td><td class="g">Gratuit</td><td><span class="g">Ultra rapide</span></td></tr>
<tr><td>🤖 Claude</td><td class="stars">★★★★☆</td><td class="stars">★★★★★</td><td class="r">$15/M</td><td>Très intelligent</td></tr>
<tr><td>🌊 DeepSeek</td><td class="stars">★★★★☆</td><td class="stars">★★★★★</td><td class="g">$0.14/M</td><td>GPT-4 level</td></tr>
<tr><td>💎 Gemini</td><td class="stars">★★★★☆</td><td class="stars">★★★★☆</td><td class="g">Gratuit+</td><td>Multimodal</td></tr>
<tr><td>🦙 Ollama</td><td class="stars">★★★☆☆</td><td class="stars">★★★★☆</td><td class="g">Local</td><td>Offline, GPU</td></tr>
</table>
</div>
</div>
<!-- RIGHT -->
<div class="panel">
<h2><i class="fas fa-eye"></i> Aperçu</h2>
<div class="preview-box"><i class="fas fa-exclamation-triangle"></i> Pas de GPU local</div>
<div class="bubble-float">
<span class="bubble-text" id="prevBubble"><?= htmlspecialchars($config['bubble_text']) ?></span>
</div>
<div class="widget-preview" id="widgetPrev" style="--c1:<?= $config['color1'] ?>;--c2:<?= $config['color2'] ?>;">
<div class="widget-header">
<div class="widget-avatar"><img id="prevAvatar" src="<?= htmlspecialchars($config['robot_image']) ?>" onerror="this.parentNode.innerHTML='🤖'"></div>
<div class="widget-title" id="prevTitle"><?= htmlspecialchars($config['bot_name']) ?></div>
</div>
<div class="widget-body">
<div class="widget-message" id="prevMsg"><?= htmlspecialchars($config['welcome_message']) ?></div>
</div>
<div class="widget-input">
<input type="text" placeholder="Posez votre question..." disabled>
<button type="button"><i class="fas fa-paper-plane"></i></button>
</div>
</div>
<div class="status-box">
<h3>Status Providers</h3>
<div id="statusList"></div>
</div>
</div>
</div>
</form>
<script>
// Preview live
document.getElementById('inp_name').addEventListener('input', e => document.getElementById('prevTitle').textContent = e.target.value);
document.getElementById('inp_welcome').addEventListener('input', e => document.getElementById('prevMsg').textContent = e.target.value);
document.getElementById('inp_bubble').addEventListener('input', e => document.getElementById('prevBubble').textContent = e.target.value);
document.getElementById('inp_c1').addEventListener('input', updateColors);
document.getElementById('inp_c2').addEventListener('input', updateColors);
function updateColors() {
document.getElementById('widgetPrev').style.setProperty('--c1', document.getElementById('inp_c1').value);
document.getElementById('widgetPrev').style.setProperty('--c2', document.getElementById('inp_c2').value);
}
function previewImage(input) {
if (input.files && input.files[0]) {
const reader = new FileReader();
reader.onload = e => {
document.getElementById('robotImg').src = e.target.result;
document.getElementById('prevAvatar').src = e.target.result;
};
reader.readAsDataURL(input.files[0]);
}
}
// Status storage
const statuses = {};
function updateStatusList() {
const list = document.getElementById('statusList');
list.innerHTML = '';
const providers = ['groq','deepseek','claude','gemini','mistral','ollama','vllm'];
providers.forEach(p => {
const s = statuses[p] || {status:'unk', message:'Non testé'};
const cls = s.status === 'ok' ? 'ok' : (s.status === 'error' ? 'err' : 'unk');
list.innerHTML += `<div class="status-item"><span class="dot ${cls}"></span><b>${p}:</b> ${s.message}</div>`;
});
}
updateStatusList();
async function testAPI(provider, btn) {
btn.textContent = '⏳...';
btn.className = 'test-btn testing';
try {
const r = await fetch('?test_provider=' + provider);
const d = await r.json();
statuses[provider] = d;
if (d.status === 'ok') {
btn.textContent = '✅ ' + d.latency + 'ms';
btn.className = 'test-btn success';
} else {
btn.textContent = '❌';
btn.className = 'test-btn error';
}
} catch(e) {
statuses[provider] = {status:'error', message: e.message};
btn.textContent = '❌';
btn.className = 'test-btn error';
}
updateStatusList();
setTimeout(() => {
btn.textContent = '▶ Test';
btn.className = 'test-btn';
}, 2500);
}
// Test all on load
window.addEventListener('load', () => {
setTimeout(() => testAPI('groq', document.querySelector('[onclick*="groq"]')), 500);
setTimeout(() => testAPI('ollama', document.querySelector('[onclick*="ollama"]')), 1000);
});
</script>
</body>
</html>