victor34593993 commited on
Commit
30481d2
·
verified ·
1 Parent(s): 6318270

widget: single-bubble home screen for chat+whatsapp both mode

Browse files
Files changed (2) hide show
  1. app/static/widget.js +66 -6
  2. tests/test_widget_assets.py +18 -0
app/static/widget.js CHANGED
@@ -58,6 +58,8 @@
58
  speak: "Hablar por voz",
59
  listening: "Escuchando\u2026",
60
  micDenied: "Permiso de micr\u00f3fono denegado",
 
 
61
  starters: ["\u00bfQu\u00e9 ofrec\u00e9is?", "Quiero m\u00e1s informaci\u00f3n", "Hablar con una persona"]
62
  },
63
  en: {
@@ -89,6 +91,8 @@
89
  speak: "Speak",
90
  listening: "Listening\u2026",
91
  micDenied: "Microphone permission denied",
 
 
92
  starters: ["What do you offer?", "I want more information", "Talk to a person"]
93
  },
94
  fr: {
@@ -120,6 +124,8 @@
120
  speak: "Parler",
121
  listening: "\u00c9coute\u2026",
122
  micDenied: "Acc\u00e8s au microphone refus\u00e9",
 
 
123
  starters: ["Que proposez-vous ?", "Je souhaite plus d'informations", "Parler \u00e0 une personne"]
124
  },
125
  de: {
@@ -151,6 +157,8 @@
151
  speak: "Sprechen",
152
  listening: "H\u00f6re zu\u2026",
153
  micDenied: "Mikrofonzugriff verweigert",
 
 
154
  starters: ["Was bieten Sie an?", "Ich m\u00f6chte mehr Informationen", "Mit einer Person sprechen"]
155
  },
156
  it: {
@@ -182,6 +190,8 @@
182
  speak: "Parla",
183
  listening: "In ascolto\u2026",
184
  micDenied: "Accesso al microfono negato",
 
 
185
  starters: ["Cosa offrite?", "Vorrei maggiori informazioni", "Parlare con una persona"]
186
  },
187
  pt: {
@@ -213,6 +223,8 @@
213
  speak: "Falar",
214
  listening: "A ouvir\u2026",
215
  micDenied: "Permiss\u00e3o de microfone negada",
 
 
216
  starters: ["O que oferecem?", "Quero mais informa\u00e7\u00f5es", "Falar com uma pessoa"]
217
  }
218
  };
@@ -301,6 +313,14 @@
301
  "#ssb-wa.solo{bottom:20px;right:20px;width:auto;height:60px;border-radius:30px;padding:0 22px 0 18px;box-shadow:0 8px 24px rgba(37,211,102,.45)}" +
302
  "#ssb-wa.solo svg{width:30px;height:30px}" +
303
  "#ssb-wa .ssb-wa-lbl{color:#fff;font-family:system-ui,-apple-system,Segoe UI,sans-serif;font-weight:700;font-size:15px}" +
 
 
 
 
 
 
 
 
304
  "@keyframes ssbpulse{0%{box-shadow:0 0 0 0 " + c + "66}70%{box-shadow:0 0 0 16px " + c + "00}100%{box-shadow:0 0 0 0 " + c + "00}}" +
305
  "#ssb-panel{position:fixed;bottom:20px;right:20px;width:380px;max-width:94vw;height:600px;max-height:80vh;background:#fff;border-radius:18px;box-shadow:0 18px 60px rgba(0,0,0,.3);display:none;flex-direction:column;overflow:hidden;z-index:99999;font-family:system-ui,-apple-system,Segoe UI,sans-serif;opacity:0;transform:translateY(16px) scale(.98);transition:opacity .22s,transform .22s}" +
306
  "#ssb-panel.open{display:flex;opacity:1;transform:none}" +
@@ -396,13 +416,18 @@
396
  // Embed mode chosen by the store: "chat" | "whatsapp" | "both".
