Ana içeriğe atla
Geliştirici API Rehberi

Web Chat API Entegrasyonu

Kendi web chat widget'ınızı UppyPro AI asistanına bağlayın. REST API ile mesaj gönderin, AI yanıtları alın, görsel paylaşın ve konuşma geçmişini çekin.

Kod yazmadan hazır widget arıyorsanız: Canlı Destek — Yapay Zeka Destekli Web Chat

Webhook URL Kimlik Doğrulama
REST JSON API
CORS Enabled
01

Genel Bakış

API yapısı ve temel kavramlar

Bu Rehber Kimler İçin?

Web sitenize zaten bir chat widget'ı entegre ettiyseniz veya kendi özel widget'ınızı geliştirmek istiyorsanız, bu rehber widget'ınızı UppyPro AI asistanına bağlamanız için ihtiyacınız olan tüm bilgileri içerir.

Base URL

https://www.upgunai.com/api/webhooks/webchat/

Content Type

application/json

Nasıl Çalışır?

1Kullanıcı web sitenizde chat widget'ınıza bir mesaj yazar
2Widget'ınız bu mesajı UppyPro Webhook URL'sine POST isteği olarak gönderir
3UppyPro AI asistanı mesajı işler ve JSON response olarak yanıt döner
4Widget'ınız bu yanıtı alır ve kullanıcıya gösterir
5Widget'ınız arka planda düzenli GET isteğiyle (polling) yeni mesajları kontrol eder — insan temsilcinin panelden yazdığı yanıtlar bu yolla gelir
02

Kimlik Doğrulama (Webhook URL)

Widget'ınızı tanıtmak için Webhook URL kullanımı

Webhook URL Nereden Alınır?

İşletmenize özel Webhook URL'niz UppyPro ekibi tarafından oluşturulur ve size iletilir — panelden kendiniz üretmeniz gerekmez. Henüz elinizde yoksateknik destekbölümündeki kanallardan talep edebilirsiniz. Web Chat kanalınız pasifse tüm istekler 403 döner.

Size iletilen Webhook URL'sini doğrudan kullanabilirsiniz. Ayrı bir header veya API anahtarı göndermenize gerek yoktur. Adresin sonundaki anahtar UUID biçimindedir:

Endpoint Yapısı
url
POST {WEBHOOK_URL}
GET  {WEBHOOK_URL}?session_id={SESSION_ID}

Örnek:
POST https://www.upgunai.com/api/webhooks/webchat/3f7c1a92-5b84-4e0d-9a16-2c8ef0b7d431

🔐 Anahtarın niteliği: Widget tarayıcıda çalıştığı için bu adres sayfanızın kaynağında görünür — bunu bir parola değil, işletmenize ait sohbet adresi olarak düşünün. İstekleri yalnızca bu adres tanımlar; ziyaretçilerinize ait ekstra bir gizli bilgi taşımaz. Kötüye kullanım fark ederseniz ekibimize yazın, adresiniz yenilenir (eski adres o anda geçersiz olur, widget kodunuzu güncellemeniz gerekir).

03

Mesaj Gönderme (POST)

Widget'tan AI asistana mesaj iletme

Request Body (JSON)

ParametreTipZorunluAçıklama
session_id
Alternatif: chatId, sessionId
string✅ EvetHer ziyaretçi oturumu için benzersiz ID. Aynı session_id ile gelen mesajlar aynı konuşmada gruplanır.
message
Alternatif: text, input, chatInput, content
string✅ EvetKullanıcının gönderdiği mesaj metni. Maksimum 2000 karakter.
visitor_name
Alternatif: name, userName
string❌ HayırZiyaretçinin adı. UppyPro panelinde konuşma kartında görünür. Her mesajda güncellenir — sonradan öğrendiğiniz ismi göndermeye devam edebilirsiniz.
visitor_email
Alternatif: email
string❌ HayırZiyaretçinin e-posta adresi. CRM müşteri kartı oluşturmak için kullanılır. ⚠️ Yalnızca oturumun İLK mesajında dikkate alınır (kart o anda açılır); sonraki mesajlarda gönderilse de yok sayılır.
visitor_phone
Alternatif: phone
string❌ HayırZiyaretçinin telefon numarası. CRM müşteri eşleştirmesi için kullanılır. ⚠️ E-posta ile aynı kural: yalnızca oturumun İLK mesajında dikkate alınır.

💡 session_id İpuçları: Her tarayıcı oturumu için benzersiz bir ID oluşturun.crypto.randomUUID() veyaDate.now() + Math.random() kullanabilirsiniz. Aynı session_id ile gönderilen mesajlar aynı konuşma altında gruplanır.

