# 03 — Chat Widget & Session Modülü

## Ne İş Yapar?

Tenant'ın web sitesine yerleştirilen JS snippet'i aracılığıyla ziyaretçilerle sohbet edilmesini sağlar. Her sohbet bir `ChatSession`'dır. Mesajlar `ChatMessage` olarak saklanır. Operatör panel'den sohbetleri görür ve gerektiğinde devralır.

---

## Veri Modeli

```
chat_widgets
├── id (UUID)
├── tenant_id (FK)
├── name                      ← "Ana Site Widget"
├── api_key                   ← embed snippet'te kullanılır
├── allowed_domains (JSON)    ← ["ornek.com", "www.ornek.com"]
├── ai_enabled (BOOL)
├── is_online (BOOL)          ← Tenant aktif mi?
├── persona (JSON)            ← ton, dil, kısıtlar
├── settings (JSON)           ← tema rengi, pozisyon, mesajlar
├── ai_budget_monthly (DECIMAL)
├── ai_budget_used (DECIMAL)
├── ai_budget_period          ← "2026-06"
└── created_at

chat_sessions
├── id (UUID)
├── tenant_id
├── widget_id (FK)
├── visitor_id (FK → flovy_visitors)  ← Tanınan ziyaretçi
├── session_token             ← Widget'ın her işlemde kullandığı token
├── status (open/closed/escalated)
├── visitor_name
├── visitor_email
├── visitor_phone
├── visitor_ip
├── visitor_browser
├── page_url                  ← Sohbet başladığında hangi sayfadaydı
├── referrer
├── kvkk_consent_at
├── intent                    ← Son tespit edilen niyet
├── nps_score
├── nps_comment
├── nps_submitted_at
├── escalated_at
├── idle_ping_sent_at
├── metadata (JSON)           ← Sepet, teslimat bilgisi vb.
├── started_at
└── ended_at

chat_messages
├── id (UUID)
├── session_id (FK)
├── tenant_id
├── sender_type (visitor/ai/operator/system)
├── message (TEXT)
├── message_type (text/ai/system/tool_result)
├── meta (JSON)               ← ui_blocks, tool_calls, tokens_in/out
├── model                     ← kullanılan Gemini model
├── latency_ms
├── is_read (BOOL)
└── created_at
```

---

## API Endpoint'leri

### Widget (api_key, session_token — JWT yok)
```
GET  /api/widget/{api_key}/settings         ← Widget ayarları + reverb config
POST /api/widget/{api_key}/session/create   ← Yeni sohbet otur
POST /api/widget/message/send               ← Mesaj gönder (session_token)
GET  /api/widget/message/history            ← Mesaj geçmişi
POST /api/widget/session/close              ← Sohbeti kapat
POST /api/widget/nps                        ← NPS skoru gönder
POST /api/widget/idle-ping                  ← 30s boşluk sonrası ping
POST /api/widget/visitor/identify           ← Fingerprint + UUID tanıma
```

### Panel (JWT gerekli)
```
GET  /api/sessions                          ← Sohbet listesi (filtreli)
GET  /api/sessions/{id}                     ← Sohbet detayı + mesajlar
POST /api/sessions/{id}/takeover            ← AI'dan devral
POST /api/sessions/{id}/close
POST /api/sessions/{id}/message             ← Operatör mesajı
GET  /api/sessions/live                     ← Aktif sohbetler (WebSocket)
```

### Streaming (Node.js köprüsü için — STREAM_SECRET ile)
```
POST /api/stream/init       ← Widget'tan: mesaj + session → stream_token üret
POST /api/stream/resolve    ← Node.js'ten: token → {prompt, tools, model}
POST /api/stream/done       ← Node.js'ten: final cevap → DB'ye yaz
```

---

## Sohbet Akışı (Streaming)

```
1. Ziyaretçi mesaj yazar
2. Widget → POST /api/stream/init { session_token, message }
3. FastAPI:
   a. Visitor profilini yükle (VisitorService)
   b. Mesajı DB'ye kaydet (sender_type=visitor)
   c. IntentRouter.decide(message) → RULE veya LLM
   d. RULE ise: direkt cevap üret, stream_token üretme
   e. LLM ise:
      - MemoryBuilder.build(visitor) → geçmiş context
      - RAG: KnowledgeRetriever.top_k(...)
      - PromptBuilder.build(system, memory, rag, history, tools)
      - stream_token üret (Redis'e 5dk TTL ile sakla)
      - { stream_token, stream_url: "wss://flovy.tekyerden.co/flovy-stream/{token}" } dön
4. Widget → Node.js WebSocket bağlanır
5. Node.js → Laravel'e /api/stream/resolve çağrısı → prompt alır
6. Node.js → Gemini streamGenerateContent → token-token widget'a
7. Stream bitince → Node.js → /api/stream/done
8. FastAPI: cevabı DB'ye yaz + tool_calls varsa execute et
```

---

## Widget Settings Response

```json
{
  "widget_id": "...",
  "name": "Ana Site Widget",
  "is_online": true,
  "ai_enabled": true,
  "streaming_enabled": true,
  "product_count": 47,
  "reverb": {
    "key": "...",
    "host": "...",
    "port": 8080,
    "scheme": "https"
  },
  "settings": {
    "theme_color": "#6366f1",
    "position": "bottom-right",
    "welcome_message": "Merhaba! Size nasıl yardımcı olabilirim?",
    "brand_name": "Ornek Mağaza",
    "kvkk_url": "https://ornek.com/kvkk"
  }
}
```

---

## İş Kuralları

- Domain doğrulama: `Origin` header'ı `allowed_domains` listesiyle eşleşmeli. Eşleşmiyorsa 403.
- `session_token` UUID format, Redis'te `chat:session:{token}` → `session_id` map'i (24 saat TTL).
- Idempotency: `Cache::add("msg:{session_id}:{md5(trim(message))}", 1, 30s)` — aynı mesaj 30s içinde tekrar gelirse duplicate döner, AI iki kez tetiklenmez.
- Kural (RULE) yanıtı → streaming yok, direkt JSON cevap.
- LLM yanıtı → streaming, `stream_token` + WS URL döner.
- Streaming düşerse: Widget 5s sonra fallback olarak HTTP polling'e geçer.
- Operatör "AI'dan devral" dediğinde: `session.status = escalated`, sonraki mesajlar AI'a gitmiyor, sadece operatöre.
