NETWORK TECHNOLOGY
Comprehensive IT solutions, support, security and automation for your business
Tech support: 24/7
+359 2 958 6535

Integrating an AI Assistant into Your Website: A Complete Guide (+ Code)

Ready-made AI Chat Assistant: floating button and window, PHP endpoint /api/ai_ask, key in .env, and MySQL logs with an “AI Logs” admin page. Steps, code, and c…

Fast IT help: We integrate a web AI chat assistant (launcher button, chat window, PHP API, MySQL logs) remotely in 15–20 min.

Integrate an AI Assistant into Your Website — Complete Guide (floating button/chat, PHP API /api/ai_ask, .env key, MySQL logs)

AI assistant for website — launcher button, chat window and backend

Want your own AI assistant on your site — no external widgets and full control of your data? Here’s a copy-paste solution: floating button/bubble, PHP backend (/api/ai_ask), .env key, logs in MySQL and an admin screen “AI Logs”. SEO-friendly, GDPR-ready, and scalable.

    1) What you get

    2) Logs table

    Create/upgrade the ai_logs table:

    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 with API key

    Where? Keep .env outside the web root if possible (e.g. /home/USERNAME/.env or /var/www/site/.env).

    OPENAI_API_KEY=sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

    Permissions (example):

    chmod 600 /path/to/.env

    4) API endpoint: /api/ai_ask.php

    Create /api/ai_ask.php. It loads .env, validates input, calls the model and writes a log.

    <?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); }
    
    /* key */
    $OPENAI_API_KEY = getenv('OPENAI_API_KEY') ?: '';
    if ($OPENAI_API_KEY === '') {
      echo json_encode(['ok'=>true,'answer'=>'AI key is not configured.'], JSON_UNESCAPED_UNICODE);
      exit;
    }
    
    /* fail helper + error log */
    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;
    }
    
    /* input */
    $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('Please ask a question and try again.', $pdo, compact('question','user_id','path'));
    
    /* system context */
    $system = "You are a concise, helpful assistant. Reply in clear English, without clichés.";
    
    /* payload */
    $payload = [
      'model' => 'gpt-4o-mini',
      'messages' => [
        ['role' => 'system', 'content' => $system],
        ['role' => 'user',   'content' => $question],
      ],
      'temperature' => 0.6,
      'max_tokens'  => 400,
    ];
    
    /* request */
    $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('Temporary connection issue. Please try again shortly.', $pdo, compact('question','user_id','path'));
    if ($http===429) fail('AI quota exceeded. Please try again later.',             $pdo, compact('question','user_id','path'));
    if ($http<200 || $http>=300) fail('Unsuccessful AI response (HTTP '.$http.').', $pdo, compact('question','user_id','path'));
    
    $out = json_decode($resp, true);
    $answer = trim((string)($out['choices'][0]['message']['content'] ?? ''));
    if ($answer === '') fail('Empty AI response. Please try again.', $pdo, compact('question','user_id','path'));
    
    /* log */
    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) Frontend: button + window + JS

    Place the HTML, CSS and JS below — the button toggles the window and posts to /api/ai_ask.

    HTML

    <button id="ai-btn" aria-label="AI assistant">
      <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 Assistant</strong>
        <button class="ai-close" type="button" aria-label="Close">×</button>
      </div>
      <div class="ai-log" aria-live="polite"></div>
      <form class="ai-form" autocomplete="off">
        <textarea class="form-control" rows="2" placeholder="Ask about IT, networks, security, web…"></textarea>
        <button class="btn btn-primary" type="submit" aria-label="Send message">Send</button>
      </form>
    </div>

    CSS (minimum)

    /* Example widget styles; for production use a global #ntg-ai-btn or a separate CSS file */

    JavaScript

    document.addEventListener('DOMContentLoaded', function () {
      const scope = document.getElementById('ai-guide');
      if (!scope) return;
    
      // Highlight + Copy buttons inside the article only
      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);
      });
    
      // Minimal chat demo (scoped to the section)
      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() || 'No answer.','bot'); }
          else { add('⚠️ ' + (data?.answer || data?.error || raw?.slice(0,200) || 'Error.'), 'bot'); }
        } catch {
          if (loader) loader.remove();
          add('⚠️ Network error. Please try again.','bot');
        }
      });
    });

    6) Quick console test

    curl -s -X POST https://example.com/api/ai_ask \
      -H 'Content-Type: application/json' \
      -d '{"question":"How do I set up an office VPN?","user_id":1,"path":"/test"}'

    7) Common errors

    Security & GDPR

    Performance & UX

    FAQ (extended)

    Can I use another model/provider?
    Yes. Swap the endpoint and payload per provider, but keep the /api/ai_ask interface.

    How to log safely?
    Truncate long Q/A, mask tokens, add regular backups and rotation.


    Need help integrating?

    • Prompt/context design and model setup
    • Local logs, filters and admin panels
    • Chat widget UX and tracking (GA4/GTM/FB)

    Email us at office@ntg.bg or use the contact form.

    Tip: keep the key in .env (outside web root), log answers to DB, and review “AI Logs” regularly for quality.

    Comments

    Loading…
    Only registered and logged-in users can comment.