@staylift-tech/conv-widget 0.0.1 → 0.0.2

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.
@@ -1,5 +1,5 @@
1
1
  import { h } from "@stencil/core";
2
- import { Conversation } from "@elevenlabs/client";
2
+ import { TextConversation, VoiceConversation } from "@elevenlabs/client";
3
3
  export class StayliftWidget {
4
4
  constructor() {
5
5
  this.positionX = 'right';
@@ -23,6 +23,7 @@ export class StayliftWidget {
23
23
  this.messages = [];
24
24
  this.inputText = '';
25
25
  this.copiedIndex = null;
26
+ this.selectedMode = 'text';
26
27
  // ============ PRIVATE ============
27
28
  this.conversation = null;
28
29
  this.volumeInterval = null;
@@ -126,12 +127,17 @@ export class StayliftWidget {
126
127
  const timeoutId = setTimeout(() => {
127
128
  rejectConnected(new Error('Connection timeout'));
128
129
  }, CONNECTION_TIMEOUT);
129
- this.conversation = await Conversation.startSession({
130
+ // Use TextConversation for text-only mode (no mic access), VoiceConversation for voice
131
+ const ConversationClass = textOnly ? TextConversation : VoiceConversation;
132
+ this.conversation = await ConversationClass.startSession({
130
133
  agentId: this.agentId,
131
134
  connectionType: textOnly ? 'websocket' : 'webrtc',
132
135
  overrides: {
133
- conversation: { textOnly },
134
- agent: { firstMessage: textOnly ? '' : undefined },
136
+ conversation: {
137
+ textOnly,
138
+ // Enable streaming text responses for text mode
139
+ client_events: textOnly ? ['agent_response', 'agent_chat_response_part', 'user_transcript'] : undefined,
140
+ },
135
141
  },
136
142
  onStatusChange: (statusEvent) => {
137
143
  const newStatus = statusEvent.status;
@@ -219,15 +225,16 @@ export class StayliftWidget {
219
225
  const msg = text || this.inputText.trim();
220
226
  if (!msg)
221
227
  return;
222
- // If disconnected, start text-only session
228
+ // If disconnected, start session based on selected mode
223
229
  if (this.status === 'disconnected') {
224
230
  const userMessage = { role: 'user', content: msg };
225
231
  this.inputText = '';
226
232
  this.pendingMessage = msg; // Store message to send after connection
227
233
  this.messages = [userMessage];
228
234
  this.scrollToBottom();
235
+ const textOnly = this.selectedMode === 'text';
229
236
  try {
230
- await this.handleStartConversation(true, true);
237
+ await this.handleStartConversation(textOnly, true);
231
238
  // Message will be sent in onConnect callback
232
239
  }
233
240
  catch {
@@ -255,6 +262,14 @@ export class StayliftWidget {
255
262
  await this.handleEndConversation();
256
263
  }
257
264
  }
265
+ async handleTextButton() {
266
+ if (this.status === 'disconnected' || this.status === null) {
267
+ await this.handleStartConversation(true);
268
+ }
269
+ else if (this.status === 'connected') {
270
+ await this.handleEndConversation();
271
+ }
272
+ }
258
273
  startVolumeMonitoring() {
259
274
  this.volumeInterval = setInterval(() => {
260
275
  if (this.conversation && this.status === 'connected') {
@@ -304,6 +319,12 @@ export class StayliftWidget {
304
319
  termsWarning: 'If you do not wish to have your conversations recorded, please refrain from using this service.',
305
320
  termsAgree: 'Agree',
306
321
  termsDecline: 'Decline',
322
+ modeText: 'Text',
323
+ modeVoice: 'Voice',
324
+ startVoice: 'Start Voice Call',
325
+ endVoice: 'End Call',
326
+ startText: 'Start Text Chat',
327
+ endText: 'End Chat',
307
328
  },
308
329
  pl: {
309
330
  microphoneError: 'Proszę włączyć uprawnienia mikrofonu.',
@@ -323,6 +344,12 @@ export class StayliftWidget {
323
344
  termsWarning: 'Jeśli nie chcesz, aby Twoje rozmowy były nagrywane, prosimy o nieużywanie tej usługi.',
324
345
  termsAgree: 'Zgadzam się',
325
346
  termsDecline: 'Odrzuć',
347
+ modeText: 'Tekst',
348
+ modeVoice: 'Głos',
349
+ startVoice: 'Rozpocznij rozmowę głosową',
350
+ endVoice: 'Zakończ',
351
+ startText: 'Rozpocznij czat',
352
+ endText: 'Zakończ czat',
326
353
  },
327
354
  de: {
328
355
  microphoneError: 'Bitte aktivieren Sie die Mikrofonberechtigung in Ihrem Browser.',
@@ -342,6 +369,12 @@ export class StayliftWidget {
342
369
  termsWarning: 'Wenn Sie nicht möchten, dass Ihre Gespräche aufgezeichnet werden, verwenden Sie diesen Dienst bitte nicht.',
343
370
  termsAgree: 'Zustimmen',
344
371
  termsDecline: 'Ablehnen',
372
+ modeText: 'Text',
373
+ modeVoice: 'Stimme',
374
+ startVoice: 'Sprachanruf starten',
375
+ endVoice: 'Beenden',
376
+ startText: 'Text-Chat starten',
377
+ endText: 'Chat beenden',
345
378
  },
346
379
  };
347
380
  return translations[this.language]?.[key] || translations['en'][key] || key;
@@ -381,18 +414,18 @@ export class StayliftWidget {
381
414
  '--sl-surface': theme.surface,
382
415
  };
383
416
  if (this.variant === 'inline') {
384
- return (h("div", { class: "sl-widget sl-inline", style: cssVars }, this.renderCard(isCallActive, isTransitioning)));
417
+ return (h("div", { class: "sl-widget sl-inline", style: cssVars }, this.renderCard(isTransitioning)));
385
418
  }
386
- return (h("div", { class: `sl-widget sl-floating ${this.getPositionClasses()}`, style: cssVars }, this.isExpanded ? (h("div", { class: "sl-card" }, this.renderCard(isCallActive, isTransitioning))) : (h("div", { class: "sl-fab-pill" }, h("div", { class: "sl-fab-avatar" }, this.avatarUrl ? (h("img", { src: this.avatarUrl, alt: "", class: "sl-fab-avatar-img" })) : (h("staylift-orb", { size: 48, primaryColor: this.primaryColor, inputVolume: this.inputVolume, outputVolume: this.outputVolume, isActive: isCallActive }))), h("div", { class: "sl-fab-content" }, h("span", { class: "sl-fab-prompt" }, this.fabPrompt), h("button", { class: "sl-fab-btn", onClick: this.handleToggleExpand }, this.fabButtonText))))));
419
+ return (h("div", { class: `sl-widget sl-floating ${this.getPositionClasses()}`, style: cssVars }, this.isExpanded ? (h("div", { class: "sl-card" }, this.renderCard(isTransitioning))) : (h("div", { class: "sl-fab-pill" }, h("div", { class: "sl-fab-avatar" }, this.avatarUrl ? (h("img", { src: this.avatarUrl, alt: "", class: "sl-fab-avatar-img" })) : (h("staylift-orb", { size: 48, primaryColor: this.primaryColor, inputVolume: this.inputVolume, outputVolume: this.outputVolume, isActive: isCallActive }))), h("div", { class: "sl-fab-content" }, h("span", { class: "sl-fab-prompt" }, this.fabPrompt), h("button", { class: "sl-fab-btn", onClick: this.handleToggleExpand }, this.fabButtonText))))));
387
420
  }
388
- renderCard(isCallActive, isTransitioning) {
421
+ renderCard(isTransitioning) {
389
422
  if (!this.termsAccepted) {
390
423
  return this.renderTerms();
391
424
  }
392
425
  return [
393
426
  this.renderHeader(isTransitioning),
394
427
  this.renderContent(),
395
- this.renderFooter(isCallActive, isTransitioning),
428
+ this.renderFooter(isTransitioning),
396
429
  ];
397
430
  }
398
431
  renderTerms() {
@@ -404,10 +437,15 @@ export class StayliftWidget {
404
437
  renderContent() {
405
438
  const isConnecting = this.status === 'connecting';
406
439
  const isConnected = this.status === 'connected';
407
- return (h("div", { class: "sl-content", ref: (el) => this.messagesContainer = el ?? null }, this.messages.length === 0 ? (h("div", { class: "sl-empty" }, h("staylift-orb", { size: 48, primaryColor: this.primaryColor, isActive: false }), h("h3", { class: "sl-empty-title" }, isConnecting ? this.t('starting') : isConnected ? this.t('talkOrType') : this.t('emptyTitle')), h("p", { class: "sl-empty-desc" }, isConnecting ? this.t('connecting') : isConnected ? this.t('ready') : this.t('emptyDesc')))) : (this.messages.map((message, index) => (h("div", { class: `sl-msg sl-msg--${message.role}`, key: index }, h("div", { class: "sl-msg-row" }, h("div", { class: "sl-msg-bubble" }, message.content), message.role === 'assistant' && (h("div", { class: "sl-msg-orb" }, h("staylift-orb", { size: 24, primaryColor: this.primaryColor, isActive: false })))), message.role === 'assistant' && (h("div", { class: "sl-msg-actions" }, h("button", { class: "sl-action", onClick: () => this.copyToClipboard(message.content, index) }, this.copiedIndex === index ? (h("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("polyline", { points: "20 6 9 17 4 12" }))) : (h("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }), h("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" }))))))))))));
440
+ return (h("div", { class: "sl-content", ref: (el) => this.messagesContainer = el ?? null }, this.messages.length === 0 ? (h("div", { class: "sl-empty" }, h("staylift-orb", { size: 48, primaryColor: this.primaryColor, isActive: false }), h("h3", { class: "sl-empty-title" }, isConnecting ? this.t('starting') : isConnected ? this.t('talkOrType') : this.t('emptyTitle')), h("p", { class: "sl-empty-desc" }, isConnecting ? this.t('connecting') : isConnected ? this.t('ready') : this.t('emptyDesc')))) : (this.messages.map((message, index) => (h("div", { class: `sl-msg sl-msg--${message.role}`, key: index }, h("div", { class: "sl-msg-row" }, h("div", { class: "sl-msg-bubble" }, message.content)), message.role === 'assistant' && (h("div", { class: "sl-msg-actions" }, h("button", { class: "sl-action", onClick: () => this.copyToClipboard(message.content, index) }, this.copiedIndex === index ? (h("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("polyline", { points: "20 6 9 17 4 12" }))) : (h("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }), h("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" }))))))))))));
408
441
  }
409
- renderFooter(isCallActive, isTransitioning) {
410
- return (h("div", { class: "sl-footer" }, this.showBranding && (h("div", { class: "sl-branding" }, h("a", { href: "https://staylift.com", target: "_blank", rel: "noopener noreferrer" }, this.t('poweredBy')))), h("div", { class: "sl-input-row" }, h("input", { type: "text", class: "sl-input", placeholder: this.t('placeholder'), value: this.inputText, onInput: this.handleInputChange, onKeyDown: this.handleInputKeyDown, disabled: isTransitioning }), h("button", { class: "sl-btn", onClick: () => this.handleSendText(), disabled: !this.inputText.trim() || isTransitioning }, h("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("line", { x1: "22", y1: "2", x2: "11", y2: "13" }), h("polygon", { points: "22 2 15 22 11 13 2 9 22 2" }))), !isCallActive ? (h("button", { class: "sl-btn", onClick: () => this.handleVoiceButton(), disabled: isTransitioning }, h("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { d: "M2 10v3" }), h("path", { d: "M6 6v11" }), h("path", { d: "M10 3v18" }), h("path", { d: "M14 8v7" }), h("path", { d: "M18 5v13" }), h("path", { d: "M22 10v3" })))) : (h("button", { class: "sl-btn sl-btn--end", onClick: () => this.handleVoiceButton(), disabled: isTransitioning }, h("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { d: "M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91" }), h("line", { x1: "22", y1: "2", x2: "2", y2: "22" })))))));
442
+ renderFooter(isTransitioning) {
443
+ const isDisconnected = this.status === 'disconnected';
444
+ const isConnectedText = this.status === 'connected' && this.isTextOnlyMode;
445
+ const isConnectedVoice = this.status === 'connected' && !this.isTextOnlyMode;
446
+ // Only show text input when connected in text mode
447
+ const showTextInput = isConnectedText;
448
+ return (h("div", { class: "sl-footer" }, isDisconnected && (h("div", { class: "sl-mode-toggle" }, h("button", { class: `sl-mode-btn ${this.selectedMode === 'text' ? 'sl-mode-btn--active' : ''}`, onClick: () => this.selectedMode = 'text' }, h("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" })), this.t('modeText')), h("button", { class: `sl-mode-btn ${this.selectedMode === 'voice' ? 'sl-mode-btn--active' : ''}`, onClick: () => this.selectedMode = 'voice' }, h("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { d: "M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" }), h("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }), h("line", { x1: "12", y1: "19", x2: "12", y2: "23" }), h("line", { x1: "8", y1: "23", x2: "16", y2: "23" })), this.t('modeVoice')))), showTextInput && (h("div", { class: "sl-input-row" }, h("button", { class: "sl-btn sl-btn--end", onClick: () => this.handleTextButton(), disabled: isTransitioning, title: this.t('endText') }, h("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), h("line", { x1: "6", y1: "6", x2: "18", y2: "18" }))), h("input", { type: "text", class: "sl-input", placeholder: this.t('placeholder'), value: this.inputText, onInput: this.handleInputChange, onKeyDown: this.handleInputKeyDown, disabled: isTransitioning }), h("button", { class: "sl-btn", onClick: () => this.handleSendText(), disabled: !this.inputText.trim() || isTransitioning }, h("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("line", { x1: "22", y1: "2", x2: "11", y2: "13" }), h("polygon", { points: "22 2 15 22 11 13 2 9 22 2" }))))), this.selectedMode === 'text' && isDisconnected && (h("div", { class: "sl-voice-controls" }, h("button", { class: "sl-voice-btn", onClick: () => this.handleTextButton(), disabled: isTransitioning }, h("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" })), isTransitioning ? this.t('connecting') : this.t('startText')))), this.selectedMode === 'voice' && isDisconnected && (h("div", { class: "sl-voice-controls" }, h("button", { class: "sl-voice-btn", onClick: () => this.handleVoiceButton(), disabled: isTransitioning }, h("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { d: "M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" }), h("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }), h("line", { x1: "12", y1: "19", x2: "12", y2: "23" }), h("line", { x1: "8", y1: "23", x2: "16", y2: "23" })), isTransitioning ? this.t('connecting') : this.t('startVoice')))), isConnectedVoice && (h("div", { class: "sl-voice-controls" }, h("button", { class: "sl-voice-btn sl-voice-btn--end", onClick: () => this.handleVoiceButton(), disabled: isTransitioning }, h("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { d: "M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91" }), h("line", { x1: "22", y1: "2", x2: "2", y2: "22" })), this.t('endVoice')))), this.showBranding && (h("div", { class: "sl-branding" }, h("a", { href: "https://staylift.com", target: "_blank", rel: "noopener noreferrer" }, h("img", { src: "data:image/png;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBMRXhpZgAATU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8IAEQgAIAAgAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAMCBAEFAAYHCAkKC//EAMMQAAEDAwIEAwQGBAcGBAgGcwECAAMRBBIhBTETIhAGQVEyFGFxIweBIJFCFaFSM7EkYjAWwXLRQ5I0ggjhU0AlYxc18JNzolBEsoPxJlQ2ZJR0wmDShKMYcOInRTdls1V1pJXDhfLTRnaA40dWZrQJChkaKCkqODk6SElKV1hZWmdoaWp3eHl6hoeIiYqQlpeYmZqgpaanqKmqsLW2t7i5usDExcbHyMnK0NTV1tfY2drg5OXm5+jp6vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAQIAAwQFBgcICQoL/8QAwxEAAgIBAwMDAgMFAgUCBASHAQACEQMQEiEEIDFBEwUwIjJRFEAGMyNhQhVxUjSBUCSRoUOxFgdiNVPw0SVgwUThcvEXgmM2cCZFVJInotIICQoYGRooKSo3ODk6RkdISUpVVldYWVpkZWZnaGlqc3R1dnd4eXqAg4SFhoeIiYqQk5SVlpeYmZqgo6SlpqeoqaqwsrO0tba3uLm6wMLDxMXGx8jJytDT1NXW19jZ2uDi4+Tl5ufo6ery8/T19vf4+fr/2wBDAAICAgICAgMCAgMFAwMDBQYFBQUFBggGBgYGBggKCAgICAgICgoKCgoKCgoMDAwMDAwODg4ODg8PDw8PDw8PDw//2wBDAQICAgQEBAcEBAcQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/2gAMAwEAAhEDEQAAAfiPo6T9gTw/lbxH1R8xAWn0XwXsuXwv2J+cvsHnGXj/AP/aAAgBAQABBQJ2O0btujuvDXiGygZS/B6bCDwr9Yvii93+QRKL5RL8C7HvO8yb3t22f0cO3xxpg2+r22+3Pbre83K/u0XUoD//2gAIAQMRAT8BwnczyRDk/enDhl9/j+j8l+8kutyx9kUA/wD/2gAIAQIRAT8B0OVnO3//2gAIAQEABj8CajttnLc48eWgqo1XV5t08MMftLWggDvtxssUw8hKlH+VTrr9rTFZoWnZ41URJQhMyx+avw8nw7SWKbmWDaf+BASqiVfyR8S7mwljSi1jhISnyTQdNPtfB8GLe1uVxRjXFLwurhcqR5E6dv/EADMQAQADAAICAgICAwEBAAACCwERACExQVFhcYGRobHB8NEQ4fEgMEBQYHCAkKCwwNDg/9oACAEBAAE/IZsYHlgL2hFFykgNxq/8i4rZQAQHMl53M1Hu+FPRCE0H7+OCoZU37nAvVx5U656pS9EIT8wQj3YdyrMaSOzIAl5fmjVxMtl8cUFL/9oADAMBAAIRAxEAABD2kov/xAAzEQEBAQADAAECBQUBAQABAQkBABEhMRBBUWEgcfCRgaGx0cHh8TBAUGBwgJCgsMDQ4P/aAAgBAxEBPxDT3M5sJdI+Ma/bs4t0zZy8v7fT4v/aAAgBAhEBPxBPA2RcX//aAAgBAQABPxCXMUdNxPzQSDHSzRnYYmQIwJUDyoU5qBhvVIgVuSPBo61iI8V5sIdq5DxIPbJQrnR+qCA5rryHQleAAiIk2UhJuwnSp6JKa/KjgYm/NRmPignYSnymSrtfVNewxg4eEjzFnxEX/9k=", alt: "Staylift", class: "sl-branding-logo" }), this.t('poweredBy'))))));
411
449
  }
412
450
  static get is() { return "staylift-widget"; }
413
451
  static get encapsulation() { return "shadow"; }
@@ -717,7 +755,8 @@ export class StayliftWidget {
717
755
  "outputVolume": {},
718
756
  "messages": {},
719
757
  "inputText": {},
720
- "copiedIndex": {}
758
+ "copiedIndex": {},
759
+ "selectedMode": {}
721
760
  };
722
761
  }
723
762
  static get events() {