/* ======================================================================== screen-webchat.jsx, emirates.com on LAPTOP Animated flow: home -> typing-name -> typing-pnr -> submit -> loading -> manage (Manage Booking page, with chat FAB peek + 1 unread) -> opening -> open (chat widget runs) ======================================================================== */ function ScreenWebChat({ active }) { const turns = window.DEMO_SCRIPT.web_chat; const lukas = window.DEMO_SCRIPT.passenger; const hala = window.DEMO_SCRIPT.hala; const opts = window.DEMO_SCRIPT.options; const dates = window.DEMO_DATES; const BOOKING_REF = lukas.booking_ref; // "K7RJ4X" const LAST_NAME = "Weber"; const PHASES = ["home", "typing-name", "typing-pnr", "submit", "loading", "manage", "opening", "open"]; const [phase, setPhase] = useState("home"); const [typedName, setTypedName] = useState(""); const [typedPnr, setTypedPnr] = useState(""); const timersRef = useRef([]); const clearTimers = () => { timersRef.current.forEach(clearTimeout); timersRef.current = []; }; const after = (ms, fn) => { const id = setTimeout(fn, ms); timersRef.current.push(id); return id; }; // Choreograph useEffect(() => { if (!active) { clearTimers(); setPhase("home"); setTypedName(""); setTypedPnr(""); return; } clearTimers(); setPhase("home"); setTypedName(""); setTypedPnr(""); // Faster intro pacing, get to the chat quickly. after(900, () => setPhase("typing-name")); const FULLNAME = LAST_NAME; // "Weber", 5 chars FULLNAME.split("").forEach((ch, i) => { after(900 + 200 + i * 90, () => setTypedName(FULLNAME.slice(0, i + 1))); }); // 900 + 200 + 5*90 = 1550 -> switch to PNR field after(1700, () => setPhase("typing-pnr")); const FULLPNR = BOOKING_REF; // "K7RJ4X", 6 chars FULLPNR.split("").forEach((ch, i) => { after(1700 + 200 + i * 90, () => setTypedPnr(FULLPNR.slice(0, i + 1))); }); // 1700 + 200 + 7*90 = 2530 -> submit after(2700, () => setPhase("submit")); after(3000, () => setPhase("loading")); after(3700, () => setPhase("manage")); // FAB peek for ~1.8s, then open after(5500, () => setPhase("opening")); after(6100, () => setPhase("open")); const skip = () => { clearTimers(); setTypedName(LAST_NAME); setTypedPnr(BOOKING_REF); setPhase("open"); }; window.addEventListener("demo:skip", skip); return () => { clearTimers(); window.removeEventListener("demo:skip", skip); }; }, [active]); const openWidget = () => { if (phase !== "manage") return; clearTimers(); setPhase("opening"); after(700, () => setPhase("open")); }; // Readable playback pace (1.0x = script time exactly). const { shown, typing } = window.useChatPlayback(turns, { sceneActive: active && phase === "open", paused: false, speed: 1.0, }); // Smart auto-scroll: only follow the latest message if the user is already pinned // to the bottom. If they have scrolled up to re-read something, leave them alone. const feedRef = useRef(null); const pinnedRef = useRef(true); // assume pinned at start const handleFeedScroll = () => { const el = feedRef.current; if (!el) return; const dist = el.scrollHeight - el.scrollTop - el.clientHeight; pinnedRef.current = dist < 48; // within 48px of bottom counts as "pinned" }; useEffect(() => { const el = feedRef.current; if (!el) return; if (pinnedRef.current) { el.scrollTop = el.scrollHeight; } }, [shown, typing]); // Cursor target by phase const cursorPos = (() => { switch (phase) { case "home": return { x: "47%", y: "62%" }; // moving toward last-name case "typing-name": return { x: "47%", y: "70%" }; // hovering near last-name case "typing-pnr": return { x: "60%", y: "70%" }; // hovering near pnr case "submit": return { x: "75%", y: "70%" }; // on Manage booking button case "loading": return { x: "75%", y: "70%" }; case "manage": return { x: "92%", y: "92%" }; // moving to FAB case "opening": case "open": return { x: "92%", y: "92%" }; default: return { x: "50%", y: "50%" }; } })(); const showHomepage = ["home","typing-name","typing-pnr","submit","loading"].includes(phase); const showManage = ["manage","opening","open"].includes(phase); // URL bar text changes by phase const urlText = showHomepage ? "emirates.com" : `emirates.com/manage-booking · ${BOOKING_REF}`; const tabText = showHomepage ? "Emirates · Fly Better" : "Emirates · Manage Booking"; return (
{tabText}
{urlText}
{/* HOMEPAGE LAYER */} {showHomepage && (
Enjoy flexible travel with
A FREE DATE CHANGE
on all new tickets
FLY BETTER
Search flights
Manage booking / Check in
What's on your flight
Flight status
Log in to view your trips
Last name {typedName} {phase === "typing-name" && }
Booking reference {typedPnr} {phase === "typing-pnr" && }
{["Hotels","Car rentals","Tours & activities","Book a holiday","Chauffeur drive","Meet & greet"].map(t => (
{t}
))}
Featured destinations from Dubai
{[ {city:"Singapore", price:"AED 2,820"}, {city:"Munich", price:"AED 3,090"}, {city:"Osaka", price:"AED 5,410"}, ].map(d => (
{d.city}
from {d.price}*
))}
{/* Loading veil */} {phase === "loading" && (
Loading your booking…
)}
)} {/* MANAGE BOOKING LAYER */} {showManage && (
My trips
Hello, {lukas.first}
Booking reference {BOOKING_REF} · Skywards Silver
!
Your flight EK202 has been cancelled
Your ticket is still valid. We've held your fare and seat preference. Choose a new flight at no extra cost, or request a refund.
EK202 · Dubai → New York
CANCELLED
08:30
DXB
Dubai Intl · {dates.TODAY_LONG}
13:45
JFK
John F. Kennedy · {dates.TODAY_LONG}
Passenger
LW
Lukas Weber
Skywards Silver · Seat 23A · Meal: vegetarian
)} {/* CURSOR */}
{/* CHAT FAB, visible only on manage page, before opening */}
{hala.name}
Online · Replies instantly
Virtual Assistant
{shown.map((t, i) => { if (t.who === "hala") { return (
{t.text}
{t.t}
); } if (t.who === "lukas") { const cls = "bubble from-user" + (t.tone === "stressed" ? " tone-stressed" : t.tone === "personal" ? " tone-personal" : ""); return (
{t.text}
{t.t}
); } if (t.who === "system") { return
{t.text}
; } if (t.who === "options") { return (
{opts.map(o => (
{o.starred && RECOMMENDED} {o.label}
{o.flight}
{o.from} {o.depart_time} → {o.to} {o.arrive_time} · {o.depart_date}
{o.seats_available} seats · {o.stops} {o.fare_diff}
{o.reason && (
{o.reason}
)}
))}
Ranked using what you told me about tomorrow.
); } if (t.who === "seat-hold") { return (
23A
Preference noted · seat 23A
Hold placed, awaiting specialist approval
); } if (t.who === "channel-choice") { return (
Call you on your number
Hala rings +971 50 ••• 4421, no hold, no dialling
Continue on web voice
Stay in this chat and switch to voice in-browser
); } if (t.who === "call-cta") { return (
Calling {lukas.first} now …
Full chat context comes with you
); } return null; })} {typing?.who === "hala" && (
)} {typing?.who === "lukas" && (
)}
Message Hala…
Powered by Wonderful
); } /* Inline highlight: wrap any occurrence of `phrase` in . */ function renderWithHighlight(text, phrase) { if (!phrase) return text; const i = text.indexOf(phrase); if (i === -1) return text; return ( <> {text.slice(0, i)} {phrase} {text.slice(i + phrase.length)} ); } /* ---------- Header subcomponent ---------- */ function EkHeader({ signedIn }) { return ( <>
● Important Travel advisory · Changes to certain operating schedules Show more ▾
Emirates
BOOKMANAGEEXPERIENCEWHERE WE FLYLOYALTYHELP
AE EN {signedIn ? `👤 ${signedIn}` : "👤 LOG IN"}
); } window.ScreenWebChat = ScreenWebChat;