⏱️ Yanıt süresi: AI yanıtı POST isteğinin cevabında döner, ancak model ürün/stok/takvim sorgusu yapabildiği için istek tipik olarak 3–15 saniye sürer. Bu yüzden isteği await ederken mutlaka bir "yazıyor…" göstergesi gösterin ve giriş kutusunu kilitleyin; aksi halde kullanıcı aynı soruyu üst üste gönderir. fetch tarafında AbortController ile 30 saniyelik bir üst sınır koymanızı öneririz.

Örnek POST İsteği
curl
curl -X POST \
  "WEBHOOK_URL_BURAYA" \
  -H "Content-Type: application/json" \
  -d '{
    "session_id": "visitor_abc123",
    "message": "Yarın akşam 4 kişilik masa var mı?",
    "visitor_name": "Ahmet Yılmaz",
    "visitor_email": "ahmet@example.com",
    "visitor_phone": "+905551234567"
  }'
04

Yanıt Formatı

AI asistandan dönen response yapısı

Başarılı Yanıt (200 OK)
json
{
  "output": "Merhaba Ahmet Bey! 🌟 Yarın akşam...",
  "text": "Merhaba Ahmet Bey! 🌟 Yarın akşam...",
  "reply": "Merhaba Ahmet Bey! 🌟 Yarın akşam...",
  "success": true,
  "conversation_id": "uuid-conversation-id",
  "mode": "ai",
  "images": [
    {
      "url": "https://storage.example.com/photo.jpg",
      "caption": "📸 VIP Masa - Teras"
    }
  ]
}
AlanTipAçıklama
output / text / replystringAI asistanın yanıt metni. Üçü de aynı değeri döner — widget'ınıza uygun olanı kullanın. Markdown içerebilir (aşağıdaki nota bakın).
successbooleanİstek işlendi mi? Dikkat: AI tarafında bir sorun oluşsa bile 200 + success:true döner, mode alanı "error" olur. "Yanıtı göster" kararını success'e, "AI gerçekten cevapladı mı" kararını mode'a bakarak verin.
conversation_idstringKonuşmanın benzersiz ID'si. Loglama ve hata ayıklama için kullanabilirsiniz.
modestring"ai" = AI yanıtladı · "human" = konuşma insan temsilciye alınmış, dönen metin sabit bir bilgilendirmedir · "error" = AI tarafında geçici sorun, dönen metin özür metnidir (yeniden denemeye değer).
imagesarrayAI'ın paylaştığı görseller. Her öğe { url, caption } formatında. Widget'ta img olarak render edin. Yalnızca AI bir görsel paylaştığında dolu gelir; çoğu yanıtta boş dizidir/hiç gelmez.

Yanıt metni markdown içerebilir — render etmelisiniz

Web Chat kanalında yanıt metni olduğu gibi iletilir; WhatsApp ve Instagram'da yapılan markdown sadeleştirmesi burada bilerek uygulanmaz — çünkü tarayıcıda çalışan bir widget kalın yazıyı gerçekten gösterebilir. Bu yüzden metni ham olaraktextContent'e yazarsanız müşteriniz **899 TL** gibi yıldızları ham hâliyle görür.

Minimum güvenli render (XSS'e kapalı)
javascript
function renderText(raw) {
  // 1) Önce HTML kaç — gelen metni asla doğrudan innerHTML'e verme
  const safe = raw
    .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");

  // 2) Sonra sadece izin verdiğin biçimleri geri aç
  return safe
    .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")   // **kalın**
    .replace(/~~(.+?)~~/g, "<del>$1</del>")             // ~~üstü çizili~~
    .replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener">$1</a>')
    .replace(/\n/g, "<br>");
}

bubble.innerHTML = renderText(data.output);

Sıra önemli: önce kaçış, sonra biçimlendirme. Ters çevirirseniz sohbete yazılan HTML sayfanızda çalışır.

💡 İnsan Temsilci Modu: mode: "human" döndüğünde konuşma temsilciye alınmıştır; dönen metin AI yanıtı değil, sabit bir "mesajınız alındı" bilgilendirmesidir ve o konuşmadaki her mesajda aynı metin döner — bunu bir sohbet balonu gibi tekrar tekrar basmayın. Temsilcinin gerçek yanıtları yalnızcapollingile gelir. Önemli: polling'i mode değerine bağlamayın — nedeni bir sonraki bölümde.

05

Görsel Gönderme

Base64 formatında görsel yükleme

Kullanıcı widget'ınız üzerinden bir görsel gönderdiğinde, görseli base64 formatında message alanına ekleyerek gönderebilirsiniz. UppyPro görseli otomatik olarak yükler ve AI'a iletir.

Görsel Gönderme Örneği
javascript
// Kullanıcının seçtiği dosyayı base64'e çevir
const fileInput = document.getElementById('file-input');
const file = fileInput.files[0];

const reader = new FileReader();
reader.onload = async () => {
  const base64Data = reader.result; // "data:image/jpeg;base64,/9j/4AAQ..."
  
  const response = await fetch(WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      session_id: sessionId,
      message: base64Data,        // Base64 data URL
      visitor_name: "Ahmet Yılmaz"
    })
  });
  
  const data = await response.json();
  // data.output → AI'ın görsel hakkındaki yanıtı
};
reader.readAsDataURL(file);

