@syntrologie/adapt-chatbot 2.38.0 → 2.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/runtime.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  getElementInstanceStore,
18
18
  setElementInstanceStore
19
19
  } from "./chunk-EWPPVPJ4.js";
20
- import "./chunk-P74YY3MD.js";
20
+ import "./chunk-3Z52KSYZ.js";
21
21
  import "./chunk-VZIUXXAM.js";
22
22
  import "./chunk-OIGVZGSX.js";
23
23
  import {
@@ -266,6 +266,33 @@ var ChatSession = class {
266
266
  msg.toolCalls = [...msg.toolCalls ?? [], { ...toolCall }];
267
267
  this.notify();
268
268
  }
269
+ /**
270
+ * Create an empty, already-settled assistant message at `id` if one
271
+ * doesn't already exist. Idempotent — safe to call on every tool-call
272
+ * sighting.
273
+ *
274
+ * Exists for TOOL-ONLY turns: pydantic-ai's AG-UI adapter mints a fresh
275
+ * `parent_message_id` per model response regardless of content
276
+ * (`before_response` → `self.new_message_id()`) and stamps it on every
277
+ * `TOOL_CALL_START`, even when the response has no text. When the
278
+ * model's entire action is one or more tool calls with no narration
279
+ * (a parallel `mount_*` + `suggest_replies` batch, most commonly), no
280
+ * `TEXT_MESSAGE_START` is ever emitted for that id, so nothing would
281
+ * otherwise create the host message `addToolCall` needs — and the tool
282
+ * call (and any chips/receipts derived from it) silently vanishes
283
+ * (BUG-1786654199). Call this before `addToolCall` on a tool call's
284
+ * first sighting so it always has a message to land on.
285
+ *
286
+ * Deliberately status:'complete' (not 'streaming' via `receiveStart`):
287
+ * a tool-only message has no text to stream, so there's nothing for a
288
+ * later `receiveEnd` to settle — leaving it 'streaming' would paint
289
+ * the trail's streaming/typing visual state forever.
290
+ */
291
+ ensureAssistantMessage(id) {
292
+ if (this._messages.some((m) => m.id === id)) return;
293
+ this._messages.push({ id, role: "assistant", text: "", status: "complete" });
294
+ this.notify();
295
+ }
269
296
  /**
270
297
  * Partially update a tool call by id. Used by the transport to
271
298
  * advance status (args-streaming → running → done) as AG-UI events
@@ -1222,6 +1249,7 @@ var ChatTransport = class {
1222
1249
  if (!targetMessageId) return;
1223
1250
  const parsedArgs = parseToolCallArgs(event.toolCall);
1224
1251
  if (!existing) {
1252
+ chatSession.ensureAssistantMessage(targetMessageId);
1225
1253
  chatSession.addToolCall(targetMessageId, {
1226
1254
  id: event.toolCall.id,
1227
1255
  name: event.toolCall.name,
@@ -1806,6 +1834,7 @@ var crispBridge = {
1806
1834
  (msg) => {
1807
1835
  if (!active) return;
1808
1836
  push(["do", "chat:hide"]);
1837
+ if (msg?.from !== "operator") return;
1809
1838
  if (typeof msg?.content === "string") cb(msg.content);
1810
1839
  }
1811
1840
  ]);
@@ -2268,51 +2297,177 @@ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2268
2297
  var box = {
2269
2298
  display: "flex",
2270
2299
  flexDirection: "column",
2271
- gap: "var(--sc-content-item-gap, 10px)",
2300
+ gap: "10px",
2272
2301
  fontFamily: "var(--sc-font-family, system-ui)",
2273
- fontSize: "var(--sc-content-body-font-size, 13px)"
2302
+ fontSize: "13px",
2303
+ // Defect: card content sat flush against its container's edges (no
2304
+ // padding anywhere). Fixed, compact — matches the rest of this file's
2305
+ // "form scale" sizing, not the content section's chat-bubble padding.
2306
+ padding: "16px",
2307
+ boxSizing: "border-box"
2274
2308
  };
2275
- var fieldStyles = {
2276
- padding: "var(--sc-content-item-padding, 8px 10px)",
2309
+ var fieldWrapStyles = {
2310
+ position: "relative",
2277
2311
  borderRadius: "var(--sc-content-border-radius, 8px)",
2278
2312
  border: "var(--sc-content-bubble-border, 1px solid currentColor)",
2279
2313
  background: "var(--sc-content-bubble-background, transparent)",
2314
+ boxSizing: "border-box",
2315
+ // Clips the (square-cornered, 100%-filled) inner field to this shell's
2316
+ // rounded corners — without it, flat textarea/input corners can poke
2317
+ // past the shell's curve at the very edges.
2318
+ overflow: "hidden"
2319
+ };
2320
+ var messageWrapStyles = {
2321
+ ...fieldWrapStyles,
2322
+ height: "84px"
2323
+ };
2324
+ var singleLineWrapStyles = {
2325
+ ...fieldWrapStyles,
2326
+ height: "44px"
2327
+ };
2328
+ var fieldInnerBase = {
2329
+ border: "none",
2330
+ outline: "none",
2331
+ background: "transparent",
2280
2332
  color: "inherit",
2281
- font: "inherit"
2333
+ font: "inherit",
2334
+ width: "100%",
2335
+ height: "100%",
2336
+ boxSizing: "border-box",
2337
+ resize: "none"
2338
+ };
2339
+ var messageInnerStyles = {
2340
+ ...fieldInnerBase,
2341
+ padding: "8px 42px 8px 10px",
2342
+ overflowY: "auto"
2343
+ };
2344
+ var singleLineInnerStyles = {
2345
+ ...fieldInnerBase,
2346
+ padding: "0 42px 0 10px"
2347
+ };
2348
+ var srOnlyStyles = {
2349
+ position: "absolute",
2350
+ width: "1px",
2351
+ height: "1px",
2352
+ padding: "0",
2353
+ margin: "-1px",
2354
+ overflow: "hidden",
2355
+ whiteSpace: "nowrap",
2356
+ border: "0"
2357
+ };
2358
+ var sendIcon = html`<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 14V2M8 2L3 7M8 2L13 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
2359
+ var embeddedIconButtonStyles = {
2360
+ position: "absolute",
2361
+ right: "5px",
2362
+ bottom: "5px",
2363
+ display: "flex",
2364
+ alignItems: "center",
2365
+ justifyContent: "center",
2366
+ width: "30px",
2367
+ height: "30px",
2368
+ padding: "0",
2369
+ borderRadius: "50%",
2370
+ border: "none",
2371
+ background: "var(--sc-color-primary, #000000)",
2372
+ color: "var(--sc-launcher-color, #ffffff)",
2373
+ cursor: "pointer"
2374
+ };
2375
+ var embeddedIconButtonCenteredStyles = {
2376
+ ...embeddedIconButtonStyles,
2377
+ bottom: "auto",
2378
+ top: "50%",
2379
+ transform: "translateY(-50%)"
2380
+ };
2381
+ var embeddedIconButtonDisabledStyles = {
2382
+ ...embeddedIconButtonStyles,
2383
+ background: "rgba(120, 120, 120, 0.25)",
2384
+ color: "rgba(90, 90, 90, 0.9)",
2385
+ cursor: "default"
2386
+ };
2387
+ var embeddedIconButtonCenteredDisabledStyles = {
2388
+ ...embeddedIconButtonCenteredStyles,
2389
+ background: "rgba(120, 120, 120, 0.25)",
2390
+ color: "rgba(90, 90, 90, 0.9)",
2391
+ cursor: "default"
2282
2392
  };
2283
2393
  var buttonStyles = {
2284
- // No chip padding token exists (ChipElementConfigZ only has
2285
- // background/foreground/border/shadow) — plain constant, no fake var().
2286
- padding: "10px 12px",
2287
- borderRadius: "var(--sc-chip-border-radius, 9999px)",
2288
- border: "var(--sc-chip-border, 1px solid currentColor)",
2289
- background: "var(--sc-chip-background, transparent)",
2290
- color: "var(--sc-tile-title-color, inherit)",
2394
+ padding: "11px 14px",
2395
+ borderRadius: "var(--sc-border-radius, 8px)",
2396
+ border: "none",
2397
+ background: "var(--sc-color-primary, #000000)",
2398
+ color: "var(--sc-launcher-color, #ffffff)",
2291
2399
  fontWeight: "600",
2292
2400
  cursor: "pointer",
2293
- font: "inherit"
2401
+ font: "inherit",
2402
+ width: "100%",
2403
+ boxSizing: "border-box"
2294
2404
  };
2295
- var textStyles = {
2296
- margin: "0"
2405
+ var fieldRowStyles = {
2406
+ display: "flex",
2407
+ alignItems: "center",
2408
+ gap: "8px"
2409
+ };
2410
+ var skipButtonStyles = {
2411
+ flexShrink: "0",
2412
+ padding: "0",
2413
+ border: "none",
2414
+ background: "transparent",
2415
+ color: "inherit",
2416
+ textDecoration: "underline",
2417
+ cursor: "pointer",
2418
+ font: "inherit",
2419
+ fontSize: "12px",
2420
+ opacity: "0.8",
2421
+ whiteSpace: "nowrap"
2297
2422
  };
2298
2423
  var mutedTextStyles = {
2299
2424
  margin: "0",
2300
2425
  // No opacity token exists in any publishable section — plain constant.
2301
2426
  opacity: "0.8"
2302
2427
  };
2428
+ var trailHostStyles = {
2429
+ display: "flex",
2430
+ flexDirection: "column",
2431
+ height: "160px",
2432
+ overflow: "hidden",
2433
+ width: "100%",
2434
+ boxSizing: "border-box"
2435
+ };
2303
2436
  var PERSIST_NAMESPACE = "supportchat";
2304
2437
  var PERSIST_KEY = "leave-a-message";
2438
+ var lamUidCounter = 0;
2305
2439
  var LeaveAMessageLit = class extends LitElement {
2306
2440
  constructor() {
2307
2441
  super(...arguments);
2308
2442
  this.message = "";
2309
2443
  this.contextSummary = "";
2310
2444
  this.state = "form";
2445
+ /** Unique per-instance id prefix so `<label for>` never collides across
2446
+ * more than one card mounted on the same page. */
2447
+ this._uid = `lam-${++lamUidCounter}`;
2311
2448
  this._email = "";
2312
2449
  this._reply = "";
2313
- this._operatorMessages = [];
2450
+ /** Visitor↔operator conversation, in order — rendered as a chat
2451
+ * transcript once the visitor taps into the mail state (see render()'s
2452
+ * 'flipped' branch). Seeded with the visitor's own compose-step message
2453
+ * in `_submitMessage`. */
2454
+ this._transcript = [];
2455
+ /** Paint our own focus ring from the theme. The border now lives on the
2456
+ * field SHELL (`data-lam-field-wrap`), not the borderless inner
2457
+ * textarea/input, so the ring targets that ancestor via `closest()`. */
2458
+ this._focusOn = (e) => {
2459
+ const field = e.target;
2460
+ const target = field.closest("[data-lam-field-wrap]") ?? field;
2461
+ target.style.outline = "2px solid var(--sc-color-primary, currentColor)";
2462
+ target.style.outlineOffset = "1px";
2463
+ };
2464
+ this._focusOff = (e) => {
2465
+ const field = e.target;
2466
+ const target = field.closest("[data-lam-field-wrap]") ?? field;
2467
+ target.style.outline = "none";
2468
+ };
2314
2469
  this._onReply = (text) => {
2315
- this._operatorMessages = [...this._operatorMessages, text];
2470
+ this._transcript = [...this._transcript, { role: "operator", text }];
2316
2471
  if (this.state !== "flipped") {
2317
2472
  this.state = "mail";
2318
2473
  this.runtime?.events.publish("supportchat:reply_received", { text });
@@ -2328,12 +2483,37 @@ var LeaveAMessageLit = class extends LitElement {
2328
2483
  this._persist();
2329
2484
  this.requestUpdate();
2330
2485
  };
2331
- this._submit = () => {
2332
- if (!this.bridge || !EMAIL_RE.test(this._email)) return;
2333
- this.bridge.setVisitorEmail(this._email);
2486
+ /** Compose step → email step. Delivers the message immediately; email
2487
+ * capture is a SEPARATE, later, optional step (owner redesign). Seeds
2488
+ * the chat transcript with the visitor's OWN plain message text (not
2489
+ * the context-prefixed string sent to the bridge — that prefix is
2490
+ * internal plumbing for the human agent, not something the visitor
2491
+ * should see echoed back at themselves). */
2492
+ this._submitMessage = () => {
2493
+ if (!this.bridge || !this.message.trim()) return;
2334
2494
  const context = this.contextSummary ? `[Site assistant context: ${this.contextSummary}]
2335
2495
  ` : "";
2336
2496
  this.bridge.sendMessage(`${context}${this.message}`);
2497
+ this._transcript = [...this._transcript, { role: "visitor", text: this.message.trim() }];
2498
+ this.state = "email";
2499
+ this._persist();
2500
+ };
2501
+ /** Email step → sent. Blank email is a valid submission (the field is
2502
+ * optional) and simply skips `setVisitorEmail`; an invalid non-blank
2503
+ * email blocks advancing so a mistyped address doesn't silently vanish. */
2504
+ this._submitEmail = () => {
2505
+ if (!this.bridge) return;
2506
+ const trimmed = this._email.trim();
2507
+ if (trimmed && !EMAIL_RE.test(trimmed)) return;
2508
+ if (trimmed) this.bridge.setVisitorEmail(trimmed);
2509
+ this.state = "sent";
2510
+ this._persist();
2511
+ };
2512
+ /** Explicit decline — advances regardless of whatever is typed, without
2513
+ * ever calling `setVisitorEmail`. Distinct from submitting a blank
2514
+ * field only in that it discards partial input the visitor may not
2515
+ * have meant to send. */
2516
+ this._skipEmail = () => {
2337
2517
  this.state = "sent";
2338
2518
  this._persist();
2339
2519
  };
@@ -2343,8 +2523,11 @@ var LeaveAMessageLit = class extends LitElement {
2343
2523
  };
2344
2524
  this._sendReply = () => {
2345
2525
  if (!this.bridge || !this._reply.trim()) return;
2346
- this.bridge.sendMessage(this._reply.trim());
2526
+ const text = this._reply.trim();
2527
+ this.bridge.sendMessage(text);
2528
+ this._transcript = [...this._transcript, { role: "visitor", text }];
2347
2529
  this._reply = "";
2530
+ this._persist();
2348
2531
  this.requestUpdate();
2349
2532
  };
2350
2533
  }
@@ -2369,25 +2552,25 @@ var LeaveAMessageLit = class extends LitElement {
2369
2552
  _persistStore() {
2370
2553
  return this.runtime?.state?.user?.ns?.(PERSIST_NAMESPACE);
2371
2554
  }
2372
- /** Hydrate {state, operatorMessages, email} from a prior page's mount. */
2555
+ /** Hydrate {state, transcript, email} from a prior page's mount. */
2373
2556
  _loadPersisted() {
2374
2557
  const store = this._persistStore();
2375
2558
  if (!store) return;
2376
2559
  const saved = store.get(PERSIST_KEY);
2377
2560
  if (!saved) return;
2378
2561
  if (saved.state) this.state = saved.state;
2379
- if (Array.isArray(saved.operatorMessages)) this._operatorMessages = saved.operatorMessages;
2562
+ if (Array.isArray(saved.transcript)) this._transcript = saved.transcript;
2380
2563
  if (typeof saved.email === "string") this._email = saved.email;
2381
2564
  }
2382
- /** Snapshot {state, operatorMessages, email} so the NEXT mount (a fresh
2383
- * page in the same session) can pick the thread back up. */
2565
+ /** Snapshot {state, transcript, email} so the NEXT mount (a fresh page
2566
+ * in the same session) can pick the thread back up. */
2384
2567
  _persist() {
2385
2568
  const store = this._persistStore();
2386
2569
  if (!store) return;
2387
2570
  const state = this.state;
2388
- const operatorMessages = this._operatorMessages;
2571
+ const transcript = this._transcript;
2389
2572
  const email = this._email;
2390
- const snapshot = { state, operatorMessages, email };
2573
+ const snapshot = { state, transcript, email };
2391
2574
  store.set(PERSIST_KEY, snapshot);
2392
2575
  }
2393
2576
  /**
@@ -2419,65 +2602,164 @@ var LeaveAMessageLit = class extends LitElement {
2419
2602
  console.warn("supportChat.widget: reply toast failed", err);
2420
2603
  }
2421
2604
  }
2605
+ /** Maps this card's OWN `_transcript` (never the concierge's) to the
2606
+ * shape `<adaptive-chat-trail>` renders. `visitor` → `'user'` (right-
2607
+ * aligned, branded) and `operator` → `'assistant'` (left-aligned,
2608
+ * neutral) purely for the trail's existing visual treatment — no
2609
+ * claim that a human Crisp operator IS the AI assistant. Freshly
2610
+ * computed on every render from local state; nothing here reads from
2611
+ * or writes to `chatSession`. */
2612
+ _trailMessages() {
2613
+ return this._transcript.map((entry, i) => ({
2614
+ id: `lam-${this._uid}-${i}`,
2615
+ role: entry.role === "visitor" ? "user" : "assistant",
2616
+ text: entry.text,
2617
+ status: "complete"
2618
+ }));
2619
+ }
2422
2620
  render() {
2423
2621
  if (!this.bridge || !this.bridge.detect()) return nothing;
2622
+ const copy = this.copy;
2424
2623
  if (this.state === "form") {
2624
+ const messageId = `${this._uid}-message`;
2625
+ const canSend = this.message.trim().length > 0;
2425
2626
  return html`<div data-lam style=${styleMap(box)}>
2426
- <textarea
2427
- data-lam-message
2428
- rows="3"
2429
- style=${styleMap(fieldStyles)}
2430
- .value=${this.message}
2431
- @input=${(e) => {
2627
+ ${copy?.framing ? html`<p data-lam-framing style=${styleMap(mutedTextStyles)}>${copy.framing}</p>` : nothing}
2628
+ ${copy?.messageLabel ? html`<label data-lam-message-label for=${messageId} style=${styleMap(srOnlyStyles)}
2629
+ >${copy.messageLabel}</label
2630
+ >` : nothing}
2631
+ <div data-lam-message-wrap data-lam-field-wrap style=${styleMap(messageWrapStyles)}>
2632
+ <textarea
2633
+ data-lam-message
2634
+ id=${messageId}
2635
+ style=${styleMap(messageInnerStyles)}
2636
+ @focus=${this._focusOn}
2637
+ @blur=${this._focusOff}
2638
+ .value=${this.message}
2639
+ @input=${(e) => {
2432
2640
  this.message = e.target.value;
2433
2641
  }}
2434
- ></textarea>
2435
- <input
2436
- data-lam-email
2437
- type="email"
2438
- placeholder="Your email — so the team can reach you"
2439
- style=${styleMap(fieldStyles)}
2440
- @input=${(e) => {
2642
+ ></textarea>
2643
+ ${copy?.sendLabel ? html`<button
2644
+ type="button"
2645
+ data-lam-send
2646
+ aria-label=${copy.sendLabel}
2647
+ ?disabled=${!canSend}
2648
+ style=${styleMap(
2649
+ canSend ? embeddedIconButtonStyles : embeddedIconButtonDisabledStyles
2650
+ )}
2651
+ @click=${this._submitMessage}
2652
+ >
2653
+ ${sendIcon}
2654
+ </button>` : nothing}
2655
+ </div>
2656
+ </div>`;
2657
+ }
2658
+ if (this.state === "email") {
2659
+ const emailId = `${this._uid}-email`;
2660
+ return html`<div data-lam style=${styleMap(box)}>
2661
+ ${copy?.emailPrompt ? html`<p data-lam-email-prompt style=${styleMap(mutedTextStyles)}>${copy.emailPrompt}</p>` : nothing}
2662
+ ${copy?.emailLabel ? html`<label data-lam-email-label for=${emailId} style=${styleMap(srOnlyStyles)}
2663
+ >${copy.emailLabel}</label
2664
+ >` : nothing}
2665
+ <div data-lam-email-row style=${styleMap(fieldRowStyles)}>
2666
+ <div
2667
+ data-lam-email-wrap
2668
+ data-lam-field-wrap
2669
+ style=${styleMap({ ...singleLineWrapStyles, flex: "1" })}
2670
+ >
2671
+ <input
2672
+ data-lam-email
2673
+ id=${emailId}
2674
+ type="email"
2675
+ placeholder=${copy?.emailPlaceholder ?? nothing}
2676
+ style=${styleMap(singleLineInnerStyles)}
2677
+ @focus=${this._focusOn}
2678
+ @blur=${this._focusOff}
2679
+ @input=${(e) => {
2441
2680
  this._email = e.target.value;
2442
2681
  this.requestUpdate();
2443
2682
  }}
2444
- />
2445
- <button type="button" data-lam-send style=${styleMap(buttonStyles)} @click=${this._submit}>
2446
- Send to the team
2447
- </button>
2683
+ />
2684
+ ${copy?.emailSubmitLabel ? html`<button
2685
+ type="button"
2686
+ data-lam-email-submit
2687
+ aria-label=${copy.emailSubmitLabel}
2688
+ style=${styleMap(embeddedIconButtonCenteredStyles)}
2689
+ @click=${this._submitEmail}
2690
+ >
2691
+ ${sendIcon}
2692
+ </button>` : nothing}
2693
+ </div>
2694
+ ${copy?.emailSkipLabel ? html`<button
2695
+ type="button"
2696
+ data-lam-email-skip
2697
+ style=${styleMap(skipButtonStyles)}
2698
+ @click=${this._skipEmail}
2699
+ >
2700
+ ${copy.emailSkipLabel}
2701
+ </button>` : nothing}
2702
+ </div>
2448
2703
  </div>`;
2449
2704
  }
2450
2705
  if (this.state === "sent") {
2451
2706
  return html`<div data-lam style=${styleMap(box)}>
2452
- <p style=${styleMap(mutedTextStyles)}>
2453
- Sent. The team will reply here — or by email if you step away.
2454
- </p>
2707
+ ${copy?.sentBody ? html`<p style=${styleMap(mutedTextStyles)}>${copy.sentBody}</p>` : nothing}
2455
2708
  </div>`;
2456
2709
  }
2457
2710
  if (this.state === "mail") {
2458
2711
  return html`<div data-lam style=${styleMap(box)}>
2459
- <button type="button" data-lam-mail style=${styleMap(buttonStyles)} @click=${this._flip}>
2460
- 📬 You got mail! — tap to read
2461
- </button>
2712
+ ${copy?.mailLabel ? html`<button
2713
+ type="button"
2714
+ data-lam-mail
2715
+ style=${styleMap(buttonStyles)}
2716
+ @click=${this._flip}
2717
+ >
2718
+ ${copy.mailLabel}
2719
+ </button>` : nothing}
2462
2720
  </div>`;
2463
2721
  }
2722
+ const replyId = `${this._uid}-reply`;
2723
+ const canReply = this._reply.trim().length > 0;
2464
2724
  return html`<div data-lam style=${styleMap(box)}>
2465
- ${this._operatorMessages.map(
2466
- (m) => html`<p data-lam-operator style=${styleMap(textStyles)}>${m}</p>`
2467
- )}
2468
- <input
2469
- data-lam-reply
2470
- type="text"
2471
- placeholder="Reply to the team…"
2472
- style=${styleMap(fieldStyles)}
2473
- .value=${this._reply}
2474
- @input=${(e) => {
2725
+ <adaptive-chat-trail
2726
+ data-lam-trail
2727
+ .messages=${this._trailMessages()}
2728
+ .forceExpanded=${true}
2729
+ .hideTopBorder=${true}
2730
+ style=${styleMap(trailHostStyles)}
2731
+ ></adaptive-chat-trail>
2732
+ ${copy?.replyLabel ? html`<label data-lam-reply-label for=${replyId} style=${styleMap(srOnlyStyles)}
2733
+ >${copy.replyLabel}</label
2734
+ >` : nothing}
2735
+ <div data-lam-reply-wrap data-lam-field-wrap style=${styleMap(singleLineWrapStyles)}>
2736
+ <input
2737
+ data-lam-reply
2738
+ id=${replyId}
2739
+ type="text"
2740
+ placeholder=${copy?.replyPlaceholder ?? nothing}
2741
+ style=${styleMap(singleLineInnerStyles)}
2742
+ @focus=${this._focusOn}
2743
+ @blur=${this._focusOff}
2744
+ .value=${this._reply}
2745
+ @input=${(e) => {
2475
2746
  this._reply = e.target.value;
2747
+ this.requestUpdate();
2476
2748
  }}
2477
- />
2478
- <button type="button" data-lam-reply-send style=${styleMap(buttonStyles)} @click=${this._sendReply}>
2479
- Send
2480
- </button>
2749
+ />
2750
+ ${copy?.replySendLabel ? html`<button
2751
+ type="button"
2752
+ data-lam-reply-send
2753
+ aria-label=${copy.replySendLabel}
2754
+ ?disabled=${!canReply}
2755
+ style=${styleMap(
2756
+ canReply ? embeddedIconButtonCenteredStyles : embeddedIconButtonCenteredDisabledStyles
2757
+ )}
2758
+ @click=${this._sendReply}
2759
+ >
2760
+ ${sendIcon}
2761
+ </button>` : nothing}
2762
+ </div>
2481
2763
  </div>`;
2482
2764
  }
2483
2765
  };
@@ -2487,7 +2769,8 @@ LeaveAMessageLit.properties = {
2487
2769
  contextSummary: { attribute: false },
2488
2770
  state: { type: String },
2489
2771
  runtime: { attribute: false },
2490
- replyToast: { attribute: false }
2772
+ replyToast: { attribute: false },
2773
+ copy: { attribute: false }
2491
2774
  };
2492
2775
  function registerLeaveAMessageLit() {
2493
2776
  if (typeof customElements !== "undefined" && !customElements.get("syntro-leave-a-message")) {
@@ -2639,6 +2922,11 @@ var LeaveAMessageMountable = {
2639
2922
  // Visitor-facing toast copy from the workspace config — see
2640
2923
  // LeaveAMessageLit#replyToast (presentation is configuration).
2641
2924
  replyToast: stripped.replyToast,
2925
+ // Every OTHER visitor-facing string on the card — see
2926
+ // LeaveAMessageLit#copy / LeaveAMessageCopy. Same "config carries the
2927
+ // words" pattern as replyToast, just covering the whole card instead
2928
+ // of one notification.
2929
+ copy: stripped.copy,
2642
2930
  runtime: runtime2
2643
2931
  });
2644
2932
  container.appendChild(el);