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
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/jsonNasıl Çalışır?
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:
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).
Mesaj Gönderme (POST)
Widget'tan AI asistana mesaj iletme
Request Body (JSON)
| Parametre | Tip | Zorunlu | Açıklama |
|---|---|---|---|
session_idAlternatif: chatId, sessionId | string | ✅ Evet | Her ziyaretçi oturumu için benzersiz ID. Aynı session_id ile gelen mesajlar aynı konuşmada gruplanır. |
messageAlternatif: text, input, chatInput, content | string | ✅ Evet | Kullanıcının gönderdiği mesaj metni. Maksimum 2000 karakter. |
visitor_nameAlternatif: name, userName | string | ❌ Hayır | Ziyaretç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_emailAlternatif: email | string | ❌ Hayır | Ziyaretç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_phoneAlternatif: phone | string | ❌ Hayır | Ziyaretç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.
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"
}'Yanıt Formatı
AI asistandan dönen response yapısı
{
"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"
}
]
}| Alan | Tip | Açıklama |
|---|---|---|
output / text / reply | string | AI 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). |
success | boolean | İ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_id | string | Konuşmanın benzersiz ID'si. Loglama ve hata ayıklama için kullanabilirsiniz. |
mode | string | "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). |
images | array | AI'ı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.
function renderText(raw) {
// 1) Önce HTML kaç — gelen metni asla doğrudan innerHTML'e verme
const safe = raw
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
// 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.
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.
// 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.
Konuşma Geçmişini Çekme (GET)
Mevcut oturumdaki mesaj geçmişini alma
curl -X GET \
"WEBHOOK_URL_BURAYA?session_id=visitor_abc123"{
"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.
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.
modebilgisini 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.
// 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.
Hata Kodları
API'nin döndürdüğü HTTP durum kodları
| Kod | Durum | Açıklama | Çözüm |
|---|---|---|---|
| 200 | OK | İ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) | — |
| 400 | Bad Request | Geçersiz JSON, eksik session_id veya message, ya da 2000 karakteri aşan metin | Request body'yi kontrol edin |
| 403 | Forbidden | Geçersiz Webhook URL veya Web Chat kanalı pasif | Adresi birebir kopyaladığınızdan emin olun; doğruysa kanalın aktif edilmesi için ekibimize yazın |
| 413 | Payload 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) |
| 429 | Too Many Requests | Hı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 |
| 500 | Server Error | Sunucu hatası (konuşma veya mesaj kaydedilemedi) | Tekrar deneyin, devam ederse destek ekibiyle iletişime geçin |
| 504 | Timeout | AI 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.
Rate Limiting
API kullanım limitleri
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.
Örnek Kodlar
Farklı dillerde implementasyon örnekleri
// 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);
}
}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
// 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ı?");
?>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.
<!-- 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, "&").replace(/</g, "<").replace(/>/g, ">");
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.
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.
Aşağıdaki iletişim kanallarından bize ulaşın, teknik ekibimiz en kısa sürede size geri dönüş yapacaktır.