⚠️ Kısıtlamalar: Desteklenen formatlar:JPEG, PNG, WebP, GIF. Base64 string data:image/ ile başlamalıdır.

📐 Boyut — dikkat: API'nin kendi sınırı çözülmüş veri üzerinden 5 MB'tır, ancak base64 kodlaması dosyayı yaklaşık %33 şişirir ve isteğin tamamı için sunucu tarafında ~4,5 MB'lık bir gövde tavanı vardır. Pratikte ~3 MB üzerindeki dosyalar API'ye hiç ulaşamadan reddedilir (bu durumda JSON değil, ham bir 413 yanıtı alırsınız —response.json() çağrınız patlar, bunu try/catch ile karşılayın).

Önerimiz: Göndermeden önce görseli tarayıcıda küçültün — uzun kenarı 1600 px'e indiripcanvas.toDataURL("image/jpeg", 0.8) ile yeniden kodlamak telefon kameralarından gelen 6–10 MB'lık fotoğrafları güvenli aralığa çeker ve yanıt süresini de kısaltır.

06

Konuşma Geçmişini Çekme (GET)

Mevcut oturumdaki mesaj geçmişini alma

GET İsteği
curl
curl -X GET \
  "WEBHOOK_URL_BURAYA?session_id=visitor_abc123"
Yanıt Formatı
json
{
  "success": true,
  "messages": [
    {
      "role": "user",
      "text": "Yarın akşam masa var mı?",
      "sender": "CUSTOMER",
      "timestamp": "2024-04-09T12:00:00.000Z",
      "message_type": "text",
      "media_url": null
    },
    {
      "role": "assistant",
      "text": "Merhaba! Yarın akşam için birkaç uygun masamız var 🌟",
      "sender": "BOT",
      "timestamp": "2024-04-09T12:00:02.000Z",
      "message_type": "text",
      "media_url": null
    },
    {
      "role": "assistant",
      "text": "📸 VIP Masa - Teras",
      "sender": "BOT",
      "timestamp": "2024-04-09T12:00:03.000Z",
      "message_type": "image",
      "media_url": "https://storage.example.com/photo.jpg"
    }
  ]
}

💡 Kullanım Alanı: Sayfa yenilendiğinde veya widget yeniden açıldığında, mevcut session_id ile GET isteği yaparak konuşma geçmişini geri yükleyebilirsiniz. Maksimum 50 mesaj döner (kronolojik sırayla). Bu uç noktada hız sınırı uygulanmaz, dolayısıyla polling için gönül rahatlığıyla kullanılabilir.

🔒 session_id'yi ziyaretçiye özel tutun. Bir oturumun geçmişini okumak için Webhook URL ve o oturumun session_id'si birlikte gerekir. Bu yüzden session_id'yi tahmin edilemez üretin (crypto.randomUUID() idealdir; sıralı sayı veya müşteri numarası kullanmayın), yalnızca ilgili ziyaretçinin tarayıcısında saklayın ve paylaşılabilir bir sayfa adresinin içine koymayın.

07

Polling Mekanizması

Temsilci yanıtlarını yakalamak için periyodik sorgulama

AI yanıtları POST isteğinin response'unda anında döner. Ancak işletme temsilcisi panelden bir mesaj yazdığında bu mesaj widget'ınıza kendiliğinden gitmez — onu almanın tek yolu düzenli aralıklarla GET isteği yapmaktır.

Polling'i mode değerine bağlamayın

