feat & fix : intégration IA éditeur + onglet admin IA + corrections CSP (v1.6.24-25)

- #96 : boutons IA sidebar éditeur (analyse critique / réécriture) via Anthropic API
- #97 : onglet admin /admin/ia — provider anthropic/claude_code, modèle, procédure CLI
- #95 : extraction scripts inline vers fichiers JS (comments.js, post_confirm.js, admin.js)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 12:18:38 +02:00
parent fabe5a9f53
commit 298f18dabe
16 changed files with 527 additions and 32 deletions
+125
View File
@@ -73,6 +73,10 @@ function adminStatusBadge(array $a, int $now): string
<a class="nav-link <?= $tab === 'flux' ? 'active' : '' ?>"
href="/admin/flux">Flux</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab === 'ia' ? 'active' : '' ?>"
href="/admin/ia">IA</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab === 'stats' ? 'active' : '' ?>"
href="/admin/stats">Statistiques</a>
@@ -1486,6 +1490,127 @@ foreach (COLOR_PALETTE_16 as $_i => $_rgb):
<?php endif; ?>
<?php if ($tab === 'ia' && isAdmin()): ?>
<?php
$_aiNotice = $adminData['ai_notice'] ?? '';
$_aiProvider = $adminData['ai_provider'] ?? 'anthropic';
$_aiModel = $adminData['ai_model'] ?? '';
$_anthropicOk = $adminData['anthropic_key_set'] ?? false;
$_cliOk = $adminData['claude_cli_found'] ?? false;
?>
<?php if ($_aiNotice === 'saved'): ?>
<div class="alert alert-success py-2 small">Configuration IA enregistrée.</div>
<?php elseif ($_aiNotice === 'error'): ?>
<div class="alert alert-danger py-2 small">Erreur lors de l'enregistrement.</div>
<?php endif; ?>
<h5 class="mb-3">Intelligence artificielle</h5>
<!-- Section 1 — Statut -->
<div class="card mb-4">
<div class="card-header fw-semibold small">Statut</div>
<div class="card-body p-0">
<table class="table table-sm mb-0">
<tbody>
<tr>
<th class="ps-3" scope="row">Clé Anthropic (<code>ANTHROPIC_API_KEY</code>)</th>
<td><?= $_anthropicOk ? '<span class="text-success">✓ Configurée</span>' : '<span class="text-danger">✗ Absente</span>' ?></td>
</tr>
<tr>
<th class="ps-3" scope="row">Claude Code CLI (<code>/usr/local/bin/claude</code>)</th>
<td><?= $_cliOk ? '<span class="text-success">✓ Trouvé</span>' : '<span class="text-danger">✗ Introuvable</span>' ?></td>
</tr>
<tr>
<th class="ps-3" scope="row">Provider actif</th>
<td><code><?= htmlspecialchars($_aiProvider) ?></code></td>
</tr>
<tr>
<th class="ps-3" scope="row">Modèle actif</th>
<td><code><?= htmlspecialchars($_aiModel ?: 'claude-haiku-4-5-20251001 (défaut)') ?></code></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Section 2 — Configuration -->
<div class="card mb-4">
<div class="card-header fw-semibold small">Configuration</div>
<div class="card-body">
<form method="POST" action="/?action=admin_save_ai_config">
<div class="mb-3">
<label class="form-label fw-semibold small">Provider</label>
<div class="d-flex gap-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="ai_provider" id="ai_provider_anthropic"
value="anthropic" <?= $_aiProvider === 'anthropic' ? 'checked' : '' ?>>
<label class="form-check-label" for="ai_provider_anthropic">Anthropic (API)</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="ai_provider" id="ai_provider_claude_code"
value="claude_code" <?= $_aiProvider === 'claude_code' ? 'checked' : '' ?>>
<label class="form-check-label" for="ai_provider_claude_code">Claude Code CLI</label>
</div>
</div>
</div>
<div class="mb-3">
<label for="ai_model" class="form-label fw-semibold small">Modèle Anthropic</label>
<input type="text" class="form-control form-control-sm font-monospace" id="ai_model" name="ai_model"
value="<?= htmlspecialchars($_aiModel) ?>"
placeholder="claude-haiku-4-5-20251001">
<div class="form-text">Laisser vide pour utiliser le défaut (<code>claude-haiku-4-5-20251001</code>). Ignoré si le provider est Claude Code CLI.</div>
</div>
<button type="submit" class="btn btn-primary btn-sm">Enregistrer</button>
</form>
</div>
</div>
<!-- Section 3 — Clé Anthropic -->
<div class="card mb-4">
<div class="card-header fw-semibold small">Clé API Anthropic</div>
<div class="card-body">
<div class="alert alert-warning py-2 small mb-0">
<strong>La clé API Anthropic ne peut pas être saisie ici.</strong><br>
Elle doit être définie dans le fichier <code>.env</code> du serveur :
<pre class="mt-2 mb-0 small"><code>ANTHROPIC_API_KEY=sk-ant-...</code></pre>
<div class="mt-2">Statut actuel : <?= $_anthropicOk ? '<span class="text-success">✓ Configurée</span>' : '<span class="text-danger">✗ Absente</span>' ?></div>
</div>
</div>
</div>
<!-- Section 4 — Procédure Claude Code CLI -->
<div class="card mb-4">
<div class="card-header fw-semibold small">Procédure d'installation de Claude Code CLI</div>
<div class="card-body">
<?php if ($_cliOk): ?>
<div class="alert alert-success py-2 small mb-3">✓ <code>/usr/local/bin/claude</code> détecté.</div>
<?php else: ?>
<div class="alert alert-secondary py-2 small mb-3">✗ <code>/usr/local/bin/claude</code> introuvable — suivez les étapes ci-dessous.</div>
<?php endif; ?>
<p class="small text-muted">À exécuter en SSH sur le serveur (en root ou via sudo) :</p>
<pre class="bg-dark text-light p-3 rounded small"><code># 1. Installer Claude Code CLI (en root)
sudo npm install -g @anthropic-ai/claude-code
# Vérifier l'installation
/usr/local/bin/claude --version
# 2. Créer le répertoire HOME de www-data pour Claude
sudo mkdir -p /var/lib/claude-www
sudo chown www-data:www-data /var/lib/claude-www
# 3. Authentifier Claude en tant que www-data
sudo -u www-data HOME=/var/lib/claude-www /usr/local/bin/claude auth login
# → Suivre les instructions (OAuth navigateur ou clé API)
# 4. Vérifier que ça fonctionne
sudo -u www-data HOME=/var/lib/claude-www /usr/local/bin/claude --print "Réponds juste OK"</code></pre>
</div>
</div>
<?php endif; ?>
<?php if ($tab === 'stats' && isAdmin()): ?>
<?php include __DIR__ . '/admin_stats.php'; ?>
+1 -26
View File
@@ -142,29 +142,4 @@ setcookie('_csrf_c', $_csrfToken, [
</div>
</div>
<script>
(function () {
var maxAge = 365 * 24 * 3600;
function getCookie(name) {
var m = document.cookie.match('(?:^|; )' + name + '=([^;]*)');
return m ? decodeURIComponent(m[1]) : '';
}
function setCookie(name, value) {
document.cookie = name + '=' + encodeURIComponent(value) + ';max-age=' + maxAge + ';path=/;SameSite=Lax';
}
var nameEl = document.getElementById('comment-name');
var emailEl = document.getElementById('comment-email');
if (!nameEl || !emailEl) { return; }
var savedName = getCookie('cmt_name');
var savedEmail = getCookie('cmt_email');
if (savedName) { nameEl.value = savedName; }
if (savedEmail) { emailEl.value = savedEmail; }
var form = document.getElementById('comment-form');
if (form) {
form.addEventListener('submit', function () {
if (nameEl.value.trim()) { setCookie('cmt_name', nameEl.value.trim()); }
if (emailEl.value.trim()) { setCookie('cmt_email', emailEl.value.trim()); }
});
}
}());
</script>
<script src="/assets/js/comments.js" defer></script>
+3
View File
@@ -166,6 +166,9 @@ $_layoutCurrentCat = trim($_GET['cat'] ?? '');
<?php if (!empty($shareBar ?? false)): ?>
<script src="<?= _av($_pub, 'js/share.js') ?>" defer></script>
<?php endif; ?>
<?php if (!empty($aiEditor ?? false)): ?>
<script src="<?= _av($_pub, 'js/ai-editor.js') ?>"></script>
<?php endif; ?>
</body>
</html>
+1 -2
View File
@@ -115,8 +115,7 @@ $slugOriginal = $postSlug;
</label>
<input type="text" class="form-control form-control-sm font-monospace" id="confirm-slug" name="slug"
value="<?= htmlspecialchars($slugDefault) ?>"
pattern="[a-z0-9][a-z0-9\-]*"
oninput="document.getElementById('slug-display').textContent=this.value">
pattern="[a-z0-9][a-z0-9\-]*">
<?php if ($titleChanged && $autoSlug !== $slugOriginal): ?>
<div class="mt-2 d-flex align-items-center gap-2 flex-wrap">
<small class="text-muted">Slug recalculé depuis le nouveau titre. Slug initial :</small>
+33
View File
@@ -9,6 +9,7 @@ $dateValue = isset($published_at)
?>
<?php if ($action === 'edit'): ?>
<?php $aiEditor = true; ?>
<div id="vl-page"
data-uuid="<?= htmlspecialchars($uuid) ?>"
data-insert-url="<?= htmlspecialchars($insertUrl ?? '') ?>"
@@ -221,6 +222,38 @@ $dateValue = isset($published_at)
<hr class="my-3">
<div class="mb-3">
<p class="fw-semibold small mb-2">IA</p>
<div class="d-flex flex-column gap-2">
<button type="button" id="btn-ai-critique"
class="btn btn-outline-secondary btn-sm">
Analyse critique
</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-3" style="display:none">
<div class="d-flex align-items-center justify-content-between mb-1">
<span id="ai-result-label" class="fw-semibold small"></span>
<button type="button" id="btn-ai-close" class="btn-close btn-sm"
aria-label="Fermer"></button>
</div>
<div id="ai-result-content"
class="border rounded p-2 small"
style="max-height:400px;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"
style="display:none">
Appliquer dans l'éditeur
</button>
</div>
</div>
<hr class="my-3">
<?php if (!empty($existingFiles)): ?>
<?php $coverFile = $article['cover'] ?? ''; ?>
<?php $filesMeta = $article['files_meta'] ?? []; ?>
+3 -1
View File
@@ -26,7 +26,9 @@ $_formAction = '/edit/' . rawurlencode($uuid) . '/6';
<div class="d-flex gap-2 flex-wrap align-items-center">
<a href="<?= htmlspecialchars($_backUrl) ?>" class="btn btn-outline-secondary btn-sm">← Retour</a>
<button type="button" class="btn btn-outline-danger btn-sm"
onclick="if(confirm('Abandonner les modifications et supprimer ce brouillon ?')) window.location='/edit/<?= rawurlencode($uuid) ?>/discard'">
data-confirm-discard
data-discard-url="/edit/<?= rawurlencode($uuid) ?>/discard"
data-confirm-msg="Abandonner les modifications et supprimer ce brouillon ?">
Abandonner
</button>
<button type="submit" class="btn btn-success">✓ Confirmer et enregistrer</button>