Osadzenie widgetu flowhelp — instrukcja dla klienta (kontrakt v1)
Wersja 1.2 · 2026-09-17 · Źródło prawdy:
packages/config/src/widget-embed.tsi DECYZJE.md ADR-026 (osadzenie), ADR-027 (markdown), ADR-028 (identify), ADR-029 (oceny, formularz kontaktowy, podgląd w panelu). Wszystko w tej instrukcji jest zamrożone — snippet wklejony dziś będzie działał z każdą przyszłą wersją widgetu.
1. Snippet
Jeden tag, bez kodu inline, przed </body>:
<script async src="https://cdn.flowhelp.ai/loader.js" data-key="pk_live_…"></script>| Atrybut | Wymagany | Znaczenie |
|---|---|---|
data-key | tak | klucz publiczny agenta (pk_live_ + 32 znaki hex) — z panelu |
data-locale | nie | wymuszony język UI (pl/en); brak = konfiguracja agenta / przeglądarka |
data-api | nie | tylko dev/staging; produkcja używa https://app.flowhelp.ai |
Klucz publiczny z założenia jest widoczny w HTML-u. Nie chroni go tajność, tylko allowlista domen (poniżej), limity i dobowy limit kredytów agenta.
2. Allowlista domen (Origin)
Widget działa wyłącznie na domenach wpisanych w konfiguracji agenta. Semantyka:
https://sklep.pl— dokładnie ta domena (port domyślny),*.sklep.pl— każda poddomena po https (www.,blog.), bez apeksu — apex wpisuje się osobno,http://localhost:3000— tylko jawnie, do developmentu;http://dla innych hostów nigdy nie działa.
Strona spoza allowlisty: przeglądarka blokuje wywołanie (serwer odpowiada 403 bez nagłówków CORS), widget nie pojawia się i wypisuje błąd w konsoli. Nic nie kosztuje.
3. Content-Security-Policy gospodarza
Jeśli strona ma CSP, potrzebne są dokładnie dwa źródła:
script-src … https://cdn.flowhelp.ai;
connect-src … https://app.flowhelp.ai;Widget nie używa inline JS, eval, ani zewnętrznych zasobów poza tymi dwoma hostami. Style żyją w Shadow DOM (adoptedStyleSheets), więc style-src nie musi być zmieniany. Loader ma stałą ścieżkę (cache 5 min); core jest adresowany hashem z integrity (SRI) i crossorigin.
4. Publiczne API
Wywołania można kolejkować przed załadowaniem widgetu — zostaną wykonane w kolejności:
window.flowhelp = window.flowhelp || [];
window.flowhelp.push(['open']);
window.flowhelp.push(['on', 'ready', () => console.log('widget gotowy')]);| Metoda | Działanie |
|---|---|
open / close / toggle / isOpen | okno czatu |
sendMessage(text) | wysyła wiadomość jak użytkownik |
setLocale('pl'|'en') | język UI |
setContext(obj) | kontekst strony (v0: zapamiętywany, jeszcze nieużywany) |
on(event, cb) / off(event, cb) | zdarzenia: ready, open, close, message:sent, message:received, error |
grantConsent() / revokeConsent() | zgoda (patrz §6) |
reset() | kasuje identyfikator odwiedzającego, bieżącą rozmowę i tożsamość z identify |
identify({ userId, hash }) | tożsamość zalogowanego użytkownika z podpisem HMAC (§5); identify(null) = wylogowanie |
init | bez efektu (widget startuje sam) |
Nieznana metoda = ostrzeżenie w konsoli, bez wyjątku.
5. Zalogowani użytkownicy — identify() z podpisem HMAC
Jeśli Twoja strona wie, kto jest zalogowany, możesz to przekazać widgetowi. Rozmowy takiego użytkownika będą w panelu podpisane jego identyfikatorem, a przy włączonej opcji „wymagaj zweryfikowanej tożsamości" anonimowi odwiedzający nie będą mogli pisać.
Widget nie liczy podpisu i nie zna sekretu. Podpis liczy Twój backend, z sekretu HMAC agenta (panel → Ustawienia → Widget; do Etapu 4 przekazywany przez nas):
hash = hex( HMAC-SHA256( key = sekret_agenta, message = userId ) )Node.js:
const { createHmac } = require('node:crypto');
const hash = createHmac('sha256', process.env.FLOWHELP_HMAC_SECRET).update(user.id).digest('hex');PHP:
$hash = hash_hmac('sha256', $user->id, getenv('FLOWHELP_HMAC_SECRET'));Python:
import hmac, hashlib
hash_ = hmac.new(SECRET.encode(), user_id.encode(), hashlib.sha256).hexdigest()Na stronie (może być przed załadowaniem widgetu — trafi do kolejki):
window.flowhelp = window.flowhelp || [];
window.flowhelp.push(['identify', { userId: 'u_123', hash: '<64 znaki hex>' }]);
// po wylogowaniu:
window.flowhelp.push(['identify', null]);Zasady:
userIdto Twój wewnętrzny identyfikator (albo e-mail): 1–200 drukowalnych znaków ASCII bez spacji. Imię i nazwisko z polskimi znakami nie przejdzie — wartość jedzie w nagłówku HTTP. Zły ładunek widget odrzuca z ostrzeżeniem w konsoli; poprzednia tożsamość (jeśli była) zostaje bez zmian. Wylogowanie to wyłącznieidentify(null); zmiana użytkownika (albo wylogowanie) zaczyna nową rozmowę — poprzedni transkrypt znika z okna.- Podpis jest wysyłany przy każdej wiadomości i weryfikowany serwerowo stałym czasem. Niezgodny podpis =
403, zanim cokolwiek zostanie policzone. - Tożsamość nie zmienia limitów ani właściciela rozmowy — te idą po identyfikatorze odwiedzającego z przeglądarki. Rozmowa zaczęta anonimowo dostaje tożsamość po
identify()w jej trakcie. - Widget wysyła tylko
userIdi podpis.email/namenie są dziś przekazywane. - Sekret trzymaj po stronie serwera. Jeśli wyciekł — zmiana sekretu unieważnia wszystkie podpisy naraz.
6. Zgoda (GDPR) i disclaimer AI
consentMode z konfiguracji agenta:
open— widget działa od razu,deferred(domyślny dla UE) — przycisk widoczny, pierwsza wiadomość czeka na zgodę (baner w oknie albograntConsent()),blocked— nic nie renderuje, dopóki strona nie wywołagrantConsent().
Zgoda nie jest utrwalana między wizytami (v0). Identyfikator odwiedzającego to UUID w localStorage (flowhelp:visitor:<klucz>), bez cookies. Pierwsza wiadomość zawsze zawiera disclaimer AI (EU AI Act art. 50) — edytowalny w panelu, nie wyłączalny.
7. Limity i błędy
| Sytuacja | Zachowanie |
|---|---|
| 20 wiadomości / 5 min od jednego odwiedzającego (domyślnie; konfigurowalne per agent) | 429, widget blokuje pole na Retry-After sekund i pokazuje komunikat |
| druga wiadomość, zanim skończy się poprzednia odpowiedź | 429 (Retry-After: 5) |
| dobowy limit kredytów agenta wyczerpany | 429 do północy UTC |
agent wstrzymany / nie w statusie ready | 403, widget się nie pojawia |
agent wymaga tożsamości, a strona nie wywołała identify() | wiadomość nie wychodzi; komunikat „Zaloguj się na tej stronie, aby korzystać z czatu." |
niezgodny podpis identify() | 403 przed odpowiedzią, zero kosztu |
| pytanie spoza bazy wiedzy | odpowiedź „Nie mam tej informacji w swojej bazie wiedzy." — bez zmyślania |
8. Formatowanie odpowiedzi (markdown)
Odpowiedzi asystenta są renderowane z markdownu: akapity, pogrubienie, kursywa, ~~przekreślenie~~, kod i bloki kodu, listy (także zagnieżdżone), cytaty, linia pozioma, tabele, linki. Nagłówki # są renderowane jako pogrubione akapity, nie jako <h1> — nie wpływają na strukturę Twojej strony.
Czego widget nie renderuje, bo tekst modelu nigdy nie staje się HTML-em (ADR-027): surowego HTML-a (widoczny jako tekst), obrazków (zostaje opis), linków z protokołem innym niż http/https/mailto (zostają tekstem). Każdy link i cytat [n] otwiera się w nowej karcie z rel="noopener noreferrer".
9. Wygląd, motyw, RTL, mobile
- Kolor główny, zaokrąglenie i czcionka z konfiguracji agenta; motyw
light/dark/auto(zaprefers-color-scheme). Czcionka spoza konfiguracji to stos systemowy — widget nie pobiera fontów. - Style Twojej strony nie wchodzą do widgetu (Shadow DOM z pełnym resetem), a style widgetu nie wychodzą na stronę. Nawet globalne reguły w rodzaju
* { letter-spacing: 2px }nie mają wpływu. - Strona z
dir="rtl"dostaje lustrzany układ nagłówka i kompozytora; kierunek tekstu wiadomości wynika z ich treści (dir="auto"), więc odpowiedź po arabsku na polskiej stronie i odwrotnie wyglądają poprawnie. Róg ekranu (bottom-right/bottom-left) jest wyborem z konfiguracji i nie odwraca się. - Poniżej 640 px szerokości okno czatu wypełnia ekran i śledzi klawiaturę ekranową (
visualViewport) — pole do pisania nie chowa się pod klawiaturą. z-indexhosta to 2 147 483 000; nagłówki „sticky" i stopki z wysokimz-indexnie przykrywają widgetu.
10. Zgodność z frameworkami i CMS-ami (zweryfikowane 2026-09-14)
Test wycieku stylów w obie strony (docs/qa/etap3-zamkniecie-2026-09-14/): dwa przebiegi tej samej strony (bez widgetu i z widgetem), porównanie sond gospodarza i elementów widgetu, z-index, mobile 390 px, zero błędów konsoli.
| Środowisko | Wynik |
|---|---|
Bootstrap 5.3 (navbar fixed-top, stopka z-1050, agresywny arkusz globalny) | 0 różnic w sondach gospodarza, style widgetu nietknięte |
| Tailwind (Play CDN, preflight) | jak wyżej |
| WordPress 7.1 + Elementor 4.2.4 + Hello Elementor 3.5.1 | widget ładuje się przez snippet w wp_footer, style Elementora i gospodarza bez zmian, zero błędów konsoli |
Strona dir="rtl" | nagłówek i kompozytor lustrzane, róg ekranu bez zmian |
Jeśli Twój motyw ma reguły globalne w rodzaju * { letter-spacing: 2px } — nie mają wpływu (widget ma pełny reset z !important na hoście). Jedyne, czego widget potrzebuje od strony: dwa hosty w CSP (§3); X-Frame-Options Twojej strony nie ma znaczenia, bo widget nie jest iframe'em.
11. Test osadzenia
Strona QA: https://flowhelp.ai/qa/widget-embed-test.html (kod: docs/qa/). Sprawdza, że loader ładuje core, okno otwiera się z kolejki, odpowiedź ma cytat, a style gospodarza pozostają nietknięte. Ten sam plik serwowany z innego origin nie uruchamia widgetu.
12. Ocena odpowiedzi i formularz kontaktowy (ADR-029)
Obie rzeczy włącza się w panelu; w snippecie nic się nie zmienia i nie ma tu żadnego API do wołania ze strony.
Kciuki pod odpowiedzią
Gdy agent ma włączone zbieranie ocen, pod każdą odpowiedzią asystenta są dwa przyciski (👍/👎). Kliknięcie tego samego drugi raz cofa ocenę. Oceny widać w panelu w Rozmowach (filtr „kciuk w dół") i we Wnioskach — to jedyny sygnał jakości pochodzący od Twoich odwiedzających.
Wyłączenie ocen w panelu wyłącza je naprawdę: przyciski znikają, a żądanie wysłane mimo to dostaje 403. Kciuk nie wymaga zgody na cookies ani logowania — przypisujemy go do tej samej rozmowy, w której powstała odpowiedź, i tylko ta przeglądarka może ją ocenić.
Formularz kontaktowy (lead) i „porozmawiaj z człowiekiem"
Formularz pokazuje się według wyzwalacza ustawionego w panelu:
| Wyzwalacz | Kiedy widget pokazuje formularz |
|---|---|
after_messages | po N odpowiedziach asystenta (N z panelu) |
on_fallback | zaraz po odpowiedzi „nie mam tej informacji" — czyli tam, gdzie asystent nie pomógł |
manual | nigdy sam; w nagłówku okna jest przycisk z tytułem formularza |
Pola (etykiety, typy, wymagalność — maks. 8) pochodzą z panelu. Zamknięcie formularza krzyżykiem wyłącza wyzwalacz automatyczny do końca tej rozmowy — nie wraca po każdej wiadomości. Po wysłaniu odwiedzający widzi Twój tekst podziękowania.
Eskalacja to ten sam formularz z dodatkowym polem „Wiadomość" i własnym przyciskiem w nagłówku (jego treść ustawiasz w panelu). Rozmowa dostaje wtedy znacznik „eskalowana", który zasila wskaźnik deflekcji we Wnioskach.
Każdy wysłany formularz trafia do panelu i na e-mail: adres z ustawień akcji, a gdy go nie ma — adres powiadomień workspace'u, a gdy i tego nie ma — e-mail właściciela. Wiadomość zawiera pola formularza i ostatnie 10 wiadomości rozmowy, żeby dało się odpowiedzieć bez wchodzenia do panelu.
Limit: 5 formularzy na odwiedzającego na dobę (doba UTC). To bezpiecznik Twojej skrzynki i naszej reputacji nadawcy — nie da się go podnieść w panelu. Odbity formularz dostaje 429 z komunikatem, treść zostaje w polach.
13. Podgląd w panelu (widget-preview.js)
Sekcja „Wygląd" w panelu pokazuje ten sam widget, który chodzi na Twojej stronie — to nie jest osobna makieta. Panel ładuje https://cdn.flowhelp.ai/widget-preview.js, czyli drugie wejście tego samego kodu, i montuje widget w ramce formularza zamiast w rogu ekranu.
Co się różni od widgetu na stronie:
- transport jest atrapą — podgląd nie wysyła pytań do modelu i nie kosztuje kredytów; po ~0,6 s pokazuje stałą odpowiedź z przykładowym cytatem. Do rozmowy z własnym agentem służy Playground, który liczy kredyty;
- kciuk i formularz w podglądzie nic nie zapisują — mają pokazać układ, nie tworzyć leadów z panelu;
- host dostaje atrybut
data-previewi jest pozycjonowany wewnątrz ramki (position: absolute; inset: 0), więc kontener musi mieć własną wysokość.
Podgląd nie jest częścią kontraktu osadzenia: to narzędzie panelu i nie wklejaj go na swoją stronę — nie rozmawia z Twoim agentem. Gdy skrypt podglądu nie załaduje się w 5 s (CSP panelu, CDN), panel pokazuje statyczną kartę z kolorem i powitaniem, a formularz działa dalej.