"mode: human dönerse polling başlat" sezgisel görünür ama çalışmaz, çünkü:

  • Temsilci panelden cevap yazdığında konuşmanın modu otomatik olarak "human" olmaz; mod ayrı bir düğmeyle değiştirilir ve temsilci bunu yapmadan da yazabilir.
  • mode bilgisini yalnızca ziyaretçi yeni bir mesaj gönderdiğinde öğrenirsiniz. Ziyaretçi sorusunu sorup beklemeye geçtiyse yeni bir POST hiç olmaz — dolayısıyla polling de hiç başlamaz.

Doğrusu: sohbet penceresi açık olduğu sürece koşulsuz olarak 3 saniyede bir sorgulayın. Maliyeti yok sayılır (GET isteklerinde hız sınırı uygulanmaz), kazancı temsilci yanıtlarının kaybolmamasıdır.

💡 İki koruma zorunlu. GET her seferinde tüm geçmişi döndürdüğü için, aşağıdaki kodda işaretlenmiş iki adımı atlarsanız mesajlar ekrana iki kez basılır:
1) İlk turda hizalama: ilk sorguda sayacı sessizce ayarlayın, hiçbir şey basmayın — yoksa widget açılır açılmaz konuşmanın tamamı "yeni mesaj" sanılıp baştan yazılır.
2) İmza kontrolü: POST yanıtında zaten gösterdiğiniz AI cevabı, birkaç saniye sonra polling'de tekrar karşınıza çıkar. Ekrana basılan her bot mesajının imzasını bir Set'te tutup ikinci kez basmayın.

Polling Implementasyonu (önerilen)
javascript
// UppyPro ekibinden aldığınız Webhook URL
const WEBHOOK_URL = "https://www.upgunai.com/api/webhooks/webchat/3f7c1a92-5b84-4e0d-9a16-2c8ef0b7d431";

// Ekrana basılmış bot mesajlarının imzaları — POST yanıtı ile polling çakışmasın
const shownBotMessages = new Set();
let lastMessageCount = 0;
let primed = false;

// Bot mesajlarını HER ZAMAN bu fonksiyondan geçirin (hem POST yanıtı hem polling)
function showBotMessage(text, mediaUrl) {
  const signature = (mediaUrl || text || "").slice(0, 80);
  if (!signature || shownBotMessages.has(signature)) return;   // ← 2) İMZA KONTROLÜ
  shownBotMessages.add(signature);
  displayMessage(text, "bot", mediaUrl);   // kendi render fonksiyonunuz
}

async function pollMessages() {
  if (!isChatOpen()) return;   // kapalı pencerede sorgu yapmayın
  try {
    const res = await fetch(
      `${WEBHOOK_URL}?session_id=${encodeURIComponent(sessionId)}`
    );
    const data = await res.json();
    if (!data.success || !data.messages) return;

    // ← 1) İLK TURDA HİZALAMA: sayacı ayarla, hiçbir şey basma
    if (!primed) {
      lastMessageCount = data.messages.length;
      primed = true;
      return;
    }

    if (data.messages.length > lastMessageCount) {
      data.messages.slice(lastMessageCount).forEach(m => {
        if (m.role !== "assistant") return;
        const media = m.message_type === "image" ? m.media_url : null;
        showBotMessage(m.text, media);
      });
      lastMessageCount = data.messages.length;
    }
  } catch (err) {
    // Ağ hatasında sessizce geç — 3 saniye sonraki tur zaten yeniden deneyecek
  }
}

// KOŞULSUZ başlat. mode "ai" iken de temsilci araya girebilir.
setInterval(pollMessages, 3000);

// POST tarafı da aynı fonksiyonu kullanmalı ki polling aynı mesajı tekrar basmasın
async function sendMessage(text) {
  const res = await fetch(WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ session_id: sessionId, message: text })
  });
  const data = await res.json();

  showBotMessage(data.output);
  (data.images || []).forEach(img => showBotMessage(img.caption, img.url));
}

Ödünleşme: İmza, metnin ilk 80 karakteridir. Asistan birebir aynı cümleyi ikinci kez gönderirse ("Başka bir konuda yardımcı olabilir miyim?" gibi) ikincisi bastırılır. Bu, aynı mesajın çift görünmesine kıyasla kabul edilebilir bir ödünleşmedir; kendi widget'ımız da aynı yöntemi kullanır. Daha hassas bir ayrım isterseniz imzayıtimestamp + textolarak kurup POST yanıtındaki mesajı bir sonraki poll turunda eşleştirebilirsiniz.

08

Hata Kodları

API'nin döndürdüğü HTTP durum kodları

