obi-sdk 0.3.13 → 0.4.1

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/README.md CHANGED
@@ -21,6 +21,7 @@ The loader accepts the following configuration options:
21
21
  | `apiKey` | string | - | Your Obi API key (required for production) |
22
22
  | `user` | object | - | User information with `id` (required), `email` (optional) and `metadata` (optional) |
23
23
  | `isActive` | boolean | `true` | Whether to show the widget. Set to `false` to disable the widget for specific users |
24
+ | `linkOnlyAccess` | boolean | `false` | Hide the widget unless accessed via a session link. Perfect for trial periods where only customers with direct links should see the widget |
24
25
  | `primaryColor` | string | - | Custom primary color for the widget UI (CSS color value) |
25
26
  | `secondaryColor` | string | - | Custom secondary color for the widget UI (CSS color value) |
26
27
 
@@ -41,6 +42,30 @@ window.obiWidgetConfig = {
41
42
  }
42
43
  ```
43
44
 
45
+ ## Trial Mode / Link-Only Access
46
+
47
+ The `linkOnlyAccess` option is perfect for trial periods where you want to control who can see and interact with the widget. When enabled, the widget remains hidden until a session is started via a direct link.
48
+
49
+ ```js
50
+ window.obiWidgetConfig = {
51
+ apiKey: "YOUR_API_KEY",
52
+ linkOnlyAccess: true, // Widget only visible when accessed via session link
53
+ }
54
+ ```
55
+
56
+ This is ideal for:
57
+
58
+ - Trial periods where only specific customers should see the widget
59
+ - Beta testing with selected users
60
+ - Controlled rollouts using session-specific links
61
+ - Demo environments where widget access is link-based
62
+
63
+ When `linkOnlyAccess` is `true`:
64
+
65
+ - Widget is invisible to regular website visitors
66
+ - Widget becomes visible only when a session is started (e.g., via URL parameters)
67
+ - Once a session ends, the widget returns to hidden state
68
+
44
69
  ## Session Persistence
45
70
 
46
71
  The SDK now supports session persistence across page reloads and navigation. If a user refreshes the page or navigates to another page on your site, the session with Obi will be automatically restored when they return, providing a seamless experience.
package/dist/loader.d.ts CHANGED
@@ -10,9 +10,12 @@ interface ObiWidgetConfig {
10
10
  user?: {
11
11
  id: string;
12
12
  email?: string;
13
- metadata?: any;
14
- };
13
+ metadata?: Record<string, any>;
14
+ } | null;
15
15
  isActive?: boolean;
16
+ linkOnlyAccess?: boolean;
17
+ primaryColor?: string;
18
+ secondaryColor?: string;
16
19
  }
17
20
  declare class ObiWidgetLoader {
18
21
  private config;
@@ -1,4 +1,4 @@
1
- import { S as SDKState, E as EventEmitter, R as RoomEvent, T as Track, z as z$2, a as Room, A as API_BASE_URL, N as N$1 } from "./types-82772f00.js";
1
+ import { S as SDKState, E as EventEmitter, R as RoomEvent, T as Track, z as z$2, a as Room, A as API_BASE_URL, N as N$1 } from "./types-e0297e7b.js";
2
2
  const PATH_PARAM_RE = /\{[^{}]+\}/g;
3
3
  function randomID() {
4
4
  return Math.random().toString(36).slice(2, 11);
@@ -534,6 +534,9 @@ class ObiSession {
534
534
  this._userAudioTimer = null;
535
535
  this.sessionId = sessionId;
536
536
  this.apiBaseUrl = apiBaseUrl || DEFAULT_API_BASE_URL;
537
+ this.client = new ObiClient({
538
+ baseUrl: this.apiBaseUrl
539
+ });
537
540
  this.emitter = new EventEmitter();
538
541
  }
539
542
  emit(event, data) {
@@ -590,7 +593,7 @@ class ObiSession {
590
593
  if (this.currentState === SDKState.RESEARCHING || this.currentState === SDKState.PAUSED)
591
594
  return;
592
595
  const state = attributes["lk.agent.state"];
593
- const newState = z$2(state).with("listening", () => SDKState.USER_SPEAKING).with("speaking", () => SDKState.AGENT_SPEAKING).with("thinking", () => SDKState.AGENT_SPEAKING).otherwise(() => void 0);
596
+ const newState = z$2(state).with("listening", () => SDKState.USER_SPEAKING).with("speaking", () => SDKState.AGENT_SPEAKING).with("thinking", () => SDKState.THINKING).otherwise(() => void 0);
594
597
  if (!newState)
595
598
  return;
596
599
  this.setState(newState);
@@ -660,10 +663,8 @@ class ObiSession {
660
663
  dynacast: true
661
664
  });
662
665
  this.setupRoomEventListeners();
663
- const params = new URLSearchParams({ token: this.sessionId, skip_intro: "true" });
664
- const joinEndpoint = `${this.apiBaseUrl}/join-token?${params.toString()}`;
665
- const joinToken = await fetch(joinEndpoint).then((res) => res.json());
666
- await this.room.connect(joinToken.url, joinToken.token);
666
+ const joinToken = await this.client.getJoinToken(this.sessionId, { skipIntro: true });
667
+ await this.room.connect(joinToken.data.url, joinToken.data.token);
667
668
  if (this.microphoneStream) {
668
669
  const micTrack = this.microphoneStream.getAudioTracks()[0];
669
670
  await this.room.localParticipant.publishTrack(micTrack, {
@@ -672,8 +673,8 @@ class ObiSession {
672
673
  });
673
674
  }
674
675
  return {
675
- url: joinToken.url,
676
- token: joinToken.token
676
+ url: joinToken.data.url,
677
+ token: joinToken.data.token
677
678
  };
678
679
  } catch (error) {
679
680
  console.error("Failed to connect to LiveKit:", error);
@@ -718,6 +719,9 @@ class ObiSession {
718
719
  this.currentState = newState;
719
720
  this.emitter.emit("stateChanged", newState);
720
721
  }
722
+ getCurrentState() {
723
+ return this.currentState;
724
+ }
721
725
  async requestMicrophone() {
722
726
  try {
723
727
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
@@ -874,7 +878,6 @@ class ObiSession {
874
878
  }
875
879
  }
876
880
  const SESSION_URL_PARAM = "49206C6F7665204F6269_session";
877
- const API_KEY_URL_PARAM = "49206C6F7665204F6269_client";
878
881
  var extendStatics = function(d2, b2) {
879
882
  extendStatics = Object.setPrototypeOf || { __proto__: [] } instanceof Array && function(d3, b3) {
880
883
  d3.__proto__ = b3;
@@ -11140,14 +11143,9 @@ class SessionStartModal extends i$1 {
11140
11143
  this.onClose();
11141
11144
  }
11142
11145
  }
11143
- handleBackdropClick(e2) {
11144
- if (e2.target === e2.currentTarget) {
11145
- this.handleClose();
11146
- }
11147
- }
11148
11146
  render() {
11149
11147
  return x`