397
  var mode = cfg.widget_mode || "chat";
398
  var waDigits = cfg.whatsapp_number ? String(cfg.whatsapp_number).replace(/[^0-9]/g, "") : "";
399
- var showWa = (mode === "whatsapp" || mode === "both") && waDigits;
400
  var wantChat = (mode === "chat" || mode === "both");
401
- var showChat = wantChat || !showWa; // always show something (fallback to chat)
 
 
 
 
 
 
402
 
403
  // WhatsApp launcher: direct link to wa.me. "solo" = primary spot when it's
404
  // the only launcher; otherwise it sits stacked above the chat bubble.
405
- if (showWa) {
406
  var wab = document.createElement("a");
407
  wab.id = "ssb-wa";
408
  if (!showChat) wab.className = "solo";
@@ -419,6 +444,16 @@
419
  launch.id = "ssb-launch"; launch.setAttribute("aria-label", T.open);
420
  launch.innerHTML = '<span class="ssb-pulse"></span>' + ICON_AGENT + '<span class="ssb-dot"></span><span class="ssb-badge"></span>';
421
 
 
 
 
 
 
 
 
 
 
 
422
  var panel = document.createElement("div");
423
  panel.id = "ssb-panel";
424
  panel.innerHTML =
@@ -427,6 +462,8 @@
427
  '<div class="ssb-status"><i></i>' + T.status + '</div></div>' +
428
  '<button id="ssb-mute" type="button"></button>' +
429
  '<button class="ssb-x" aria-label="' + T.close + '">&times;</button></div>' +
 
 
430
  '<div id="ssb-msgs"></div>' +
431
  '<div id="ssb-att"></div>' +
432
  '<form id="ssb-form">' +
@@ -435,7 +472,7 @@
435
  '<input id="ssb-input" type="text" autocomplete="off" placeholder="' + T.input + '" />' +
436
  (HAS_SR ? '<button id="ssb-mic" type="button" aria-label="' + T.speak + '" title="' + T.speak + '">' + ICON_MIC + '</button>' : '') +
437
  '<button id="ssb-send" type="submit" aria-label="' + T.send + '">' + ICON_SEND + '</button></form>' +
438
- '<div id="ssb-foot">' + T.footer + '</div>';
439
 
440
  document.body.appendChild(launch);
441
  document.body.appendChild(panel);
@@ -445,6 +482,25 @@
445
  var form = panel.querySelector("#ssb-form");
446
  var input = panel.querySelector("#ssb-input");
447
  var greeted = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
 
449
  // ---- Visitor uploads (Phase 3): paperclip -> chip -> ids on next send ----
450
  var attBox = panel.querySelector("#ssb-att");
@@ -744,8 +800,12 @@
744
  panel.classList.toggle("open");
745
  if (panel.classList.contains("open")) {
746
  setBadge(0); stopFlash();
747
- if (!greeted) { botMsg(cfg.welcome); renderStarters(); greeted = true; }
748
- input.focus();
 
 
 
 
749
  }
750
  }
751
  launch.addEventListener("click", toggle);
 
58
  speak: "Hablar por voz",
59
  listening: "Escuchando\u2026",
60
  micDenied: "Permiso de micr\u00f3fono denegado",
61
+ homeTitle: "\u00bfC\u00f3mo prefieres escribirnos?",
62
+ homeChat: "Chatear aqu\u00ed", homeWa: "Seguir por WhatsApp",
63
  starters: ["\u00bfQu\u00e9 ofrec\u00e9is?", "Quiero m\u00e1s informaci\u00f3n", "Hablar con una persona"]
64
  },
65
  en: {
 
91
  speak: "Speak",
92
  listening: "Listening\u2026",
93
  micDenied: "Microphone permission denied",
94
+ homeTitle: "How would you like to talk to us?",
95
+ homeChat: "Chat here", homeWa: "Continue on WhatsApp",
96
  starters: ["What do you offer?", "I want more information", "Talk to a person"]
97
  },
