NETWORK TECHNOLOGY
Комплексни ИТ решения, поддръжка, сигурност и автоматизация за Вашия бизнес
Техническа поддръжка: 24/7
+359 2 958 6535

Интеграция на AI асистент в сайта: пълно ръководство (+ код)

Готово решение за AI чат асистент: плаващ бутон и прозорец, PHP endpoint /api/ai_ask, ключ в .env и логове в MySQL с админ „AI Logs“.

Бърза ИТ помощ: Интегрираме уеб AI чат асистент (бутон, прозорец, PHP API, MySQL логове) дистанционно за 15–20 мин.

Интеграция на AI асистент в уебсайт — пълно ръководство (плаващ бутон/чат, PHP API /api/ai_ask, .env ключ, MySQL логове)

Интеграция на AI асистент в уебсайт – бутон, чат прозорец и бекенд

Искаш собствен AI асистент в сайта — без външни джаджи и с пълен контрол върху данните? Ето решение за копиране: плаващ бутон/балон, PHP бекенд (/api/ai_ask), .env ключ, логове в MySQL и админ екран „AI Logs“. Подходящо за SEO, GDPR и мащабиране.

1) Какво получавате

2) Таблица за логове

Създайте/обновете таблицата 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;

3) .env с API ключ

Къде? Дръжте .env извън web root-а, ако е възможно (напр. /home/USERNAME/.env или /var/www/site/.env).

OPENAI_API_KEY=sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Права (пример):

chmod 600 /path/to/.env

4) API endpoint: /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);

5) Фронтенд: бутон + прозорец + JS

Поставете HTML, CSS и JS (долу) — бутонът отваря/затваря прозореца и праща към /api/ai_ask.

HTML

<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>

CSS (минимум)

/* Стиловете на уиджета са примерни; за жив уиджет ползвай глобалния #ntg-ai-btn или отделен CSS файл */

JavaScript

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');
    }
  });
});

6) Бърз тест през конзола

curl -s -X POST https://example.com/api/ai_ask \
  -H 'Content-Type: application/json' \
  -d '{"question":"Как да настроя офис VPN?","user_id":1,"path":"/test"}'

7) Често срещани грешки

Сигурност и GDPR

Производителност и UX

FAQ (разширено)

Мога ли да използвам друг модел/доставчик?
Да. Сменете endpoint и payload според доставчика, но запазете интерфейса на /api/ai_ask.

Как да логвам безопасно?
Съкращавайте дълги въпроси/отговори, маскирайте токени, добавете регулярни бекъпи и ротация.


Нужда от съдействие при интеграция?

  • Дизайн на промпт/контекст и настройка на модели
  • Локални логове, филтри и админ панели
  • UX на чат уиджети и проследяване (GA4/GTM/FB)

Пишете на office@ntg.bg или използвайте формата за контакт.

Съвет: дръжте ключа в .env (извън web root), логвайте отговорите в база и редовно преглеждайте „AI Logs“ за качество.


Снимка на автора
Автор

инж. Свилен Арсов

Ръководител ИТ инфраструктура и сигурност в Network Technology.

Коментари

Зареждане…
Само регистрирани и влезли потребители могат да коментират.