diff --git a/public/assets/js/ai-editor.js b/public/assets/js/ai-editor.js index 5d0e3b8..4612e5b 100644 --- a/public/assets/js/ai-editor.js +++ b/public/assets/js/ai-editor.js @@ -1,36 +1,24 @@ -// ai-editor.js — boutons IA dans la sidebar éditeur +// ai-editor.js — bouton IA dans la sidebar éditeur document.addEventListener('DOMContentLoaded', function () { - var btnCritique = document.getElementById('btn-ai-critique'); - var btnRewrite = document.getElementById('btn-ai-rewrite'); - if (!btnCritique || !btnRewrite) return; + var btnAnalyze = document.getElementById('btn-ai-analyze'); + if (!btnAnalyze) return; - var panel = document.getElementById('ai-result-panel'); - var labelEl = document.getElementById('ai-result-label'); - var resultEl = document.getElementById('ai-result-content'); - var btnApply = document.getElementById('btn-ai-apply'); - var btnClose = document.getElementById('btn-ai-close'); - var ta = document.getElementById('wz-content') || document.getElementById('content'); - var titleEl = document.getElementById('title'); + var panel = document.getElementById('ai-result-panel'); + var critiqueEl = document.getElementById('ai-critique-content'); + var rewriteEl = document.getElementById('ai-rewrite-content'); + var btnApply = document.getElementById('btn-ai-apply'); + var btnClose = document.getElementById('btn-ai-close'); + var ta = document.getElementById('wz-content') || document.getElementById('content'); + var titleEl = document.getElementById('title'); var lastRewrite = ''; - function setLoading(btn, loading) { - btn.disabled = loading; - if (loading) { - btn._origText = btn.textContent; - btn.textContent = 'En cours…'; - } else { - btn.textContent = btn._origText || btn.textContent; - } - } - - async function callAi(action) { - var btn = (action === 'critique') ? btnCritique : btnRewrite; - - setLoading(btn, true); - panel.style.display = 'none'; - btnApply.style.display = 'none'; - lastRewrite = ''; + btnAnalyze.addEventListener('click', async function () { + btnAnalyze.disabled = true; + btnAnalyze._origText = btnAnalyze.textContent; + btnAnalyze.textContent = 'En cours…'; + panel.style.display = 'none'; + lastRewrite = ''; try { var titleVal = titleEl ? titleEl.value : ''; @@ -39,57 +27,52 @@ document.addEventListener('DOMContentLoaded', function () { if (m) { titleVal = m[1].trim(); } } - var res = await fetch('/?action=ai_query', { + var res = await fetch('/?action=ai_query', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ - action: action, + action: 'analyze', title: titleVal, content: ta ? ta.value : '', }), }); - var data = await res.json(); if (!data.ok) { - labelEl.textContent = 'Erreur'; - resultEl.textContent = data.error || 'Erreur inconnue.'; + critiqueEl.textContent = data.error || 'Erreur inconnue.'; + rewriteEl.textContent = ''; + btnApply.style.display = 'none'; } else { - labelEl.textContent = (action === 'critique') ? 'Analyse critique' : 'Réécriture'; - resultEl.textContent = data.text; - if (action === 'rewrite') { - lastRewrite = data.text; - btnApply.style.display = ''; - } + critiqueEl.textContent = data.critique || ''; + rewriteEl.textContent = data.rewrite || ''; + lastRewrite = data.rewrite || ''; + btnApply.style.display = lastRewrite ? '' : 'none'; } panel.style.display = ''; } catch (e) { - labelEl.textContent = 'Erreur'; - resultEl.textContent = 'Erreur de connexion.'; - panel.style.display = ''; + critiqueEl.textContent = 'Erreur de connexion.'; + rewriteEl.textContent = ''; + btnApply.style.display = 'none'; + panel.style.display = ''; } finally { - setLoading(btn, false); + btnAnalyze.disabled = false; + btnAnalyze.textContent = btnAnalyze._origText; } - } - - btnCritique.addEventListener('click', function () { callAi('critique'); }); - btnRewrite.addEventListener('click', function () { callAi('rewrite'); }); + }); btnApply.addEventListener('click', function () { if (!lastRewrite) return; - if (!confirm("Remplacer le contenu de l’éditeur par la réécriture IA ?")) return; + if (!confirm("Remplacer le contenu de l'éditeur par la proposition IA ?")) return; if (ta) { ta.value = lastRewrite; ta.dispatchEvent(new Event('input')); } - panel.style.display = 'none'; - btnApply.style.display = 'none'; - lastRewrite = ''; + panel.style.display = 'none'; + lastRewrite = ''; }); btnClose.addEventListener('click', function () { - panel.style.display = 'none'; - btnApply.style.display = 'none'; - lastRewrite = ''; + panel.style.display = 'none'; + lastRewrite = ''; }); }); diff --git a/public/index.php b/public/index.php index 1ab135b..51bed50 100644 --- a/public/index.php +++ b/public/index.php @@ -1698,7 +1698,7 @@ switch ($action) { $_aiAction = trim($_POST['action'] ?? ''); $_aiTitle = trim($_POST['title'] ?? ''); $_aiContent = str_replace("\r\n", "\n", trim($_POST['content'] ?? '')); - if (!in_array($_aiAction, ['critique', 'rewrite'], true) || $_aiContent === '') { + if (!in_array($_aiAction, ['critique', 'rewrite', 'analyze'], true) || $_aiContent === '') { echo json_encode(['ok' => false, 'error' => 'Paramètres invalides']); exit; } diff --git a/src/Service/AiService.php b/src/Service/AiService.php index ca7d39b..e41db0a 100644 --- a/src/Service/AiService.php +++ b/src/Service/AiService.php @@ -10,6 +10,21 @@ PROMPT; private const SYSTEM_REWRITE = <<<'PROMPT' Tu es un rédacteur expert. Réécris l'article ci-dessous en améliorant le style, la clarté et la structure, sans modifier le sens ni les faits. Conserve le format markdown, les liens et les références aux images. Réponds uniquement avec l'article réécrit, sans commentaire ni explication. +PROMPT; + + private const SYSTEM_ANALYZE = <<<'PROMPT' +Tu es un relecteur et rédacteur expert de blogs. Pour l'article ci-dessous, fais deux choses : + +1. Identifie ses faiblesses (arguments faibles, imprécisions, manques de clarté, structure à revoir, points à développer). Sois bref et précis — quelques lignes suffisent. + +2. Propose une version améliorée de l'article : meilleur style, clarté, structure. Conserve le sens, les faits, le format markdown, les liens et les références aux images. + +Réponds EXACTEMENT dans ce format (les deux séparateurs doivent être présents tels quels) : + +===CRITIQUE=== +[ton analyse ici] +===REWRITE=== +[l'article réécrit ici] PROMPT; private string $apiKey; @@ -40,6 +55,16 @@ PROMPT; return ['ok' => false, 'error' => "Contenu de l'article vide"]; } + $userMsg = $title !== '' ? "# {$title}\n\n{$content}" : $content; + + if ($action === 'analyze') { + $raw = $this->provider === 'claude_code' + ? $this->queryClaudeCode(self::SYSTEM_ANALYZE, $userMsg) + : $this->queryAnthropicRaw(self::SYSTEM_ANALYZE, $userMsg, 4096); + if (!$raw['ok']) return $raw; + return $this->parseAnalyzeResponse($raw['text'] ?? ''); + } + $systemPrompt = match ($action) { 'critique' => self::SYSTEM_CRITIQUE, 'rewrite' => self::SYSTEM_REWRITE, @@ -50,8 +75,6 @@ PROMPT; return ['ok' => false, 'error' => 'Action inconnue']; } - $userMsg = $title !== '' ? "# {$title}\n\n{$content}" : $content; - if ($this->provider === 'claude_code') { return $this->queryClaudeCode($systemPrompt, $userMsg); } @@ -59,6 +82,63 @@ PROMPT; return $this->queryAnthropic($action, $systemPrompt, $userMsg); } + /** @return array{ok: bool, critique?: string, rewrite?: string, error?: string} */ + private function parseAnalyzeResponse(string $text): array + { + $parts = preg_split('/===CRITIQUE===|===REWRITE===/', $text); + if (count($parts) < 3) { + // Fallback : pas de séparateurs trouvés, on met tout en critique + return ['ok' => true, 'critique' => trim($text), 'rewrite' => '']; + } + return [ + 'ok' => true, + 'critique' => trim($parts[1]), + 'rewrite' => trim($parts[2]), + ]; + } + + /** @return array{ok: bool, text?: string, error?: string} */ + private function queryAnthropicRaw(string $systemPrompt, string $userMsg, int $maxTokens): array + { + if ($this->apiKey === '') { + return ['ok' => false, 'error' => 'Clé Anthropic non configurée (ANTHROPIC_API_KEY manquante dans .env)']; + } + + $payload = json_encode([ + 'model' => $this->model, + 'max_tokens' => $maxTokens, + 'system' => $systemPrompt, + 'messages' => [['role' => 'user', 'content' => $userMsg]], + ]); + + $ch = curl_init('https://api.anthropic.com/v1/messages'); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_TIMEOUT => 90, + CURLOPT_HTTPHEADER => [ + 'x-api-key: ' . $this->apiKey, + 'anthropic-version: 2023-06-01', + 'Content-Type: application/json', + ], + ]); + + $resp = curl_exec($ch); + $http = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $err = curl_error($ch); + curl_close($ch); + + if ($err !== '') return ['ok' => false, 'error' => 'Erreur réseau : ' . $err]; + + $data = json_decode((string) $resp, true); + if ($http !== 200) { + return ['ok' => false, 'error' => $data['error']['message'] ?? ('Anthropic HTTP ' . $http)]; + } + + return ['ok' => true, 'text' => $data['content'][0]['text'] ?? '']; + } + /** @return array{ok: bool, text?: string, error?: string} */ private function queryAnthropic(string $action, string $systemPrompt, string $userMsg): array { diff --git a/templates/wizard/step1.php b/templates/wizard/step1.php index 0b77541..1a29ce5 100644 --- a/templates/wizard/step1.php +++ b/templates/wizard/step1.php @@ -63,25 +63,25 @@ $_hasUuid = $_wizUuid !== '';