refactor : IA éditeur — un seul bouton analyse+réécriture combinées
Un seul appel API retourne l'analyse critique ET la proposition d'article via le séparateur ===CRITIQUE===/===REWRITE===. Le panneau affiche les deux sections avec un bouton « Appliquer la proposition ». Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
var btnCritique = document.getElementById('btn-ai-critique');
|
var btnAnalyze = document.getElementById('btn-ai-analyze');
|
||||||
var btnRewrite = document.getElementById('btn-ai-rewrite');
|
if (!btnAnalyze) return;
|
||||||
if (!btnCritique || !btnRewrite) return;
|
|
||||||
|
|
||||||
var panel = document.getElementById('ai-result-panel');
|
var panel = document.getElementById('ai-result-panel');
|
||||||
var labelEl = document.getElementById('ai-result-label');
|
var critiqueEl = document.getElementById('ai-critique-content');
|
||||||
var resultEl = document.getElementById('ai-result-content');
|
var rewriteEl = document.getElementById('ai-rewrite-content');
|
||||||
var btnApply = document.getElementById('btn-ai-apply');
|
var btnApply = document.getElementById('btn-ai-apply');
|
||||||
var btnClose = document.getElementById('btn-ai-close');
|
var btnClose = document.getElementById('btn-ai-close');
|
||||||
var ta = document.getElementById('wz-content') || document.getElementById('content');
|
var ta = document.getElementById('wz-content') || document.getElementById('content');
|
||||||
var titleEl = document.getElementById('title');
|
var titleEl = document.getElementById('title');
|
||||||
|
|
||||||
var lastRewrite = '';
|
var lastRewrite = '';
|
||||||
|
|
||||||
function setLoading(btn, loading) {
|
btnAnalyze.addEventListener('click', async function () {
|
||||||
btn.disabled = loading;
|
btnAnalyze.disabled = true;
|
||||||
if (loading) {
|
btnAnalyze._origText = btnAnalyze.textContent;
|
||||||
btn._origText = btn.textContent;
|
btnAnalyze.textContent = 'En cours…';
|
||||||
btn.textContent = 'En cours…';
|
panel.style.display = 'none';
|
||||||
} else {
|
lastRewrite = '';
|
||||||
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 = '';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var titleVal = titleEl ? titleEl.value : '';
|
var titleVal = titleEl ? titleEl.value : '';
|
||||||
@@ -39,57 +27,52 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
if (m) { titleVal = m[1].trim(); }
|
if (m) { titleVal = m[1].trim(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
var res = await fetch('/?action=ai_query', {
|
var res = await fetch('/?action=ai_query', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
action: action,
|
action: 'analyze',
|
||||||
title: titleVal,
|
title: titleVal,
|
||||||
content: ta ? ta.value : '',
|
content: ta ? ta.value : '',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
var data = await res.json();
|
var data = await res.json();
|
||||||
|
|
||||||
if (!data.ok) {
|
if (!data.ok) {
|
||||||
labelEl.textContent = 'Erreur';
|
critiqueEl.textContent = data.error || 'Erreur inconnue.';
|
||||||
resultEl.textContent = data.error || 'Erreur inconnue.';
|
rewriteEl.textContent = '';
|
||||||
|
btnApply.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
labelEl.textContent = (action === 'critique') ? 'Analyse critique' : 'Réécriture';
|
critiqueEl.textContent = data.critique || '';
|
||||||
resultEl.textContent = data.text;
|
rewriteEl.textContent = data.rewrite || '';
|
||||||
if (action === 'rewrite') {
|
lastRewrite = data.rewrite || '';
|
||||||
lastRewrite = data.text;
|
btnApply.style.display = lastRewrite ? '' : 'none';
|
||||||
btnApply.style.display = '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
panel.style.display = '';
|
panel.style.display = '';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
labelEl.textContent = 'Erreur';
|
critiqueEl.textContent = 'Erreur de connexion.';
|
||||||
resultEl.textContent = 'Erreur de connexion.';
|
rewriteEl.textContent = '';
|
||||||
panel.style.display = '';
|
btnApply.style.display = 'none';
|
||||||
|
panel.style.display = '';
|
||||||
} finally {
|
} 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 () {
|
btnApply.addEventListener('click', function () {
|
||||||
if (!lastRewrite) return;
|
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) {
|
if (ta) {
|
||||||
ta.value = lastRewrite;
|
ta.value = lastRewrite;
|
||||||
ta.dispatchEvent(new Event('input'));
|
ta.dispatchEvent(new Event('input'));
|
||||||
}
|
}
|
||||||
panel.style.display = 'none';
|
panel.style.display = 'none';
|
||||||
btnApply.style.display = 'none';
|
lastRewrite = '';
|
||||||
lastRewrite = '';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
btnClose.addEventListener('click', function () {
|
btnClose.addEventListener('click', function () {
|
||||||
panel.style.display = 'none';
|
panel.style.display = 'none';
|
||||||
btnApply.style.display = 'none';
|
lastRewrite = '';
|
||||||
lastRewrite = '';
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -1698,7 +1698,7 @@ switch ($action) {
|
|||||||
$_aiAction = trim($_POST['action'] ?? '');
|
$_aiAction = trim($_POST['action'] ?? '');
|
||||||
$_aiTitle = trim($_POST['title'] ?? '');
|
$_aiTitle = trim($_POST['title'] ?? '');
|
||||||
$_aiContent = str_replace("\r\n", "\n", trim($_POST['content'] ?? ''));
|
$_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']);
|
echo json_encode(['ok' => false, 'error' => 'Paramètres invalides']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,21 @@ PROMPT;
|
|||||||
|
|
||||||
private const SYSTEM_REWRITE = <<<'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.
|
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;
|
PROMPT;
|
||||||
|
|
||||||
private string $apiKey;
|
private string $apiKey;
|
||||||
@@ -40,6 +55,16 @@ PROMPT;
|
|||||||
return ['ok' => false, 'error' => "Contenu de l'article vide"];
|
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) {
|
$systemPrompt = match ($action) {
|
||||||
'critique' => self::SYSTEM_CRITIQUE,
|
'critique' => self::SYSTEM_CRITIQUE,
|
||||||
'rewrite' => self::SYSTEM_REWRITE,
|
'rewrite' => self::SYSTEM_REWRITE,
|
||||||
@@ -50,8 +75,6 @@ PROMPT;
|
|||||||
return ['ok' => false, 'error' => 'Action inconnue'];
|
return ['ok' => false, 'error' => 'Action inconnue'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$userMsg = $title !== '' ? "# {$title}\n\n{$content}" : $content;
|
|
||||||
|
|
||||||
if ($this->provider === 'claude_code') {
|
if ($this->provider === 'claude_code') {
|
||||||
return $this->queryClaudeCode($systemPrompt, $userMsg);
|
return $this->queryClaudeCode($systemPrompt, $userMsg);
|
||||||
}
|
}
|
||||||
@@ -59,6 +82,63 @@ PROMPT;
|
|||||||
return $this->queryAnthropic($action, $systemPrompt, $userMsg);
|
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} */
|
/** @return array{ok: bool, text?: string, error?: string} */
|
||||||
private function queryAnthropic(string $action, string $systemPrompt, string $userMsg): array
|
private function queryAnthropic(string $action, string $systemPrompt, string $userMsg): array
|
||||||
{
|
{
|
||||||
|
|||||||
+14
-14
@@ -63,25 +63,25 @@ $_hasUuid = $_wizUuid !== '';
|
|||||||
<div class="card border-secondary-subtle">
|
<div class="card border-secondary-subtle">
|
||||||
<div class="card-header bg-transparent py-2 small fw-semibold text-muted">IA</div>
|
<div class="card-header bg-transparent py-2 small fw-semibold text-muted">IA</div>
|
||||||
<div class="card-body p-2">
|
<div class="card-body p-2">
|
||||||
<div class="d-flex flex-column gap-2">
|
<button type="button" id="btn-ai-analyze" class="btn btn-outline-secondary btn-sm w-100">
|
||||||
<button type="button" id="btn-ai-critique" class="btn btn-outline-secondary btn-sm">
|
Analyser et proposer
|
||||||
Analyse critique
|
</button>
|
||||||
</button>
|
|
||||||
<button type="button" id="btn-ai-rewrite" class="btn btn-outline-secondary btn-sm">
|
|
||||||
Réécrire l'article
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="ai-result-panel" class="mt-2" style="display:none">
|
<div id="ai-result-panel" class="mt-2" style="display:none">
|
||||||
<div class="d-flex align-items-center justify-content-between mb-1">
|
<div class="d-flex align-items-center justify-content-between mb-1">
|
||||||
<span id="ai-result-label" class="fw-semibold small"></span>
|
<span class="fw-semibold small">Ce qui n'allait pas</span>
|
||||||
<button type="button" id="btn-ai-close" class="btn-close btn-sm" aria-label="Fermer"></button>
|
<button type="button" id="btn-ai-close" class="btn-close btn-sm" aria-label="Fermer"></button>
|
||||||
</div>
|
</div>
|
||||||
<div id="ai-result-content"
|
<div id="ai-critique-content"
|
||||||
class="border rounded p-2 small"
|
class="border rounded p-2 small mb-2"
|
||||||
style="max-height:340px;overflow-y:auto;white-space:pre-wrap;font-family:inherit;background:#f8f9fa">
|
style="max-height:220px;overflow-y:auto;white-space:pre-wrap;font-family:inherit;background:#f8f9fa">
|
||||||
</div>
|
</div>
|
||||||
<button type="button" id="btn-ai-apply" class="btn btn-warning btn-sm mt-2" style="display:none">
|
<div class="fw-semibold small mb-1">Proposition</div>
|
||||||
Appliquer dans l'éditeur
|
<div id="ai-rewrite-content"
|
||||||
|
class="border rounded p-2 small"
|
||||||
|
style="max-height:220px;overflow-y:auto;white-space:pre-wrap;font-family:inherit;background:#f8f9fa">
|
||||||
|
</div>
|
||||||
|
<button type="button" id="btn-ai-apply" class="btn btn-warning btn-sm mt-2 w-100" style="display:none">
|
||||||
|
Appliquer la proposition
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user