11150
- <div class="backdrop" @click=${this.handleBackdropClick}></div>
11148
+ <div class="backdrop"></div>
11151
11149
  <div class="container">
11152
11150
  <button class="close-button" @click=${this.handleClose}>×</button>
11153
11151
 
@@ -11184,11 +11182,12 @@ SessionStartModal.styles = i$4`
11184
11182
  left: 50%;
11185
11183
  transform: translate(-50%, -50%);
11186
11184
  z-index: 50;
11185
+ gap: 32px;
11187
11186
 
11188
11187
  /* Layout from user specifications */
11189
11188
  display: flex;
11190
11189
  width: 640px;
11191
- height: 380px;
11190
+ min-height: 380px;
11192
11191
  padding: 48px 48px 32px 48px;
11193
11192
  flex-direction: column;
11194
11193
  justify-content: space-between;
@@ -11220,7 +11219,7 @@ SessionStartModal.styles = i$4`
11220
11219
  align-items: center;
11221
11220
  gap: 8px;
11222
11221
  aspect-ratio: 1/1;
11223
- border-radius: var(--border-radius-lg, 8px);
11222
+ border-radius: var(--border-radius-lg, 12px);
11224
11223
  background: var(--tailwind-colors-violet-600, #7c3aed);
11225
11224
  box-shadow:
11226
11225
  0px 0px 8px 0px rgba(168, 85, 247, 0.12),
@@ -11242,15 +11241,15 @@ SessionStartModal.styles = i$4`
11242
11241
  font-family: "Syne", sans-serif;
11243
11242
  font-size: 32px;
11244
11243
  font-weight: 700;
11245
- margin: 32px 0 0 0;
11246
11244
  color: #111827;
11245
+ margin: 0;
11247
11246
  }
11248
11247
 
11249
11248
  .subtitle {
11250
11249
  font-size: 16px;
11251
11250
  color: #6b7280;
11252
- margin: 16px 0 0 0;
11253
11251
  line-height: 1.5;
11252
+ margin: 0;
11254
11253
  }
11255
11254
 
11256
11255
  .button {
@@ -11328,11 +11327,13 @@ var __decorateClass = (decorators, target, key, kind) => {
11328
11327
  __defProp(target, key, result);
11329
11328
  return result;
11330
11329
  };
11330
+ const WIDGET_PARAMS_KEY = "io.obi.widget-parameters";
11331
11331
  class ObiWidget extends i$1 {
11332
11332
  constructor() {
11333
11333
  super();
11334
11334
  this.apiKey = "";
11335
11335
  this.isActive = true;
11336
+ this.linkOnlyAccess = false;
11336
11337
  this.position = "bottom-right";
11337
11338
  this.user = null;
11338
11339
  this.state = SDKState.READY;
@@ -11354,6 +11355,7 @@ class ObiWidget extends i$1 {
11354
11355
  this.boundSaveSessionData = null;
11355
11356
  this.obiClient = null;
11356
11357
  this.closeNavTimeoutRef = null;
11358
+ this.researchingTimeoutRef = null;
11357
11359
  this.handleCourseSelectEvent = (event) => {
11358
11360
  const customEvent = event;
11359
11361
  this.selectedCourse = customEvent.detail;
@@ -11374,10 +11376,13 @@ class ObiWidget extends i$1 {
11374
11376
  const sessionWithPlan = matchingSession;
11375
11377
  this.selectedCourse = {
11376
11378
  id: sessionToken,
11377
- name: sessionWithPlan.onboarding_plan?.product?.name || "Session from URL",
11378
- description: sessionWithPlan.onboarding_plan?.product?.description || "Continue your session"
11379
+ name: sessionWithPlan.onboarding_plan?.name || "",
11380
+ description: sessionWithPlan.onboarding_plan?.description || ""
11379
11381
  };
11382
+ this.state = SDKState.LOADING;
11380
11383
  this.showSessionStartModal = true;
11384
+ } else {
11385
+ console.log("No session found with token:", sessionToken);
11381
11386
  }
11382
11387
  }
11383
11388
  } catch (error) {
@@ -11404,15 +11409,22 @@ class ObiWidget extends i$1 {
11404
11409
  if (window.obiWidgetConfig.isActive !== void 0) {
11405
11410
  this.isActive = window.obiWidgetConfig.isActive;
11406
11411
  }
11412
+ if (window.obiWidgetConfig.linkOnlyAccess !== void 0) {
11413
+ this.linkOnlyAccess = window.obiWidgetConfig.linkOnlyAccess;
11414
+ }
11407
11415
  this.style.setProperty("--obi-primary", window.obiWidgetConfig?.primaryColor || "#9500ff");
11408
11416
  this.style.setProperty("--obi-secondary", window.obiWidgetConfig?.secondaryColor || "#c4b5fd");
11409
11417
  }
11410
11418
  }
11411
- removeSessionFromUrl() {
11419
+ removeSessionUrlParams() {
11412
11420
  const url = new URL(window.location.href);
11413
11421
  url.searchParams.delete(SESSION_URL_PARAM);
11414
- url.searchParams.delete(API_KEY_URL_PARAM);
11415
11422
  window.history.replaceState({}, "", url.toString());
11423
+ try {
11424
+ localStorage.removeItem(WIDGET_PARAMS_KEY);
11425
+ } catch (error) {
11426
+ console.warn("Failed to remove widget parameters from localStorage:", error);
11427
+ }
11416
11428
  }
11417
11429
  /**
11418
11430
  * Create a new ObiSession instance with common configuration
@@ -11438,9 +11450,27 @@ class ObiWidget extends i$1 {
11438
11450
  */
11439
11451
  setupSessionEventListeners(session, onError) {
11440
11452
  session.on("stateChanged", (newState) => {
11441
- this.state = newState;
11442
- if (newState !== SDKState.READY) {
11453
+ if (newState === SDKState.RESEARCHING) {
11454
+ if (this.researchingTimeoutRef) {
11455
+ window.clearTimeout(this.researchingTimeoutRef);
11456
+ }
11457
+ this.state = newState;
11458
+ this.researchingTimeoutRef = window.setTimeout(() => {
11459
+ this.researchingTimeoutRef = null;
11460
+ const currentSessionState = session.getCurrentState();
11461
+ this.state = currentSessionState;
11462
+ if (currentSessionState !== SDKState.READY) {
11463
+ this.storedActiveState = currentSessionState;
11464
+ }
11465
+ }, 1500);
11443
11466
  this.storedActiveState = newState;
11467
+ return;
11468
+ }
11469
+ if (this.researchingTimeoutRef === null) {
11470
+ this.state = newState;
11471
+ if (newState !== SDKState.READY) {
11472
+ this.storedActiveState = newState;
11473
+ }
11444
11474
  }
11445
11475
  });
11446
11476
  session.on("volume", ({ speaker, spectrum, volume }) => {
@@ -11473,10 +11503,10 @@ class ObiWidget extends i$1 {
11473
11503
  try {
11474
11504
  const session = this.createSession(sessionToken);
11475
11505
  if (!session) {
11476
- this.handleSessionCreationFailure(() => this.removeSessionFromUrl());
11506
+ this.handleSessionCreationFailure(() => this.removeSessionUrlParams());
11477
11507
  return;
11478
11508
  }
11479
- this.setupSessionEventListeners(session, () => this.removeSessionFromUrl());
11509
+ this.setupSessionEventListeners(session, () => this.removeSessionUrlParams());
11480
11510
  session.on("screenCaptureRequested", async () => {
11481
11511
  try {
11482
11512
  const canvas = await html2canvas(document.documentElement, {
@@ -11496,12 +11526,12 @@ class ObiWidget extends i$1 {
11496
11526
  this.sessionToken = sessionToken;
11497
11527
  this.roomToken = connectionInfo.token;
11498
11528
  this.roomUrl = connectionInfo.url;
11499
- this.removeSessionFromUrl();
11529
+ this.removeSessionUrlParams();
11500
11530
  }
11501
11531
  this.activeSession = session;
11502
11532
  } catch (error) {
11503
11533
  console.error("Failed to start session:", error);
11504
- this.handleSessionCreationFailure(() => this.removeSessionFromUrl());
11534
+ this.handleSessionCreationFailure(() => this.removeSessionUrlParams());
11505
11535
  }
11506
11536
  }
11507
11537
  async handleSessionStart(sessionToken) {
@@ -11582,11 +11612,26 @@ class ObiWidget extends i$1 {
11582
11612
  async sessionConnectionCheck() {
11583
11613
  await this.checkExistingSession();
11584
11614
  if (!this.activeSession) {
11585
- const urlParams = new URLSearchParams(window.location.search);
11586
- const sessionId = urlParams.get(SESSION_URL_PARAM);
11587
- this.apiKey = urlParams.get(API_KEY_URL_PARAM) || this.apiKey;
11615
+ let storedParams = {};
11616
+ try {
11617
+ const storedParamsJson = localStorage.getItem(WIDGET_PARAMS_KEY);
11618
+ if (storedParamsJson) {
11619
+ storedParams = JSON.parse(storedParamsJson);
11620
+ }
11621
+ } catch (error) {
11622
+ console.warn("Failed to parse stored widget parameters:", error);
11623
+ }
11624
+ if (Object.keys(storedParams).length === 0) {
11625
+ const urlParams = new URLSearchParams(window.location.search);
11626
+ urlParams.forEach((value, key) => {
11627
+ storedParams[key] = value;
11628
+ });
11629
+ }
11630
+ const sessionId = storedParams[SESSION_URL_PARAM];
11588
11631
  if (sessionId && this.apiKey) {
11589
11632
  await this.handleUrlSessionEvent(sessionId);
11633
+ } else {
11634
+ console.log("No session ID found or API key is not set");
11590
11635
  }
11591
11636
  }
11592
11637
  }
@@ -11603,11 +11648,15 @@ class ObiWidget extends i$1 {
11603
11648
  if (this.closeNavTimeoutRef !== null) {
11604
11649
  window.clearTimeout(this.closeNavTimeoutRef);
11605
11650
  }
11651
+ if (this.researchingTimeoutRef !== null) {
11652
+ window.clearTimeout(this.researchingTimeoutRef);
11653
+ this.researchingTimeoutRef = null;
11654
+ }
11606
11655
  if (this.boundSaveSessionData) {
11607
11656
  window.removeEventListener("beforeunload", this.boundSaveSessionData);
11608
11657
  window.removeEventListener("pagehide", this.boundSaveSessionData);
11609
11658
  }
11610
- this.removeSessionFromUrl();
11659
+ this.removeSessionUrlParams();
11611
11660
  super.disconnectedCallback();
11612
11661
  }
11613
11662
  handleMouseEnter() {
@@ -11635,6 +11684,10 @@ class ObiWidget extends i$1 {
11635
11684
  this.sessionToken = null;
11636
11685
  this.roomToken = null;
11637
11686
  this.roomUrl = null;
11687
+ if (this.researchingTimeoutRef !== null) {
11688
+ window.clearTimeout(this.researchingTimeoutRef);
11689
+ this.researchingTimeoutRef = null;
11690
+ }
11638
11691
  if (this.activeSession) {
11639
11692
  this.activeSession.disconnect();
11640
11693
  this.activeSession = null;
@@ -11659,13 +11712,26 @@ class ObiWidget extends i$1 {
11659
11712
  render() {
11660
11713
  if (!this.isActive)
11661
11714
  return E;
11715
+ if (this.linkOnlyAccess && this.state === SDKState.READY)
11716
+ return E;
11662
11717
  const stateRender = z$2(this.state).with(SDKState.LOADING, () => x`<obi-dot-loader></obi-dot-loader>`).with(SDKState.RESEARCHING, () => x`<obi-searching-loader></obi-searching-loader>`).with(
11663
11718
  N$1.union(SDKState.USER_SPEAKING, SDKState.AGENT_SPEAKING),
11664
11719
  () => x`<obi-audio-equalizer .volume=${this.volume}></obi-audio-equalizer>`
11665
- ).with(SDKState.PAUSED, () => obiIcon).otherwise(() => obiIcon);
11720
+ ).with(SDKState.THINKING, () => x`<obi-dot-loader></obi-dot-loader>`).with(SDKState.PAUSED, () => obiIcon).otherwise(() => obiIcon);
11721
+ const isPulseState = this.state === SDKState.USER_SPEAKING || this.state === SDKState.AGENT_SPEAKING;
11722
+ const isResearching = this.state === SDKState.RESEARCHING;
11723
+ const isUserSpeaking = this.state === SDKState.USER_SPEAKING;
11724
+ const isRotated = this.state !== SDKState.READY || this.navVisible;
11725
+ const containerClasses = [
11726
+ "widget-container",
11727
+ isRotated ? "rotated" : "",
11728
+ isPulseState ? "pulse" : "",
11729
+ isResearching ? "researching" : "",
11730
+ isUserSpeaking ? "user-speaking" : ""
11731
+ ].filter(Boolean).join(" ");
11666
11732
  return x`
11667
11733
  <div
11668
- class="widget-container ${this.state === SDKState.USER_SPEAKING || this.state === SDKState.AGENT_SPEAKING ? "pulse" : ""} ${this.state !== SDKState.READY || this.navVisible ? "rounded" : ""} ${this.state === SDKState.RESEARCHING ? "researching" : ""} ${this.state === SDKState.USER_SPEAKING ? "user-speaking" : ""}"
11734
+ class="${containerClasses}"
11669
11735
  @mouseenter=${this.handleMouseEnter}
11670
11736
  @mouseleave=${this.handleMouseLeave}
11671
11737
  >
@@ -11691,6 +11757,8 @@ class ObiWidget extends i$1 {
11691
11757
  .onClose=${() => {
11692
11758
  this.showSessionStartModal = false;
11693
11759
  this.selectedCourse = null;
11760
+ this.state = SDKState.READY;
11761
+ this.removeSessionUrlParams();
11694
11762
  }}
11695
11763
  ></obi-session-start-modal>` : E}
11696
11764
  `;
@@ -11773,7 +11841,7 @@ ObiWidget.styles = i$4`
11773
11841
  position: fixed;
11774
11842
  width: 56px;
11775
11843
  height: 56px;
11776
- border-radius: 28px;
11844
+ border-radius: 12px;
11777
11845
  border-color: transparent;
11778
11846
  background-color: var(--obi-primary);
11779
11847
  display: flex;
@@ -11789,17 +11857,8 @@ ObiWidget.styles = i$4`
11789
11857
  linear-gradient(195.84deg, var(--obi-secondary) 00 11.05%, var(--obi-secondary) 117.01%);
11790
11858
  }
11791
11859
 
11792
- .widget-container:hover {
11793
- border-radius: 12px;
11794
- }
11795
-
11796
- .widget-container.rounded {
11797
- border-radius: 12px;
11798
- }
11799
-
11800
11860
  .widget-container.researching {
11801
11861
  width: 273px;
11802
- border-radius: 12px;
11803
11862
  }
11804
11863
 
11805
11864
  .widget-icon {
@@ -11808,7 +11867,7 @@ ObiWidget.styles = i$4`
11808
11867
  transition: transform 0.5s ease-in-out;
11809
11868
  }
11810
11869
 
11811
- .widget-container.rounded .widget-icon {
11870
+ .widget-container.rotated .widget-icon {
11812
11871
  transform: rotate(90deg);
11813
11872
  }
11814
11873
 
@@ -11834,6 +11893,9 @@ __decorateClass([
11834
11893
  __decorateClass([
11835
11894
  r$1()
11836
11895
  ], ObiWidget.prototype, "isActive", 2);
11896
+ __decorateClass([
11897
+ r$1()
11898
+ ], ObiWidget.prototype, "linkOnlyAccess", 2);
11837
11899
  __decorateClass([
11838
11900
  r$1()
11839
11901
  ], ObiWidget.prototype, "position", 2);
@@ -11894,4 +11956,4 @@ export {
11894
11956
  searchingLoader as s,
11895
11957
  x
11896
11958
  };
11897
- //# sourceMappingURL=obi-widget-d7e7c6bf.js.map
11959
+ //# sourceMappingURL=obi-widget-58dc98b0.js.map