KodDurumAçıklamaÇözüm
200OKİstek işlendi. Yanıt gövdesindeki mode alanına da bakın — AI hatası da 200 döner (aşağıdaki nota bakın)
400Bad RequestGeçersiz JSON, eksik session_id veya message, ya da 2000 karakteri aşan metinRequest body'yi kontrol edin
403ForbiddenGeçersiz Webhook URL veya Web Chat kanalı pasifAdresi birebir kopyaladığınızdan emin olun; doğruysa kanalın aktif edilmesi için ekibimize yazın
413Payload Too Largeİstek gövdesi çok büyük — base64 görsel sınırı aşmış (bkz. Görsel Gönderme)Görseli göndermeden önce küçültün (uzun kenar ~1600px, JPEG %80)
429Too Many RequestsHız sınırı aşıldı (POST için IP başına ~20 istek/dakika). GET/polling istekleri bu sınıra dahil değildirİstekler arasında bekleme süresi ekleyin; kullanıcı yanıt beklerken giriş kutusunu kilitleyin
500Server ErrorSunucu hatası (konuşma veya mesaj kaydedilemedi)Tekrar deneyin, devam ederse destek ekibiyle iletişime geçin
504TimeoutAI yanıtı beklenenden uzun sürdü ve istek zaman aşımına uğradıKullanıcıya kibar bir hata balonu gösterip tekrar denemesini önerin; mesaj panelde görünür, temsilci devralabilir

⚠️ Her hata HTTP koduna yansımaz. AI tarafında geçici bir sorun oluştuğunda istek 200 + success: true döner; ayırt edici alan mode: "error"'dur veoutputiçinde ziyaretçiye gösterilebilir bir özür metni bulunur. Metni olduğu gibi gösterebilirsiniz, ancak bu turu başarılı bir AI yanıtı olarak loglamayın.

09

Rate Limiting

API kullanım limitleri

~20
POST / dakika
IP başına · GET hariç
2000
karakter / mesaj
Metin mesajları
~3 MB
pratik görsel sınırı
base64 şişmesi dahil

Sınır neyi kapsar? Hız sınırı yalnızca POST (mesaj gönderme) isteklerine uygulanır; geçmiş çekme ve polling için kullanılan GET istekleri sınıra dahil değildir — bu yüzden 3 saniyelik polling gönül rahatlığıyla kullanılabilir.

Neden "~20"? Sayaç sunucu belleğinde tutulur ve istekleriniz birden fazla sunucu örneğine dağılabileceği için 20 rakamı kesin bir eşik değil, kabaca bir frendir — bazen biraz üstüne çıkabilir. Ayrıca sayım IP başınadır: aynı ofis ağı ya da mobil operatör NAT'ı arkasındaki farklı ziyaretçiler tek IP olarak sayılabilir. Normal bir sohbet akışında bu sınıra takılmazsınız; otomatik test/yük denemelerinde takılırsınız.

10

Örnek Kodlar

Farklı dillerde implementasyon örnekleri

JavaScript (Fetch API)
javascript
// UppyPro ekibinden aldığınız Webhook URL'yi buraya yapıştırın
const WEBHOOK_URL = "https://www.upgunai.com/api/webhooks/webchat/3f7c1a92-5b84-4e0d-9a16-2c8ef0b7d431";

// Oturum ID'si oluştur (tarayıcı başına benzersiz)
const sessionId = localStorage.getItem("chat_session") 
  || (() => {
       const id = "visitor_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
       localStorage.setItem("chat_session", id);
       return id;
     })();

async function sendMessage(userMessage) {
  const response = await fetch(WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      session_id: sessionId,
      message: userMessage,
      visitor_name: "Web Ziyaretçisi"
    })
  });

  const data = await response.json();

  if (data.success) {
    console.log("AI Yanıtı:", data.output);
    
    // Görseller varsa render et
    if (data.images && data.images.length > 0) {
      data.images.forEach(img => {
        console.log("Görsel:", img.url, "Başlık:", img.caption);
      });
    }
  } else {
    console.error("Hata:", data.error);
  }
}
Python (requests)
python
import requests
import uuid

# UppyPro ekibinden aldığınız Webhook URL'yi buraya yapıştırın
WEBHOOK_URL = "https://www.upgunai.com/api/webhooks/webchat/3f7c1a92-5b84-4e0d-9a16-2c8ef0b7d431"

session_id = f"py_{uuid.uuid4().hex[:12]}"

