widget: switch-channel back button fix
Browse files- app/static/widget.js +27 -3
- tests/test_widget_assets.py +10 -0
app/static/widget.js
CHANGED
|
@@ -59,6 +59,7 @@
|
|
| 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 |
},
|
|
@@ -92,6 +93,7 @@
|
|
| 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 |
},
|
|
@@ -125,6 +127,7 @@
|
|
| 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 |
},
|
|
@@ -158,6 +161,7 @@
|
|
| 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 |
},
|
|
@@ -191,6 +195,7 @@
|
|
| 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 |
},
|
|
@@ -224,6 +229,7 @@
|
|
| 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 |
}
|
|
@@ -263,6 +269,7 @@
|
|
| 263 |
|
| 264 |
// Paperclip (attach a PDF/photo to the conversation).
|
| 265 |
var ICON_CLIP = '<svg viewBox="0 0 24 24"><path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z"/></svg>';
|
|
|
|
| 266 |
var ICON_MIC = '<svg viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.49 6-3.31 6-6.72h-1.7z"/></svg>';
|
| 267 |
|
| 268 |
// Voice input: browser-native speech recognition (free, no external service).
|
|
@@ -347,6 +354,8 @@
|
|
| 347 |
"#ssb-head .ssb-x{margin-left:auto;background:transparent;border:0;color:#fff;font-size:24px;cursor:pointer;opacity:.85;line-height:1}" +
|
| 348 |
"#ssb-mute{margin-left:auto;width:32px;height:32px;background:transparent;border:0;cursor:pointer;opacity:.85;display:flex;align-items:center;justify-content:center;padding:0;flex:none}" +
|
| 349 |
"#ssb-mute:hover{opacity:1}#ssb-mute svg{width:20px;height:20px;fill:#fff}" +
|
|
|
|
|
|
|
| 350 |
"#ssb-head .ssb-x{margin-left:6px}" +
|
| 351 |
"#ssb-launch .ssb-badge{position:absolute;top:-4px;right:-4px;min-width:20px;height:20px;box-sizing:border-box;padding:0 5px;border-radius:10px;background:#e11d48;border:2px solid #fff;color:#fff;font-family:system-ui,-apple-system,Segoe UI,sans-serif;font-size:11px;font-weight:700;line-height:16px;text-align:center;display:none}" +
|
| 352 |
"#ssb-msgs{flex:1;overflow-y:auto;padding:16px 14px;background:#f4f5f7;display:flex;flex-direction:column;gap:10px}" +
|
|
@@ -460,6 +469,8 @@
|
|
| 460 |
'<div id="ssb-head"><div class="ssb-av"></div><div>' +
|
| 461 |
'<div class="ssb-name">' + esc(name) + '</div>' +
|
| 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 + '">×</button></div>' +
|
| 465 |
homeHtml +
|
|
@@ -484,22 +495,33 @@
|
|
| 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 ----
|
|
@@ -801,8 +823,10 @@
|
|
| 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 |
-
|
|
|
|
|
|
|
| 806 |
if (!greeted) { botMsg(cfg.welcome); renderStarters(); greeted = true; }
|
| 807 |
input.focus();
|
| 808 |
}
|
|
|
|
| 59 |
listening: "Escuchando\u2026",
|
| 60 |
micDenied: "Permiso de micr\u00f3fono denegado",
|
| 61 |
homeTitle: "\u00bfC\u00f3mo prefieres escribirnos?",
|
| 62 |
+
switchChannel: "Cambiar canal",
|
| 63 |
homeChat: "Chatear aqu\u00ed", homeWa: "Seguir por WhatsApp",
|
| 64 |
starters: ["\u00bfQu\u00e9 ofrec\u00e9is?", "Quiero m\u00e1s informaci\u00f3n", "Hablar con una persona"]
|
| 65 |
},
|
|
|
|
| 93 |
listening: "Listening\u2026",
|
| 94 |
micDenied: "Microphone permission denied",
|
| 95 |
homeTitle: "How would you like to talk to us?",
|
| 96 |
+
switchChannel: "Switch channel",
|
| 97 |
homeChat: "Chat here", homeWa: "Continue on WhatsApp",
|
| 98 |
starters: ["What do you offer?", "I want more information", "Talk to a person"]
|
| 99 |
},
|
|
|
|
| 127 |
listening: "\u00c9coute\u2026",
|
| 128 |
micDenied: "Acc\u00e8s au microphone refus\u00e9",
|
| 129 |
homeTitle: "Comment pr\u00e9f\u00e9rez-vous nous \u00e9crire ?",
|
| 130 |
+
switchChannel: "Changer de canal",
|
| 131 |
homeChat: "Discuter ici", homeWa: "Continuer sur WhatsApp",
|
| 132 |
starters: ["Que proposez-vous ?", "Je souhaite plus d'informations", "Parler \u00e0 une personne"]
|
| 133 |
},
|
|
|
|
| 161 |
listening: "H\u00f6re zu\u2026",
|
| 162 |
micDenied: "Mikrofonzugriff verweigert",
|
| 163 |
homeTitle: "Wie m\u00f6chten Sie uns schreiben?",
|
| 164 |
+
switchChannel: "Kanal wechseln",
|
| 165 |
homeChat: "Hier chatten", homeWa: "Weiter auf WhatsApp",
|
| 166 |
starters: ["Was bieten Sie an?", "Ich m\u00f6chte mehr Informationen", "Mit einer Person sprechen"]
|
| 167 |
},
|
|
|
|
| 195 |
listening: "In ascolto\u2026",
|
| 196 |
micDenied: "Accesso al microfono negato",
|
| 197 |
homeTitle: "Come preferisci scriverci?",
|
| 198 |
+
switchChannel: "Cambia canale",
|
| 199 |
homeChat: "Chatta qui", homeWa: "Continua su WhatsApp",
|
| 200 |
starters: ["Cosa offrite?", "Vorrei maggiori informazioni", "Parlare con una persona"]
|
| 201 |
},
|
|
|
|
| 229 |
listening: "A ouvir\u2026",
|
| 230 |
micDenied: "Permiss\u00e3o de microfone negada",
|
| 231 |
homeTitle: "Como prefere falar connosco?",
|
| 232 |
+
switchChannel: "Mudar de canal",
|
| 233 |
homeChat: "Conversar aqui", homeWa: "Continuar no WhatsApp",
|
| 234 |
starters: ["O que oferecem?", "Quero mais informa\u00e7\u00f5es", "Falar com uma pessoa"]
|
| 235 |
}
|
|
|
|
| 269 |
|
| 270 |
// Paperclip (attach a PDF/photo to the conversation).
|
| 271 |
var ICON_CLIP = '<svg viewBox="0 0 24 24"><path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z"/></svg>';
|
| 272 |
+
var ICON_BACK = '<svg viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>';
|
| 273 |
var ICON_MIC = '<svg viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.49 6-3.31 6-6.72h-1.7z"/></svg>';
|
| 274 |
|
| 275 |
// Voice input: browser-native speech recognition (free, no external service).
|
|
|
|
| 354 |
"#ssb-head .ssb-x{margin-left:auto;background:transparent;border:0;color:#fff;font-size:24px;cursor:pointer;opacity:.85;line-height:1}" +
|
| 355 |
"#ssb-mute{margin-left:auto;width:32px;height:32px;background:transparent;border:0;cursor:pointer;opacity:.85;display:flex;align-items:center;justify-content:center;padding:0;flex:none}" +
|
| 356 |
"#ssb-mute:hover{opacity:1}#ssb-mute svg{width:20px;height:20px;fill:#fff}" +
|
| 357 |
+
"#ssb-switch{margin-left:auto;width:32px;height:32px;background:transparent;border:0;cursor:pointer;opacity:.85;align-items:center;justify-content:center;padding:0;flex:none}" +
|
| 358 |
+
"#ssb-switch:hover{opacity:1}#ssb-switch svg{width:19px;height:19px;fill:#fff}" +
|
| 359 |
"#ssb-head .ssb-x{margin-left:6px}" +
|
| 360 |
"#ssb-launch .ssb-badge{position:absolute;top:-4px;right:-4px;min-width:20px;height:20px;box-sizing:border-box;padding:0 5px;border-radius:10px;background:#e11d48;border:2px solid #fff;color:#fff;font-family:system-ui,-apple-system,Segoe UI,sans-serif;font-size:11px;font-weight:700;line-height:16px;text-align:center;display:none}" +
|
| 361 |
"#ssb-msgs{flex:1;overflow-y:auto;padding:16px 14px;background:#f4f5f7;display:flex;flex-direction:column;gap:10px}" +
|
|
|
|
| 469 |
'<div id="ssb-head"><div class="ssb-av"></div><div>' +
|
| 470 |
'<div class="ssb-name">' + esc(name) + '</div>' +
|
| 471 |
'<div class="ssb-status"><i></i>' + T.status + '</div></div>' +
|
| 472 |
+
(showHome ? '<button id="ssb-switch" type="button" aria-label="' + T.switchChannel +
|
| 473 |
+
'" title="' + T.switchChannel + '" style="display:none">' + ICON_BACK + '</button>' : '') +
|
| 474 |
'<button id="ssb-mute" type="button"></button>' +
|
| 475 |
'<button class="ssb-x" aria-label="' + T.close + '">×</button></div>' +
|
| 476 |
homeHtml +
|
|
|
|
| 495 |
var greeted = false;
|
| 496 |
var homeEl = panel.querySelector("#ssb-home");
|
| 497 |
var chatarea = panel.querySelector("#ssb-chatarea");
|
| 498 |
+
var switchBtn = panel.querySelector("#ssb-switch");
|
| 499 |
|
| 500 |
// "both" mode: visitor picks a channel on open. Chat -> reveal the normal
|
| 501 |
// chat area (and greet, same as chat-only mode). WhatsApp -> open wa.me;
|
| 502 |
+
// the panel stays open behind it so they can still switch to chat. A small
|
| 503 |
+
// back button in the header (once chat is showing) lets them return to the
|
| 504 |
+
// picker at any time -- reopening the panel later must NOT strand them on
|
| 505 |
+
// whichever channel they picked first with no way back.
|
| 506 |
function chooseChat() {
|
| 507 |
if (homeEl) homeEl.style.display = "none";
|
| 508 |
if (chatarea) chatarea.style.display = "flex";
|
| 509 |
+
if (switchBtn) switchBtn.style.display = "flex";
|
| 510 |
if (!greeted) { botMsg(cfg.welcome); renderStarters(); greeted = true; }
|
| 511 |
input.focus();
|
| 512 |
}
|
| 513 |
function chooseWa() {
|
| 514 |
window.open("https://wa.me/" + waDigits, "_blank", "noopener");
|
| 515 |
}
|
| 516 |
+
function backToHome() {
|
| 517 |
+
if (chatarea) chatarea.style.display = "none";
|
| 518 |
+
if (switchBtn) switchBtn.style.display = "none";
|
| 519 |
+
if (homeEl) homeEl.style.display = "flex";
|
| 520 |
+
}
|
| 521 |
if (homeEl) {
|
| 522 |
panel.querySelector("#ssb-home-chat").addEventListener("click", chooseChat);
|
| 523 |
panel.querySelector("#ssb-home-wa").addEventListener("click", chooseWa);
|
| 524 |
+
if (switchBtn) switchBtn.addEventListener("click", backToHome);
|
| 525 |
}
|
| 526 |
|
| 527 |
// ---- Visitor uploads (Phase 3): paperclip -> chip -> ids on next send ----
|
|
|
|
| 823 |
if (panel.classList.contains("open")) {
|
| 824 |
setBadge(0); stopFlash();
|
| 825 |
// With a home screen, wait for the visitor to pick a channel before
|
| 826 |
+
// greeting/focusing the input (avoids a wasted bot turn pre-choice). Once
|
| 827 |
+
// they've already chosen chat (chatarea visible), reopening later still
|
| 828 |
+
// focuses the input like the chat-only mode does.
|
| 829 |
+
if (!homeEl || (chatarea && chatarea.style.display !== "none")) {
|
| 830 |
if (!greeted) { botMsg(cfg.welcome); renderStarters(); greeted = true; }
|
| 831 |
input.focus();
|
| 832 |
}
|
tests/test_widget_assets.py
CHANGED
|
@@ -35,6 +35,16 @@ def test_widget_both_mode_shows_a_single_bubble_with_channel_picker():
|
|
| 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
|
|
|
|
| 35 |
assert js.count(key + ":") == 6, f"{key} must be defined in all 6 languages"
|
| 36 |
|
| 37 |
|
| 38 |
+
def test_widget_both_mode_can_switch_back_to_the_channel_picker():
|
| 39 |
+
"""Regression (reported live): once a visitor picked a channel, closing and
|
| 40 |
+
reopening the panel left them stuck with no way back to the picker. A header
|
| 41 |
+
button must let them return to it at any point, even after a re-open."""
|
| 42 |
+
js = WIDGET.read_text(encoding="utf-8")
|
| 43 |
+
assert 'id="ssb-switch"' in js
|
| 44 |
+
assert "backToHome" in js
|
| 45 |
+
assert js.count("switchChannel:") == 6, "switchChannel label must be defined in all 6 languages"
|
| 46 |
+
|
| 47 |
+
|
| 48 |
def test_block_is_app_embed_loading_widget_in_proxy_mode():
|
| 49 |
liquid = BLOCK.read_text(encoding="utf-8")
|
| 50 |
assert "{% schema %}" in liquid
|