98
  fr: {
 
124
  speak: "Parler",
125
  listening: "\u00c9coute\u2026",
126
  micDenied: "Acc\u00e8s au microphone refus\u00e9",
127
+ homeTitle: "Comment pr\u00e9f\u00e9rez-vous nous \u00e9crire ?",
128
+ homeChat: "Discuter ici", homeWa: "Continuer sur WhatsApp",
129
  starters: ["Que proposez-vous ?", "Je souhaite plus d'informations", "Parler \u00e0 une personne"]
130
  },
131
  de: {
 
157
  speak: "Sprechen",
158
  listening: "H\u00f6re zu\u2026",
159
  micDenied: "Mikrofonzugriff verweigert",
160
+ homeTitle: "Wie m\u00f6chten Sie uns schreiben?",
161
+ homeChat: "Hier chatten", homeWa: "Weiter auf WhatsApp",
162
  starters: ["Was bieten Sie an?", "Ich m\u00f6chte mehr Informationen", "Mit einer Person sprechen"]
163
  },
164
  it: {
 
190
  speak: "Parla",
191
  listening: "In ascolto\u2026",
192
  micDenied: "Accesso al microfono negato",
193
+ homeTitle: "Come preferisci scriverci?",
194
+ homeChat: "Chatta qui", homeWa: "Continua su WhatsApp",
195
  starters: ["Cosa offrite?", "Vorrei maggiori informazioni", "Parlare con una persona"]
196
  },
197
  pt: {
 
223
  speak: "Falar",
224
  listening: "A ouvir\u2026",
225
  micDenied: "Permiss\u00e3o de microfone negada",
226
+ homeTitle: "Como prefere falar connosco?",
227
+ homeChat: "Conversar aqui", homeWa: "Continuar no WhatsApp",
228
  starters: ["O que oferecem?", "Quero mais informa\u00e7\u00f5es", "Falar com uma pessoa"]
229
  }
230
  };
 
313
  "#ssb-wa.solo{bottom:20px;right:20px;width:auto;height:60px;border-radius:30px;padding:0 22px 0 18px;box-shadow:0 8px 24px rgba(37,211,102,.45)}" +
314
  "#ssb-wa.solo svg{width:30px;height:30px}" +
315
  "#ssb-wa .ssb-wa-lbl{color:#fff;font-family:system-ui,-apple-system,Segoe UI,sans-serif;font-weight:700;font-size:15px}" +
316
+ "#ssb-home{padding:20px 16px;display:flex;flex-direction:column;gap:10px;flex:1;justify-content:center}" +
317
+ "#ssb-home .ssb-home-title{font:inherit;font-size:14.5px;color:#555;text-align:center;margin-bottom:6px}" +
318
+ ".ssb-home-btn{display:flex;align-items:center;justify-content:center;gap:9px;border-radius:14px;padding:14px 16px;font:inherit;font-size:14.5px;font-weight:700;cursor:pointer;border:none;transition:transform .15s}" +
319
+ ".ssb-home-btn:hover{transform:scale(1.02)}" +
320
+ ".ssb-home-btn svg{width:20px;height:20px;flex:none;fill:#fff}" +
321
+ ".ssb-home-btn-chat{background:" + c + ";color:#fff}" +
322
+ ".ssb-home-btn-wa{background:#25D366;color:#fff}" +
323
+ "#ssb-chatarea{display:flex;flex-direction:column;flex:1;min-height:0}" +
324
  "@keyframes ssbpulse{0%{box-shadow:0 0 0 0 " + c + "66}70%{box-shadow:0 0 0 16px " + c + "00}100%{box-shadow:0 0 0 0 " + c + "00}}" +
