|
338 | 338 | <p>Tap “Connect” to connect to the bridge and TeamTalk.</p> |
339 | 339 | </div> |
340 | 340 |
|
341 | | - <div id="input-area"> |
342 | | - <input id="chat-input" |
343 | | - type="text" |
344 | | - placeholder="Type your message…" |
345 | | - aria-label="Chat message" |
346 | | - onkeydown="if(event.key==='Enter'){ sendTeamTalkChat(); }"> |
347 | | - <button id="send-btn" |
348 | | - onclick="sendTeamTalkChat()"> |
349 | | - Send |
350 | | - </button> |
351 | | - </div> |
| 341 | + <div id="input-area"> |
| 342 | + <input id="chat-input" type="text" placeholder="Type a message…"> |
| 343 | + |
| 344 | + <button onclick="sendTeamTalkChat()">Send</button> |
| 345 | + |
| 346 | + <div id="mode-status" |
| 347 | + aria-live="polite" |
| 348 | + style="font-size: 0.9rem; margin-top: 0.25rem;"> |
| 349 | + Messages will be sent in the mode the system thinks is least effort. |
| 350 | + </div> |
| 351 | + |
| 352 | + <button id="toggle-speech-button" |
| 353 | + aria-pressed="false" |
| 354 | + onclick="toggleSpeech()" |
| 355 | + style="margin-top: 0.5rem;"> |
| 356 | + Toggle Speech |
| 357 | +</button> |
| 358 | + <button id="ptt-button" |
| 359 | + aria-pressed="false" |
| 360 | + style="margin-top: 0.5rem;"> |
| 361 | + Hold to talk |
| 362 | + </button> |
| 363 | +</div> |
352 | 364 |
|
353 | 365 | <div id="async-thread" style=" |
354 | 366 | display: none; |
@@ -433,7 +445,168 @@ <h2 id="users-heading">Users</h2> |
433 | 445 | <script src="bridge.js"></script> |
434 | 446 |
|
435 | 447 | <script> |
436 | | -window.addEventListener("DOMContentLoaded", () => { |
| 448 | + |
| 449 | + <script> |
| 450 | + // ------------------------------- |
| 451 | + // Text send with automation |
| 452 | + // ------------------------------- |
| 453 | + window.sendTeamTalkChat = function () { |
| 454 | + const input = document.getElementById("chat-input"); |
| 455 | + if (!input) return; |
| 456 | + const text = input.value.trim(); |
| 457 | + if (!text) return; |
| 458 | + |
| 459 | + const modeUsed = bridge.autoSend(text); |
| 460 | + |
| 461 | + const modeStatus = document.getElementById("mode-status"); |
| 462 | + if (modeStatus) { |
| 463 | + modeStatus.textContent = |
| 464 | + modeUsed === "live" |
| 465 | + ? "Sent as live message" |
| 466 | + : "Sent as async message"; |
| 467 | + } |
| 468 | + |
| 469 | + input.value = ""; |
| 470 | + }; |
| 471 | + |
| 472 | + // ------------------------------- |
| 473 | + // Render live and async streams |
| 474 | + // ------------------------------- |
| 475 | + bridge.on("chat-message", (msg) => { |
| 476 | + const chat = document.getElementById("chat"); |
| 477 | + if (!chat) return; |
| 478 | + const p = document.createElement("p"); |
| 479 | + p.textContent = `${msg.user}: ${msg.text}`; |
| 480 | + chat.appendChild(p); |
| 481 | + chat.scrollTop = chat.scrollHeight; |
| 482 | + }); |
| 483 | + |
| 484 | + bridge.on("async-message", (msg) => { |
| 485 | + const asyncThread = document.getElementById("async-thread"); |
| 486 | + if (!asyncThread) return; |
| 487 | + const div = document.createElement("div"); |
| 488 | + const time = new Date(msg.timestamp || Date.now()).toLocaleTimeString(); |
| 489 | + div.textContent = `${time} — ${msg.text}`; |
| 490 | + asyncThread.appendChild(div); |
| 491 | + asyncThread.scrollTop = asyncThread.scrollHeight; |
| 492 | + }); |
| 493 | + |
| 494 | + // ------------------------------- |
| 495 | + // Dwell helper for symbols |
| 496 | + // ------------------------------- |
| 497 | + function attachDwell(element, symbolId, label) { |
| 498 | + let dwellTimer = null; |
| 499 | + const DWELL_MS = 800; // tune this |
| 500 | + |
| 501 | + element.addEventListener("pointerdown", () => { |
| 502 | + dwellTimer = setTimeout(() => { |
| 503 | + bridge.sendDwellSymbol(symbolId, label); |
| 504 | + }, DWELL_MS); |
| 505 | + }); |
| 506 | + |
| 507 | + const cancel = () => { |
| 508 | + if (dwellTimer) { |
| 509 | + clearTimeout(dwellTimer); |
| 510 | + dwellTimer = null; |
| 511 | + } |
| 512 | + }; |
| 513 | + |
| 514 | + element.addEventListener("pointerup", cancel); |
| 515 | + element.addEventListener("pointerleave", cancel); |
| 516 | + } |
| 517 | + |
| 518 | + // You can call attachDwell(buttonEl, symbolId, label) when creating symbol buttons. |
| 519 | + |
| 520 | + // ------------------------------- |
| 521 | + // Mic capture + PTT for live audio |
| 522 | + // ------------------------------- |
| 523 | + let mediaStream = null; |
| 524 | + let mediaRecorder = null; |
| 525 | + let isRecording = false; |
| 526 | + |
| 527 | + async function initMic() { |
| 528 | + if (mediaStream) return; |
| 529 | + try { |
| 530 | + mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true }); |
| 531 | + } catch (err) { |
| 532 | + console.error("Mic error:", err); |
| 533 | + alert("Could not access microphone."); |
| 534 | + } |
| 535 | + } |
| 536 | + |
| 537 | + function startLiveAudio() { |
| 538 | + if (isRecording) return; |
| 539 | + if (!mediaStream) { |
| 540 | + initMic().then(() => { |
| 541 | + if (mediaStream) startLiveAudio(); |
| 542 | + }); |
| 543 | + return; |
| 544 | + } |
| 545 | + |
| 546 | + mediaRecorder = new MediaRecorder(mediaStream, { |
| 547 | + mimeType: "audio/webm;codecs=opus" |
| 548 | + }); |
| 549 | + |
| 550 | + mediaRecorder.ondataavailable = (e) => { |
| 551 | + if (!e.data || !e.data.size) return; |
| 552 | + const reader = new FileReader(); |
| 553 | + reader.onloadend = () => { |
| 554 | + const base64Data = btoa( |
| 555 | + String.fromCharCode(...new Uint8Array(reader.result)) |
| 556 | + ); |
| 557 | + bridge.sendLiveAudioChunk(base64Data); |
| 558 | + }; |
| 559 | + reader.readAsArrayBuffer(e.data); |
| 560 | + }; |
| 561 | + |
| 562 | + mediaRecorder.start(250); // send chunk every 250ms |
| 563 | + bridge.startLiveAudioSession(); |
| 564 | + isRecording = true; |
| 565 | + } |
| 566 | + |
| 567 | + function stopLiveAudio() { |
| 568 | + if (!isRecording) return; |
| 569 | + if (mediaRecorder && mediaRecorder.state !== "inactive") { |
| 570 | + mediaRecorder.stop(); |
| 571 | + } |
| 572 | + bridge.stopLiveAudioSession(); |
| 573 | + isRecording = false; |
| 574 | + } |
| 575 | + |
| 576 | + const pttButton = document.getElementById("ptt-button"); |
| 577 | + if (pttButton) { |
| 578 | + pttButton.addEventListener("pointerdown", () => { |
| 579 | + pttButton.setAttribute("aria-pressed", "true"); |
| 580 | + startLiveAudio(); |
| 581 | + }); |
| 582 | + |
| 583 | + const stop = () => { |
| 584 | + pttButton.setAttribute("aria-pressed", "false"); |
| 585 | + stopLiveAudio(); |
| 586 | + }; |
| 587 | + |
| 588 | + pttButton.addEventListener("pointerup", stop); |
| 589 | + pttButton.addEventListener("pointerleave", stop); |
| 590 | + let isSpeechToggled = false; |
| 591 | + |
| 592 | +function toggleSpeech() { |
| 593 | + if (!isSpeechToggled) { |
| 594 | + // Turn ON continuous transmit |
| 595 | + isSpeechToggled = true; |
| 596 | + startLiveAudio(); |
| 597 | + const btn = document.getElementById("toggle-speech-button"); |
| 598 | + if (btn) btn.setAttribute("aria-pressed", "true"); |
| 599 | + } else { |
| 600 | + // Turn OFF continuous transmit |
| 601 | + isSpeechToggled = false; |
| 602 | + stopLiveAudio(); |
| 603 | + const btn = document.getElementById("toggle-speech-button"); |
| 604 | + if (btn) btn.setAttribute("aria-pressed", "false"); |
| 605 | + } |
| 606 | +} |
| 607 | +</script> |
| 608 | + |
| 609 | + window.addEventListener("DOMContentLoaded", () => { |
437 | 610 |
|
438 | 611 | /* --------------------------------------------------------- |
439 | 612 | NICKNAME RESOLUTION MODULE |
|
0 commit comments