ttp-agent-sdk 2.45.7 → 2.45.12

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/index.html CHANGED
@@ -23,7 +23,7 @@
23
23
  <img src="https://talktopc.com/logo192.png" alt="TTP Logo" style="width: 40px; height: 40px; border-radius: 8px;">
24
24
  <div>
25
25
  <h1 style="margin: 0; font-size: 1.4rem;">TTP Agent SDK</h1>
26
- <p class="version" style="margin: 0;">v2.34.9</p>
26
+ <p class="version" style="margin: 0;">v2.45.11</p>
27
27
  </div>
28
28
  </div>
29
29
  </div>
@@ -46,6 +46,7 @@
46
46
  <li><a href="#variables" class="nav-link">Variables in Hello Request</a></li>
47
47
  <li><a href="#events" class="nav-link">Events & Callbacks</a></li>
48
48
  <li><a href="#protocol-v2" class="nav-link">Protocol v2 - Format Negotiation</a></li>
49
+ <li><a href="#server-driven-disclaimer" class="nav-link">Server-driven disclaimer (voice)</a></li>
49
50
  </ul>
50
51
  </div>
51
52
 
@@ -56,6 +57,9 @@
56
57
  <li><a href="#vanilla-js" class="nav-link">Vanilla JavaScript</a></li>
57
58
  <li><a href="#react" class="nav-link">React Integration</a></li>
58
59
  <li><a href="#voice-button" class="nav-link">VoiceButton Component</a></li>
60
+ <li><a href="#fly-to-cart" class="nav-link">Fly-to-Cart Animation</a></li>
61
+ <li><a href="#ecommerce-cart-flow" class="nav-link">E-commerce cart flows</a></li>
62
+ <li><a href="#widget-flavors" class="nav-link">Widget Flavors</a></li>
59
63
  <li><a href="#client-script-tools" class="nav-link">Client-Script Tools</a></li>
60
64
  <li><a href="#chain-tools" class="nav-link">Chain Tools</a></li>
61
65
  </ul>
@@ -242,7 +246,7 @@ const voiceSDK = new VoiceSDK({
242
246
  // Optional: Configure audio formats (v2 protocol)
243
247
  outputContainer: 'raw', // 'raw' or 'wav'
244
248
  outputEncoding: 'pcm', // 'pcm', 'pcmu', 'pcma'
245
- outputSampleRate: 44100, // Higher quality audio
249
+ outputSampleRate: 24000, // Typical server/TTS output (default)
246
250
  protocolVersion: 2 // Use v2 protocol for format negotiation
247
251
  });
248
252
 
