Готово решение за AI чат асистент: плаващ бутон и прозорец, PHP endpoint /api/ai_ask, ключ в .env и логове в MySQL с админ „AI Logs“.
/api/ai_ask, .env ключ, MySQL логове)
Искаш собствен AI асистент в сайта — без външни джаджи и с пълен контрол върху данните?
Ето решение за копиране: плаващ бутон/балон,
PHP бекенд (/api/ai_ask), .env ключ,
логове в MySQL и админ екран „AI Logs“. Подходящо за SEO, GDPR и мащабиране.
/api/ai_ask — приема въпрос и връща отговор.ai_logs) + админ страница „AI Logs“.Създайте/обновете таблицата ai_logs:
CREATE TABLE IF NOT EXISTS `ai_logs` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`user_id` INT NULL,
`ip` VARCHAR(45) NULL,
`path` VARCHAR(512) NULL,
`user_agent` TEXT NULL,
`question` MEDIUMTEXT NOT NULL,
`answer` MEDIUMTEXT NULL,
PRIMARY KEY (`id`),
KEY `idx_created_at` (`created_at`),
KEY `idx_user_id` (`user_id`),
KEY `idx_ip` (`ip`),
KEY `idx_path` (`path`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Къде? Дръжте .env извън web root-а, ако е възможно (напр. /home/USERNAME/.env или /var/www/site/.env).
OPENAI_API_KEY=sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Права (пример):
chmod 600 /path/to/.env
/api/ai_ask.phpСъздайте /api/ai_ask.php. Зарежда .env, валидира входа, извиква модела и записва лог.
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
require __DIR__ . '/../config/db.php';
$pdo = db();
/* .env loader */
function load_env(string $path): void {
if (!is_readable($path)) return;
foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') continue;
if (strpos($line, '=') === false) continue;
[$k, $v] = explode('=', $line, 2);
$k = trim($k);
$v = trim($v, " \\t\\n\\r\\0\\x0B\"'");
putenv("$k=$v"); $_ENV[$k] = $v; $_SERVER[$k] = $v;
}
}
$paths = [
__DIR__.'/../.env',
dirname(__DIR__,2).'/.env',
(isset($_SERVER['DOCUMENT_ROOT'])?$_SERVER['DOCUMENT_ROOT'].'/../.env':null),
(getenv('HOME')?rtrim(getenv('HOME'),'/').'/.env':null),
];
foreach (array_filter($paths) as $p) { load_env($p); }
/* ключ */
$OPENAI_API_KEY = getenv('OPENAI_API_KEY') ?: '';
if ($OPENAI_API_KEY === '') {
echo json_encode(['ok'=>true,'answer'=>'AI ключът не е конфигуриран.'], JSON_UNESCAPED_UNICODE);
exit;
}
/* fail helper + лог при грешка */
function fail($msg, PDO $pdo, $ctx=[]) {
try {
$st=$pdo->prepare("INSERT INTO ai_logs (created_at,user_id,ip,path,user_agent,question,answer)
VALUES (NOW(), :uid, :ip, :path, :ua, :q, :a)");
$st->execute([
':uid'=>$ctx['user_id']??null, ':ip'=>$_SERVER['REMOTE_ADDR']??'',
':path'=>$ctx['path']??($_SERVER['HTTP_REFERER']??''), ':ua'=>$_SERVER['HTTP_USER_AGENT']??'',
':q'=>$ctx['question']??'', ':a'=>'[ERROR] '.$msg,
]);
} catch(Throwable $e) {}
echo json_encode(['ok'=>true,'answer'=>$msg], JSON_UNESCAPED_UNICODE); exit;
}
/* вход */
$raw = file_get_contents('php://input') ?: '';
$data = json_decode($raw, true);
$question = trim((string)($data['question'] ?? ''));
$user_id = isset($data['user_id']) ? (int)$data['user_id'] : null;
$path = trim((string)($data['path'] ?? ($_SERVER['HTTP_REFERER'] ?? '')));
if ($question === '') fail('Задайте въпрос и опитайте пак.', $pdo, compact('question','user_id','path'));
/* системен контекст */
$system = "Ти си кратък и полезен асистент. Отговаряй на български, ясно и без клишета.";
/* payload */
$payload = [
'model' => 'gpt-4o-mini',
'messages' => [
['role' => 'system', 'content' => $system],
['role' => 'user', 'content' => $question],
],
'temperature' => 0.6,
'max_tokens' => 400,
];
/* заявка */
$ch = curl_init('https://api.openai.com/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer '.$OPENAI_API_KEY,
],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
CURLOPT_TIMEOUT => 30,
]);
$resp = curl_exec($ch);
$errno = curl_errno($ch);
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($errno) fail('Временен проблем с връзката. Опитайте след малко.', $pdo, compact('question','user_id','path'));
if ($http===429) fail('AI квотата е изчерпана. Опитайте по-късно.', $pdo, compact('question','user_id','path'));
if ($http<200 || $http>=300) fail('Неуспешен AI отговор (HTTP '.$http.').', $pdo, compact('question','user_id','path'));
$out = json_decode($resp, true);
$answer = trim((string)($out['choices'][0]['message']['content'] ?? ''));
if ($answer === '') fail('Празен AI отговор. Опитайте пак.', $pdo, compact('question','user_id','path'));
/* лог */
try {
$st=$pdo->prepare("INSERT INTO ai_logs (created_at,user_id,ip,path,user_agent,question,answer)
VALUES (NOW(), :uid, :ip, :path, :ua, :q, :a)");
$st->execute([
':uid'=>$user_id, ':ip'=>$_SERVER['REMOTE_ADDR']??'',
':path'=>$path, ':ua'=>$_SERVER['HTTP_USER_AGENT']??'',
':q'=>$question, ':a'=>$answer,
]);
} catch(Throwable $e) {}
echo json_encode(['ok'=>true,'answer'=>$answer], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Поставете HTML, CSS и JS (долу) — бутонът отваря/затваря прозореца и праща към /api/ai_ask.
<button id="ai-btn" aria-label="AI асистент">
<i class="fa-solid fa-message"></i>
</button>
<div id="ai-wrap" role="dialog" aria-modal="true" aria-labelledby="ai-title" style="display:none">
<div class="ai-head">
<strong id="ai-title">AI асистент</strong>
<button class="ai-close" type="button" aria-label="Затвори">×</button>
</div>
<div class="ai-log" aria-live="polite"></div>
<form class="ai-form" autocomplete="off">
<textarea class="form-control" rows="2" placeholder="Питай за ИТ, мрежи, сигурност, уеб…"></textarea>
<button class="btn btn-primary" type="submit" aria-label="Изпрати съобщение">Изпрати</button>
</form>
</div>
/* Стиловете на уиджета са примерни; за жив уиджет ползвай глобалния #ntg-ai-btn или отделен CSS файл */
document.addEventListener('DOMContentLoaded', function () {
const scope = document.getElementById('ai-guide');
if (!scope) return;
// Highlight + Copy бутони само вътре в статията
scope.querySelectorAll('pre code').forEach((block) => {
try { if (window.hljs) hljs.highlightElement(block); } catch(e) {}
const btn = document.createElement('button');
btn.className = 'copy-btn';
btn.type = 'button';
btn.innerHTML = 'Copy';
btn.addEventListener('click', async () => {
const codeText = block.innerText;
try {
await navigator.clipboard.writeText(codeText);
btn.dataset.state = 'copied';
const old = btn.textContent;
btn.textContent = 'Copied';
setTimeout(() => { btn.dataset.state=''; btn.textContent = old; }, 1200);
} catch (e) {
// fallback selection
const sel = window.getSelection();
const range = document.createRange();
range.selectNodeContents(block);
sel.removeAllRanges();
sel.addRange(range);
try { document.execCommand('copy'); } catch(_) {}
sel.removeAllRanges();
}
});
block.parentElement.style.position = 'relative';
block.parentElement.appendChild(btn);
});
// Мини чат логика – демонстрационна (скоупната в секцията)
const btn = scope.querySelector('#ai-btn');
const box = scope.querySelector('#ai-wrap');
const close = scope.querySelector('.ai-close');
const log = scope.querySelector('.ai-log');
const form = scope.querySelector('.ai-form');
const input = form ? form.querySelector('textarea') : null;
const scroll = () => { if (log) log.scrollTop = log.scrollHeight; };
const add = (txt, who='bot') => {
if (!log) return;
const d = document.createElement('div');
d.className = 'ai-msg ' + (who==='user'?'ai-user':'ai-bot');
d.textContent = txt;
log.appendChild(d); scroll(); return d;
};
const typing = () => {
if (!log) return null;
const d = document.createElement('div');
d.className = 'ai-msg ai-bot typing';
d.innerHTML = '...';
log.appendChild(d); scroll(); return d;
};
const show = () => { if (box){ box.style.display='block'; setTimeout(()=>{ try{ input && input.focus(); }catch(e){} },0); } };
const hide = () => { if (box) box.style.display='none'; };
btn && btn.addEventListener('click', (e) => { e.preventDefault(); (box && box.style.display === 'block') ? hide() : show(); });
close && close.addEventListener('click', hide);
if (location.search.includes('ai=1')) show();
form && form.addEventListener('submit', async (e) => {
e.preventDefault();
const q = (input?.value || '').trim();
if (!q) return;
add(q, 'user'); if (input) { input.value = ''; input.focus(); }
const loader = typing();
try {
const res = await fetch('/api/ai_ask', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'Accept': 'application/json'},
body: JSON.stringify({ question: q, user_id: window.currentUserId || null, path: location.pathname })
});
const raw = await res.text(); let data = null; try { data = JSON.parse(raw); } catch(_){}
if (loader) loader.remove();
if (!res.ok) { add('⚠️ HTTP ' + res.status + (data?.answer ? (': ' + data.answer) : ''), 'bot'); return; }
if (data && data.ok) { add(String(data.answer || '').trim() || 'Няма отговор.','bot'); }
else { add('⚠️ ' + (data?.answer || data?.error || raw?.slice(0,200) || 'Грешка.'), 'bot'); }
} catch {
if (loader) loader.remove();
add('⚠️ Мрежова грешка. Опитайте пак.','bot');
}
});
});
curl -s -X POST https://example.com/api/ai_ask \
-H 'Content-Type: application/json' \
-d '{"question":"Как да настроя офис VPN?","user_id":1,"path":"/test"}'
.env./api/ai_ask (без .php) и правилен Rewrite.ai_logs без основание. Маскирайте IP при нужда..env; ограничете достъпа (напр. chmod 600).defer/lazy).aria-*, контраст и видими focus състояния.Мога ли да използвам друг модел/доставчик?
Да. Сменете endpoint и payload според доставчика, но запазете интерфейса на /api/ai_ask.
Как да логвам безопасно?
Съкращавайте дълги въпроси/отговори, маскирайте токени, добавете регулярни бекъпи и ротация.
Пишете на office@ntg.bg или използвайте формата за контакт.
Съвет: дръжте ключа в .env (извън web root), логвайте отговорите в база и редовно преглеждайте „AI Logs“ за качество.