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:
@@ -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'; ?>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'] ?? []; ?>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user