325
  "#ssb-panel{position:fixed;bottom:20px;right:20px;width:380px;max-width:94vw;height:600px;max-height:80vh;background:#fff;border-radius:18px;box-shadow:0 18px 60px rgba(0,0,0,.3);display:none;flex-direction:column;overflow:hidden;z-index:99999;font-family:system-ui,-apple-system,Segoe UI,sans-serif;opacity:0;transform:translateY(16px) scale(.98);transition:opacity .22s,transform .22s}" +
326
  "#ssb-panel.open{display:flex;opacity:1;transform:none}" +
 
416
  // Embed mode chosen by the store: "chat" | "whatsapp" | "both".
417
  var mode = cfg.widget_mode || "chat";
418
  var waDigits = cfg.whatsapp_number ? String(cfg.whatsapp_number).replace(/[^0-9]/g, "") : "";
 
419
  var wantChat = (mode === "chat" || mode === "both");
420
+ var hasWa = (mode === "whatsapp" || mode === "both") && waDigits;
421
+ // "both" with a real WhatsApp number -> ONE bubble that opens a home screen
422
+ // where the visitor picks the channel, instead of two separate floating
423
+ // launchers. "whatsapp"-only keeps the standalone floating WA button.
424
+ var showHome = mode === "both" && hasWa;
425
+ var showWaStandalone = hasWa && !showHome;
426
+ var showChat = wantChat || !hasWa; // always show something (fallback to chat)
427
 
428
  // WhatsApp launcher: direct link to wa.me. "solo" = primary spot when it's
429
  // the only launcher; otherwise it sits stacked above the chat bubble.
430
+ if (showWaStandalone) {
431
  var wab = document.createElement("a");
432
  wab.id = "ssb-wa";
433
  if (!showChat) wab.className = "solo";
 
444
  launch.id = "ssb-launch"; launch.setAttribute("aria-label", T.open);
445
  launch.innerHTML = '<span class="ssb-pulse"></span>' + ICON_AGENT + '<span class="ssb-dot"></span><span class="ssb-badge"></span>';
446
 
447
+ // "both" mode: a home screen inside the panel lets the visitor pick chat vs
448
+ // WhatsApp, instead of two separate floating buttons on the page.
449
+ var homeHtml = showHome
450
+ ? '<div id="ssb-home"><div class="ssb-home-title">' + esc(T.homeTitle) + '</div>' +
451
+ '<button id="ssb-home-chat" type="button" class="ssb-home-btn ssb-home-btn-chat">' +
452
+ ICON_AGENT + '<span>' + esc(T.homeChat) + '</span></button>' +
453
+ '<button id="ssb-home-wa" type="button" class="ssb-home-btn ssb-home-btn-wa">' +
454
+ ICON_WA + '<span>' + esc(T.homeWa) + '</span></button></div>'
455
+ : '';
456
+
457
  var panel = document.createElement("div");
458
  panel.id = "ssb-panel";
459
  panel.innerHTML =
 
462
  '<div class="ssb-status"><i></i>' + T.status + '</div></div>' +
463
  '<button id="ssb-mute" type="button"></button>' +
464
  '<button class="ssb-x" aria-label="' + T.close + '">&times;</button></div>' +
465
+ homeHtml +
466
+ '<div id="ssb-chatarea"' + (showHome ? ' style="display:none"' : '') + '>' +
467
  '<div id="ssb-msgs"></div>' +
468
  '<div id="ssb-att"></div>' +
469
  '<form id="ssb-form">' +
 
472
  '<input id="ssb-input" type="text" autocomplete="off" placeholder="' + T.input + '" />' +
473
  (HAS_SR ? '<button id="ssb-mic" type="button" aria-label="' + T.speak + '" title="' + T.speak + '">' + ICON_MIC + '</button>' : '') +
474
  '<button id="ssb-send" type="submit" aria-label="' + T.send + '">' + ICON_SEND + '</button></form>' +
475
+ '<div id="ssb-foot">' + T.footer + '</div></div>';
476
 
477
  document.body.appendChild(launch);
478
  document.body.appendChild(panel);
 
482
  var form = panel.querySelector("#ssb-form");
483
  var input = panel.querySelector("#ssb-input");
484
  var greeted = false;
485
+ var homeEl = panel.querySelector("#ssb-home");
486
+ var chatarea = panel.querySelector("#ssb-chatarea");
487
+
488
+ // "both" mode: visitor picks a channel on open. Chat -> reveal the normal
489
+ // chat area (and greet, same as chat-only mode). WhatsApp -> open wa.me;
490
+ // the panel stays open behind it so they can still switch to chat.
491
+ function chooseChat() {
492
+ if (homeEl) homeEl.style.display = "none";
493
+ if (chatarea) chatarea.style.display = "flex";
494
+ if (!greeted) { botMsg(cfg.welcome); renderStarters(); greeted = true; }
495
+ input.focus();
496
+ }
497
+ function chooseWa() {
498
+ window.open("https://wa.me/" + waDigits, "_blank", "noopener");
499
+ }
500
+ if (homeEl) {
501
+ panel.querySelector("#ssb-home-chat").addEventListener("click", chooseChat);
502
+ panel.querySelector("#ssb-home-wa").addEventListener("click", chooseWa);
503
+ }
504
 
505
  // ---- Visitor uploads (Phase 3): paperclip -> chip -> ids on next send ----
506
  var attBox = panel.querySelector("#ssb-att");
 
800
  panel.classList.toggle("open");
801
  if (panel.classList.contains("open")) {
802
  setBadge(0); stopFlash();
803
+ // With a home screen, wait for the visitor to pick a channel before
804
+ // greeting/focusing the input (avoids a wasted bot turn pre-choice).
805
+ if (!homeEl) {
806
+ if (!greeted) { botMsg(cfg.welcome); renderStarters(); greeted = true; }
807
+ input.focus();
808
+ }
809
  }
810
  }