def send_message(message: str, name: str = "Web Ziyaretçisi"):
    response = requests.post(WEBHOOK_URL, json={
        "session_id": session_id,
        "message": message,
        "visitor_name": name,
    })
    
    data = response.json()
    
    if data.get("success"):
        print(f"AI: {data['output']}")
        
        # Görselleri kontrol et
        for img in data.get("images", []):
            print(f"📸 {img['caption']}: {img['url']}")
    else:
        print(f"Hata: {data.get('error')}")

# Kullanım
send_message("Merhaba, fiyatlarınız hakkında bilgi alabilir miyim?")
PHP (cURL)
php
<?php
// UppyPro ekibinden aldığınız Webhook URL'yi buraya yapıştırın
$webhookUrl = "https://www.upgunai.com/api/webhooks/webchat/3f7c1a92-5b84-4e0d-9a16-2c8ef0b7d431";

$sessionId = "php_" . bin2hex(random_bytes(6));

function sendMessage($url, $sessionId, $message, $name = "Web Ziyaretçisi") {
    $payload = json_encode([
        "session_id"   => $sessionId,
        "message"      => $message,
        "visitor_name" => $name,
    ]);

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

    $response = curl_exec($ch);
    curl_close($ch);

    $data = json_decode($response, true);

    if ($data["success"]) {
        echo "AI: " . $data["output"] . "\n";
        
        // Görseller
        foreach ($data["images"] ?? [] as $img) {
            echo "📸 " . $img["caption"] . ": " . $img["url"] . "\n";
        }
    } else {
        echo "Hata: " . ($data["error"] ?? "Bilinmeyen") . "\n";
    }
}

sendMessage($webhookUrl, $sessionId, "Yarın için müsait odalar var mı?");
?>
11

Tam Widget Örneği

Kopyala-yapıştır hazır minimal chat widget

Aşağıdaki örnek, web sitenize ekleyebileceğiniz minimal ama eksiksiz bir chat widget'ıdır: markdown render'ı, "yazıyor…" göstergesi, koşulsuz polling, çift-basma koruması ve hata yönetimi dahil. Tek yapmanız gereken en üstteki UPPY_URL değerini size iletilen Webhook URL ile değiştirmek.

Bu örnekte özellikle dikkat edin: görünürlük tek bir.is-open sınıfıyla yönetiliyor — aynı style özniteliğine hemdisplay:none hemdisplay:flex yazmak sık yapılan bir hatadır; CSS'te sonuncusu kazanır ve pencere sayfa açılır açılmaz açık doğar. Ayrıca tüm bot mesajları tek biraddBot() kapısından geçiyor ve polling mode değerine bakmadan çalışıyor.

