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.