811
  launch.addEventListener("click", toggle);
tests/test_widget_assets.py CHANGED
@@ -17,6 +17,24 @@ def test_widget_js_supports_signed_proxy_mode():
17
  assert "localStorage" in js
18
 
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  def test_block_is_app_embed_loading_widget_in_proxy_mode():
21
  liquid = BLOCK.read_text(encoding="utf-8")
22
  assert "{% schema %}" in liquid
 
17
  assert "localStorage" in js
18
 
19
 
20
+ def test_widget_both_mode_shows_a_single_bubble_with_channel_picker():
21
+ """'both' (chat+WhatsApp) must be ONE bubble that opens a home screen letting
22
+ the visitor pick a channel -- not two separate floating launchers. Verified
23
+ behaviorally in a real headless browser during development (22/22 assertions:
24
+ single bubble, no premature greet, correct wa.me resolution, chat/whatsapp-only
25
+ modes unaffected); this structural test guards the markers that behavior relies
26
+ on so a future edit can't silently remove them."""
27
+ js = WIDGET.read_text(encoding="utf-8")
28
+ assert 'var showHome = mode === "both" && hasWa' in js
29
+ assert 'var showWaStandalone = hasWa && !showHome' in js
30
+ assert 'id="ssb-home"' in js and 'id="ssb-chatarea"' in js
31
+ assert 'id="ssb-home-chat"' in js and 'id="ssb-home-wa"' in js
32
+ assert "chooseChat" in js and "chooseWa" in js
33
+ # every language block carries the picker copy (real i18n, not just Spanish)
34
+ for key in ("homeTitle", "homeChat", "homeWa"):
35
+ assert js.count(key + ":") == 6, f"{key} must be defined in all 6 languages"
36
+
37
+
38
  def test_block_is_app_embed_loading_widget_in_proxy_mode():
39
  liquid = BLOCK.read_text(encoding="utf-8")
40
  assert "{% schema %}" in liquid