Minimal Chat Widget (HTML + JS)
html
<!-- UppyPro Web Chat Widget -->
<style>
  #uppy-toggle{position:fixed;bottom:20px;right:20px;width:60px;height:60px;border:none;
    border-radius:50%;background:#f97316;cursor:pointer;z-index:9999;
    box-shadow:0 4px 12px rgba(249,115,22,.4);display:flex;align-items:center;justify-content:center}

  /* Kutu VARSAYILAN OLARAK GİZLİ. Görünürlük TEK bir yerden yönetilir: .is-open
     Aynı style özniteliğine iki kez display yazmayın — sonuncusu kazanır. */
  #uppy-box{display:none;position:fixed;bottom:90px;right:20px;
    width:min(380px,calc(100vw - 40px));height:min(520px,calc(100vh - 140px));
    background:#fff;border-radius:16px;box-shadow:0 8px 30px rgba(0,0,0,.15);z-index:9999;
    flex-direction:column;overflow:hidden;font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
  #uppy-box.is-open{display:flex}

  #uppy-head{background:#f97316;color:#fff;padding:16px;display:flex;align-items:center;gap:10px}
  #uppy-msgs{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:8px;background:#f8fafc}
  #uppy-foot{padding:12px;border-top:1px solid #e2e8f0;display:flex;gap:8px}
  #uppy-in{flex:1;border:1px solid #e2e8f0;border-radius:24px;padding:10px 16px;font-size:14px;outline:none}
  #uppy-in:disabled{background:#f1f5f9}

  .uppy-b{max-width:80%;padding:10px 14px;border-radius:16px;font-size:13px;line-height:1.5;
    word-wrap:break-word;white-space:pre-wrap}
  .uppy-b.me{align-self:flex-end;background:#f97316;color:#fff}
  .uppy-b.bot{align-self:flex-start;background:#fff;color:#1e293b;border:1px solid #e2e8f0}
  .uppy-b img{max-width:100%;border-radius:8px;display:block}

  .uppy-dots span{display:inline-block;width:6px;height:6px;margin-right:3px;border-radius:50%;
    background:#94a3b8;animation:uppy-blink 1.2s infinite}
  .uppy-dots span:nth-child(2){animation-delay:.2s}
  .uppy-dots span:nth-child(3){animation-delay:.4s}
  @keyframes uppy-blink{0%,80%,100%{opacity:.3}40%{opacity:1}}
</style>

<button id="uppy-toggle" aria-label="Sohbeti aç">
  <svg width="28" height="28" fill="white" viewBox="0 0 24 24">
    <path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/>
  </svg>
</button>

<div id="uppy-box" role="dialog" aria-label="Canlı sohbet">
  <div id="uppy-head">
    <div style="width:36px;height:36px;background:rgba(255,255,255,.2);border-radius:50%;
      display:flex;align-items:center;justify-content:center">🤖</div>
    <div>
      <div style="font-weight:700;font-size:14px">AI Asistan</div>
      <div style="font-size:11px;opacity:.8">Çevrimiçi</div>
    </div>
    <button id="uppy-close" aria-label="Kapat"
      style="margin-left:auto;background:none;border:none;color:#fff;font-size:20px;cursor:pointer">✕</button>
  </div>

  <div id="uppy-msgs"></div>

  <div id="uppy-foot">
    <input id="uppy-in" type="text" placeholder="Mesajınızı yazın..." autocomplete="off"/>
    <button id="uppy-send" aria-label="Gönder"
      style="background:#f97316;color:#fff;border:none;border-radius:50%;
      width:40px;height:40px;cursor:pointer;font-size:16px">➤</button>
  </div>
</div>

<script>
(function () {
  // ─── KONFİGÜRASYON ───
  var UPPY_URL = "https://www.upgunai.com/api/webhooks/webchat/3f7c1a92-5b84-4e0d-9a16-2c8ef0b7d431";
  var VISITOR_NAME = "";   // biliyorsanız doldurun; boşsa panelde "Web Ziyaretçisi" görünür

  var box     = document.getElementById("uppy-box");
  var msgs    = document.getElementById("uppy-msgs");
  var input   = document.getElementById("uppy-in");
  var sendBtn = document.getElementById("uppy-send");

  // Oturum kimliği — aynı tarayıcıda konuşma kaldığı yerden devam eder
  var SESSION = localStorage.getItem("uppy_sid");
  if (!SESSION) {
    SESSION = "w_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
    localStorage.setItem("uppy_sid", SESSION);
  }

  var busy = false, primed = false, lastCount = 0;
  var shown = new Set();   // ekrana basılmış bot mesajlarının imzaları

  // ─── Metin render: ÖNCE HTML kaç, SONRA izin verilen biçimleri aç ───
  // Sıra ters olursa sohbete yazılan HTML sayfanızda çalışır (XSS).
  function render(raw) {
    var safe = String(raw || "")
      .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
    return safe
      .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
      .replace(/~~(.+?)~~/g, "<del>$1</del>")
      .replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener">$1</a>');
  }

  function bubble(html, who) {
    var b = document.createElement("div");
    b.className = "uppy-b " + who;
    if (html) b.innerHTML = html;
    msgs.appendChild(b);
    msgs.scrollTop = msgs.scrollHeight;
    return b;
  }

  function addMe(text) { bubble(render(text), "me"); }

  // TÜM bot mesajları buradan geçer → POST yanıtı ile polling asla çakışmaz
  function addBot(text, mediaUrl) {
    var sig = (mediaUrl || text || "").slice(0, 80);
    if (!sig || shown.has(sig)) return;
    shown.add(sig);

    var b = bubble("", "bot");
    if (mediaUrl) {
      var img = document.createElement("img");
      img.src = mediaUrl;                       // öznitelik enjeksiyonuna kapalı
      img.alt = text || "Paylaşılan görsel";
      b.appendChild(img);
    }
    if (text) {
      var d = document.createElement("div");
      d.innerHTML = render(text);
      b.appendChild(d);
    }
  }

  // ─── "Yazıyor…" göstergesi + giriş kilidi ───
  var typingEl = null;
  function typing(on) {
    if (on && !typingEl) {
      typingEl = bubble('<span class="uppy-dots"><span></span><span></span><span></span></span>', "bot");
    } else if (!on && typingEl) {
      typingEl.remove();
      typingEl = null;
    }
  }
  function setBusy(v) {
    busy = v;
    input.disabled = v;
    sendBtn.disabled = v;
    if (!v) input.focus();
  }

  // ─── Mesaj gönder ───
  async function send() {
    var text = input.value.trim();
    if (!text || busy) return;

    input.value = "";
    addMe(text);
    setBusy(true);
    typing(true);

    // AI yanıtı tipik olarak 3-15 sn sürer; 30 sn'de üst sınır koyuyoruz
    var ctrl = new AbortController();
    var timer = setTimeout(function () { ctrl.abort(); }, 30000);

    try {
      var res = await fetch(UPPY_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          session_id: SESSION,
          message: text,
          visitor_name: VISITOR_NAME || undefined
        }),
        signal: ctrl.signal
      });

      // 413 / 504 gibi durumlarda gövde JSON olmayabilir — patlamasın
      var data = await res.json().catch(function () { return null; });
      typing(false);

      if (data && data.output) {
        addBot(data.output);
        (data.images || []).forEach(function (img) { addBot(img.caption, img.url); });
      } else {
        addBot("Mesajınızı şu anda iletemedik. Lütfen biraz sonra tekrar deneyin.");
      }
    } catch (e) {
      typing(false);
      addBot("Bağlantı hatası. Lütfen tekrar deneyin.");
    } finally {
      clearTimeout(timer);
      setBusy(false);
    }
  }

  // ─── Geçmişi yükle (pencere açılırken) ve polling sayacını hizala ───
  async function loadHistory() {
    try {
      var res = await fetch(UPPY_URL + "?session_id=" + encodeURIComponent(SESSION));
      var data = await res.json();
      if (!data.success || !data.messages) return;

      msgs.innerHTML = "";
      shown.clear();
      data.messages.forEach(function (m) {
        var media = m.message_type === "image" ? m.media_url : null;
        if (m.role === "user") addMe(m.text);
        else addBot(m.text, media);
      });

      lastCount = data.messages.length;   // ← sayacı hizala
      primed = true;
    } catch (e) { /* sessizce geç */ }
  }

  // ─── Polling: temsilcinin panelden yazdığı yanıtlar YALNIZCA bu yolla gelir ───
  async function poll() {
    if (!box.classList.contains("is-open")) return;
    try {
      var res = await fetch(UPPY_URL + "?session_id=" + encodeURIComponent(SESSION));
      var data = await res.json();
      if (!data.success || !data.messages) return;

      // İlk tur: sayacı ayarla, hiçbir şey basma
      if (!primed) { lastCount = data.messages.length; primed = true; return; }

      if (data.messages.length > lastCount) {
        data.messages.slice(lastCount).forEach(function (m) {
          if (m.role !== "assistant") return;
          addBot(m.text, m.message_type === "image" ? m.media_url : null);
        });
        lastCount = data.messages.length;
      }
    } catch (e) { /* sessizce geç — 3 sn sonra tekrar denenecek */ }
  }
  setInterval(poll, 3000);   // KOŞULSUZ — mode değerine bakmıyoruz

  // ─── Aç / kapat ───
  function toggle(open) {
    box.classList.toggle("is-open", open);
    if (open) { loadHistory(); input.focus(); }
  }
  document.getElementById("uppy-toggle").onclick = function () {
    toggle(!box.classList.contains("is-open"));
  };
  document.getElementById("uppy-close").onclick = function () { toggle(false); };

  sendBtn.onclick = send;
  input.addEventListener("keydown", function (e) {
    if (e.key === "Enter") { e.preventDefault(); send(); }
  });
})();
</script>

Not: Bu örnek, UppyPro'nun kendi hazır widget'ında kullanılan korumaların aynısını içerir. Kendi tasarımınızı yaparken görünümü serbestçe değiştirebilirsiniz; ancak imza kontrolü, ilk turda hizalama, koşulsuz polling ve markdown render'ı dört adımını korumanızı öneririz — bunlar olmadan hatalar ancak canlıda, gerçek müşteri konuşmalarında fark edilir.

12

Teknik Destek

Entegrasyon yardımı ve yazılım desteği

Yazılım Desteğine mi İhtiyacınız Var?

Web sitenize bir chatbot eklemek istiyorsanız veya mevcut widget'ınızı UppyPro'ya entegre ederken teknik desteğe ihtiyaç duyuyorsanız, UpgunAI teknik ekibi size yardımcı olabilir.

Web sitenize özel chat widget tasarımı ve kodlaması
Mevcut chatbot'unuzun UppyPro API'sine bağlanması
Polling mekanizması ve görsel render implementasyonu
Test ve hata ayıklama desteği

Aşağıdaki iletişim kanallarından bize ulaşın, teknik ekibimiz en kısa sürede size geri dönüş yapacaktır.