PageSourceSearch

https://www.mentza.com/assets/controllers/ai_call_controller-6241866f.js

js mentza.com collected 2026-10-01 12:10:42 UTC 9,956 bytes, 283 lines download raw bytes

1// app/typescript/controllers/ai_call_controller.ts
2import { Controller } from "@hotwired/stimulus";
3import AgoraRTC from "agora-rtc-sdk-ng";
4import { createConsumer } from "@rails/actioncable";
5import { mentzaPost } from "utils";
6
7class ai_call_controller_default extends Controller {
8  static targets = [
9    "leftButtonText",
10    "speakingAnimation",
11    "instructionsHeading",
12    "instructions",
13    "muteButton",
14    "startButton",
15    "muteIcon",
16    "unmuteIcon",
17    "timer",
18    "starter",
19    "startStatus",
20    "mainBox",
21    "people",
22    "beginButton",
23    "botAvatarCircle",
24    "userAvatarCircle"
25  ];
26  async initialize() {
27    const self = this;
28    this.client = AgoraRTC.createClient({ mode: "live", codec: "vp8" });
29    this.client.on("volume-indicator", (volumes) => {
30      if (volumes) {
31        volumes.forEach((volume) => {
32          console.log(`${volume.uid} volume ${volume.level}`);
33          if (volume.uid != 8103) {
34            if (volume.level > 0) {
35              console.log("Volume for user");
36              if (!self.isBotSpeaking) {
37                console.log("Volume for user setting ring");
38                self.userAvatarCircleTarget.classList.remove("ring-transparent");
39                self.userAvatarCircleTarget.classList.add("ring-blue-500");
40              } else {
41                self.userAvatarCircleTarget.classList.add("ring-transparent");
42                self.userAvatarCircleTarget.classList.remove("ring-blue-500");
43              }
44            } else {
45              console.log("0 Volume for user");
46              self.userAvatarCircleTarget.classList.add("ring-transparent");
47              self.userAvatarCircleTarget.classList.remove("ring-blue-500");
48            }
49          }
50        });
51      }
52    });
53    this.client.on("user-published", async (user, mediaType) => {
54      if (mediaType === "audio") {
55        console.log("User Published");
56        this.remoteTrack = await this.client.subscribe(user, mediaType);
57        console.log(user.audioTrack);
58        user?.audioTrack?.play();
59        console.log("Playing remote audio");
60      }
61    });
62    this.client.on("user-unpublished", async (user) => {
63      console.log("User unpublished");
64      await this.client.unsubscribe(user);
65    });
66    this.localTrack = null;
67    this.remoteTrack = null;
68    this.step = 0;
69    this.muted = true;
70    this.microphonePermissionGranted = false;
71    this.microphonePermissionChecked = false;
72    this.isBotSpeaking = false;
73    this.callStarted = false;
74    this.timer = setInterval(function() {
75      let totalSeconds = Math.ceil((Date.now() - window.gon.startTime) / 1000);
76      let seconds = 0;
77      let minutes = 0;
78      if (totalSeconds > 0) {
79        minutes = Math.floor(totalSeconds / 60);
80        seconds = totalSeconds % 60;
81      }
82      self.timerTarget.innerHTML = `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
83    }, 1000);
84  }
85  async trackError(error) {
86    await mentzaPost(`/calls/${window.gon.channel}/track-error`, {
87      error
88    });
89  }
90  async begin() {
91    this.beginButtonTarget.remove();
92    this.muteButtonTarget.classList.add("flex");
93    this.muteButtonTarget.classList.remove("hidden");
94    await fetch(`https://www.mentza.com/api/v1/mentza_circles/${window.gon.circle.id}/start_interview`, {
95      method: "POST",
96      headers: {
97        "Content-Type": "application/json"
98      },
99      body: JSON.stringify({})
100    });
101    this.setBotSpeaking();
102  }
103  setBotFinished() {
104    this.isBotSpeaking = false;
105    this.speakingAnimationTarget.classList.add("hidden");
106    this.muteIconTarget.classList.remove("hidden");
107    this.muteButtonTarget.disabled = false;
108    this.leftButtonTextTarget.innerText = "Unmute";
109    this.botAvatarCircleTarget.classList.remove("ring-blue-500");
110    this.botAvatarCircleTarget.classList.add("ring-transparent");
111  }
112  setBotSpeaking() {
113    this.unmuteIconTarget.classList.add("hidden");
114    this.muteIconTarget.classList.add("hidden");
115    this.muteButtonTarget.disabled = true;
116    this.speakingAnimationTarget.classList.remove("hidden");
117    this.leftButtonTextTarget.innerText = "Processing / Speaking";
118    this.isBotSpeaking = true;
119    this.botAvatarCircleTarget.classList.remove("ring-transparent");
120    this.botAvatarCircleTarget.classList.add("ring-blue-500");
121  }
122  async connectToCall() {
123    let self = this;
124    console.log("Connecting to call");
125    await fetch(`https://www.mentza.com/api/v1/mentza_circles/${window.gon.circle.id}/start_web_interview`, {
126      method: "POST",
127      headers: {
128        "Content-Type": "application/json"
129      },
130      body: JSON.stringify({})
131    });
132    try {
133      const channel = window.gon.channel;
134      const appId = window.gon.appId;
135      const token = window.gon.token;
136      const uid = await this.client.join(appId, channel, token, window.gon.uid);
137      console.log("connected as ", uid);
138      const self = this;
139      await this.client.setClientRole("host");
140      this.localTrack = await AgoraRTC.createMicrophoneAudioTrack();
141      this.localTrack.setMuted(true);
142      await this.client.publish([this.localTrack]);
143      this.client.enableAudioVolumeIndicator();
144      self.trackError(`Connected to Agora`);
145    } catch (error) {
146      self.trackError(`Error with connecting to call: ${error.toString()}`);
147      console.error("Failed to join call:", error);
148    }
149  }
150  disconnect() {
151    if (this.localTrack) {
152      this.localTrack.stop();
153      this.localTrack.close();
154      this.localTrack = null;
155    }
156    this.client.leave();
157  }
158  async endCall() {
159    const confirmEnd = window.confirm("Are you sure you want to end this call?");
160    if (!confirmEnd) {
161      return;
162    }
163    if (this.localTrack != null) {
164      await this.client.unpublish([this.localTrack]);
165    }
166    this.localTrack?.close();
167    await this.client.leave();
168    window.location.href = `/call-ended/${window.gon.circle.id}`;
169  }
170  mutePress() {
171    if (this.isBotSpeaking) {
172      return;
173    }
174    if (this.muted) {
175      this.muted = false;
176      this.setSpeak();
177    } else {
178      this.muted = true;
179      this.setMute();
180      this.setBotSpeaking();
181    }
182  }
183  setMute() {
184    this.localTrack?.setMuted(true);
185    this.unmuteIconTarget.classList.add("hidden");
186    this.muteIconTarget.classList.remove("hidden");
187  }
188  setSpeak() {
189    this.muteIconTarget.classList.add("hidden");
190    this.unmuteIconTarget.classList.remove("hidden");
191    this.leftButtonTextTarget.innerText = "Mute";
192    this.localTrack?.setMuted(false);
193  }
194  start() {
195    this.instructionsTarget.classList.add("hidden");
196    this.instructionsHeadingTarget.classList.add("hidden");
197    this.startButtonTarget.classList.add("hidden");
198    this.startStatusTarget.innerText = "Checking microphone permissions...";
199    this.checkMicPermission();
200    this.setupAudioProcessing();
201  }
202  sleep(ms) {
203    return new Promise((resolve) => setTimeout(resolve, ms));
204  }
205  async transitionFromStart() {
206    this.mainBoxTarget.classList.remove("max-w-md");
207    this.mainBoxTarget.classList.add("max-w-screen-md");
208    this.starterTarget.classList.add("hidden");
209    this.peopleTarget.classList.remove("hidden");
210    await this.sleep(200);
211    console.log("2");
212    this.peopleTarget.classList.remove("scale-0");
213    await this.sleep(200);
214    console.log("3");
215    this.peopleTarget.classList.remove("invisible");
216    this.peopleTarget.classList.remove("opacity-0");
217  }
218  async checkMicPermission() {
219    try {
220      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
221      this.startStatusTarget.innerText = "Microphone permission granted. Connecting to call. Please wait for a few seconds...";
222      stream.getTracks().forEach((track) => track.stop());
223      await this.connectToCall();
224      await this.sleep(1e4);
225      this.transitionFromStart();
226    } catch (error) {
227      if (error.name === "NotAllowedError") {
228        this.startStatusTarget.innerText = "Microphone permission denied. Please refresh and try again.";
229      } else if (error.name === "NotFoundError") {
230        this.startStatusTarget.innerText = "No microphone found. Please connect a microphone and try again.";
231      } else {
232        console.error("Error checking microphone permission:", error);
233        this.startStatusTarget.innerText = "Error accessing microphone. Please check your settings.";
234      }
235      this.trackError(`Error with checking mic permission: ${error.toString()}`);
236    }
237  }
238  async setupAudioProcessing() {
239    console.log("Audio Processing...");
240    const audioContext = new AudioContext;
241    await audioContext.audioWorklet.addModule("/processor.js");
242    const volumeNode = new AudioWorkletNode(audioContext, "volume-processor");
243    console.log("requesting...");
244    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
245    const source = audioContext.createMediaStreamSource(stream);
246    source.connect(volumeNode);
247    volumeNode.port.onmessage = (_) => {};
248  }
249  connect() {
250    console.log("Connecting....");
251    const consumer = createConsumer("/cable?token=" + window.gon.user_token);
252    console.log("Creating Call Channel");
253    let self = this;
254    consumer.subscriptions.create({
255      channel: "RoomChannel",
256      room: window.gon.channel,
257      token: window.gon.user_token
258    }, {
259      connected() {
260        console.log("Connected to Call Updates");
261      },
262      disconnected() {
263        console.log("Disconnected from Call Updates");
264      },
265      received(data) {
266        console.log(data);
267        if (data?.message?.event?.ai_speaking == false) {
268          self.setBotFinished();
269        }
270        if (data?.message?.event?.ai_speaking == true) {
271          self.setBotSpeaking();
272        }
273      }
274    });
275    setTimeout(function() {
276      self.startButtonTarget.innerText = "Connect to Session";
277      self.startButtonTarget.disabled = false;
278    }, 5000);
279  }
280}
281export {
282  ai_call_controller_default as default
283};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.