@@ -268,7 +272,7 @@ voiceSDK.on('message', (msg) => {
268
272
  <div class="step-number">2</div>
269
273
  <div class="step-content">
270
274
  <h3>Connect & Start Recording</h3>
271
- <p>Connect to the agent and start capturing audio:</p>
275
+ <p>Connect to the agent and start capturing audio. If your agent uses a <a href="#server-driven-disclaimer">server-driven disclaimer</a>, listen for <code>disclaimersRequired</code>, call <code>sendDisclaimerAck(true)</code> after the user accepts, then call <code>startRecording()</code>.</p>
272
276
  <pre><code>// Connect
273
277
  await voiceSDK.connect();
274
278
 
@@ -446,12 +450,12 @@ const voiceSDK = new VoiceSDK_v2({
446
450
  },
447
451
 
448
452
  // Audio format configuration
449
- sampleRate: 44100,
453
+ sampleRate: 16000,
450
454
  channels: 1,
451
455
  bitDepth: 16,
452
456
  outputContainer: 'raw',
453
457
  outputEncoding: 'pcm',
454
- outputSampleRate: 44100,
458
+ outputSampleRate: 24000,
455
459
  outputChannels: 1,
456
460
  outputBitDepth: 16,
457
461
  outputFrameDurationMs: 600
@@ -471,13 +475,13 @@ await voiceSDK.connect();</code></pre>
471
475
  },
472
476
  "inputFormat": {
473
477
  "encoding": "pcm",
474
- "sampleRate": 44100,
478
+ "sampleRate": 16000,
475
479
  "channels": 1,
476
480
  "bitDepth": 16
477
481
  },
478
482
  "requestedOutputFormat": {
479
483
  "encoding": "pcm",
480
- "sampleRate": 44100,
484
+ "sampleRate": 24000,
481
485
  "channels": 1,
482
486
  "bitDepth": 16,
483
487
  "container": "raw"
@@ -564,13 +568,13 @@ ws.onopen = () => {
564
568
  },
565
569
  inputFormat: {
566
570
  encoding: 'pcm',
567
- sampleRate: 44100,
571
+ sampleRate: 16000,
568
572
  channels: 1,
569
573
  bitDepth: 16
570
574
  },
571
575
  requestedOutputFormat: {
572
576
  encoding: 'pcm',
573
- sampleRate: 44100,
577
+ sampleRate: 24000,
574
578
  channels: 1,
575
579
  bitDepth: 16,
576
580
  container: 'raw'
@@ -596,13 +600,13 @@ def on_open(ws):
596
600
  },
597
601
  "inputFormat": {
598
602
  "encoding": "pcm",
599
- "sampleRate": 44100,
603
+ "sampleRate": 16000,
600
604
  "channels": 1,
601
605
  "bitDepth": 16
602
606
  },
603
607
  "requestedOutputFormat": {
604
608
  "encoding": "pcm",
605
- "sampleRate": 44100,
609
+ "sampleRate": 24000,
606
610
  "channels": 1,
607
611
  "bitDepth": 16,
608
612
  "container": "raw"
@@ -623,7 +627,7 @@ ws.run_forever()</code></pre>
623
627
  wscat -c "wss://speech.talktopc.com/ws/conv?agentId=agent_5a2b984c1&appId=app_Bc01EqMQt2Euehl4qqZSi6l3FJP42Q9vJ0pC"
624
628
 
625
629
  # Then send:
626
- {"t":"hello","v":2,"variables":{"USER_NAME":"John","ACCOUNT_TYPE":"premium"},"inputFormat":{"encoding":"pcm","sampleRate":44100,"channels":1,"bitDepth":16},"requestedOutputFormat":{"encoding":"pcm","sampleRate":44100,"channels":1,"bitDepth":16,"container":"raw"},"outputFrameDurationMs":600}</code></pre>
630
+ {"t":"hello","v":2,"variables":{"USER_NAME":"John","ACCOUNT_TYPE":"premium"},"inputFormat":{"encoding":"pcm","sampleRate":16000,"channels":1,"bitDepth":16},"requestedOutputFormat":{"encoding":"pcm","sampleRate":24000,"channels":1,"bitDepth":16,"container":"raw"},"outputFrameDurationMs":600}</code></pre>
627
631
 
628
632
  <h2>Agent Prompt Setup</h2>
629
633
  <p>To use variables in your agent, include placeholders in the system prompt or first message:</p>
@@ -824,6 +828,22 @@ voiceSDK.on('audioData', (audioData) => {
824
828
  // Raw audio data (Uint8Array)
825
829
  });</code></pre>
826
830
 
831
+ <h3>Pause Events</h3>
832
+ <pre><code>// Call paused (server acknowledged)
833
+ voiceSDK.on('callPaused', (data) => {
834
+ console.log('⏸️ Call paused, timeout:', data.timeoutSeconds, 'seconds');
835
+ });
836
+
837
+ // Call resumed (server acknowledged, STT ready)
838
+ voiceSDK.on('callResumed', () => {
839
+ console.log('▶️ Call resumed');
840
+ });
841
+
842
+ // Pause timeout (call auto-ended because pause lasted too long)
843
+ voiceSDK.on('pauseTimeout', () => {
844
+ console.log('⏱️ Pause timeout — call ended');
845
+ });</code></pre>
846
+
827
847
  <h3>Special Events</h3>
828
848
  <pre><code>// Barge-in (user interrupts agent)
829
849
  voiceSDK.on('bargeIn', (message) => {
@@ -844,6 +864,14 @@ voiceSDK.on('greetingStarted', () => {
844
864
  // Domain whitelist error
845
865
  voiceSDK.on('domainError', (error) => {
846
866
  console.error('Domain not whitelisted:', error.reason);
867
+ });
868
+
869
+ // Server-driven disclaimer (voice v2) — see #server-driven-disclaimer
870
+ voiceSDK.on('disclaimersRequired', (payload) => {
871
+ // Show your UI, then call voiceSDK.sendDisclaimerAck(true|false)
872
+ });
873
+ voiceSDK.on('disclaimerRejected', ({ code, message }) => {
874
+ // DISCLAIMER_DECLINED, DISCLAIMER_TIMEOUT, DISCLAIMER_HASH_MISMATCH, etc.
847
875
  });</code></pre>
848
876
  </section>
849
877
 
@@ -852,6 +880,149 @@ voiceSDK.on('domainError', (error) => {
852
880
  <h1>Protocol v2 - Format Negotiation</h1>
853
881
  <p>The SDK v2 introduces format negotiation, allowing you to specify exactly what audio format you want to receive from the server.</p>
854
882
 
883
+ <h2 id="server-driven-disclaimer">Server-driven disclaimer (voice)</h2>
884
+ <p>Some deployments must show <strong>exact legal or policy copy</strong> from the server before speech recognition and the agent greeting run. The conversation server can require an explicit acknowledgement step after <code>hello_ack</code>. This applies to <strong>VoiceSDK v2</strong> (protocol version 2) and the <strong>Voice &amp; Chat Widget</strong> voice path.</p>
885
+
886
+ <h3>When the gate is active</h3>
887
+ <ul>
888
+ <li>The agent has a <strong>non-empty</strong> disclaimers list in backend storage (Redis field <code>disclaimers</code> as a JSON array of plain-text strings). An empty array <code>[]</code> means <strong>no</strong> disclaimer gate.</li>
889
+ <li>The session is <strong>not</strong> a resumed voice call (resume skips the gate).</li>
890
+ </ul>
891
+ <p>While the gate is open, the server:</p>
892
+ <ul>
893
+ <li>Does <strong>not</strong> open STT or play the greeting.</li>
894
+ <li>Rejects <code>start_continuous_mode</code> with <code>{"ok":false,"t":"error","code":"DISCLAIMER_PENDING",...}</code>.</li>
895
+ <li>Drops uplink <strong>binary</strong> audio (microphone data) until the gate clears.</li>
896
+ <li>Starts a server-side timer; if the user never acknowledges, the session is closed with <code>DISCLAIMER_TIMEOUT</code> (duration is configured on the server, typically on the order of minutes).</li>
897
+ </ul>
898
+
899
+ <h3 id="ios-uplink-audio">iOS Safari: microphone uplink</h3>
900
+ <p>Voice capture uses an <code>AudioWorklet</code>. On iPhone and iPad, WebKit often runs the capture <code>AudioContext</code> at the device hardware rate (commonly 44.1&nbsp;kHz or 48&nbsp;kHz) even when a lower rate was requested, while the server expects PCM at the input rate negotiated in <code>hello_ack</code> (typically 16&nbsp;kHz). The SDK <strong>resamples uplink PCM</strong> to that negotiated rate before sending binary frames. The recorder connects the worklet through a <strong>zero-gain node to the destination</strong> so WebKit reliably pulls the processor (graphs that dead-end at the worklet alone may not run on some builds). On mobile, after <code>getUserMedia</code> succeeds, the SDK may <strong>prime</strong> the shared recorder context while user activation is still fresh, before the WebSocket handshake and <code>hello_ack</code> finish. For embedded widgets, use <code>allow=&quot;microphone&quot;</code> on the iframe when the host page is cross-origin.</p>
901
+
902
+ <h3 id="host-csp-audioworklet">Embed sites: CSP and “Unable to load a worklet's module”</h3>
903
+ <p>Strict <strong>Content-Security-Policy</strong> on the parent page can block <code>audioWorklet.addModule()</code> when the processor URL points at another host (for example <code>cdn.talktopc.com</code>), which surfaces as <code>AbortError: Unable to load a worklet's module</code> and voice never reaches the server. The SDK tries that URL first, then automatically retries using the <strong>capture worklet source bundled inside the widget script</strong> and a <code>blob:</code> URL—this works on many shops without whitelisting our CDN. If both attempts fail, relax CSP (often <code>worker-src</code> / <code>script-src</code> must allow <code>blob:</code>, and/or your CDN origin for the processor), or host <code>audio-processor.js</code> on your own domain and set <code>voice.audioProcessorPath</code> to that same-origin URL.</p>
904
+
905
+ <h3><code>hello_ack</code> fields (gate active)</h3>
906
+ <p>When disclaimers apply, the server includes:</p>
907
+ <table class="properties-table">
908
+ <thead>
909
+ <tr>
910
+ <th>Field</th>
911
+ <th>Type</th>
912
+ <th>Description</th>
913
+ </tr>
914
+ </thead>
915
+ <tbody>
916
+ <tr>
917
+ <td><code>disclaimersRequired</code></td>
918
+ <td>boolean</td>
919
+ <td>Must be <code>true</code> when the gate is active</td>
920
+ </tr>
921
+ <tr>
922
+ <td><code>disclaimerTexts</code></td>
923
+ <td>string[]</td>
924
+ <td>Plain-text lines to show the user (no HTML; escape/sanitize in your UI)</td>
925
+ </tr>
926
+ <tr>
927
+ <td><code>disclaimersHash</code></td>
928
+ <td>string</td>
929
+ <td>SHA-256 (hex) over the canonical text list; echoed in <code>disclaimer_ack</code> for verification</td>
930
+ </tr>
931
+ <tr>
932
+ <td><code>disclaimerTimeoutMs</code></td>
933
+ <td>number</td>
934
+ <td>Hint for UI (countdown copy); server enforces the real timeout independently</td>
935
+ </tr>
936
+ </tbody>
937
+ </table>
938
+
939
+ <h3>Client message: <code>disclaimer_ack</code></h3>
940
+ <p>After the user accepts or declines, send:</p>
941
+ <pre><code class="language-json">{
942
+ "t": "disclaimer_ack",
943
+ "accepted": true,
944
+ "disclaimersHash": "sha256-from-hello_ack",
945
+ "conversationId": "optional-matches-hello_ack"
946
+ }</code></pre>
947
+ <p>For decline, set <code>"accepted": false</code>. Duplicate acks for the same session are ignored. The SDK method <code>sendDisclaimerAck(accepted)</code> builds this frame using the hash and conversation id from <code>hello_ack</code>.</p>
948
+
949
+ <h3>Server error frames (<code>t: "error"</code>)</h3>
950
+ <table class="properties-table">
951
+ <thead>
952
+ <tr>
953
+ <th><code>code</code></th>
954
+ <th>Meaning</th>
955
+ </tr>
956
+ </thead>
957
+ <tbody>
958
+ <tr>
959
+ <td><code>DISCLAIMER_PENDING</code></td>
960
+ <td>Client tried to start continuous mode or stream audio before sending a successful ack; session stays open—send <code>disclaimer_ack</code> then retry</td>
961
+ </tr>
962
+ <tr>
963
+ <td><code>DISCLAIMER_DECLINED</code></td>
964
+ <td>User declined; server ends the conversation</td>
965
+ </tr>
966
+ <tr>
967
+ <td><code>DISCLAIMER_TIMEOUT</code></td>
968
+ <td>No acknowledgement in time; connection closed</td>
969
+ </tr>
970
+ <tr>
971
+ <td><code>DISCLAIMER_HASH_MISMATCH</code></td>
972
+ <td>Ack hash did not match server expectation; connection closed</td>
973
+ </tr>
974
+ </tbody>
975
+ </table>
976
+
977
+ <h3>Using VoiceSDK v2 (custom apps)</h3>
978
+ <ol>
979
+ <li>Use <strong>protocol v2</strong> (default in current SDK): <code>protocolVersion: 2</code>.</li>
980
+ <li>After <code>connect()</code>, wait for <code>hello_ack</code>. The SDK sets <code>voiceSDK.disclaimersPending === true</code> when the gate is active.</li>
981
+ <li>Listen for <code>disclaimersRequired</code>. The payload includes <code>texts</code>, <code>disclaimersHash</code>, <code>disclaimerTimeoutMs</code>, and <code>conversationId</code> for your UI.</li>
982
+ <li>Show your own modal or screen with the given texts. Do not call <code>startRecording()</code> until the user has accepted and you have called <code>sendDisclaimerAck(true)</code> (calling <code>startRecording()</code> while <code>disclaimersPending</code> is still true throws with <code>error.code === 'DISCLAIMER_PENDING'</code>).</li>
983
+ <li>On accept: <code>voiceSDK.sendDisclaimerAck(true)</code>. The server then opens STT, plays the greeting, and accepts <code>start_continuous_mode</code>.</li>
984
+ <li>On decline: <code>voiceSDK.sendDisclaimerAck(false)</code>. The server closes the session; the SDK emits <code>disclaimerRejected</code> and the raw <code>message</code> event for the error frame.</li>
985
+ <li>Handle <code>disclaimerRejected</code> for terminal server outcomes (<code>DISCLAIMER_DECLINED</code>, <code>DISCLAIMER_TIMEOUT</code>, <code>DISCLAIMER_HASH_MISMATCH</code>). Handle <code>error</code> for <code>DISCLAIMER_PENDING</code> (ordering bug or race—fix by ack first).</li>
986
+ </ol>
987
+ <pre><code class="language-javascript">const voiceSDK = new VoiceSDK({ agentId, appId, protocolVersion: 2 });
988
+
989
+ voiceSDK.on('disclaimersRequired', (payload) => {
990
+ showMyModal({
991
+ texts: payload.texts,
992
+ onAccept: () => voiceSDK.sendDisclaimerAck(true),
993
+ onDecline: () => voiceSDK.sendDisclaimerAck(false)
994
+ });
995
+ });
996
+
997
+ voiceSDK.on('disclaimerRejected', ({ code, message }) => {
998
+ console.warn('Disclaimer flow ended:', code, message);
999
+ });
1000
+
1001
+ voiceSDK.on('error', (err) => {
1002
+ if (err.code === 'DISCLAIMER_PENDING') {
1003
+ console.warn('Start recording only after sendDisclaimerAck(true)');
1004
+ }
1005
+ });
1006
+
1007
+ await voiceSDK.connect();
1008
+ // Only after ack (or if disclaimersRequired never fired):
1009
+ await voiceSDK.startRecording();</code></pre>
1010
+ <p><strong>SDK state you may read:</strong> <code>disclaimersPending</code>, <code>disclaimersHash</code>, <code>lastDisclaimerPayload</code> (set from <code>hello_ack</code>). After <code>sendDisclaimerAck</code> runs, the SDK clears <code>disclaimersPending</code> locally when the WebSocket send succeeds.</p>
1011
+
1012
+ <h3>Voice &amp; Chat Widget (built-in behavior)</h3>
1013
+ <p>No extra widget options are required. When the server sends the disclaimer gate:</p>
1014
+ <ul>
1015
+ <li><code>VoiceInterface</code> waits after the WebSocket is up and <code>hello_ack</code> is processed.</li>
1016
+ <li>It opens a built-in modal (<strong>Notice</strong> / <strong>Accept</strong> / <strong>Decline</strong>) with <code>disclaimerTexts</code> from the server.</li>
1017
+ <li>Accept: on <strong>desktop</strong>, the widget sends <code>sendDisclaimerAck(true)</code> immediately, then requests the microphone and <code>startListening</code>. On <strong>mobile</strong>, it waits until the user has granted microphone access (and the post-grant audio delay) before sending <code>sendDisclaimerAck(true)</code>, so the server does not stream the greeting over the system permission sheet or get cut off when capture starts.</li>
1018
+ <li>Decline (<strong>No thanks</strong>) calls <code>sendDisclaimerAck(false)</code>, waits briefly so the ack reaches the server (which ends the conversation and closes the socket), then disconnects the client, invokes <code>onConversationEnd</code> on the wrapper SDK since recording never started, resets UI, and returns to landing (or idle voice in <code>voice-only</code> mode).</li>
1019
+ </ul>
1020
+ <p>To match your site language, use the widget’s existing <code>language</code> / translation hooks for general UI; disclaimer <strong>body text</strong> always comes from the server (compliance copy).</p>
1021
+
1022
+ <h3>Resume &amp; text chat</h3>
1023
+ <p><strong>Resume:</strong> Resumed voice sessions omit the disclaimer gate.</p>
1024
+ <p><strong>Text chat:</strong> Server-driven disclaimer is implemented for <strong>voice</strong> in this release. The text WebSocket <code>hello</code> path does not yet mirror these fields; extend the backend and <code>TextChatSDK</code> if you need the same gate for text-only sessions.</p>
1025
+
855
1026
  <div class="info-box">
856
1027
  <strong>🎯 Key Benefits:</strong>
857
1028
  <ul style="margin-top: 10px;">
@@ -1017,6 +1188,17 @@ voiceSDK.on('formatNegotiated', (format) => {
1017
1188
  <h1>Voice & Chat Widget</h1>
1018
1189
  <p>Pre-built, customizable widget with voice and text chat - perfect for adding AI conversation to any website.</p>
1019
1190
 
1191
+ <div class="info-box" id="widget-agent-display-name" style="margin-bottom: 20px;">
1192
+ <p><strong>Agent display name</strong></p>
1193
+ <ul style="margin: 8px 0 0 18px;">
1194
+ <li>Set the name on the <strong>voice idle hero</strong> (and the letter inside the avatar when no image URL is set) with the <strong>root</strong> property <code>agentName</code> only.</li>
1195
+ <li>Use <code>header.pillTitle</code> for a short CTA on the <strong>desktop floating pill</strong> and the <strong>mobile FAB</strong> (e.g. “Talk to me” / “דברו איתי”); when empty, both use <code>header.title</code>. Override the mobile FAB line only with <code>header.mobileLabel</code>. Separate from <code>agentName</code>.</li>
1196
+ <li><code>voice.agentName</code> is <strong>not supported</strong> — it is removed from config when the widget merges settings.</li>
1197
+ <li>Legacy root <code>headerTitle</code> is <strong>ignored</strong>. Use <code>agentName</code> for the hero name and <code>header.title</code> for the general assistant title line (pill / mobile landing fallback).</li>
1198
+ <li>In <strong>unified</strong> mode, the text chat screen has a top bar with <strong>Voice or text</strong> (widget translation key <code>backToModeChoice</code>) to return to the voice/text choice inside the panel.</li>
1199
+ </ul>
1200
+ </div>
1201
+
1020
1202
  <div class="feature-grid">
1021
1203
  <div class="feature-card">
1022
1204
  <div class="feature-icon">💬</div>
@@ -1057,6 +1239,9 @@ voiceSDK.on('formatNegotiated', (format) => {
1057
1239
  // Required
1058
1240
  agentId: 'agent_123', // Your AI agent ID
1059
1241
  appId: 'your_app_id', // Your application ID
1242
+
1243
+ // Optional — root only (voice idle hero name & avatar initial when no image)
1244
+ agentName: 'Alex',
1060
1245
 
1061
1246
  // Optional - Agent Settings Override (available when domain whitelist is configured)
1062
1247
  agentSettingsOverride: {
@@ -1071,7 +1256,13 @@ voiceSDK.on('formatNegotiated', (format) => {
1071
1256
 
1072
1257
  // Optional - Appearance
1073
1258
  primaryColor: '#7C3AED', // Widget theme color
1074
- position: 'bottom-right', // 'bottom-right', 'bottom-left'
1259
+ position: { // Or shorthand: 'bottom-right', 'bottom-left'
1260
+ vertical: 'bottom',
1261
+ horizontal: 'right',
1262
+ offset: { x: 20, y: 20 },
1263
+ draggable: false, // true = user can drag launcher + panel
1264
+ draggablePersist: true // remember drag position in localStorage
1265
+ },
1075
1266
  language: 'en', // 'en', 'es', 'fr', 'de', 'he', etc.
1076
1267
  direction: 'ltr', // 'ltr' or 'rtl' for right-to-left languages
1077
1268
 
@@ -1083,6 +1274,9 @@ voiceSDK.on('formatNegotiated', (format) => {
1083
1274
  }
1084
1275
  });</code></pre>
1085
1276
 
1277
+ <h2>Server-driven disclaimer (voice)</h2>
1278
+ <p>If your agent has non-empty <code>disclaimers</code> configured on the server, the widget shows a <strong>Notice</strong> modal with the server-provided text before microphone streaming and the greeting. The user must tap <strong>Accept</strong> or <strong>Decline</strong>; you do not need extra embed code. For protocol fields, SDK hooks, and custom implementations, see <a href="#server-driven-disclaimer">Server-driven disclaimer (voice)</a>.</p>
1279
+
1086
1280
  <h2>Access Control</h2>
1087
1281
  <div class="info-box">
1088
1282
  <strong>💡 Domain Whitelist:</strong> For production applications, configure a domain whitelist in your agent's admin panel to control which websites can connect to your agent.
@@ -1104,7 +1298,7 @@ voiceSDK.on('formatNegotiated', (format) => {
1104
1298
  <ul style="margin-top: 10px; margin-left: 20px;">
1105
1299
  <li>The widget only needs <code>agentId</code> and <code>appId</code> to connect</li>
1106
1300
  <li>Access control is managed via domain whitelist in the admin panel</li>
1107
- <li>Optionally pass <code>websocketUrl</code> to override the default WebSocket endpoint</li>
1301
+ <li>WebSocket URLs use the production TalkToPC endpoint by default (same as voice). Pass <code>websocketUrl</code> only if you need a non-default backend.</li>
1108
1302
  </ul>
1109
1303
  </div>
1110
1304
 
@@ -1116,8 +1310,9 @@ voiceSDK.on('formatNegotiated', (format) => {
1116
1310
  appId: 'your_app_id',
1117
1311
 
1118
1312
  icon: {
1119
- type: 'custom', // 'default', 'emoji', or 'custom'
1120
- customImage: 'https://your-site.com/logo.png', // Custom image URL
1313
+ type: 'custom', // 'microphone', 'emoji', or 'custom'
1314
+ // Omit customImage (or use '') for default animated waveform on the desktop pill
1315
+ customImage: 'https://your-site.com/logo.png', // Optional: pill icon image URL
1121
1316
  size: 60, // Icon size in pixels
1122
1317
  backgroundColor: '#FFFFFF', // Background color
1123
1318
  borderRadius: '50%' // Border radius (50% for circle)
@@ -1216,6 +1411,31 @@ document.getElementById('myButton').onclick = () => {
1216
1411
  <pre><code>widget.toggle();</code></pre>
1217
1412
  </div>
1218
1413
 
1414
+ <div class="method-card">
1415
+ <h3><code>widget.minimize()</code></h3>
1416
+ <p>Collapse the chat panel back to the round launcher pill — same visual
1417
+ state as if the user had clicked the launcher while the panel was open.
1418
+ Idempotent (no-op when already minimized) and does <strong>not</strong>
1419
+ end an active voice call: the WebSocket and conversation state are
1420
+ preserved, the user just sees the bubble until they re-open. Also
1421
+ triggered automatically by a backend-pushed
1422
+ <code>{ t: 'minimize_widget' }</code> control message, so partner
1423
+ integrations can choreograph the chat panel from the server side
1424
+ (e.g. minimize the widget when opening a native trolley drawer).</p>
1425
+ <pre><code>widget.minimize();</code></pre>
1426
+ </div>
1427
+
1428
+ <div class="method-card">
1429
+ <h3><code>widget.maximize()</code></h3>
1430
+ <p>Expand the chat panel from the round launcher pill — same visual
1431
+ state as if the user had clicked the launcher while the panel was
1432
+ closed. Idempotent (no-op when already open) and runs the same
1433
+ auto-connect side-effect a real click would when configured. Also
1434
+ triggered automatically by a backend-pushed
1435
+ <code>{ t: 'maximize_widget' }</code> control message.</p>
1436
+ <pre><code>widget.maximize();</code></pre>
1437
+ </div>
1438
+
1219
1439
  <div class="method-card">
1220
1440
  <h3><code>widget.destroy()</code></h3>
1221
1441
  <p>Remove the widget from the page.</p>
@@ -1227,36 +1447,47 @@ document.getElementById('myButton').onclick = () => {
1227
1447
  <p>Update widget configuration dynamically.</p>
1228
1448
  <pre><code>widget.updateConfig({
1229
1449
  primaryColor: '#FF5733',
1230
- language: 'es'
1450
+ language: 'es',
1451
+ agentName: 'Jordan' // root only; voice.agentName in this object is ignored
1231
1452
  });</code></pre>
1232
1453
  </div>
1233
1454
 
1234
- <h2>Widget Events</h2>
1455
+ <h2>Widget Event Callbacks</h2>
1456
+ <p>Pass these as top-level config properties when constructing <code>TTPChatWidget</code>:</p>
1235
1457
  <pre><code>const widget = new TTPAgentSDK.TTPChatWidget({
1236
1458
  agentId: 'agent_123',
1237
1459
  appId: 'your_app_id',
1238
-
1239
- // Event callbacks
1240
- onOpen: () => {
1241
- console.log('Chat opened');
1460
+
1461
+ onConversationStart: () => {
1462
+ console.log('Voice conversation started');
1242
1463
  },
1243
-
1244
- onClose: () => {
1245
- console.log('Chat closed');
1464
+
1465
+ onConversationEnd: () => {
1466
+ console.log('Voice conversation ended');
1246
1467
  },
1247
-
1248
- onMessage: (message) => {
1249
- console.log('New message:', message);
1468
+
1469
+ onBargeIn: () => {
1470
+ console.log('User interrupted the agent');
1250
1471
  },
1251
-
1252
- onError: (error) => {
1253
- console.error('Widget error:', error);
1472
+
1473
+ onAudioStartPlaying: () => {
1474
+ console.log('Agent audio started');
1254
1475
  },
1255
-
1256
- onReady: () => {
1257
- console.log('Widget initialized');
1476
+
1477
+ onAudioStoppedPlaying: () => {
1478
+ console.log('Agent audio stopped');
1479
+ },
1480
+
1481
+ onSubtitleDisplay: (subtitle) => {
1482
+ console.log('Subtitle:', subtitle);
1483
+ },
1484
+
1485
+ onVoiceCallButtonClick: () => {
1486
+ // Return false to prevent starting the call
1487
+ return true;
1258
1488
  }
1259
1489
  });</code></pre>
1490
+ <p>For lower-level VoiceSDK events (<code>onConnected</code>, <code>onMessage</code>, etc.), use <code>widget.voiceInterface.sdk</code> or instantiate <code>VoiceSDK</code> directly. See <a href="#events">Events &amp; Callbacks</a>.</p>
1260
1491
 
1261
1492
  <h2>Complete Example</h2>
1262
1493
  <pre><code>&lt;!DOCTYPE html&gt;
@@ -1312,13 +1543,15 @@ document.getElementById('myButton').onclick = () => {
1312
1543
  &lt;/body&gt;
1313
1544
  &lt;/html&gt;</code></pre>
1314
1545
 
1315
- <h2>Configuration Reference</h2>
1546
+ <h2 id="configuration-reference">Configuration Reference</h2>
1316
1547
  <div class="info-box">
1317
1548
  <p><strong>🎨 Extensive Customization</strong></p>
1318
1549
  <p>The Voice & Chat Widget can be customized in almost every aspect - colors, text, icons, sizes, behaviors, and more!</p>
1319
1550
  </div>
1320
1551
 
1321
- <p>See the <a href="../examples/test-text-chat.html" target="_blank">live demo</a> to experiment with all customization options interactively.</p>
1552
+ <p><strong>Agent naming:</strong> use root <code>agentName</code> for the voice idle hero and avatar letter fallback. Do not use <code>voice.agentName</code> (removed on merge). Do not use legacy root <code>headerTitle</code> (ignored). See <a href="#widget-agent-display-name">Agent display name</a> above.</p>
1553
+
1554
+ <p>See the <a href="../examples/widget-customization.html" target="_blank">live customization demo</a> to experiment with all options interactively, including quick themes (Default, Light, Sunset, Hebrew, S-Law).</p>
1322
1555
 
1323
1556
  <details open>
1324
1557
  <summary><strong>Required Configuration</strong></summary>
@@ -1366,8 +1599,8 @@ document.getElementById('myButton').onclick = () => {
1366
1599
  <tr>
1367
1600
  <td><code>direction</code></td>
1368
1601
  <td>string</td>
1369
- <td>'ltr'</td>
1370
- <td>'ltr' or 'rtl'</td>
1602
+ <td><code>'ltr'</code> (or <code>'rtl'</code> when <code>language</code> is <code>he</code> / <code>ar</code>)</td>
1603
+ <td><code>'ltr'</code> or <code>'rtl'</code>. If omitted, Hebrew and Arabic default to <code>'rtl'</code>. Sets the shadow host <code>dir</code> so the desktop pill launcher matches the panel (fixes extra padding beside the logo on RTL sites). In <code>'rtl'</code>, transcript, bubbles, and mobile bar use RTL punctuation order.</td>
1371
1604
  </tr>
1372
1605
  <tr>
1373
1606
  <td><code>language</code></td>
@@ -1375,6 +1608,18 @@ document.getElementById('myButton').onclick = () => {
1375
1608
  <td>'en'</td>
1376
1609
  <td>Language code (en, es, fr, de, he, ar, etc.)</td>
1377
1610
  </tr>
1611
+ <tr>
1612
+ <td><code>agentName</code></td>
1613
+ <td>string</td>
1614
+ <td>—</td>
1615
+ <td><strong>Root only.</strong> Name on the voice idle hero and first-letter avatar fallback when no header image is set. If omitted or empty, the widget defaults to <code>Sasha</code>. Color: <code>voice.agentNameColor</code>. Not the desktop pill text — use <code>header.pillTitle</code> or <code>header.title</code> for that.</td>
1616
+ </tr>
1617
+ <tr>
1618
+ <td><code>headerTitle</code></td>
1619
+ <td>string</td>
1620
+ <td>—</td>
1621
+ <td><strong>Deprecated / ignored.</strong> Former optional root field; it is no longer read. Use <code>agentName</code> for the voice hero name and <code>header.title</code> for the shared assistant title line.</td>
1622
+ </tr>
1378
1623
  <tr>
1379
1624
  <td><code>variables</code></td>
1380
1625
  <td>object</td>
@@ -1384,8 +1629,8 @@ document.getElementById('myButton').onclick = () => {
1384
1629
  <tr>
1385
1630
  <td><code>websocketUrl</code></td>
1386
1631
  <td>string</td>
1387
- <td>null</td>
1388
- <td>Optional custom WebSocket base URL (defaults to wss://speech.talktopc.com/ws/conv)</td>
1632
+ <td><code>wss://speech.talktopc.com/ws/conv</code> (built-in)</td>
1633
+ <td>Optional override for voice and text. Omit unless you point at a custom backend; text still uses <code>/chat/text</code> on the same host as this base.</td>
1389
1634
  </tr>
1390
1635
  <tr>
1391
1636
  <td><code>agentSettingsOverride</code></td>
@@ -1405,6 +1650,30 @@ document.getElementById('myButton').onclick = () => {
1405
1650
  <td>true</td>
1406
1651
  <td>Enable Shadow DOM for CSS isolation. Set to <code>false</code> for Shopify compatibility. See <a href="#shadow-dom-config">Shadow DOM Configuration</a> below.</td>
1407
1652
  </tr>
1653
+ <tr>
1654
+ <td><code>mobileVoiceUI</code></td>
1655
+ <td>boolean</td>
1656
+ <td>auto</td>
1657
+ <td><strong>Root-level override.</strong> Force mobile-style voice call UI (bottom bar + overlay) or desktop hero. Takes precedence over <code>behavior.mobileVoiceUI</code> when both are set. Omit for auto: native iOS/Android, or viewport ≤768px with touch/coarse pointer.</td>
1658
+ </tr>
1659
+ <tr>
1660
+ <td><code>inputFormat</code></td>
1661
+ <td>object</td>
1662
+ <td>—</td>
1663
+ <td>Optional v2 input audio format passed to the voice WebSocket hello message: <code>{ encoding, sampleRate, channels, bitDepth }</code>. Same fields as top-level VoiceSDK config (<code>sampleRate</code>, etc.). See <a href="#protocol-v2">Protocol v2</a>.</td>
1664
+ </tr>
1665
+ <tr>
1666
+ <td><code>visualAssistant</code></td>
1667
+ <td>object</td>
1668
+ <td>null</td>
1669
+ <td>Enable browser-side visual assistant tools (page read, highlight, scroll, navigate, form fill, click, screenshot). Also accepted under <code>agentSettingsOverride.visualAssistant</code>. See <a href="#visual-assistant-config">Visual Assistant</a> below.</td>
1670
+ </tr>
1671
+ <tr>
1672
+ <td><code>whatsapp</code></td>
1673
+ <td>object</td>
1674
+ <td>null</td>
1675
+ <td>Optional WhatsApp handoff on the voice idle hero: <code>{ number: '972501234567', text: 'optional pre-filled message' }</code>. Non-digits are stripped from <code>number</code>. Omit to hide the WhatsApp button.</td>
1676
+ </tr>
1408
1677
  </tbody>
1409
1678
  </table>
1410
1679
  </details>
@@ -1487,6 +1756,9 @@ const widget = new TTPChatWidget({
1487
1756
  <ul style="margin: 10px 0 10px 20px;">
1488
1757
  <li>When <code>useShadowDOM: false</code>, the widget uses targeted CSS resets instead of aggressive resets to preserve internal widget styles</li>
1489
1758
  <li>If you experience layout issues with <code>useShadowDOM: false</code>, check if your theme's CSS is overriding widget styles - you may need to add more specific CSS rules</li>
1759
+ <li><strong>Unified mode:</strong> With <code>useShadowDOM: false</code>, the widget must show only one of voice or text at a time inside the panel. A previous reset rule forced <code>display:flex</code> on multiple roots (higher specificity than the hide rules), which made them stack vertically on Shopify; this is fixed in current builds.</li>
1760
+ <li><strong>Voice call UI:</strong> Call duration updates and the red recording dot use DOM queries scoped to <code>#ttp-widget-container</code> (not <code>document</code>), and extra CSS guards the dot and pulse when themes override spans — fixes static timers and missing dots on Shopify.</li>
1761
+ <li><strong>Orb waveform:</strong> Bars are injected under <code>#ttp-widget-container</code>. With <code>useShadowDOM: false</code>, the same CSS <code>ttp-wave</code> keyframes and per-bar delays are used as in Shadow DOM; high-specificity rules protect size and animation without fixing <code>opacity</code> (which would break the keyframe fade).</li>
1490
1762
  <li>The widget automatically handles CSS injection differently based on this setting - no additional configuration needed</li>
1491
1763
  </ul>
1492
1764
  </div>
@@ -1527,8 +1799,27 @@ const widget = new TTPChatWidget({
1527
1799
  <td>{ x: 20, y: 20 }</td>
1528
1800
  <td>Offset from edges (pixels)</td>
1529
1801
  </tr>
1802
+ <tr>
1803
+ <td><code>position.draggable</code></td>
1804
+ <td>boolean</td>
1805
+ <td>false</td>
1806
+ <td>When <code>true</code>, visitors can drag the launcher pill / mobile FAB and the open chat panel around the viewport. They move <strong>together as one unit</strong> (never split). Drag handles: the pill/FAB and panel header bars (voice idle header, active-call top bar, text chat top bar). Header buttons (close, etc.) still work normally.</td>
1807
+ </tr>
1808
+ <tr>
1809
+ <td><code>position.draggablePersist</code></td>
1810
+ <td>boolean</td>
1811
+ <td>true</td>
1812
+ <td>When <code>true</code> (default), the dragged position is saved in <code>localStorage</code> per origin+path and restored on the next visit. Set <code>false</code> to reset to the configured corner on every load. The widget unit and the desktop minimized voice strip (<code>flavor.callView: 'minimized'</code>) remember positions independently.</td>
1813
+ </tr>
1814
+ <tr>
1815
+ <td><code>positionOffset</code></td>
1816
+ <td>object</td>
1817
+ <td>—</td>
1818
+ <td><strong>Legacy.</strong> Used only when <code>position</code> is a string (e.g. <code>'bottom-right'</code>) instead of an object. Prefer <code>position.offset</code>.</td>
1819
+ </tr>
1530
1820
  </tbody>
1531
1821
  </table>
1822
+ <p style="margin: 12px 0 0 0;">When <code>position.draggable</code> is enabled, the desktop <strong>minimized voice strip</strong> (<code>flavor.callView: 'minimized'</code>) is also draggable by its body (controls and text input still work). Positions are clamped to stay on-screen and re-clamped on window resize.</p>
1532
1823
  </details>
1533
1824
 
1534
1825
  <details>
@@ -1552,8 +1843,8 @@ const widget = new TTPChatWidget({
1552
1843
  <tr>
1553
1844
  <td><code>icon.customImage</code></td>
1554
1845
  <td>string</td>
1555
- <td>TTP logo</td>
1556
- <td>URL to custom icon image</td>
1846
+ <td>—</td>
1847
+ <td>Optional HTTPS image URL for the <strong>desktop</strong> floating pill icon. When omitted or empty, the pill uses the same <strong>animated waveform</strong> as the mobile launcher and pre-chat landing (white bars on a frosted circle).</td>
1557
1848
  </tr>
1558
1849
  <tr>
1559
1850
  <td><code>icon.size</code></td>
@@ -1641,7 +1932,13 @@ const widget = new TTPChatWidget({
1641
1932
  <td><code>header.title</code></td>
1642
1933
  <td>string</td>
1643
1934
  <td>'Chat Assistant'</td>
1644
- <td>Header title text</td>
1935
+ <td>Default title when more specific labels are omitted: fallback for the <strong>desktop and mobile</strong> launchers if <code>header.pillTitle</code> is not set (unless <code>header.mobileLabel</code> is set), and for the <strong>mobile pre-chat landing</strong> name when root <code>agentName</code> is not set. Does <strong>not</strong> set the voice idle hero name — use root <code>agentName</code> for that.</td>
1936
+ </tr>
1937
+ <tr>
1938
+ <td><code>header.pillTitle</code></td>
1939
+ <td>string</td>
1940
+ <td>''</td>
1941
+ <td>Optional main line on the <strong>desktop</strong> floating pill and the <strong>mobile</strong> pill launcher (same fallback chain as desktop: when empty, uses <code>header.title</code>). Set <code>header.mobileLabel</code> if the FAB needs different copy than the desktop pill. Does not change the voice idle hero name — use root <code>agentName</code> for that.</td>
1645
1942
  </tr>
1646
1943
  <tr>
1647
1944
  <td><code>header.showTitle</code></td>
@@ -1661,12 +1958,73 @@ const widget = new TTPChatWidget({
1661
1958
  <td>'#FFFFFF'</td>
1662
1959
  <td>Header text color</td>
1663
1960
  </tr>
1961
+ <tr>
1962
+ <td><code>header.mobileLabel</code></td>
1963
+ <td>string</td>
1964
+ <td>—</td>
1965
+ <td>Optional: overrides the mobile pill launcher’s main line only. When omitted, the FAB uses the same text as the desktop pill (<code>header.pillTitle</code> or <code>header.title</code>).</td>
1966
+ </tr>
1967
+ <tr>
1968
+ <td><code>header.showCloseButton</code></td>
1969
+ <td>boolean</td>
1970
+ <td>true</td>
1971
+ <td>Show or hide the panel close button in the header.</td>
1972
+ </tr>
1973
+ <tr>
1974
+ <td><code>header.onlineIndicatorText</code></td>
1975
+ <td>string</td>
1976
+ <td>Auto</td>
1977
+ <td>Online status label on the desktop pill, mobile FAB, and headers. When omitted, uses the translated “Online” string for the widget language.</td>
1978
+ </tr>
1979
+ <tr>
1980
+ <td><code>header.onlineIndicatorColor</code></td>
1981
+ <td>string</td>
1982
+ <td>header.textColor</td>
1983
+ <td>Text color for the online indicator label.</td>
1984
+ </tr>
1985
+ <tr>
1986
+ <td><code>header.onlineIndicatorDotColor</code></td>
1987
+ <td>string</td>
1988
+ <td>'#10b981'</td>
1989
+ <td>Color of the online status dot next to the indicator text.</td>
1990
+ </tr>
1991
+ <tr>
1992
+ <td><code>footer.show</code></td>
1993
+ <td>boolean</td>
1994
+ <td>true</td>
1995
+ <td>Show or hide the &quot;Powered by&quot; footer.</td>
1996
+ </tr>
1997
+ <tr>
1998
+ <td><code>footer.brand</code></td>
1999
+ <td>string</td>
2000
+ <td>'talktopc'</td>
2001
+ <td>Brand shown in the &quot;Powered by&quot; footer. <code>'talktopc'</code> links to talktopc.com; <code>'speacart'</code> links to speacart.com. Any other value defaults to TalkToPC.</td>
2002
+ </tr>
2003
+ <tr>
2004
+ <td><code>footer.backgroundColor</code></td>
2005
+ <td>string</td>
2006
+ <td>'#f9fafb'</td>
2007
+ <td>Footer background color.</td>
2008
+ </tr>
2009
+ <tr>
2010
+ <td><code>footer.textColor</code></td>
2011
+ <td>string</td>
2012
+ <td>'#9ca3af'</td>
2013
+ <td>Footer text color.</td>
2014
+ </tr>
2015
+ <tr>
2016
+ <td><code>footer.hoverColor</code></td>
2017
+ <td>string</td>
2018
+ <td>'#7C3AED'</td>
2019
+ <td>Footer link hover color.</td>
2020
+ </tr>
1664
2021
  </tbody>
1665
2022
  </table>
1666
2023
  </details>
1667
2024
 
1668
2025
  <details>
1669
2026
  <summary><strong>Messages & Chat</strong></summary>
2027
+ <p style="margin: 10px 0 12px 0;">On <strong>desktop</strong>, when the text chat view is open, the panel uses a <strong>fixed height</strong> (up to <code>min(520px, 100vh − 100px)</code>) so new messages scroll inside the transcript instead of stretching the card. By default (<code>text.useVoiceTheme</code> not <code>false</code>), the text UI shares the <strong>voice theme</strong>: <code>voice.heroGradient1</code> / <code>heroGradient2</code> surface (same as the voice idle hero), <code>voice.primaryBtnGradient*</code> and <code>startCallButtonColor</code> for send/focus and user bubbles (<code>#primary40</code> tint), <code>avatarGradient1/2</code> on the assistant avatar, and transcript-style translucent inputs. The “Powered by” footer uses the same link accent as on the hero. Set <code>text.useVoiceTheme: false</code> for a light layout driven by solid <code>panel.backgroundColor</code> and <code>messages.*</code> colors.</p>
1670
2028
  <table class="properties-table">
1671
2029
  <thead>
1672
2030
  <tr>
@@ -1705,12 +2063,36 @@ const widget = new TTPChatWidget({
1705
2063
  <td><code>messages.textColor</code></td>
1706
2064
  <td>string</td>
1707
2065
  <td>'#1F2937'</td>
1708
- <td>Message text color</td>
2066
+ <td>Message text color (fallback when role-specific colors are omitted)</td>
2067
+ </tr>
2068
+ <tr>
2069
+ <td><code>messages.userTextColor</code></td>
2070
+ <td>string</td>
2071
+ <td>messages.textColor</td>
2072
+ <td>User message text color</td>
2073
+ </tr>
2074
+ <tr>
2075
+ <td><code>messages.agentTextColor</code></td>
2076
+ <td>string</td>
2077
+ <td>messages.textColor</td>
2078
+ <td>Agent message text color</td>
2079
+ </tr>
2080
+ <tr>
2081
+ <td><code>messages.userAvatarIcon</code></td>
2082
+ <td>string</td>
2083
+ <td>'👤'</td>
2084
+ <td>Emoji/icon shown on user message avatars</td>
2085
+ </tr>
2086
+ <tr>
2087
+ <td><code>messages.agentAvatarIcon</code></td>
2088
+ <td>string</td>
2089
+ <td>'🤖'</td>
2090
+ <td>Emoji/icon shown on agent message avatars</td>
1709
2091
  </tr>
1710
2092
  <tr>
1711
2093
  <td><code>messages.fontSize</code></td>
1712
2094
  <td>string</td>
1713
- <td>'14px'</td>
2095
+ <td>'16px'</td>
1714
2096
  <td>Message font size</td>
1715
2097
  </tr>
1716
2098
  <tr>
@@ -1719,23 +2101,29 @@ const widget = new TTPChatWidget({
1719
2101
  <td>8</td>
1720
2102
  <td>Message bubble radius</td>
1721
2103
  </tr>
2104
+ <tr>
2105
+ <td><code>text.useVoiceTheme</code></td>
2106
+ <td>boolean</td>
2107
+ <td><code>true</code></td>
2108
+ <td>When <code>true</code>, text chat matches the voice idle hero gradient, primary gradients, avatar gradients, and dark translucent bubbles/inputs (see voice theme). When <code>false</code>, chrome follows solid <code>panel.backgroundColor</code> (hex) and <code>messages.userBackgroundColor</code> / <code>agentBackgroundColor</code>.</td>
2109
+ </tr>
1722
2110
  <tr>
1723
2111
  <td><code>text.sendButtonColor</code></td>
1724
2112
  <td>string</td>
1725
- <td>'#7C3AED'</td>
1726
- <td>Send button color</td>
2113
+ <td>voice accent</td>
2114
+ <td>Send button fill; defaults to the first resolvable hex from <code>voice.primaryBtnGradient1</code>, <code>voice.primaryBtnGradient2</code>, or <code>voice.startCallButtonColor</code> (including the default start-call indigo when those are unset), then <code>#7C3AED</code>.</td>
1727
2115
  </tr>
1728
2116
  <tr>
1729
2117
  <td><code>text.sendButtonHoverColor</code></td>
1730
2118
  <td>string</td>
1731
- <td>'#6D28D9'</td>
1732
- <td>Send button hover color</td>
2119
+ <td>voice accent 2 / shaded</td>
2120
+ <td>Hover state; uses <code>voice.primaryBtnGradient2</code> when it resolves to a different hex than the send color, otherwise a slightly darker shade of the accent.</td>
1733
2121
  </tr>
1734
2122
  <tr>
1735
2123
  <td><code>text.sendButtonActiveColor</code></td>
1736
2124
  <td>string</td>
1737
- <td>'#6D28D9'</td>
1738
- <td>Send button active color</td>
2125
+ <td>same as hover</td>
2126
+ <td>Active press state; same resolution as hover.</td>
1739
2127
  </tr>
1740
2128
  <tr>
1741
2129
  <td><code>text.sendButtonText</code></td>
@@ -1752,7 +2140,7 @@ const widget = new TTPChatWidget({
1752
2140
  <tr>
1753
2141
  <td><code>text.sendButtonFontSize</code></td>
1754
2142
  <td>string</td>
1755
- <td>'18px'</td>
2143
+ <td>'20px'</td>
1756
2144
  <td>Send button font size</td>
1757
2145
  </tr>
1758
2146
  <tr>
@@ -1776,8 +2164,8 @@ const widget = new TTPChatWidget({
1776
2164
  <tr>
1777
2165
  <td><code>text.inputFocusColor</code></td>
1778
2166
  <td>string</td>
1779
- <td>'#7C3AED'</td>
1780
- <td>Input focus color</td>
2167
+ <td>voice accent</td>
2168
+ <td>Input focus border and ring; same default chain as <code>text.sendButtonColor</code>.</td>
1781
2169
  </tr>
1782
2170
  <tr>
1783
2171
  <td><code>text.inputBackgroundColor</code></td>
@@ -1794,7 +2182,7 @@ const widget = new TTPChatWidget({
1794
2182
  <tr>
1795
2183
  <td><code>text.inputFontSize</code></td>
1796
2184
  <td>string</td>
1797
- <td>'14px'</td>
2185
+ <td>'16px'</td>
1798
2186
  <td>Input font size</td>
1799
2187
  </tr>
1800
2188
  <tr>
@@ -1806,15 +2194,34 @@ const widget = new TTPChatWidget({
1806
2194
  <tr>
1807
2195
  <td><code>text.inputPadding</code></td>
1808
2196
  <td>string</td>
1809
- <td>'6px 14px'</td>
2197
+ <td>'8px 16px'</td>
1810
2198
  <td>Input padding</td>
1811
2199
  </tr>
2200
+ <tr>
2201
+ <td><code>text.sendButtonHint.text</code></td>
2202
+ <td>string</td>
2203
+ <td>''</td>
2204
+ <td>Optional hint text below or near the send button</td>
2205
+ </tr>
2206
+ <tr>
2207
+ <td><code>text.sendButtonHint.color</code></td>
2208
+ <td>string</td>
2209
+ <td>'#6B7280'</td>
2210
+ <td>Send button hint text color</td>
2211
+ </tr>
2212
+ <tr>
2213
+ <td><code>text.sendButtonHint.fontSize</code></td>
2214
+ <td>string</td>
2215
+ <td>'14px'</td>
2216
+ <td>Send button hint font size</td>
2217
+ </tr>
1812
2218
  </tbody>
1813
2219
  </table>
1814
2220
  </details>
1815
2221
 
1816
2222
  <details>
1817
2223
  <summary><strong>Voice Configuration</strong></summary>
2224
+ <p style="margin: 10px 0 12px 0;">The agent’s spoken/idle <strong>display name</strong> is configured with <strong>root</strong> <code>agentName</code> only. Any <code>voice.agentName</code> value in your JSON is stripped during merge and has no effect (<code>widget.updateConfig({ voice: { agentName: '…' } })</code> is ignored for naming).</p>
1818
2225
  <table class="properties-table">
1819
2226
  <thead>
1820
2227
  <tr>
@@ -1895,7 +2302,7 @@ const widget = new TTPChatWidget({
1895
2302
  <td><code>voice.startCallButtonColor</code></td>
1896
2303
  <td>string</td>
1897
2304
  <td>'#667eea'</td>
1898
- <td>Start call button color</td>
2305
+ <td>Fills the start-call button gradient when <code>primaryBtnGradient1</code>/<code>2</code> are omitted. The “TalkToPC” footer link uses the same solid accent as the start button (<code>primaryBtnGradient1</code>, then <code>startCallButtonColor</code>, then defaults).</td>
1899
2306
  </tr>
1900
2307
  <tr>
1901
2308
  <td><code>voice.startCallButtonTextColor</code></td>
@@ -1927,6 +2334,12 @@ const widget = new TTPChatWidget({
1927
2334
  <td>'#94a3b8'</td>
1928
2335
  <td>Transcript label color</td>
1929
2336
  </tr>
2337
+ <tr>
2338
+ <td><code>voice.userTranscriptPrefix</code></td>
2339
+ <td>string | null</td>
2340
+ <td>null</td>
2341
+ <td>Prefix before live user speech (STT) in the collapsed transcript strip and mobile bar (e.g. <code>"You: "</code>). <code>null</code> uses the translation key <code>userTranscriptPrefix</code> for the widget language; empty string removes the prefix.</td>
2342
+ </tr>
1930
2343
  <tr>
1931
2344
  <td><code>voice.controlButtonColor</code></td>
1932
2345
  <td>string</td>
@@ -1945,8 +2358,170 @@ const widget = new TTPChatWidget({
1945
2358
  <td>'en'</td>
1946
2359
  <td>Voice language (overrides global)</td>
1947
2360
  </tr>
2361
+ <tr>
2362
+ <td><code>voice.statusDotColor</code></td>
2363
+ <td>string</td>
2364
+ <td>'#10b981'</td>
2365
+ <td>Status dot color on the voice idle screen</td>
2366
+ </tr>
2367
+ <tr>
2368
+ <td><code>voice.statusText</code></td>
2369
+ <td>string | null</td>
2370
+ <td>null</td>
2371
+ <td>Custom status line on the voice idle screen. <code>null</code> uses the translated default.</td>
2372
+ </tr>
2373
+ <tr>
2374
+ <td><code>voice.outputContainer</code></td>
2375
+ <td>string</td>
2376
+ <td>'raw'</td>
2377
+ <td>Output audio container for v2 format negotiation: <code>'raw'</code> or <code>'wav'</code></td>
2378
+ </tr>
2379
+ <tr>
2380
+ <td><code>voice.outputEncoding</code></td>
2381
+ <td>string</td>
2382
+ <td>'pcm'</td>
2383
+ <td>Output encoding: <code>'pcm'</code>, <code>'pcmu'</code>, or <code>'pcma'</code></td>
2384
+ </tr>
2385
+ <tr>
2386
+ <td><code>voice.outputSampleRate</code></td>
2387
+ <td>number</td>
2388
+ <td>24000</td>
2389
+ <td>Requested output sample rate (Hz): 8000, 16000, 22050, 24000, 44100, or 48000</td>
2390
+ </tr>
2391
+ <tr>
2392
+ <td><code>voice.outputChannels</code></td>
2393
+ <td>number</td>
2394
+ <td>1</td>
2395
+ <td>Output channels (mono only supported)</td>
2396
+ </tr>
2397
+ <tr>
2398
+ <td><code>voice.outputBitDepth</code></td>
2399
+ <td>number</td>
2400
+ <td>16</td>
2401
+ <td>Output bit depth: 8, 16, or 24</td>
2402
+ </tr>
2403
+ </tbody>
2404
+ </table>
2405
+ </details>
2406
+
2407
+ <details open>
2408
+ <summary><strong>Voice Theming &amp; Pill Launcher</strong></summary>
2409
+ <div class="info-box" style="margin: 10px 0;">
2410
+ <p><strong>🎨 Full Voice UI Theming</strong></p>
2411
+ <p>Customize every visual aspect of the voice interface — hero section, buttons, active call screen, pill launcher, and more. Use these properties to create branded themes or match your website's design.</p>
2412
+ <p style="margin-top:8px;">The desktop pill has a fixed width (158px) on viewports ≥769px with a taller vertical layout (padding, 36px icon circle, 13px/11px title/status). Long titles ellipsize. RTL uses logical padding so the logo sits evenly on the start edge.</p>
2413
+ </div>
2414
+
2415
+ <h4 style="margin-top: 16px;">Pill Launcher</h4>
2416
+ <table class="properties-table">
2417
+ <thead>
2418
+ <tr><th>Property</th><th>Type</th><th>Default</th><th>Description</th></tr>
2419
+ </thead>
2420
+ <tbody>
2421
+ <tr><td><code>voice.pillGradient</code></td><td>string</td><td>''</td><td>CSS gradient for the <strong>mobile</strong> floating pill, the <strong>mobile pre-chat landing sheet</strong> (<code>.ttp-mobile-landing</code>), and the <strong>mobile in-call bottom bar</strong> plus the <strong>expanded conversation header</strong>—same token everywhere. When unset, the widget uses the same default three-stop purple as the landing sheet (<code>linear-gradient(135deg, #581c87, #312e81, #1e1b4b)</code>). Example: <code>linear-gradient(135deg, #7c3aed, #6d28d9)</code>.</td></tr>
2422
+ <tr><td><code>voice.pillTextColor</code></td><td>string</td><td>'#ffffff'</td><td>Text color on the pill launcher</td></tr>
2423
+ <tr><td><code>voice.pillDotColor</code></td><td>string</td><td>'#4ade80'</td><td>Online-status dot color on the pill</td></tr>
2424
+ </tbody>
2425
+ </table>
2426
+
2427
+ <h4 style="margin-top: 16px;">Hero / Idle Screen</h4>
2428
+ <table class="properties-table">
2429
+ <thead>
2430
+ <tr><th>Property</th><th>Type</th><th>Default</th><th>Description</th></tr>
2431
+ </thead>
2432
+ <tbody>
2433
+ <tr><td><code>agentName</code></td><td>string</td><td>—</td><td><strong>Root only</strong> (duplicate of General Configuration). Idle hero name and avatar initial; default <code>Sasha</code> when unset/empty. Mobile pre-chat landing: <code>agentName</code> if set, else <code>header.title</code>.</td></tr>
2434
+ <tr><td><code>voice.avatarGradient1</code></td><td>string</td><td>'#6d56f5'</td><td>Avatar gradient start color</td></tr>
2435
+ <tr><td><code>voice.avatarGradient2</code></td><td>string</td><td>'#a78bfa'</td><td>Avatar gradient end color</td></tr>
2436
+ <tr><td><code>voice.headerAvatarImageUrl</code></td><td>string</td><td>''</td><td>Optional <strong>https</strong> (or http) URL for the <strong>idle</strong> voice header circle (desktop and mobile pre-call). When missing or invalid, the UI shows the <strong>first letter</strong> of the agent name inside the circle (e.g. &quot;S&quot; for Sasha). Same key may be set at the <strong>top level</strong> as <code>headerAvatarImageUrl</code>. Legacy: <code>voice.avatarImageUrl</code> (and snake_case <code>header_avatar_image_url</code> / <code>avatar_image_url</code> on <code>voice</code> or root) are also accepted. Invalid or non-http(s) URLs are ignored.</td></tr>
2437
+ <tr><td><code>voice.onlineDotColor</code></td><td>string</td><td>'#22c55e'</td><td>Online dot next to avatar</td></tr>
2438
+ <tr><td><code>voice.heroGradient1</code></td><td>string</td><td>'#2a2550'</td><td>Hero gradient <strong>start</strong>: desktop voice panel (idle and in-call), widget footer, and <code>.voice-interface.active</code> use <code>linear-gradient(160deg, heroGradient1, heroGradient2)</code>. <strong>Mobile</strong> in-call chrome uses <code>voice.pillGradient</code> instead (see Pill Launcher).</td></tr>
2439
+ <tr><td><code>voice.heroGradient2</code></td><td>string</td><td>'#1a1a2e'</td><td>Hero gradient <strong>end</strong>; pairs with <code>heroGradient1</code> for desktop/active surfaces above—not for the mobile minimized bar (use <code>pillGradient</code>).</td></tr>
2440
+ <tr><td><code>voice.agentNameColor</code></td><td>string</td><td>'#f0eff8'</td><td>Agent name text color</td></tr>
2441
+ <tr><td><code>voice.agentRoleColor</code></td><td>string</td><td>'rgba(255,255,255,0.35)'</td><td>Agent role text color</td></tr>
2442
+ <tr><td><code>voice.agentRole</code></td><td>string</td><td>'AI Voice Assistant'</td><td>Agent role label</td></tr>
2443
+ <tr><td><code>voice.headlineColor</code></td><td>string</td><td>'#ffffff'</td><td>Hero headline text color</td></tr>
2444
+ <tr><td><code>voice.headline</code></td><td>string</td><td>'Hi there 👋'</td><td>Hero headline text</td></tr>
2445
+ <tr><td><code>voice.sublineColor</code></td><td>string</td><td>'rgba(255,255,255,0.45)'</td><td>Hero subline text color</td></tr>
2446
+ <tr><td><code>voice.subline</code></td><td>string</td><td>'Ask me anything...'</td><td>Hero subline text (supports HTML)</td></tr>
2447
+ </tbody>
2448
+ </table>
2449
+
2450
+ <h4 style="margin-top: 16px;">Primary &amp; Secondary Buttons</h4>
2451
+ <table class="properties-table">
2452
+ <thead>
2453
+ <tr><th>Property</th><th>Type</th><th>Default</th><th>Description</th></tr>
2454
+ </thead>
2455
+ <tbody>
2456
+ <tr><td><code>voice.primaryBtnGradient1</code></td><td>string</td><td>'#6d56f5'</td><td>"Start Voice Call" button gradient start; footer “TalkToPC” link uses the same resolved accent (after <code>startCallButtonColor</code> fallback when gradients are omitted). Mobile <strong>user bubbles</strong> and <strong>send</strong> accent use this palette with <code>primaryBtnGradient2</code> / <code>sendButtonColor</code>.</td></tr>
2457
+ <tr><td><code>voice.primaryBtnGradient2</code></td><td>string</td><td>'#9d8df8'</td><td>"Start Voice Call" button gradient end; pairs with <code>sendButtonColor</code> for mobile message/send styling.</td></tr>
2458
+ <tr><td><code>voice.startCallButtonTextColor</code></td><td>string</td><td>'#FFFFFF'</td><td>Primary button text color</td></tr>
2459
+ <tr><td><code>voice.startCallButtonText</code></td><td>string</td><td>'Start Voice Call'</td><td>Primary button label</td></tr>
2460
+ <tr><td><code>voice.sendMessageText</code></td><td>string</td><td>'Send a Message'</td><td>Secondary button label</td></tr>
2461
+ <tr><td><code>voice.secondaryBtnBg</code></td><td>string</td><td>'rgba(255,255,255,0.05)'</td><td>Secondary button background</td></tr>
2462
+ <tr><td><code>voice.secondaryBtnBorder</code></td><td>string</td><td>'rgba(255,255,255,0.09)'</td><td>Secondary button border color</td></tr>
2463
+ <tr><td><code>voice.secondaryBtnTextColor</code></td><td>string</td><td>'rgba(255,255,255,0.6)'</td><td>Secondary button text color</td></tr>
2464
+ </tbody>
2465
+ </table>
2466
+
2467
+ <h4 style="margin-top: 16px;">Active Call View</h4>
2468
+ <table class="properties-table">
2469
+ <thead>
2470
+ <tr><th>Property</th><th>Type</th><th>Default</th><th>Description</th></tr>
2471
+ </thead>
2472
+ <tbody>
2473
+ <tr><td><code>voice.waveformBarColor</code></td><td>string</td><td>'#7C3AED'</td><td>Waveform bar color during call</td></tr>
2474
+ <tr><td><code>voice.speakerButtonColor</code></td><td>string</td><td>'#FFFFFF'</td><td>Speaker button color</td></tr>
1948
2475
  </tbody>
1949
2476
  </table>
2477
+
2478
+ <h4 style="margin-top: 16px;">Quick Theme Example</h4>
2479
+ <p>Apply a complete light theme:</p>
2480
+ <pre><code class="language-javascript">const widget = new TTPAgentSDK.TTPChatWidget({
2481
+ agentId: 'your-agent-id',
2482
+ appId: 'your-app-id',
2483
+ panel: {
2484
+ backgroundColor: '#ffffff',
2485
+ border: '1px solid rgba(0,0,0,0.06)'
2486
+ },
2487
+ voice: {
2488
+ pillGradient: 'linear-gradient(135deg, #7c3aed, #6d28d9)',
2489
+ pillTextColor: '#ffffff',
2490
+ heroGradient1: '#ede9fe',
2491
+ heroGradient2: '#f5f3ff',
2492
+ agentNameColor: '#1e1b4b',
2493
+ headlineColor: '#1e1b4b',
2494
+ sublineColor: '#6b7280',
2495
+ primaryBtnGradient1: '#7c3aed',
2496
+ primaryBtnGradient2: '#a78bfa',
2497
+ secondaryBtnBg: '#f5f3ff',
2498
+ secondaryBtnBorder: 'rgba(124,58,237,0.15)',
2499
+ secondaryBtnTextColor: '#6d28d9'
2500
+ }
2501
+ });</code></pre>
2502
+
2503
+ <p style="margin-top: 12px;">RTL Hebrew theme example:</p>
2504
+ <pre><code class="language-javascript">const widget = new TTPAgentSDK.TTPChatWidget({
2505
+ agentId: 'your-agent-id',
2506
+ appId: 'your-app-id',
2507
+ direction: 'rtl',
2508
+ header: { title: 'עוזרת חכמה', onlineIndicatorText: 'מחוברת' },
2509
+ panel: { backgroundColor: '#0f172a' },
2510
+ voice: {
2511
+ pillGradient: 'linear-gradient(135deg, #1e3a5f, #1e40af, #0f172a)',
2512
+ avatarGradient1: '#3b82f6',
2513
+ avatarGradient2: '#1d4ed8',
2514
+ heroGradient1: '#1a2744',
2515
+ heroGradient2: '#0f172a',
2516
+ primaryBtnGradient1: '#3b82f6',
2517
+ primaryBtnGradient2: '#1d4ed8',
2518
+ startCallButtonText: 'התחל שיחה קולית',
2519
+ sendMessageText: 'שלח הודעה',
2520
+ agentRole: 'עוזרת קולית חכמה',
2521
+ headline: 'היי, מה שלומך? 👋',
2522
+ subline: 'שאל/י אותי הכל — אני עונה מיידית בקול או בטקסט.'
2523
+ }
2524
+ });</code></pre>
1950
2525
  </details>
1951
2526
 
1952
2527
  <details>
@@ -1985,6 +2560,12 @@ const widget = new TTPChatWidget({
1985
2560
  <td>false</td>
1986
2561
  <td>Hide the widget completely</td>
1987
2562
  </tr>
2563
+ <tr>
2564
+ <td><code>behavior.mobileVoiceUI</code></td>
2565
+ <td>boolean</td>
2566
+ <td>auto</td>
2567
+ <td>Force mobile-style voice call UI (bottom bar + overlay) or desktop hero. Omit for auto: native iOS/Android, or viewport ≤768px with touch/coarse pointer (helps in-app browsers with desktop User-Agent). Root-level <code>mobileVoiceUI</code> overrides this if both are set.</td>
2568
+ </tr>
1988
2569
  <tr>
1989
2570
  <td><code>behavior.autoConnect</code></td>
1990
2571
  <td>boolean</td>
@@ -2014,7 +2595,7 @@ const widget = new TTPChatWidget({
2014
2595
  </details>
2015
2596
 
2016
2597
  <details>
2017
- <summary><strong>Animation & Tooltips</strong></summary>
2598
+ <summary><strong>Animation, Prompt Bubble &amp; Tooltips</strong></summary>
2018
2599
  <table class="properties-table">
2019
2600
  <thead>
2020
2601
  <tr>
@@ -2049,6 +2630,66 @@ const widget = new TTPChatWidget({
2049
2630
  <td>0.3</td>
2050
2631
  <td>Animation duration (seconds)</td>
2051
2632
  </tr>
2633
+ <tr>
2634
+ <td><code>promptAnimation.enabled</code></td>
2635
+ <td>boolean</td>
2636
+ <td>false</td>
2637
+ <td>Show an animated “Try me!” prompt bubble next to the launcher pill. Must be explicitly set to <code>true</code>.</td>
2638
+ </tr>
2639
+ <tr>
2640
+ <td><code>promptAnimation.text</code></td>
2641
+ <td>string</td>
2642
+ <td>'Try me!'</td>
2643
+ <td>Prompt bubble label text</td>
2644
+ </tr>
2645
+ <tr>
2646
+ <td><code>promptAnimation.backgroundColor</code></td>
2647
+ <td>string</td>
2648
+ <td>purple gradient</td>
2649
+ <td>Prompt bubble background (CSS color or gradient)</td>
2650
+ </tr>
2651
+ <tr>
2652
+ <td><code>promptAnimation.textColor</code></td>
2653
+ <td>string</td>
2654
+ <td>'#ffffff'</td>
2655
+ <td>Prompt bubble text color</td>
2656
+ </tr>
2657
+ <tr>
2658
+ <td><code>promptAnimation.animationType</code></td>
2659
+ <td>string</td>
2660
+ <td>'bounce'</td>
2661
+ <td><code>'bounce'</code>, <code>'pulse'</code>, <code>'float'</code>, or <code>'none'</code></td>
2662
+ </tr>
2663
+ <tr>
2664
+ <td><code>promptAnimation.showShimmer</code></td>
2665
+ <td>boolean</td>
2666
+ <td>true</td>
2667
+ <td>Shimmer effect on the prompt bubble</td>
2668
+ </tr>
2669
+ <tr>
2670
+ <td><code>promptAnimation.showPulseRings</code></td>
2671
+ <td>boolean</td>
2672
+ <td>true</td>
2673
+ <td>Pulse rings around the launcher while the prompt is visible</td>
2674
+ </tr>
2675
+ <tr>
2676
+ <td><code>promptAnimation.hideAfterClick</code></td>
2677
+ <td>boolean</td>
2678
+ <td>true</td>
2679
+ <td>Hide the prompt after the user opens the widget</td>
2680
+ </tr>
2681
+ <tr>
2682
+ <td><code>promptAnimation.hideAfterSeconds</code></td>
2683
+ <td>number | null</td>
2684
+ <td>null</td>
2685
+ <td>Auto-hide after N seconds. <code>null</code> = never auto-hide.</td>
2686
+ </tr>
2687
+ <tr>
2688
+ <td><code>promptAnimation.position</code></td>
2689
+ <td>string</td>
2690
+ <td>'top'</td>
2691
+ <td>Prompt placement relative to the launcher: <code>'top'</code>, <code>'left'</code>, or <code>'right'</code></td>
2692
+ </tr>
2052
2693
  <tr>
2053
2694
  <td><code>tooltips.newChat</code></td>
2054
2695
  <td>string</td>
@@ -2090,7 +2731,8 @@ const widget = new TTPChatWidget({
2090
2731
  </details>
2091
2732
 
2092
2733
  <details>
2093
- <summary><strong>Landing Screen (Unified Mode)</strong></summary>
2734
+ <summary><strong>Mobile pre-chat overlay &amp; legacy <code>landing</code> keys (Unified Mode)</strong></summary>
2735
+ <p style="margin: 0 0 12px 0;">On <strong>desktop</strong>, unified mode opens the <strong>voice idle hero</strong> inside the panel—there is <strong>no</strong> in-panel “Voice / Text” mode card screen. Back navigation and call end return to that hero. On <strong>mobile</strong>, the full-screen pre-chat sheet (<code>ttpMobileLanding</code>) still offers call vs text; the following options apply there. During an active call, the <strong>bottom minimized bar</strong> stays visible first; tapping the transcript row opens a <strong>conversation sheet</strong> (type while voice is active). Close the sheet to return to the bar—mute, speaker, and end call remain on the bar when the sheet is closed. On mobile, <strong>Back</strong> (header or text bar) closes the panel and opens the <strong>Call vs Chat</strong> landing overlay directly—the same sheet as tapping the FAB. If the user <strong>declines</strong> the server-driven disclaimer during voice setup, the widget also returns to that <strong>Call vs Chat</strong> overlay (and disconnects) instead of leaving the in-panel “Start call” card. After <strong>accepting</strong> the disclaimer, the minimized <strong>voice bar</strong> (waveform / mic / end call) stays the default view; the expandable conversation sheet with the text field does not open automatically. Other <code>landing.*</code> keys (e.g. mode card colors, in-panel title) remain in config merges for backward compatibility but are <strong>not</strong> rendered on desktop.</p>
2094
2736
  <table class="properties-table">
2095
2737
  <thead>
2096
2738
  <tr>
@@ -2102,70 +2744,40 @@ const widget = new TTPChatWidget({
2102
2744
  </thead>
2103
2745
  <tbody>
2104
2746
  <tr>
2105
- <td><code>landing.backgroundColor</code></td>
2106
- <td>string</td>
2107
- <td>Gradient</td>
2108
- <td>Landing screen background (color or gradient)</td>
2109
- </tr>
2110
- <tr>
2111
- <td><code>landing.logo</code></td>
2112
- <td>string</td>
2113
- <td>'🤖'</td>
2114
- <td>Landing screen logo (emoji or text)</td>
2115
- </tr>
2116
- <tr>
2117
- <td><code>landing.title</code></td>
2747
+ <td><code>landing.voiceCardTitle</code></td>
2118
2748
  <td>string</td>
2119
2749
  <td>null</td>
2120
- <td>Landing screen title (null uses translation)</td>
2121
- </tr>
2122
- <tr>
2123
- <td><code>landing.titleColor</code></td>
2124
- <td>string</td>
2125
- <td>'#1e293b'</td>
2126
- <td>Landing title text color</td>
2127
- </tr>
2128
- <tr>
2129
- <td><code>landing.modeCardBackgroundColor</code></td>
2130
- <td>string</td>
2131
- <td>'#FFFFFF'</td>
2132
- <td>Mode selection card background</td>
2133
- </tr>
2134
- <tr>
2135
- <td><code>landing.modeCardBorderColor</code></td>
2136
- <td>string</td>
2137
- <td>'#E2E8F0'</td>
2138
- <td>Mode card border color</td>
2750
+ <td>Fallback label for the mobile “call” action when <code>landing.callButtonText</code> is not set</td>
2139
2751
  </tr>
2140
2752
  <tr>
2141
- <td><code>landing.modeCardHoverBorderColor</code></td>
2753
+ <td><code>landing.textCardTitle</code></td>
2142
2754
  <td>string</td>
2143
- <td>header.backgroundColor</td>
2144
- <td>Mode card border color on hover</td>
2755
+ <td>null</td>
2756
+ <td>Fallback label for the mobile “chat” action when <code>landing.chatButtonText</code> is not set</td>
2145
2757
  </tr>
2146
2758
  <tr>
2147
- <td><code>landing.modeCardIconBackgroundColor</code></td>
2759
+ <td><code>landing.callButtonText</code></td>
2148
2760
  <td>string</td>
2149
- <td>header.backgroundColor</td>
2150
- <td>Mode card icon background</td>
2761
+ <td>null</td>
2762
+ <td>Mobile overlay primary button text (falls back to <code>voiceCardTitle</code> / translation)</td>
2151
2763
  </tr>
2152
2764
  <tr>
2153
- <td><code>landing.modeCardTitleColor</code></td>
2765
+ <td><code>landing.chatButtonText</code></td>
2154
2766
  <td>string</td>
2155
- <td>'#111827'</td>
2156
- <td>Mode card title text color</td>
2767
+ <td>null</td>
2768
+ <td>Mobile overlay secondary button text (falls back to <code>textCardTitle</code> / translation)</td>
2157
2769
  </tr>
2158
2770
  <tr>
2159
- <td><code>landing.voiceCardIcon</code></td>
2771
+ <td><code>landing.statusText</code></td>
2160
2772
  <td>string</td>
2161
- <td>'🎤'</td>
2162
- <td>Voice mode card icon</td>
2773
+ <td>null</td>
2774
+ <td>Mobile overlay status line; when unset, uses online translation plus <code>landing.subtitle</code> (e.g. “Ready to help”)</td>
2163
2775
  </tr>
2164
2776
  <tr>
2165
- <td><code>landing.textCardIcon</code></td>
2777
+ <td><code>landing.subtitle</code></td>
2166
2778
  <td>string</td>
2167
- <td>'💬'</td>
2168
- <td>Text mode card icon</td>
2779
+ <td>—</td>
2780
+ <td>Shown after the online dot in the mobile overlay status when <code>landing.statusText</code> is not set</td>
2169
2781
  </tr>
2170
2782
  </tbody>
2171
2783
  </table>
@@ -2243,27 +2855,85 @@ const widget = new TTPChatWidget({
2243
2855
  <td>true</td>
2244
2856
  <td>Enable keyboard navigation</td>
2245
2857
  </tr>
2858
+ <tr>
2859
+ <td><code>onConversationStart</code></td>
2860
+ <td>function</td>
2861
+ <td>—</td>
2862
+ <td>Called when a voice conversation starts (after connect + recording begins).</td>
2863
+ </tr>
2864
+ <tr>
2865
+ <td><code>onConversationEnd</code></td>
2866
+ <td>function</td>
2867
+ <td>—</td>
2868
+ <td>Called when a voice conversation ends (including disclaimer decline before recording).</td>
2869
+ </tr>
2870
+ <tr>
2871
+ <td><code>onBargeIn</code></td>
2872
+ <td>function</td>
2873
+ <td>—</td>
2874
+ <td>Called when the user interrupts the agent (barge-in).</td>
2875
+ </tr>
2876
+ <tr>
2877
+ <td><code>onAudioStartPlaying</code></td>
2878
+ <td>function</td>
2879
+ <td>—</td>
2880
+ <td>Called when agent audio playback starts.</td>
2881
+ </tr>
2882
+ <tr>
2883
+ <td><code>onAudioStoppedPlaying</code></td>
2884
+ <td>function</td>
2885
+ <td>—</td>
2886
+ <td>Called when agent audio playback stops.</td>
2887
+ </tr>
2888
+ <tr>
2889
+ <td><code>onSubtitleDisplay</code></td>
2890
+ <td>function</td>
2891
+ <td>—</td>
2892
+ <td>Called when a subtitle/transcript line is displayed during a call.</td>
2893
+ </tr>
2894
+ <tr>
2895
+ <td><code>onVoiceCallButtonClick</code></td>
2896
+ <td>function</td>
2897
+ <td>—</td>
2898
+ <td>Called when the user taps “Start Voice Call”. Return <code>false</code> to cancel the call start.</td>
2899
+ </tr>
2246
2900
  </tbody>
2247
2901
  </table>
2248
2902
  </details>
2249
2903
 
2904
+ <div id="visual-assistant-config" class="info-box" style="margin: 20px 0; padding: 20px; background: #f8f9fa; border-left: 4px solid #7C3AED;">
2905
+ <h3 style="margin-top: 0;">Visual Assistant (<code>visualAssistant</code>)</h3>
2906
+ <p>When enabled, the SDK registers browser-side tools the agent can call to read the page, highlight elements, scroll, navigate, fill forms, click elements, and capture screenshots. Tools are always registered on the client; the backend agent config decides which are available.</p>
2907
+ <pre><code class="language-javascript">visualAssistant: {
2908
+ enabled: true, // Required to activate visual assistant
2909
+ allowHighlight: true, // highlight_element
2910
+ allowScroll: true, // scroll_to_element
2911
+ allowNavigate: true, // navigate_to
2912
+ allowFillForm: true, // fill_form
2913
+ allowClick: true // click_element
2914
+ }</code></pre>
2915
+ <p>May be set at the widget root or inside <code>agentSettingsOverride.visualAssistant</code>. See <code>examples/test-client-tools.html</code> for a live demo.</p>
2916
+ </div>
2917
+
2250
2918
  <div class="info-box" style="margin: 20px 0;">
2251
2919
  <p><strong>✅ Configuration Verified:</strong></p>
2252
- <p>All configuration options listed above have been verified against the source code and are fully supported. The widget has 80+ customization options across 13 categories:</p>
2920
+ <p>All configuration options listed above have been verified against the source code and are fully supported. The widget has 100+ customization options across 15+ categories:</p>
2253
2921
  <ul style="margin: 10px 0 0 20px; columns: 2; column-gap: 20px;">
2254
- <li>General (5 options)</li>
2255
- <li>Positioning (4 options)</li>
2256
- <li>Icon & Button (9 options)</li>
2257
- <li>Panel & Header (8 options)</li>
2258
- <li>Messages (7 options)</li>
2259
- <li>Text Chat (13 options)</li>
2260
- <li>Voice Interface (17 options)</li>
2261
- <li>Landing Screen (11 options)</li>
2262
- <li>Tooltips (6 options)</li>
2263
- <li>Animations (4 options)</li>
2264
- <li>Behavior (8 options)</li>
2265
- <li>Accessibility (3 options)</li>
2266
- <li>Advanced (11 options)</li>
2922
+ <li>General (incl. <code>visualAssistant</code>, <code>whatsapp</code>, <code>inputFormat</code>)</li>
2923
+ <li>Positioning (incl. <code>draggable</code>, <code>draggablePersist</code>)</li>
2924
+ <li>Icon &amp; Button</li>
2925
+ <li>Panel &amp; Header (incl. online indicator, footer colors)</li>
2926
+ <li>Messages &amp; Text Chat</li>
2927
+ <li>Voice Interface &amp; output format</li>
2928
+ <li>Voice Theming &amp; Pill Launcher</li>
2929
+ <li>Mobile landing / legacy <code>landing</code></li>
2930
+ <li>Prompt bubble (<code>promptAnimation</code>)</li>
2931
+ <li>Tooltips &amp; Animations</li>
2932
+ <li>Behavior (incl. <code>mobileVoiceUI</code>)</li>
2933
+ <li>Accessibility</li>
2934
+ <li>Event callbacks (voice lifecycle)</li>
2935
+ <li>Advanced &amp; legacy keys</li>
2936
+ <li>Visual Assistant</li>
2267
2937
  </ul>
2268
2938
  <p style="margin-top: 10px;">Experiment with all options interactively in the <a href="../examples/test-text-chat.html" target="_blank" style="color: inherit; text-decoration: underline;">live demo</a>.</p>
2269
2939
  </div>
@@ -2644,6 +3314,269 @@ function App() {
2644
3314
  </table>
2645
3315
  </section>
2646
3316
 
3317
+ <!-- Fly-to-Cart Animation -->
3318
+ <section id="fly-to-cart" class="doc-section">
3319
+ <h1>Fly-to-Cart Animation</h1>
3320
+ <p>When users tap "Add to Cart" in the e-commerce widget, the product card flies down into the cart icon with a tornado funnel effect. Uses <code>html2canvas</code> to capture the card as an image, avoiding stacking/overflow clipping in iframes or containers with <code>overflow:hidden</code>. Built-in for <code>TTPEcommerceWidget</code>. For custom React product widgets, use the <code>useFlyToCart</code> hook.</p>
3321
+
3322
+ <h2>React Hook (useFlyToCart)</h2>
3323
+ <pre><code>import { useFlyToCart } from 'ttp-agent-sdk';
3324
+
3325
+ const cartIconRef = useRef(null);
3326
+ const { triggerFly, isAnimating } = useFlyToCart(cartIconRef);
3327
+
3328
+ const handleAddToCart = (product) => {
3329
+ triggerFly(cardRef.current, product, () => {
3330
+ addToCartAPI(product);
3331
+ });
3332
+ };</code></pre>
3333
+
3334
+ <h2>Vanilla JS (FlyToCart)</h2>
3335
+ <pre><code>import { FlyToCart } from 'ttp-agent-sdk';
3336
+
3337
+ const flyToCart = new FlyToCart({
3338
+ getCartTarget: () => document.querySelector('.cart-icon'),
3339
+ onCartBump: () => { /* optional */ },
3340
+ });
3341
+
3342
+ flyToCart.triggerFly(cardElement, product, () => {
3343
+ addToCartAPI(product);
3344
+ });</code></pre>
3345
+ </section>
3346
+
3347
+ <!-- E-commerce: partner API vs browser cart -->
3348
+ <section id="ecommerce-cart-flow" class="doc-section">
3349
+ <h1>E-commerce: Adding to cart (partner API &amp; front-end cart)</h1>
3350
+ <p class="lead">Cart logic is owned by <strong>your integration</strong> (partner APIs, mock stores, or a Shopify storefront). The widget renders products, sends user intent over the WebSocket, and updates the cart summary when it receives the right messages. The SDK does not keep its own authoritative cart array: totals and counts should reflect what the backend (or the browser cart, then the backend) considers true.</p>
3351
+
3352
+ <h2>Who does the &quot;add&quot;?</h2>
3353
+ <table class="properties-table">
3354
+ <thead>
3355
+ <tr>
3356
+ <th>Model</th>
3357
+ <th>Where the line item is added</th>
3358
+ <th>How the widget learns the new totals</th>
3359
+ </tr>
3360
+ </thead>
3361
+ <tbody>
3362
+ <tr>
3363
+ <td><strong>Server-side partner cart</strong> (mock store, custom API, headless stack)</td>
3364
+ <td>Conversation backend calls the partner API after <code>product_selected</code> or after the model runs the internal tool <code>add_to_cart</code>.</td>
3365
+ <td>Backend sends <code>t: &quot;cart_updated&quot;</code> with <code>cartTotal</code>, <code>cartItemCount</code>, and optional <code>currency</code>. For a visible &quot;added&quot; toast, include <code>action: &quot;added&quot;</code> and a <code>product</code> object (<code>id</code>, <code>name</code>, <code>price</code>).</td>
3366
+ </tr>
3367
+ <tr>
3368
+ <td><strong>Shopify Online Store</strong> (widget embedded on the store theme)</td>
3369
+ <td>The <strong>browser</strong> must call Shopify&apos;s Ajax Cart API (<code>POST /cart/add.js</code>) so the add uses the visitor&apos;s store <strong>session cookie</strong>. Server-only Storefront API carts are a different session and will not match the theme cart.</td>
3370
+ <td>The conversation backend sends <code>t: &quot;add_to_store_cart&quot;</code> with <code>variantId</code> and <code>quantity</code>. The ecommerce flavor runs the Ajax calls, refreshes the cart bar from <code>GET /cart.js</code>, and sends <code>t: &quot;cart_add_result&quot;</code> back so the backend can continue the turn. The backend may still send <code>cart_updated</code> afterward if you want one canonical sync message.</td>
3371
+ </tr>
3372
+ <tr>
3373
+ <td><strong>Custom client tool</strong></td>
3374
+ <td>Your backend issues a <code>client_tool_call</code> with a tool name you defined (for example <code>addToCart</code>). Your page handles it via <code>registerToolHandler</code> on the widget or <code>AgentSDK</code>.</td>
3375
+ <td>The SDK replies with <code>client_tool_result</code> (or <code>client_tool_error</code>). The cart bar still expects <code>cart_updated</code> unless your handler updates UI itself.</td>
3376
+ </tr>
3377
+ </tbody>
3378
+ </table>
3379
+
3380
+ <h2>Flow 1 — User taps Add / Update on a product card</h2>
3381
+ <ol>
3382
+ <li>The widget stops playback (so the agent does not talk over the action) and sends <code>t: &quot;product_selected&quot;</code> on the voice WebSocket. Payload includes <code>productId</code>, <code>productName</code>, <code>price</code>, <code>quantity</code> (absolute units or weight), and <code>sellBy</code> (<code>quantity</code> or <code>weight</code>).</li>
3383
+ <li><strong>Your backend</strong> decides what happens next:
3384
+ <ul>
3385
+ <li>Update the partner cart via API, then push <strong><code>cart_updated</code></strong> to the session, <em>or</em></li>
3386
+ <li>For Shopify on-domain, send <strong><code>add_to_store_cart</code></strong> and let the widget perform <code>/cart/add.js</code>, then consume <strong><code>cart_add_result</code></strong> on the server, <em>or</em></li>
3387
+ <li>Hybrid: tool or internal step on the server plus a follow-up <code>cart_updated</code>.</li>
3388
+ </ul>
3389
+ </li>
3390
+ <li><code>EcommerceManager.handleCartUpdated</code> updates the bottom cart bar when <code>cartTotal</code> and <code>cartItemCount</code> are present. Optional <code>action: &quot;added&quot;</code> drives the short confirmation UX.</li>
3391
+ </ol>
3392
+
3393
+ <h2>Flow 2 — User asks in voice (or the agent adds without a card click)</h2>
3394
+ <p>For TTP&apos;s Java conversation backend, e-commerce uses <strong>internal</strong> tools such as <code>search_products</code>, <code>add_to_cart</code>, and <code>get_cart</code> (names match what the LLM sees; they are <strong>not</strong> the same as optional SDK <code>client_tool_call</code> names unless you wire them).</p>
3395
+ <ol>
3396
+ <li>The model calls <code>add_to_cart</code> with <code>productId</code> and <code>quantity</code>.</li>
3397
+ <li>The server runs the partner integration:
3398
+ <ul>
3399
+ <li><strong>Partner API cart:</strong> The service calls the partner, receives success and line items, then typically sends <strong><code>cart_updated</code></strong> to the widget with totals and item count so the UI matches the server.</li>
3400
+ <li><strong>Shopify theme cart:</strong> The service sends <strong><code>add_to_store_cart</code></strong> to the widget instead of mutating cart only on the server; the widget performs the Ajax add and returns <strong><code>cart_add_result</code></strong>. Reading the cart may use <code>get_store_cart</code> from the server, which the widget answers with <code>cart_state_result</code> after <code>GET /cart.js</code>.</li>
3401
+ </ul>
3402
+ </li>
3403
+ <li>The model receives a text summary of the tool result and can speak to the user.</li>
3404
+ </ol>
3405
+
3406
+ <h2>Message cheat sheet (widget &harr; backend)</h2>
3407
+ <table class="properties-table">
3408
+ <thead>
3409
+ <tr>
3410
+ <th>Direction</th>
3411
+ <th><code>t</code> / type</th>
3412
+ <th>Role</th>
3413
+ </tr>
3414
+ </thead>
3415
+ <tbody>
3416
+ <tr>
3417
+ <td>Widget &rarr; backend</td>
3418
+ <td><code>product_selected</code></td>
3419
+ <td>User confirmed quantity and tapped Add/Update on a card.</td>
3420
+ </tr>
3421
+ <tr>
3422
+ <td>Backend &rarr; widget</td>
3423
+ <td><code>show_products</code> / <code>show_items</code></td>
3424
+ <td>Render product cards (search or browse results).</td>
3425
+ </tr>
3426
+ <tr>
3427
+ <td>Backend &rarr; widget</td>
3428
+ <td><code>cart_updated</code></td>
3429
+ <td>Sync <code>cartTotal</code>, <code>cartItemCount</code>, optional <code>currency</code>, optional <code>action</code> + <code>product</code> for toast.</td>
3430
+ </tr>
3431
+ <tr>
3432
+ <td>Backend &rarr; widget</td>
3433
+ <td><code>add_to_store_cart</code></td>
3434
+ <td>Shopify: widget runs Ajax add for <code>variantId</code> / <code>quantity</code>. Optional <code>verbalAck: true</code> when the add was triggered by the user tapping Add (not an agent tool); echoed on <code>cart_add_result</code> so the server can prompt a brief spoken acknowledgment.</td>
3435
+ </tr>
3436
+ <tr>
3437
+ <td>Widget &rarr; backend</td>
3438
+ <td><code>cart_add_result</code></td>
3439
+ <td>Outcome of Ajax add (success, counts, totals, currency). Includes <code>verbalAck</code> when the originating <code>add_to_store_cart</code> or hook <code>add_to_site_cart</code> requested it (UI add).</td>
3440
+ </tr>
3441
+ <tr>
3442
+ <td>Backend &rarr; widget</td>
3443
+ <td><code>get_store_cart</code></td>
3444
+ <td>Shopify: widget fetches <code>/cart.js</code> and replies with cart contents.</td>
3445
+ </tr>
3446
+ <tr>
3447
+ <td>Widget &rarr; backend</td>
3448
+ <td><code>cart_state_result</code></td>
3449
+ <td>Async cart snapshot after <code>get_store_cart</code> (implementation-specific).</td>
3450
+ </tr>
3451
+ <tr>
3452
+ <td>Backend &rarr; widget</td>
3453
+ <td><code>client_tool_call</code></td>
3454
+ <td>Optional: run custom logic in the page; reply with <code>client_tool_result</code>.</td>
3455
+ </tr>
3456
+ </tbody>
3457
+ </table>
3458
+
3459
+ <p><strong>Custom client tools:</strong> Register handlers with <code>registerToolHandler</code> on the widget or <code>AgentSDK</code> so the <code>toolName</code> in each <code>client_tool_call</code> matches what your backend defines.</p>
3460
+ </section>
3461
+
3462
+ <!-- Widget Flavors -->
3463
+ <section id="widget-flavors" class="doc-section">
3464
+ <h1>Widget Flavors</h1>
3465
+ <p>The TTP SDK supports domain-specific "flavors" that customize the widget experience for different verticals. Each flavor provides specialized UI components, backend tools, and message handling.</p>
3466
+
3467
+ <h2>Available Flavors</h2>
3468
+ <table>
3469
+ <thead>
3470
+ <tr><th>Flavor Type</th><th>Partner IDs</th><th>Backend Tools</th><th>Key Features</th></tr>
3471
+ </thead>
3472
+ <tbody>
3473
+ <tr>
3474
+ <td><code>ecommerce</code></td>
3475
+ <td><code>mock-store</code>, <code>shopify</code></td>
3476
+ <td><code>search_products</code>, <code>add_to_cart</code>, <code>get_cart</code></td>
3477
+ <td>Product cards, cart bar, fly-to-cart animation</td>
3478
+ </tr>
3479
+ <tr>
3480
+ <td><code>hotels</code></td>
3481
+ <td><code>mock-hotel</code></td>
3482
+ <td><code>search_rooms</code>, <code>select_room</code>, <code>add_extra</code>, <code>get_booking</code>, <code>show_media</code></td>
3483
+ <td>Room cards, booking bar, gallery</td>
3484
+ </tr>
3485
+ <tr>
3486
+ <td><code>pharma</code></td>
3487
+ <td><code>mock-pharm</code></td>
3488
+ <td><code>search_medications</code>, <code>add_to_prescription</code>, <code>get_prescription</code></td>
3489
+ <td>Medication cards (with Rx/OTC badges), prescription summary bar</td>
3490
+ </tr>
3491
+ <tr>
3492
+ <td><code>restaurants</code></td>
3493
+ <td><code>mock-restaurant</code></td>
3494
+ <td><code>search_menu</code>, <code>add_to_order</code>, <code>get_order</code>, <code>show_media</code></td>
3495
+ <td>Menu item cards (with allergen/dietary tags), order summary bar, gallery</td>
3496
+ </tr>
3497
+ <tr>
3498
+ <td><code>tours</code></td>
3499
+ <td><code>mock-tour</code></td>
3500
+ <td><code>search_tours</code>, <code>book_tour</code>, <code>get_tour_booking</code>, <code>show_media</code></td>
3501
+ <td>Tour item cards, booking summary bar, gallery</td>
3502
+ </tr>
3503
+ </tbody>
3504
+ </table>
3505
+
3506
+ <p><strong>E-commerce WebSocket messages:</strong> The widget listens for <code>t: &quot;show_items&quot;</code> and <code>t: &quot;show_products&quot;</code> (same handler; backend tools often send <code>show_products</code> with <code>products</code>, <code>title</code>, <code>layout</code>). Use <code>TTPEcommerceWidget</code> (or <code>TTPChatWidget</code> with <code>flavor.type: 'ecommerce'</code>) so these handlers are registered. For end-to-end add-to-cart flows (partner API vs Shopify Ajax vs client tools), see <a href="#ecommerce-cart-flow">E-commerce cart flows</a>.</p>
3507
+
3508
+ <p><strong>Desktop voice strip (<code>flavor.callView: 'minimized'</code>):</strong> When <code>flavor.callView</code> is <code>'minimized'</code>, a fixed bottom voice surface is used on viewports wider than 768px. This works <strong>with or without a flavor</strong>: it requires only a <code>flavor</code> object carrying <code>callView: 'minimized'</code> (e.g. <code>flavor: { callView: 'minimized' }</code>, no <code>type</code>/partner needed) — a flavor <code>type</code> is optional. It is a <strong>rounded floating dock</strong> with layered glass styling (rim light, ambient shadow, soft indigo outer glow), an inset transcript “well,” squircle control tiles, and a blurred LIVE capsule—same layout and controls as before. It is centered with a capped width (up to about 720px with horizontal inset) and bottom spacing plus <code>env(safe-area-inset-bottom)</code>. It uses the same tokens as the floating pill (<code>voice.pillGradient</code>, <code>pillTextColor</code>, <code>pillDotColor</code>, <code>endCallButtonColor</code>). Mute, pause, speaker, keyboard (text inject), and end-call align with the in-widget voice UI. The in-panel desktop “active call” UI stays hidden; the floating panel collapses if it was open, and the launcher pill is hidden for the duration of the call. The <strong>“Powered by”</strong> footer (<code>footer.show</code>, <code>footer.brand</code>) is rendered <strong>inside the strip</strong> (below the call controls) for the duration of the call—not as a separate floating pill at the widget corner. Viewports 768px and below skip the strip and use the mobile minimized bar flow instead. A <strong>single transcript line</strong> shows either the assistant (streaming) or the user (speech-to-text interim/final), never both at once; user text uses a warm highlight color and no <code>You:</code> prefix. When the widget <code>language</code> is Hebrew or Arabic (base <code>he</code> / <code>ar</code>) or <code>direction</code> is <code>'rtl'</code>, chrome uses RTL. Transcript lines use explicit <code>dir="rtl"</code> when the string contains Hebrew or Arabic letters (even if the widget Language is English), so neutral punctuation follows the sentence; Latin-only lines use <code>dir="ltr"</code>. Mic/pause/speaker/end layout is unchanged.</p>
3509
+
3510
+ <h2>Configuration</h2>
3511
+ <p>Set the flavor when creating the widget:</p>
3512
+ <pre><code>const widget = new TTPChatWidget({
3513
+ agentId: 'agent_...',
3514
+ appId: 'app_...',
3515
+ flavor: {
3516
+ type: 'pharma', // 'ecommerce' | 'hotels' | 'pharma' | 'restaurants' | 'tours'
3517
+ partnerId: 'mock-pharm' // partner-specific data source
3518
+ }
3519
+ });</code></pre>
3520
+
3521
+ <h2>Pharma Flavor</h2>
3522
+ <p>The pharmacy flavor provides a medication search and prescription management experience.</p>
3523
+ <pre><code>// Pharma configuration
3524
+ flavor: {
3525
+ type: 'pharma',
3526
+ partnerId: 'mock-pharm'
3527
+ }
3528
+
3529
+ // Backend tools injected automatically:
3530
+ // - search_medications: Search the medication catalog
3531
+ // - add_to_prescription: Add a medication to the prescription
3532
+ // - get_prescription: View current prescription contents
3533
+
3534
+ // Frontend message types:
3535
+ // - show_items: Displays medication cards
3536
+ // - prescription_updated: Updates the prescription summary bar</code></pre>
3537
+ <p>The pharma flavor does <strong>not</strong> include gallery support (<code>show_media</code>).</p>
3538
+
3539
+ <h2>Restaurants Flavor</h2>
3540
+ <p>The restaurants flavor provides a menu browsing, ordering, and photo gallery experience.</p>
3541
+ <pre><code>// Restaurant configuration
3542
+ flavor: {
3543
+ type: 'restaurants',
3544
+ partnerId: 'mock-restaurant'
3545
+ }
3546
+
3547
+ // Backend tools injected automatically:
3548
+ // - search_menu: Search the restaurant menu
3549
+ // - add_to_order: Add a menu item to the order
3550
+ // - get_order: View current order contents
3551
+ // - show_media: Display restaurant photo gallery
3552
+
3553
+ // Frontend message types:
3554
+ // - show_items: Displays menu item cards
3555
+ // - order_updated: Updates the order summary bar
3556
+ // - show_media / dismiss_media: Gallery with dish photos, ambiance, etc.</code></pre>
3557
+ <p>Menu item cards display allergen warnings and dietary tags automatically.</p>
3558
+
3559
+ <h2>Tours Flavor</h2>
3560
+ <p>The tours flavor provides a tour browsing, booking, and photo gallery experience.</p>
3561
+ <pre><code>// Tour configuration
3562
+ flavor: {
3563
+ type: 'tours',
3564
+ partnerId: 'mock-tour'
3565
+ }
3566
+
3567
+ // Backend tools injected automatically:
3568
+ // - search_tours: Search available tours and activities by query
3569
+ // - book_tour: Book a tour or activity
3570
+ // - get_tour_booking: View current booking contents
3571
+ // - show_media: Display tour photo gallery
3572
+
3573
+ // Frontend message types:
3574
+ // - show_items: Displays tour item cards
3575
+ // - cart_updated: Updates the booking summary bar
3576
+ // - show_media / dismiss_media: Gallery with tour photos, highlights, etc.</code></pre>
3577
+ <p>Tour cards display activity tags and pricing. Reuses the same UI infrastructure as the restaurants flavor.</p>
3578
+ </section>
3579
+
2647
3580
  <!-- Client-Script Tools -->
2648
3581
  <section id="client-script-tools" class="doc-section">
2649
3582
  <h1>Client-Script Tools</h1>
@@ -2717,7 +3650,7 @@ async (ctx) => {
2717
3650
  </thead>
2718
3651
  <tbody>
2719
3652
  <tr><td>Per-step code size</td><td>32 KB</td></tr>
2720
- <tr><td>Result size</td><td>50 KB serialized (oversize → <code>ok: false, error: "RESULT_TOO_LARGE"</code>)</td></tr>
3653
+ <tr><td>Result size</td><td>512 KB serialized (oversize → <code>ok: false, error: "RESULT_TOO_LARGE"</code>)</td></tr>
2721
3654
  <tr><td>Sync timeout</td><td>1000–15000 ms (default 8000)</td></tr>
2722
3655
  <tr><td>Pending message</td><td>500 chars</td></tr>
2723
3656
  </tbody>
@@ -2795,7 +3728,7 @@ async (ctx) => {
2795
3728
  <!-- VoiceSDK API -->
2796
3729
  <section id="voicesdk" class="doc-section">
2797
3730
  <h1>VoiceSDK Class</h1>
2798
- <p>Core class for voice interaction functionality.</p>
3731
+ <p>Core class for voice interaction functionality. Server-driven <strong>disclaimer</strong> gating (compliance copy before STT/greeting) is described in detail under <a href="#server-driven-disclaimer">Server-driven disclaimer (voice)</a> and applies when using protocol v2.</p>
2799
3732
 
2800
3733
  <h2>Constructor</h2>
2801
3734
  <pre><code>new VoiceSDK(config)</code></pre>
@@ -2881,7 +3814,7 @@ async (ctx) => {
2881
3814
  <td><code>outputSampleRate</code></td>
2882
3815
  <td>number</td>
2883
3816
  <td>No</td>
2884
- <td>Output audio sample rate: 8000, 16000, 22050, 24000, 44100, or 48000 Hz (default: 16000)</td>
3817
+ <td>Output audio sample rate: 8000, 16000, 22050, 24000, 44100, or 48000 Hz (default: 24000)</td>
2885
3818
  </tr>
2886
3819
  <tr>
2887
3820
  <td><code>outputChannels</code></td>
@@ -2945,7 +3878,7 @@ async (ctx) => {
2945
3878
  // Output format (what we want from server)
2946
3879
  outputContainer: 'raw', // 'raw' or 'wav'
2947
3880
  outputEncoding: 'pcm', // 'pcm', 'pcmu', 'pcma'
2948
- outputSampleRate: 44100, // Higher quality
3881
+ outputSampleRate: 24000, // Default; typical TTS/server output
2949
3882
  outputChannels: 1,
2950
3883
  outputBitDepth: 16,
2951
3884
  outputFrameDurationMs: 600, // Frame duration for streaming
@@ -2982,10 +3915,19 @@ voiceSDK.on('formatNegotiated', (format) => {
2982
3915
  <div class="method-card">
2983
3916
  <h3><code>startRecording()</code></h3>
2984
3917
  <p>Start capturing and streaming audio.</p>
3918
+ <p><strong>Disclaimer (v2):</strong> If the server required a disclaimer and <code>disclaimersPending</code> is still <code>true</code>, this throws an <code>Error</code> with <code>error.code === 'DISCLAIMER_PENDING'</code>. Call <code>sendDisclaimerAck(true)</code> first. See <a href="#server-driven-disclaimer">Server-driven disclaimer</a>.</p>
2985
3919
  <p><strong>Returns:</strong> <code>Promise&lt;boolean&gt;</code></p>
2986
3920
  <pre><code>await voiceSDK.startRecording();</code></pre>
2987
3921
  </div>
2988
3922
 
3923
+ <div class="method-card">
3924
+ <h3><code>sendDisclaimerAck(accepted)</code> <span style="font-weight: normal; font-size: 0.9em;">(v2, voice)</span></h3>
3925
+ <p>Sends <code>disclaimer_ack</code> after the user accepts or declines server-shown disclaimer text. Uses <code>disclaimersHash</code> and <code>conversationId</code> from the last <code>hello_ack</code>.</p>
3926
+ <p><strong>Parameters:</strong> <code>accepted</code> (boolean) — <code>true</code> to continue the call, <code>false</code> to decline.</p>
3927
+ <p><strong>Returns:</strong> <code>void</code> (logs and clears <code>disclaimersPending</code> on successful send).</p>
3928
+ <pre><code>voiceSDK.sendDisclaimerAck(true);</code></pre>
3929
+ </div>
3930
+
2989
3931
  <div class="method-card">
2990
3932
  <h3><code>stopRecording()</code></h3>
2991
3933
  <p>Stop capturing audio.</p>
@@ -3000,6 +3942,37 @@ voiceSDK.on('formatNegotiated', (format) => {
3000
3942
  <pre><code>await voiceSDK.toggleRecording();</code></pre>
3001
3943
  </div>
3002
3944
 
3945
+ <div class="method-card">
3946
+ <h3><code>pauseCall()</code></h3>
3947
+ <p>Pause the active call. Stops sending audio, clears the playback queue, and notifies the server to close STT/TTS connections. The WebSocket stays open and conversation history is preserved. A configurable timeout (default 5 minutes) will automatically end the call if not resumed.</p>
3948
+ <p><strong>Returns:</strong> <code>void</code></p>
3949
+ <pre><code>voiceSDK.pauseCall();
3950
+ // or via AgentSDK:
3951
+ agentSDK.pauseCall();</code></pre>
3952
+ </div>
3953
+
3954
+ <div class="method-card">
3955
+ <h3><code>resumeCall()</code></h3>
3956
+ <p>Resume a paused call. Restarts audio recording and notifies the server to re-create STT/TTS connections. The conversation continues from where it left off with full history.</p>
3957
+ <p><strong>Returns:</strong> <code>void</code></p>
3958
+ <pre><code>voiceSDK.resumeCall();
3959
+ // or via AgentSDK:
3960
+ agentSDK.resumeCall();</code></pre>
3961
+ </div>
3962
+
3963
+ <div class="method-card">
3964
+ <h3><code>isPaused</code> <span style="font-weight: normal; font-size: 0.9em;">(property)</span></h3>
3965
+ <p>Boolean indicating whether the call is currently paused.</p>
3966
+ <p><strong>Type:</strong> <code>boolean</code></p>
3967
+ <pre><code>if (voiceSDK.isPaused) {
3968
+ console.log('Call is paused');
3969
+ }
3970
+ // or via AgentSDK:
3971
+ if (agentSDK.isPaused) {
3972
+ console.log('Call is paused');
3973
+ }</code></pre>
3974
+ </div>
3975
+
3003
3976
  <div class="method-card">
3004
3977
  <h3><code>getStatus()</code></h3>
3005
3978
  <p>Get current connection and recording status.</p>
@@ -3046,7 +4019,7 @@ if (error) {
3046
4019
  <pre><code>const error = voiceSDK.validateOutputFormat({
3047
4020
  container: 'raw',
3048
4021
  encoding: 'pcm',
3049
- sampleRate: 44100,
4022
+ sampleRate: 24000,
3050
4023
  bitDepth: 16,
3051
4024
  channels: 1
3052
4025
  });
@@ -3187,6 +4160,16 @@ if (error) {
3187
4160
  <td><code>error</code></td>
3188
4161
  <td>Emitted when domain is not whitelisted</td>
3189
4162
  </tr>
4163
+ <tr>
4164
+ <td><code>disclaimersRequired</code></td>
4165
+ <td><code>payload</code></td>
4166
+ <td><strong>v2 voice:</strong> Server requires disclaimer acknowledgement. Payload: <code>texts</code>, <code>disclaimersHash</code>, <code>disclaimerTimeoutMs</code>, <code>conversationId</code>. Call <code>sendDisclaimerAck</code> after user decision.</td>
4167
+ </tr>
4168
+ <tr>
4169
+ <td><code>disclaimerRejected</code></td>
4170
+ <td><code>{ code, message }</code></td>
4171
+ <td><strong>v2 voice:</strong> Terminal disclaimer failure from server (<code>DISCLAIMER_DECLINED</code>, <code>DISCLAIMER_TIMEOUT</code>, <code>DISCLAIMER_HASH_MISMATCH</code>). Not used for <code>DISCLAIMER_PENDING</code> (that uses <code>error</code>).</td>
4172
+ </tr>
3190
4173
  </tbody>
3191
4174
  </table>
3192
4175
  </section>
@@ -3956,7 +4939,7 @@ sdk.textToSpeechStream(
3956
4939
  .voiceId("mamre")
3957
4940
  .outputContainer("raw") // "raw" or "wav"
3958
4941
  .outputEncoding("pcm") // "pcm", "pcmu", "pcma"
3959
- .outputSampleRate(16000) // Hz (8000, 16000, 22050, 44100)
4942
+ .outputSampleRate(24000) // Hz (8000, 16000, 22050, 24000, 44100, 48000)
3960
4943
  .outputBitDepth(16) // bits (8, 16, 24)
3961
4944
  .outputChannels(1) // 1 (mono) or 2 (stereo)
3962
4945
  .outputFrameDurationMs(600) // ms per frame (for streaming)
@@ -4180,7 +5163,7 @@ TTSRequest request = TTSRequest.builder()
4180
5163
  .speed(1.2)
4181
5164
  .outputContainer("wav")
4182
5165
  .outputEncoding("pcm")
4183
- .outputSampleRate(44100)
5166
+ .outputSampleRate(24000)
4184
5167
  .outputBitDepth(16)
4185
5168
  .outputChannels(1)
4186
5169
  .build();