bosun 0.37.0 → 0.37.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.
- package/.env.example +4 -1
- package/agent-tool-config.mjs +338 -0
- package/bosun-skills.mjs +59 -4
- package/bosun.schema.json +1 -1
- package/desktop/launch.mjs +18 -0
- package/desktop/main.mjs +52 -13
- package/fleet-coordinator.mjs +34 -1
- package/kanban-adapter.mjs +30 -3
- package/library-manager.mjs +66 -0
- package/maintenance.mjs +30 -5
- package/monitor.mjs +56 -0
- package/package.json +4 -1
- package/setup-web-server.mjs +73 -12
- package/setup.mjs +3 -3
- package/ui/app.js +40 -3
- package/ui/components/session-list.js +25 -7
- package/ui/components/workspace-switcher.js +48 -1
- package/ui/demo.html +176 -0
- package/ui/modules/mic-track-registry.js +83 -0
- package/ui/modules/settings-schema.js +4 -1
- package/ui/modules/state.js +25 -0
- package/ui/modules/streaming.js +1 -1
- package/ui/modules/voice-barge-in.js +27 -0
- package/ui/modules/voice-client-sdk.js +268 -42
- package/ui/modules/voice-client.js +665 -61
- package/ui/modules/voice-overlay.js +829 -47
- package/ui/setup.html +151 -9
- package/ui/styles.css +258 -0
- package/ui/tabs/chat.js +11 -0
- package/ui/tabs/library.js +890 -15
- package/ui/tabs/settings.js +51 -11
- package/ui/tabs/telemetry.js +327 -105
- package/ui/tabs/workflows.js +86 -0
- package/ui-server.mjs +1201 -107
- package/voice-action-dispatcher.mjs +81 -0
- package/voice-agents-sdk.mjs +2 -2
- package/voice-relay.mjs +131 -14
- package/voice-tools.mjs +475 -9
- package/workflow-engine.mjs +54 -0
- package/workflow-nodes.mjs +177 -28
- package/workflow-templates/github.mjs +205 -94
- package/workflow-templates/task-batch.mjs +247 -0
- package/workflow-templates.mjs +15 -0
|
@@ -19,12 +19,15 @@ import {
|
|
|
19
19
|
voiceToolCalls, voiceDuration, isVoiceMicMuted,
|
|
20
20
|
startVoiceSession, stopVoiceSession, interruptResponse,
|
|
21
21
|
sendTextMessage, sendImageFrame, onVoiceEvent, resumeVoiceAudio, toggleMicMute,
|
|
22
|
+
audioInputDevices, audioOutputDevices, selectedAudioInput, selectedAudioOutput,
|
|
23
|
+
micInputLevel, audioSettings,
|
|
24
|
+
enumerateAudioDevices, switchAudioInput, switchAudioOutput, updateAudioSettings,
|
|
22
25
|
} from "./voice-client.js";
|
|
23
26
|
import {
|
|
24
27
|
sdkVoiceState, sdkVoiceTranscript, sdkVoiceResponse, sdkVoiceError,
|
|
25
28
|
sdkVoiceToolCalls, sdkVoiceDuration, sdkVoiceSdkActive,
|
|
26
29
|
startSdkVoiceSession, stopSdkVoiceSession, interruptSdkResponse,
|
|
27
|
-
sendSdkTextMessage, sendSdkImageFrame, onSdkVoiceEvent,
|
|
30
|
+
sendSdkTextMessage, sendSdkImageFrame, onSdkVoiceEvent, toggleSdkMicMute,
|
|
28
31
|
} from "./voice-client-sdk.js";
|
|
29
32
|
import {
|
|
30
33
|
fallbackState, fallbackTranscript, fallbackResponse,
|
|
@@ -561,6 +564,319 @@ function injectOverlayStyles() {
|
|
|
561
564
|
}
|
|
562
565
|
.voice-overlay-chat-live.user { border-color: rgba(138,180,248,0.4); }
|
|
563
566
|
.voice-overlay-chat-live.assistant { border-color: rgba(129,201,149,0.4); }
|
|
567
|
+
|
|
568
|
+
/* ── Device Picker Dropdown ────────────────────────────────────── */
|
|
569
|
+
.vm-device-picker-anchor {
|
|
570
|
+
position: relative;
|
|
571
|
+
display: inline-flex;
|
|
572
|
+
}
|
|
573
|
+
.vm-device-picker {
|
|
574
|
+
position: absolute;
|
|
575
|
+
bottom: calc(100% + 8px);
|
|
576
|
+
left: 50%;
|
|
577
|
+
transform: translateX(-50%);
|
|
578
|
+
min-width: 320px;
|
|
579
|
+
max-width: 400px;
|
|
580
|
+
background: #2d2e30;
|
|
581
|
+
border: 1px solid rgba(255,255,255,0.14);
|
|
582
|
+
border-radius: 12px;
|
|
583
|
+
box-shadow: 0 12px 48px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.06);
|
|
584
|
+
z-index: 100;
|
|
585
|
+
overflow: hidden;
|
|
586
|
+
animation: vmPickerSlideUp 0.18s ease;
|
|
587
|
+
max-height: 60vh;
|
|
588
|
+
overflow-y: auto;
|
|
589
|
+
}
|
|
590
|
+
@keyframes vmPickerSlideUp {
|
|
591
|
+
from { opacity: 0; transform: translateX(-50%) translateY(8px); }
|
|
592
|
+
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
593
|
+
}
|
|
594
|
+
.vm-device-picker-section {
|
|
595
|
+
padding: 6px 0;
|
|
596
|
+
border-bottom: 1px solid rgba(255,255,255,0.08);
|
|
597
|
+
}
|
|
598
|
+
.vm-device-picker-section:last-child { border-bottom: none; }
|
|
599
|
+
.vm-device-picker-label {
|
|
600
|
+
font-size: 11px;
|
|
601
|
+
font-weight: 600;
|
|
602
|
+
text-transform: uppercase;
|
|
603
|
+
letter-spacing: 0.04em;
|
|
604
|
+
color: rgba(255,255,255,0.5);
|
|
605
|
+
padding: 6px 16px 4px;
|
|
606
|
+
}
|
|
607
|
+
.vm-device-item {
|
|
608
|
+
display: flex;
|
|
609
|
+
align-items: center;
|
|
610
|
+
gap: 10px;
|
|
611
|
+
padding: 8px 16px;
|
|
612
|
+
cursor: pointer;
|
|
613
|
+
transition: background 0.1s;
|
|
614
|
+
border: none;
|
|
615
|
+
background: none;
|
|
616
|
+
color: #e8eaed;
|
|
617
|
+
font-size: 13px;
|
|
618
|
+
width: 100%;
|
|
619
|
+
text-align: left;
|
|
620
|
+
}
|
|
621
|
+
.vm-device-item:hover { background: rgba(255,255,255,0.06); }
|
|
622
|
+
.vm-device-item.active { color: #8ab4f8; }
|
|
623
|
+
.vm-device-check {
|
|
624
|
+
width: 20px;
|
|
625
|
+
height: 20px;
|
|
626
|
+
border-radius: 50%;
|
|
627
|
+
display: flex;
|
|
628
|
+
align-items: center;
|
|
629
|
+
justify-content: center;
|
|
630
|
+
flex-shrink: 0;
|
|
631
|
+
font-size: 14px;
|
|
632
|
+
}
|
|
633
|
+
.vm-device-check.selected {
|
|
634
|
+
background: #8ab4f8;
|
|
635
|
+
color: #202124;
|
|
636
|
+
}
|
|
637
|
+
.vm-device-name {
|
|
638
|
+
flex: 1;
|
|
639
|
+
min-width: 0;
|
|
640
|
+
overflow: hidden;
|
|
641
|
+
text-overflow: ellipsis;
|
|
642
|
+
white-space: nowrap;
|
|
643
|
+
}
|
|
644
|
+
.vm-device-default {
|
|
645
|
+
font-size: 11px;
|
|
646
|
+
color: rgba(255,255,255,0.45);
|
|
647
|
+
margin-top: 1px;
|
|
648
|
+
}
|
|
649
|
+
.vm-mic-level-bar {
|
|
650
|
+
height: 4px;
|
|
651
|
+
border-radius: 2px;
|
|
652
|
+
background: #3c4043;
|
|
653
|
+
overflow: hidden;
|
|
654
|
+
margin: 4px 16px 8px;
|
|
655
|
+
}
|
|
656
|
+
.vm-mic-level-fill {
|
|
657
|
+
height: 100%;
|
|
658
|
+
border-radius: 2px;
|
|
659
|
+
background: #8ab4f8;
|
|
660
|
+
transition: width 0.1s linear;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/* ── Settings Panel ─────────────────────────────────────────────── */
|
|
664
|
+
.vm-settings-overlay {
|
|
665
|
+
position: absolute;
|
|
666
|
+
inset: 0;
|
|
667
|
+
background: rgba(0,0,0,0.6);
|
|
668
|
+
z-index: 50;
|
|
669
|
+
display: flex;
|
|
670
|
+
align-items: center;
|
|
671
|
+
justify-content: center;
|
|
672
|
+
animation: voiceOverlayFadeIn 0.15s ease;
|
|
673
|
+
}
|
|
674
|
+
.vm-settings-panel {
|
|
675
|
+
width: min(500px, 90vw);
|
|
676
|
+
max-height: 80vh;
|
|
677
|
+
background: #2d2e30;
|
|
678
|
+
border-radius: 16px;
|
|
679
|
+
box-shadow: 0 16px 60px rgba(0,0,0,0.6);
|
|
680
|
+
overflow-y: auto;
|
|
681
|
+
animation: vmPickerSlideUp 0.2s ease;
|
|
682
|
+
}
|
|
683
|
+
.vm-settings-header {
|
|
684
|
+
display: flex;
|
|
685
|
+
align-items: center;
|
|
686
|
+
justify-content: space-between;
|
|
687
|
+
padding: 16px 20px 12px;
|
|
688
|
+
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
689
|
+
}
|
|
690
|
+
.vm-settings-title {
|
|
691
|
+
font-size: 16px;
|
|
692
|
+
font-weight: 600;
|
|
693
|
+
color: #e8eaed;
|
|
694
|
+
}
|
|
695
|
+
.vm-settings-close {
|
|
696
|
+
width: 32px; height: 32px; border-radius: 50%;
|
|
697
|
+
border: none; background: rgba(255,255,255,0.08);
|
|
698
|
+
color: #e8eaed; font-size: 16px; cursor: pointer;
|
|
699
|
+
display: flex; align-items: center; justify-content: center;
|
|
700
|
+
transition: background 0.15s;
|
|
701
|
+
}
|
|
702
|
+
.vm-settings-close:hover { background: rgba(255,255,255,0.14); }
|
|
703
|
+
.vm-settings-body { padding: 12px 20px 20px; }
|
|
704
|
+
.vm-settings-section {
|
|
705
|
+
margin-bottom: 16px;
|
|
706
|
+
}
|
|
707
|
+
.vm-settings-section-title {
|
|
708
|
+
font-size: 13px;
|
|
709
|
+
font-weight: 600;
|
|
710
|
+
color: rgba(255,255,255,0.85);
|
|
711
|
+
margin-bottom: 8px;
|
|
712
|
+
display: flex;
|
|
713
|
+
align-items: center;
|
|
714
|
+
gap: 8px;
|
|
715
|
+
}
|
|
716
|
+
.vm-settings-row {
|
|
717
|
+
display: flex;
|
|
718
|
+
align-items: center;
|
|
719
|
+
justify-content: space-between;
|
|
720
|
+
padding: 8px 0;
|
|
721
|
+
gap: 12px;
|
|
722
|
+
}
|
|
723
|
+
.vm-settings-row-info { flex: 1; min-width: 0; }
|
|
724
|
+
.vm-settings-row-label {
|
|
725
|
+
font-size: 13px;
|
|
726
|
+
color: #e8eaed;
|
|
727
|
+
}
|
|
728
|
+
.vm-settings-row-desc {
|
|
729
|
+
font-size: 11px;
|
|
730
|
+
color: rgba(255,255,255,0.5);
|
|
731
|
+
margin-top: 2px;
|
|
732
|
+
}
|
|
733
|
+
.vm-settings-toggle {
|
|
734
|
+
position: relative;
|
|
735
|
+
width: 44px;
|
|
736
|
+
height: 24px;
|
|
737
|
+
border-radius: 12px;
|
|
738
|
+
border: none;
|
|
739
|
+
background: #5f6368;
|
|
740
|
+
cursor: pointer;
|
|
741
|
+
flex-shrink: 0;
|
|
742
|
+
transition: background 0.2s;
|
|
743
|
+
}
|
|
744
|
+
.vm-settings-toggle.on { background: #8ab4f8; }
|
|
745
|
+
.vm-settings-toggle::after {
|
|
746
|
+
content: '';
|
|
747
|
+
position: absolute;
|
|
748
|
+
top: 3px;
|
|
749
|
+
left: 3px;
|
|
750
|
+
width: 18px;
|
|
751
|
+
height: 18px;
|
|
752
|
+
border-radius: 50%;
|
|
753
|
+
background: #fff;
|
|
754
|
+
transition: transform 0.2s;
|
|
755
|
+
}
|
|
756
|
+
.vm-settings-toggle.on::after { transform: translateX(20px); }
|
|
757
|
+
.vm-settings-select {
|
|
758
|
+
background: #3c4043;
|
|
759
|
+
border: 1px solid rgba(255,255,255,0.15);
|
|
760
|
+
border-radius: 8px;
|
|
761
|
+
color: #e8eaed;
|
|
762
|
+
padding: 6px 10px;
|
|
763
|
+
font-size: 13px;
|
|
764
|
+
min-width: 160px;
|
|
765
|
+
cursor: pointer;
|
|
766
|
+
}
|
|
767
|
+
.vm-settings-select:focus { outline: none; border-color: #8ab4f8; }
|
|
768
|
+
|
|
769
|
+
/* ── Three-dot menu ─────────────────────────────────────────────── */
|
|
770
|
+
.vm-more-menu {
|
|
771
|
+
position: absolute;
|
|
772
|
+
bottom: calc(100% + 8px);
|
|
773
|
+
right: 0;
|
|
774
|
+
min-width: 260px;
|
|
775
|
+
background: #2d2e30;
|
|
776
|
+
border: 1px solid rgba(255,255,255,0.14);
|
|
777
|
+
border-radius: 12px;
|
|
778
|
+
box-shadow: 0 12px 48px rgba(0,0,0,0.7);
|
|
779
|
+
z-index: 100;
|
|
780
|
+
overflow: hidden;
|
|
781
|
+
animation: vmPickerSlideUp 0.18s ease;
|
|
782
|
+
padding: 6px 0;
|
|
783
|
+
}
|
|
784
|
+
.vm-more-item {
|
|
785
|
+
display: flex;
|
|
786
|
+
align-items: center;
|
|
787
|
+
gap: 14px;
|
|
788
|
+
padding: 10px 16px;
|
|
789
|
+
cursor: pointer;
|
|
790
|
+
border: none;
|
|
791
|
+
background: none;
|
|
792
|
+
color: #e8eaed;
|
|
793
|
+
font-size: 13px;
|
|
794
|
+
width: 100%;
|
|
795
|
+
text-align: left;
|
|
796
|
+
transition: background 0.1s;
|
|
797
|
+
}
|
|
798
|
+
.vm-more-item:hover { background: rgba(255,255,255,0.06); }
|
|
799
|
+
.vm-more-item.disabled {
|
|
800
|
+
opacity: 0.4;
|
|
801
|
+
cursor: not-allowed;
|
|
802
|
+
}
|
|
803
|
+
.vm-more-icon {
|
|
804
|
+
width: 20px;
|
|
805
|
+
text-align: center;
|
|
806
|
+
font-size: 16px;
|
|
807
|
+
flex-shrink: 0;
|
|
808
|
+
}
|
|
809
|
+
.vm-more-divider {
|
|
810
|
+
height: 1px;
|
|
811
|
+
background: rgba(255,255,255,0.08);
|
|
812
|
+
margin: 4px 0;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/* ── Improved split-button for mic/speaker arrows ────────────────── */
|
|
816
|
+
.vm-btn-with-arrow {
|
|
817
|
+
display: flex;
|
|
818
|
+
align-items: stretch;
|
|
819
|
+
gap: 0;
|
|
820
|
+
}
|
|
821
|
+
.vm-btn-arrow {
|
|
822
|
+
width: 24px;
|
|
823
|
+
height: 48px;
|
|
824
|
+
border-radius: 0 24px 24px 0;
|
|
825
|
+
border: none;
|
|
826
|
+
background: #3c4043;
|
|
827
|
+
color: rgba(255,255,255,0.7);
|
|
828
|
+
cursor: pointer;
|
|
829
|
+
font-size: 10px;
|
|
830
|
+
display: flex;
|
|
831
|
+
align-items: center;
|
|
832
|
+
justify-content: center;
|
|
833
|
+
border-left: 1px solid rgba(255,255,255,0.08);
|
|
834
|
+
transition: background 0.15s;
|
|
835
|
+
}
|
|
836
|
+
.vm-btn-arrow:hover { background: #5f6368; }
|
|
837
|
+
.vm-btn.has-arrow {
|
|
838
|
+
border-radius: 24px 0 0 24px;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/* ── Bar device chips (below buttons like Meet) ─────────────────── */
|
|
842
|
+
.vm-bar-device-chips {
|
|
843
|
+
position: absolute;
|
|
844
|
+
bottom: 4px;
|
|
845
|
+
left: 0; right: 0;
|
|
846
|
+
display: flex;
|
|
847
|
+
align-items: center;
|
|
848
|
+
justify-content: center;
|
|
849
|
+
gap: 12px;
|
|
850
|
+
padding: 0 20px;
|
|
851
|
+
z-index: 2;
|
|
852
|
+
pointer-events: none;
|
|
853
|
+
}
|
|
854
|
+
.vm-bar-device-chip {
|
|
855
|
+
pointer-events: auto;
|
|
856
|
+
display: flex;
|
|
857
|
+
align-items: center;
|
|
858
|
+
gap: 6px;
|
|
859
|
+
background: rgba(60,64,67,0.85);
|
|
860
|
+
border: 1px solid rgba(255,255,255,0.12);
|
|
861
|
+
border-radius: 24px;
|
|
862
|
+
padding: 4px 12px 4px 8px;
|
|
863
|
+
color: rgba(255,255,255,0.8);
|
|
864
|
+
font-size: 11px;
|
|
865
|
+
cursor: pointer;
|
|
866
|
+
transition: background 0.15s, border-color 0.15s;
|
|
867
|
+
max-width: 220px;
|
|
868
|
+
overflow: hidden;
|
|
869
|
+
text-overflow: ellipsis;
|
|
870
|
+
white-space: nowrap;
|
|
871
|
+
}
|
|
872
|
+
.vm-bar-device-chip:hover {
|
|
873
|
+
background: rgba(95,99,104,0.92);
|
|
874
|
+
border-color: rgba(255,255,255,0.22);
|
|
875
|
+
}
|
|
876
|
+
.vm-bar-device-chip-icon {
|
|
877
|
+
font-size: 14px;
|
|
878
|
+
flex-shrink: 0;
|
|
879
|
+
}
|
|
564
880
|
`;
|
|
565
881
|
document.head.appendChild(style);
|
|
566
882
|
}
|
|
@@ -878,6 +1194,8 @@ function mergeFragmentedMeetingFeedMessages(items) {
|
|
|
878
1194
|
* executor?: string,
|
|
879
1195
|
* mode?: string,
|
|
880
1196
|
* model?: string,
|
|
1197
|
+
* voiceAgentId?: string,
|
|
1198
|
+
* onVoiceAgentChange?: (voiceAgentId: string) => void,
|
|
881
1199
|
* callType?: "voice" | "video",
|
|
882
1200
|
* initialVisionSource?: "camera" | "screen" | null
|
|
883
1201
|
* }} props
|
|
@@ -892,6 +1210,8 @@ export function VoiceOverlay({
|
|
|
892
1210
|
executor,
|
|
893
1211
|
mode,
|
|
894
1212
|
model,
|
|
1213
|
+
voiceAgentId,
|
|
1214
|
+
onVoiceAgentChange,
|
|
895
1215
|
callType = "voice",
|
|
896
1216
|
initialVisionSource = null,
|
|
897
1217
|
}) {
|
|
@@ -915,9 +1235,81 @@ export function VoiceOverlay({
|
|
|
915
1235
|
const sdkFallbackCleanupRef = useRef(null);
|
|
916
1236
|
const legacyFallbackCleanupRef = useRef(null);
|
|
917
1237
|
const autoFallbackTriedRef = useRef(false);
|
|
1238
|
+
const [showMicPicker, setShowMicPicker] = useState(false);
|
|
1239
|
+
const [showSpeakerPicker, setShowSpeakerPicker] = useState(false);
|
|
1240
|
+
const [showMoreMenu, setShowMoreMenu] = useState(false);
|
|
1241
|
+
const [showPeoplePanel, setShowPeoplePanel] = useState(false);
|
|
1242
|
+
const [showSettings, setShowSettings] = useState(false);
|
|
1243
|
+
const [voiceAgents, setVoiceAgents] = useState([]);
|
|
1244
|
+
const [loadingVoiceAgents, setLoadingVoiceAgents] = useState(false);
|
|
1245
|
+
const [selectedVoiceAgentId, setSelectedVoiceAgentId] = useState(
|
|
1246
|
+
String(voiceAgentId || "").trim() || "",
|
|
1247
|
+
);
|
|
1248
|
+
const [startRequested, setStartRequested] = useState(false);
|
|
1249
|
+
const [switchingVoiceAgent, setSwitchingVoiceAgent] = useState(false);
|
|
1250
|
+
const preserveSessionOnHideRef = useRef(false);
|
|
918
1251
|
|
|
919
1252
|
useEffect(() => { injectOverlayStyles(); }, []);
|
|
920
1253
|
|
|
1254
|
+
useEffect(() => {
|
|
1255
|
+
const incoming = String(voiceAgentId || "").trim();
|
|
1256
|
+
if (!incoming) return;
|
|
1257
|
+
setSelectedVoiceAgentId(incoming);
|
|
1258
|
+
}, [voiceAgentId]);
|
|
1259
|
+
|
|
1260
|
+
useEffect(() => {
|
|
1261
|
+
if (!visible) return;
|
|
1262
|
+
let cancelled = false;
|
|
1263
|
+
setLoadingVoiceAgents(true);
|
|
1264
|
+
(async () => {
|
|
1265
|
+
try {
|
|
1266
|
+
const params = new URLSearchParams();
|
|
1267
|
+
if (sessionId) params.set("sessionId", String(sessionId));
|
|
1268
|
+
if (voiceAgentId) params.set("voiceAgentId", String(voiceAgentId));
|
|
1269
|
+
const res = await apiFetch(`/api/voice/agents?${params.toString()}`, { _silent: true });
|
|
1270
|
+
if (cancelled) return;
|
|
1271
|
+
const agents = Array.isArray(res?.agents) ? res.agents : [];
|
|
1272
|
+
setVoiceAgents(agents);
|
|
1273
|
+
const fallbackId =
|
|
1274
|
+
String(voiceAgentId || "").trim()
|
|
1275
|
+
|| String(res?.defaultAgentId || "").trim()
|
|
1276
|
+
|| String(agents[0]?.id || "").trim()
|
|
1277
|
+
|| "";
|
|
1278
|
+
setSelectedVoiceAgentId(fallbackId);
|
|
1279
|
+
if (typeof onVoiceAgentChange === "function" && fallbackId) {
|
|
1280
|
+
onVoiceAgentChange(fallbackId);
|
|
1281
|
+
}
|
|
1282
|
+
} catch {
|
|
1283
|
+
if (!cancelled) {
|
|
1284
|
+
setVoiceAgents([]);
|
|
1285
|
+
if (!selectedVoiceAgentId) setSelectedVoiceAgentId("voice-agent");
|
|
1286
|
+
}
|
|
1287
|
+
} finally {
|
|
1288
|
+
if (!cancelled) setLoadingVoiceAgents(false);
|
|
1289
|
+
}
|
|
1290
|
+
})();
|
|
1291
|
+
return () => {
|
|
1292
|
+
cancelled = true;
|
|
1293
|
+
};
|
|
1294
|
+
}, [visible, sessionId, voiceAgentId, onVoiceAgentChange]);
|
|
1295
|
+
|
|
1296
|
+
useEffect(() => {
|
|
1297
|
+
if (visible) {
|
|
1298
|
+
preserveSessionOnHideRef.current = false;
|
|
1299
|
+
}
|
|
1300
|
+
if (visible) return;
|
|
1301
|
+
setStartRequested(false);
|
|
1302
|
+
setSwitchingVoiceAgent(false);
|
|
1303
|
+
}, [visible]);
|
|
1304
|
+
|
|
1305
|
+
// Close popups on outside click
|
|
1306
|
+
useEffect(() => {
|
|
1307
|
+
if (!visible) return;
|
|
1308
|
+
const handler = () => { setShowMicPicker(false); setShowSpeakerPicker(false); setShowMoreMenu(false); setShowPeoplePanel(false); };
|
|
1309
|
+
document.addEventListener("click", handler);
|
|
1310
|
+
return () => document.removeEventListener("click", handler);
|
|
1311
|
+
}, [visible]);
|
|
1312
|
+
|
|
921
1313
|
// Determine effective tier: SDK takes over tier 1 when active
|
|
922
1314
|
const effectiveSdk = usingSdk && sdkVoiceSdkActive.value;
|
|
923
1315
|
|
|
@@ -961,7 +1353,7 @@ export function VoiceOverlay({
|
|
|
961
1353
|
|
|
962
1354
|
// Start session on mount — try Agents SDK first, fallback to legacy
|
|
963
1355
|
useEffect(() => {
|
|
964
|
-
if (!visible || started) return;
|
|
1356
|
+
if (!visible || started || !startRequested) return;
|
|
965
1357
|
setStarted(true);
|
|
966
1358
|
autoFallbackTriedRef.current = false;
|
|
967
1359
|
let legacyFallbackStarted = false;
|
|
@@ -970,12 +1362,12 @@ export function VoiceOverlay({
|
|
|
970
1362
|
if (legacyFallbackStarted) return;
|
|
971
1363
|
legacyFallbackStarted = true;
|
|
972
1364
|
setUsingSdk(false);
|
|
973
|
-
startVoiceSession({ sessionId, executor, mode, model });
|
|
1365
|
+
startVoiceSession({ sessionId, executor, mode, model, voiceAgentId: selectedVoiceAgentId || undefined });
|
|
974
1366
|
};
|
|
975
1367
|
|
|
976
1368
|
if (tier === 1) {
|
|
977
1369
|
// Try SDK-first for tier 1
|
|
978
|
-
startSdkVoiceSession({ sessionId, executor, mode, model })
|
|
1370
|
+
startSdkVoiceSession({ sessionId, executor, mode, model, voiceAgentId: selectedVoiceAgentId || undefined })
|
|
979
1371
|
.then((result) => {
|
|
980
1372
|
if (result.sdk) {
|
|
981
1373
|
setUsingSdk(true);
|
|
@@ -995,9 +1387,9 @@ export function VoiceOverlay({
|
|
|
995
1387
|
});
|
|
996
1388
|
sdkFallbackCleanupRef.current = cleanup;
|
|
997
1389
|
} else if (sessionId) {
|
|
998
|
-
startFallbackSession(sessionId, { executor, mode, model });
|
|
1390
|
+
startFallbackSession(sessionId, { executor, mode, model, voiceAgentId: selectedVoiceAgentId || undefined });
|
|
999
1391
|
}
|
|
1000
|
-
}, [visible, started, tier, sessionId, executor, mode, model]);
|
|
1392
|
+
}, [visible, started, startRequested, tier, sessionId, executor, mode, model, selectedVoiceAgentId]);
|
|
1001
1393
|
|
|
1002
1394
|
useEffect(() => {
|
|
1003
1395
|
if (!visible || !started || tier !== 1 || !sessionId) return;
|
|
@@ -1006,11 +1398,16 @@ export function VoiceOverlay({
|
|
|
1006
1398
|
if (autoFallbackTriedRef.current) return;
|
|
1007
1399
|
autoFallbackTriedRef.current = true;
|
|
1008
1400
|
try { stopVoiceSession(); } catch { /* best effort */ }
|
|
1009
|
-
startFallbackSession(sessionId, {
|
|
1401
|
+
startFallbackSession(sessionId, {
|
|
1402
|
+
executor,
|
|
1403
|
+
mode,
|
|
1404
|
+
model,
|
|
1405
|
+
voiceAgentId: selectedVoiceAgentId || undefined,
|
|
1406
|
+
});
|
|
1010
1407
|
});
|
|
1011
1408
|
legacyFallbackCleanupRef.current = cleanup;
|
|
1012
1409
|
return cleanup;
|
|
1013
|
-
}, [visible, started, tier, sessionId, executor, mode, model, usingSdk]);
|
|
1410
|
+
}, [visible, started, tier, sessionId, executor, mode, model, usingSdk, selectedVoiceAgentId]);
|
|
1014
1411
|
|
|
1015
1412
|
useEffect(() => {
|
|
1016
1413
|
if (visible) return;
|
|
@@ -1022,8 +1419,17 @@ export function VoiceOverlay({
|
|
|
1022
1419
|
autoVisionAppliedRef.current = false;
|
|
1023
1420
|
}, [visible]);
|
|
1024
1421
|
|
|
1422
|
+
const stopAllVoiceTransports = useCallback(() => {
|
|
1423
|
+
try { stopSdkVoiceSession(); } catch { /* best effort */ }
|
|
1424
|
+
try { stopVoiceSession(); } catch { /* best effort */ }
|
|
1425
|
+
try { stopFallbackSession(); } catch { /* best effort */ }
|
|
1426
|
+
}, []);
|
|
1427
|
+
|
|
1025
1428
|
useEffect(() => {
|
|
1026
1429
|
if (visible || !started) return;
|
|
1430
|
+
if (preserveSessionOnHideRef.current) {
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1027
1433
|
stopVisionShare().catch(() => {});
|
|
1028
1434
|
if (typeof sdkFallbackCleanupRef.current === "function") {
|
|
1029
1435
|
sdkFallbackCleanupRef.current();
|
|
@@ -1033,17 +1439,11 @@ export function VoiceOverlay({
|
|
|
1033
1439
|
legacyFallbackCleanupRef.current();
|
|
1034
1440
|
legacyFallbackCleanupRef.current = null;
|
|
1035
1441
|
}
|
|
1036
|
-
|
|
1037
|
-
stopSdkVoiceSession();
|
|
1038
|
-
} else if (tier === 1) {
|
|
1039
|
-
stopVoiceSession();
|
|
1040
|
-
} else {
|
|
1041
|
-
stopFallbackSession();
|
|
1042
|
-
}
|
|
1442
|
+
stopAllVoiceTransports();
|
|
1043
1443
|
setStarted(false);
|
|
1044
1444
|
setUsingSdk(false);
|
|
1045
1445
|
autoFallbackTriedRef.current = false;
|
|
1046
|
-
}, [visible, started,
|
|
1446
|
+
}, [visible, started, stopAllVoiceTransports]);
|
|
1047
1447
|
|
|
1048
1448
|
const loadMeetingMessages = useCallback(async () => {
|
|
1049
1449
|
const activeSessionId = String(sessionId || "").trim();
|
|
@@ -1195,21 +1595,21 @@ export function VoiceOverlay({
|
|
|
1195
1595
|
legacyFallbackCleanupRef.current();
|
|
1196
1596
|
legacyFallbackCleanupRef.current = null;
|
|
1197
1597
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
} else if (tier === 1) {
|
|
1201
|
-
stopVoiceSession();
|
|
1202
|
-
} else {
|
|
1203
|
-
stopFallbackSession();
|
|
1204
|
-
}
|
|
1598
|
+
stopAllVoiceTransports();
|
|
1599
|
+
preserveSessionOnHideRef.current = false;
|
|
1205
1600
|
autoFallbackTriedRef.current = false;
|
|
1206
1601
|
setUsingSdk(false);
|
|
1207
1602
|
setStarted(false);
|
|
1208
1603
|
onClose();
|
|
1209
|
-
}, [
|
|
1604
|
+
}, [onClose, stopAllVoiceTransports]);
|
|
1210
1605
|
|
|
1211
1606
|
const handleDismiss = useCallback((detail = {}) => {
|
|
1212
1607
|
haptic("light");
|
|
1608
|
+
const reason = String(detail?.reason || "").trim().toLowerCase();
|
|
1609
|
+
if (reason === "externalize") {
|
|
1610
|
+
// Externalize hides this overlay while the live call continues.
|
|
1611
|
+
preserveSessionOnHideRef.current = true;
|
|
1612
|
+
}
|
|
1213
1613
|
const fn = typeof onDismiss === "function" ? onDismiss : onClose;
|
|
1214
1614
|
fn(detail);
|
|
1215
1615
|
}, [onDismiss, onClose]);
|
|
@@ -1275,8 +1675,12 @@ export function VoiceOverlay({
|
|
|
1275
1675
|
|
|
1276
1676
|
const handleToggleMic = useCallback(() => {
|
|
1277
1677
|
haptic("light");
|
|
1678
|
+
if (effectiveSdk) {
|
|
1679
|
+
toggleSdkMicMute();
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1278
1682
|
toggleMicMute();
|
|
1279
|
-
}, []);
|
|
1683
|
+
}, [effectiveSdk]);
|
|
1280
1684
|
|
|
1281
1685
|
const handleBackToApp = useCallback(() => {
|
|
1282
1686
|
haptic("light");
|
|
@@ -1301,17 +1705,11 @@ export function VoiceOverlay({
|
|
|
1301
1705
|
useEffect(() => () => {
|
|
1302
1706
|
try {
|
|
1303
1707
|
stopVisionShare().catch(() => {});
|
|
1304
|
-
|
|
1305
|
-
stopSdkVoiceSession();
|
|
1306
|
-
} else if (tier === 1) {
|
|
1307
|
-
stopVoiceSession();
|
|
1308
|
-
} else {
|
|
1309
|
-
stopFallbackSession();
|
|
1310
|
-
}
|
|
1708
|
+
stopAllVoiceTransports();
|
|
1311
1709
|
} catch {
|
|
1312
1710
|
// best effort cleanup
|
|
1313
1711
|
}
|
|
1314
|
-
}, [
|
|
1712
|
+
}, [stopAllVoiceTransports]);
|
|
1315
1713
|
|
|
1316
1714
|
const handleExpand = useCallback(() => {
|
|
1317
1715
|
haptic("light");
|
|
@@ -1462,12 +1860,87 @@ export function VoiceOverlay({
|
|
|
1462
1860
|
|
|
1463
1861
|
if (!visible) return null;
|
|
1464
1862
|
|
|
1863
|
+
const activeVoiceAgent = voiceAgents.find(
|
|
1864
|
+
(agent) => String(agent?.id || "").trim() === String(selectedVoiceAgentId || "").trim(),
|
|
1865
|
+
) || null;
|
|
1866
|
+
|
|
1867
|
+
const handleVoiceAgentSelection = async (nextAgentIdRaw) => {
|
|
1868
|
+
const nextAgentId = String(nextAgentIdRaw || "").trim();
|
|
1869
|
+
if (!nextAgentId) return;
|
|
1870
|
+
if (nextAgentId === String(selectedVoiceAgentId || "").trim()) return;
|
|
1871
|
+
setSelectedVoiceAgentId(nextAgentId);
|
|
1872
|
+
if (typeof onVoiceAgentChange === "function") onVoiceAgentChange(nextAgentId);
|
|
1873
|
+
if (!started) return;
|
|
1874
|
+
|
|
1875
|
+
setSwitchingVoiceAgent(true);
|
|
1876
|
+
try {
|
|
1877
|
+
stopVisionShare().catch(() => {});
|
|
1878
|
+
stopAllVoiceTransports();
|
|
1879
|
+
autoFallbackTriedRef.current = false;
|
|
1880
|
+
setUsingSdk(false);
|
|
1881
|
+
setStarted(false);
|
|
1882
|
+
setStartRequested(true);
|
|
1883
|
+
} finally {
|
|
1884
|
+
setSwitchingVoiceAgent(false);
|
|
1885
|
+
}
|
|
1886
|
+
};
|
|
1887
|
+
|
|
1888
|
+
if (!startRequested) {
|
|
1889
|
+
return html`
|
|
1890
|
+
<div class=${`voice-overlay${isCompactFollowMode ? " compact" : ""}`}>
|
|
1891
|
+
<div class="voice-overlay-main" style="display:flex;align-items:center;justify-content:center;padding:24px;">
|
|
1892
|
+
<div class="vm-settings-panel" style="max-width:560px;width:100%;">
|
|
1893
|
+
<div class="vm-settings-header">
|
|
1894
|
+
<span class="vm-settings-title">${normalizedCallType === "video" ? "Start Video Call" : "Start Voice Call"}</span>
|
|
1895
|
+
</div>
|
|
1896
|
+
<div class="vm-settings-body">
|
|
1897
|
+
<div class="vm-settings-section">
|
|
1898
|
+
<div class="vm-settings-section-title">Voice Agent</div>
|
|
1899
|
+
<div class="vm-settings-row">
|
|
1900
|
+
<div class="vm-settings-row-info">
|
|
1901
|
+
<div class="vm-settings-row-label">Choose starting agent</div>
|
|
1902
|
+
<div class="vm-settings-row-desc">
|
|
1903
|
+
${loadingVoiceAgents
|
|
1904
|
+
? "Loading available audio agents…"
|
|
1905
|
+
: (activeVoiceAgent?.description || "Pick the voice agent for this call.")}
|
|
1906
|
+
</div>
|
|
1907
|
+
</div>
|
|
1908
|
+
<select
|
|
1909
|
+
class="vm-settings-select"
|
|
1910
|
+
disabled=${loadingVoiceAgents || switchingVoiceAgent}
|
|
1911
|
+
value=${selectedVoiceAgentId || ""}
|
|
1912
|
+
onChange=${(e) => handleVoiceAgentSelection(e.target.value)}
|
|
1913
|
+
>
|
|
1914
|
+
${(voiceAgents.length ? voiceAgents : [{ id: "voice-agent", name: "Voice Agent" }]).map((agent) => html`
|
|
1915
|
+
<option key=${agent.id} value=${agent.id}>${agent.name || agent.id}</option>
|
|
1916
|
+
`)}
|
|
1917
|
+
</select>
|
|
1918
|
+
</div>
|
|
1919
|
+
</div>
|
|
1920
|
+
<div class="library-actions" style="margin-top:10px;">
|
|
1921
|
+
<button class="btn-ghost" onClick=${onClose}>Cancel</button>
|
|
1922
|
+
<button
|
|
1923
|
+
class="btn-primary"
|
|
1924
|
+
disabled=${loadingVoiceAgents || !String(selectedVoiceAgentId || "").trim()}
|
|
1925
|
+
onClick=${() => setStartRequested(true)}
|
|
1926
|
+
>
|
|
1927
|
+
Start Call
|
|
1928
|
+
</button>
|
|
1929
|
+
</div>
|
|
1930
|
+
</div>
|
|
1931
|
+
</div>
|
|
1932
|
+
</div>
|
|
1933
|
+
</div>
|
|
1934
|
+
`;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1465
1937
|
const statusLabel = state === "connected" ? "ready" : state;
|
|
1466
1938
|
const boundLabel = [
|
|
1467
1939
|
sessionId ? `session ${sessionId}` : null,
|
|
1468
1940
|
executor ? `agent ${executor}` : null,
|
|
1469
1941
|
mode ? `mode ${mode}` : null,
|
|
1470
1942
|
model ? `model ${model}` : null,
|
|
1943
|
+
activeVoiceAgent?.name ? `voice ${activeVoiceAgent.name}` : null,
|
|
1471
1944
|
]
|
|
1472
1945
|
.filter(Boolean)
|
|
1473
1946
|
.join(" · ");
|
|
@@ -1816,27 +2289,119 @@ export function VoiceOverlay({
|
|
|
1816
2289
|
|
|
1817
2290
|
<!-- Google Meet-style bottom controls bar -->
|
|
1818
2291
|
<div class="vm-bar">
|
|
1819
|
-
<!-- Left: duration -->
|
|
2292
|
+
<!-- Left: three-dot menu + duration -->
|
|
1820
2293
|
<div class="vm-bar-group left">
|
|
2294
|
+
<div class="vm-btn-wrap" style="position:relative">
|
|
2295
|
+
<button
|
|
2296
|
+
class="vm-btn"
|
|
2297
|
+
onClick=${(e) => { e.stopPropagation(); setShowMoreMenu(p => !p); setShowMicPicker(false); setShowSpeakerPicker(false); setShowPeoplePanel(false); }}
|
|
2298
|
+
title="More options"
|
|
2299
|
+
>⋯</button>
|
|
2300
|
+
<span class="vm-btn-label">More</span>
|
|
2301
|
+
${showMoreMenu && html`
|
|
2302
|
+
<div class="vm-more-menu" onClick=${(e) => e.stopPropagation()}>
|
|
2303
|
+
<button class="vm-more-item" onClick=${() => { setShowSettings(true); setShowMoreMenu(false); }}>
|
|
2304
|
+
<span class="vm-more-icon">⚙️</span> Settings
|
|
2305
|
+
</button>
|
|
2306
|
+
<button class="vm-more-item" onClick=${() => { setShowMoreMenu(false); handleMinimize(); }}>
|
|
2307
|
+
<span class="vm-more-icon">🖼</span> Open picture-in-picture
|
|
2308
|
+
</button>
|
|
2309
|
+
<button class="vm-more-item" onClick=${() => {
|
|
2310
|
+
if (document.fullscreenElement) { document.exitFullscreen().catch(() => {}); }
|
|
2311
|
+
else { document.documentElement.requestFullscreen?.().catch(() => {}); }
|
|
2312
|
+
setShowMoreMenu(false);
|
|
2313
|
+
}}>
|
|
2314
|
+
<span class="vm-more-icon">${document.fullscreenElement ? "↙" : "⛶"}</span> ${document.fullscreenElement ? "Exit full screen" : "Full screen"}
|
|
2315
|
+
</button>
|
|
2316
|
+
<div class="vm-more-divider"></div>
|
|
2317
|
+
<button class="vm-more-item disabled" disabled>
|
|
2318
|
+
<span class="vm-more-icon">⏺</span> Recording unavailable
|
|
2319
|
+
</button>
|
|
2320
|
+
<div class="vm-more-divider"></div>
|
|
2321
|
+
<button class="vm-more-item" onClick=${() => {
|
|
2322
|
+
showToast("Troubleshooting: Check mic permissions and network connection", "info");
|
|
2323
|
+
setShowMoreMenu(false);
|
|
2324
|
+
}}>
|
|
2325
|
+
<span class="vm-more-icon">🔧</span> Troubleshooting & help
|
|
2326
|
+
</button>
|
|
2327
|
+
</div>
|
|
2328
|
+
`}
|
|
2329
|
+
</div>
|
|
1821
2330
|
${duration > 0 && html`
|
|
1822
|
-
<span style="font-size:13px;color:rgba(255,255,255,0.7);font-variant-numeric:tabular-nums">
|
|
2331
|
+
<span style="font-size:13px;color:rgba(255,255,255,0.7);font-variant-numeric:tabular-nums;margin-left:8px">
|
|
1823
2332
|
${formatDuration(duration)}
|
|
1824
2333
|
</span>
|
|
1825
2334
|
`}
|
|
1826
2335
|
</div>
|
|
1827
2336
|
|
|
1828
|
-
<!-- Center: mic, camera, screen, end-call
|
|
2337
|
+
<!-- Center: mic (with arrow), camera, screen, emoji, hand-raise, end-call -->
|
|
1829
2338
|
<div class="vm-bar-group center">
|
|
1830
|
-
<!-- Mic toggle -->
|
|
2339
|
+
<!-- Mic toggle with device picker arrow -->
|
|
1831
2340
|
<div class="vm-btn-wrap">
|
|
1832
|
-
<
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2341
|
+
<div class="vm-btn-with-arrow">
|
|
2342
|
+
<button
|
|
2343
|
+
class=${`vm-btn has-arrow${micMuted ? " muted" : ""}`}
|
|
2344
|
+
onClick=${handleToggleMic}
|
|
2345
|
+
title=${micMuted ? "Unmute mic" : "Mute mic"}
|
|
2346
|
+
>
|
|
2347
|
+
${micMuted ? "🔇" : "🎙"}
|
|
2348
|
+
</button>
|
|
2349
|
+
<button
|
|
2350
|
+
class="vm-btn-arrow"
|
|
2351
|
+
onClick=${(e) => { e.stopPropagation(); setShowMicPicker(p => !p); setShowSpeakerPicker(false); setShowMoreMenu(false); }}
|
|
2352
|
+
title="Audio settings"
|
|
2353
|
+
>▲</button>
|
|
2354
|
+
</div>
|
|
1839
2355
|
<span class="vm-btn-label">${micMuted ? "Unmute" : "Mute"}</span>
|
|
2356
|
+
${showMicPicker && html`
|
|
2357
|
+
<div class="vm-device-picker" onClick=${(e) => e.stopPropagation()}>
|
|
2358
|
+
<div class="vm-device-picker-section">
|
|
2359
|
+
<div class="vm-device-picker-label">Microphone</div>
|
|
2360
|
+
${audioInputDevices.value.map(d => html`
|
|
2361
|
+
<button
|
|
2362
|
+
key=${d.deviceId}
|
|
2363
|
+
class=${`vm-device-item${(selectedAudioInput.value || "") === d.deviceId || (!selectedAudioInput.value && d.deviceId === "default") ? " active" : ""}`}
|
|
2364
|
+
onClick=${() => { switchAudioInput(d.deviceId); }}
|
|
2365
|
+
>
|
|
2366
|
+
<div class=${`vm-device-check${(selectedAudioInput.value || "") === d.deviceId || (!selectedAudioInput.value && d.deviceId === "default") ? " selected" : ""}`}>
|
|
2367
|
+
${(selectedAudioInput.value || "") === d.deviceId || (!selectedAudioInput.value && d.deviceId === "default") ? "✓" : ""}
|
|
2368
|
+
</div>
|
|
2369
|
+
<div class="vm-device-name">
|
|
2370
|
+
${d.label || "Microphone " + d.deviceId.slice(0, 8)}
|
|
2371
|
+
${d.deviceId === "default" ? html`<div class="vm-device-default">System default</div>` : ""}
|
|
2372
|
+
</div>
|
|
2373
|
+
</button>
|
|
2374
|
+
`)}
|
|
2375
|
+
${audioInputDevices.value.length === 0 && html`
|
|
2376
|
+
<div style="padding:8px 16px;font-size:12px;color:rgba(255,255,255,0.45)">No microphones detected</div>
|
|
2377
|
+
`}
|
|
2378
|
+
</div>
|
|
2379
|
+
<div class="vm-mic-level-bar">
|
|
2380
|
+
<div class="vm-mic-level-fill" style=${`width:${Math.round(micInputLevel.value * 100)}%`}></div>
|
|
2381
|
+
</div>
|
|
2382
|
+
<div class="vm-device-picker-section">
|
|
2383
|
+
<div class="vm-device-picker-label">Speakers</div>
|
|
2384
|
+
${audioOutputDevices.value.map(d => html`
|
|
2385
|
+
<button
|
|
2386
|
+
key=${d.deviceId}
|
|
2387
|
+
class=${`vm-device-item${(selectedAudioOutput.value || "") === d.deviceId || (!selectedAudioOutput.value && d.deviceId === "default") ? " active" : ""}`}
|
|
2388
|
+
onClick=${() => { switchAudioOutput(d.deviceId); }}
|
|
2389
|
+
>
|
|
2390
|
+
<div class=${`vm-device-check${(selectedAudioOutput.value || "") === d.deviceId || (!selectedAudioOutput.value && d.deviceId === "default") ? " selected" : ""}`}>
|
|
2391
|
+
${(selectedAudioOutput.value || "") === d.deviceId || (!selectedAudioOutput.value && d.deviceId === "default") ? "✓" : ""}
|
|
2392
|
+
</div>
|
|
2393
|
+
<div class="vm-device-name">
|
|
2394
|
+
${d.label || "Speaker " + d.deviceId.slice(0, 8)}
|
|
2395
|
+
${d.deviceId === "default" ? html`<div class="vm-device-default">System default</div>` : ""}
|
|
2396
|
+
</div>
|
|
2397
|
+
</button>
|
|
2398
|
+
`)}
|
|
2399
|
+
${audioOutputDevices.value.length === 0 && html`
|
|
2400
|
+
<div style="padding:8px 16px;font-size:12px;color:rgba(255,255,255,0.45)">No speakers detected</div>
|
|
2401
|
+
`}
|
|
2402
|
+
</div>
|
|
2403
|
+
</div>
|
|
2404
|
+
`}
|
|
1840
2405
|
</div>
|
|
1841
2406
|
|
|
1842
2407
|
<!-- Camera toggle -->
|
|
@@ -1874,7 +2439,7 @@ export function VoiceOverlay({
|
|
|
1874
2439
|
</div>
|
|
1875
2440
|
</div>
|
|
1876
2441
|
|
|
1877
|
-
<!-- Right: chat
|
|
2442
|
+
<!-- Right: chat, people, settings -->
|
|
1878
2443
|
<div class="vm-bar-group right">
|
|
1879
2444
|
${!isCompactFollowMode && html`
|
|
1880
2445
|
<div class="vm-btn-wrap">
|
|
@@ -1889,14 +2454,231 @@ export function VoiceOverlay({
|
|
|
1889
2454
|
<span class="vm-btn-label">Chat</span>
|
|
1890
2455
|
</div>
|
|
1891
2456
|
`}
|
|
1892
|
-
<div class="vm-btn-wrap">
|
|
1893
|
-
<button
|
|
2457
|
+
<div class="vm-btn-wrap" style="position:relative">
|
|
2458
|
+
<button
|
|
2459
|
+
class=${`vm-btn${showPeoplePanel ? " active" : ""}`}
|
|
2460
|
+
onClick=${(e) => { e.stopPropagation(); setShowPeoplePanel(p => !p); setShowMoreMenu(false); setShowMicPicker(false); setShowSpeakerPicker(false); }}
|
|
2461
|
+
title="Participants"
|
|
2462
|
+
>
|
|
1894
2463
|
👥
|
|
1895
2464
|
</button>
|
|
1896
2465
|
<span class="vm-btn-label">People</span>
|
|
2466
|
+
${showPeoplePanel && html`
|
|
2467
|
+
<div class="vm-more-menu" style="bottom:calc(100% + 10px);left:50%;transform:translateX(-50%);min-width:220px" onClick=${(e) => e.stopPropagation()}>
|
|
2468
|
+
<div style="padding:10px 14px 6px;font-size:12px;color:rgba(255,255,255,0.5);font-weight:600;letter-spacing:0.04em">PARTICIPANTS</div>
|
|
2469
|
+
<div class="vm-more-item" style="cursor:default;opacity:0.9">
|
|
2470
|
+
<span class="vm-more-icon">🧑</span> You
|
|
2471
|
+
</div>
|
|
2472
|
+
<div class="vm-more-item" style="cursor:default;opacity:0.9">
|
|
2473
|
+
<span class="vm-more-icon">🤖</span> ${activeVoiceAgent?.name || "AI Agent"}
|
|
2474
|
+
</div>
|
|
2475
|
+
<div class="vm-more-divider"></div>
|
|
2476
|
+
<div style="padding:8px 12px 6px;font-size:11px;color:rgba(255,255,255,0.5);font-weight:600;letter-spacing:0.04em">SWITCH AGENT</div>
|
|
2477
|
+
${(voiceAgents.length ? voiceAgents : [{ id: "voice-agent", name: "Voice Agent" }]).map((agent) => html`
|
|
2478
|
+
<button
|
|
2479
|
+
key=${agent.id}
|
|
2480
|
+
class="vm-more-item"
|
|
2481
|
+
disabled=${switchingVoiceAgent}
|
|
2482
|
+
onClick=${() => handleVoiceAgentSelection(agent.id)}
|
|
2483
|
+
style=${String(agent.id) === String(selectedVoiceAgentId || "").trim() ? "opacity:0.95;border:1px solid rgba(138,180,248,0.35);" : ""}
|
|
2484
|
+
>
|
|
2485
|
+
<span class="vm-more-icon">${String(agent.id) === String(selectedVoiceAgentId || "").trim() ? "✅" : "🤖"}</span>
|
|
2486
|
+
${agent.name || agent.id}
|
|
2487
|
+
</button>
|
|
2488
|
+
`)}
|
|
2489
|
+
<div class="vm-more-divider"></div>
|
|
2490
|
+
<button class="vm-more-item" onClick=${() => {
|
|
2491
|
+
const url = window.location.href;
|
|
2492
|
+
navigator.clipboard?.writeText(url).then(() => {
|
|
2493
|
+
showToast("Invite link copied!", "success");
|
|
2494
|
+
}).catch(() => {
|
|
2495
|
+
showToast(url, "info");
|
|
2496
|
+
});
|
|
2497
|
+
setShowPeoplePanel(false);
|
|
2498
|
+
}}>
|
|
2499
|
+
<span class="vm-more-icon">🔗</span> Copy invite link
|
|
2500
|
+
</button>
|
|
2501
|
+
</div>
|
|
2502
|
+
`}
|
|
2503
|
+
</div>
|
|
2504
|
+
<div class="vm-btn-wrap">
|
|
2505
|
+
<button class="vm-btn" onClick=${() => setShowSettings(true)} title="Settings">
|
|
2506
|
+
⚙️
|
|
2507
|
+
</button>
|
|
2508
|
+
<span class="vm-btn-label">Settings</span>
|
|
1897
2509
|
</div>
|
|
1898
2510
|
</div>
|
|
1899
2511
|
</div>
|
|
2512
|
+
|
|
2513
|
+
<!-- Device chips bar (below main controls, like Meet) -->
|
|
2514
|
+
<div class="vm-bar-device-chips">
|
|
2515
|
+
<button
|
|
2516
|
+
class="vm-bar-device-chip"
|
|
2517
|
+
onClick=${() => { setShowMicPicker(p => !p); setShowSpeakerPicker(false); }}
|
|
2518
|
+
>
|
|
2519
|
+
<span class="vm-bar-device-chip-icon">🎙</span>
|
|
2520
|
+
${(() => {
|
|
2521
|
+
const sel = selectedAudioInput.value;
|
|
2522
|
+
const dev = audioInputDevices.value.find(d => d.deviceId === sel);
|
|
2523
|
+
return dev?.label || "System default";
|
|
2524
|
+
})()}
|
|
2525
|
+
<span style="font-size:10px;margin-left:2px">▲</span>
|
|
2526
|
+
</button>
|
|
2527
|
+
<button
|
|
2528
|
+
class="vm-bar-device-chip"
|
|
2529
|
+
onClick=${() => { setShowSpeakerPicker(p => !p); setShowMicPicker(false); }}
|
|
2530
|
+
>
|
|
2531
|
+
<span class="vm-bar-device-chip-icon">🔊</span>
|
|
2532
|
+
${(() => {
|
|
2533
|
+
const sel = selectedAudioOutput.value;
|
|
2534
|
+
const dev = audioOutputDevices.value.find(d => d.deviceId === sel);
|
|
2535
|
+
return dev?.label || "System default";
|
|
2536
|
+
})()}
|
|
2537
|
+
<span style="font-size:10px;margin-left:2px">▲</span>
|
|
2538
|
+
</button>
|
|
2539
|
+
</div>
|
|
2540
|
+
|
|
2541
|
+
<!-- Settings panel overlay -->
|
|
2542
|
+
${showSettings && html`
|
|
2543
|
+
<div class="vm-settings-overlay" onClick=${() => setShowSettings(false)}>
|
|
2544
|
+
<div class="vm-settings-panel" onClick=${(e) => e.stopPropagation()}>
|
|
2545
|
+
<div class="vm-settings-header">
|
|
2546
|
+
<span class="vm-settings-title">Settings</span>
|
|
2547
|
+
<button class="vm-settings-close" onClick=${() => setShowSettings(false)}>✕</button>
|
|
2548
|
+
</div>
|
|
2549
|
+
<div class="vm-settings-body">
|
|
2550
|
+
<!-- Audio section -->
|
|
2551
|
+
<div class="vm-settings-section">
|
|
2552
|
+
<div class="vm-settings-section-title">🎙 Audio</div>
|
|
2553
|
+
|
|
2554
|
+
<div class="vm-settings-row">
|
|
2555
|
+
<div class="vm-settings-row-info">
|
|
2556
|
+
<div class="vm-settings-row-label">Microphone</div>
|
|
2557
|
+
</div>
|
|
2558
|
+
<select
|
|
2559
|
+
class="vm-settings-select"
|
|
2560
|
+
value=${selectedAudioInput.value || ""}
|
|
2561
|
+
onChange=${(e) => switchAudioInput(e.target.value)}
|
|
2562
|
+
>
|
|
2563
|
+
${audioInputDevices.value.map(d => html`
|
|
2564
|
+
<option key=${d.deviceId} value=${d.deviceId}>${d.label || "Mic " + d.deviceId.slice(0, 8)}</option>
|
|
2565
|
+
`)}
|
|
2566
|
+
</select>
|
|
2567
|
+
</div>
|
|
2568
|
+
|
|
2569
|
+
<!-- Mic level -->
|
|
2570
|
+
<div class="vm-mic-level-bar" style="margin:0 0 8px">
|
|
2571
|
+
<div class="vm-mic-level-fill" style=${`width:${Math.round(micInputLevel.value * 100)}%`}></div>
|
|
2572
|
+
</div>
|
|
2573
|
+
|
|
2574
|
+
<div class="vm-settings-row">
|
|
2575
|
+
<div class="vm-settings-row-info">
|
|
2576
|
+
<div class="vm-settings-row-label">Speakers</div>
|
|
2577
|
+
</div>
|
|
2578
|
+
<select
|
|
2579
|
+
class="vm-settings-select"
|
|
2580
|
+
value=${selectedAudioOutput.value || ""}
|
|
2581
|
+
onChange=${(e) => switchAudioOutput(e.target.value)}
|
|
2582
|
+
>
|
|
2583
|
+
${audioOutputDevices.value.map(d => html`
|
|
2584
|
+
<option key=${d.deviceId} value=${d.deviceId}>${d.label || "Speaker " + d.deviceId.slice(0, 8)}</option>
|
|
2585
|
+
`)}
|
|
2586
|
+
</select>
|
|
2587
|
+
</div>
|
|
2588
|
+
</div>
|
|
2589
|
+
|
|
2590
|
+
<!-- Audio processing section -->
|
|
2591
|
+
<div class="vm-settings-section">
|
|
2592
|
+
<div class="vm-settings-section-title">🔧 Audio Processing</div>
|
|
2593
|
+
|
|
2594
|
+
<div class="vm-settings-row">
|
|
2595
|
+
<div class="vm-settings-row-info">
|
|
2596
|
+
<div class="vm-settings-row-label">Echo cancellation</div>
|
|
2597
|
+
<div class="vm-settings-row-desc">Reduces echo from speakers being picked up by mic</div>
|
|
2598
|
+
</div>
|
|
2599
|
+
<button
|
|
2600
|
+
class=${`vm-settings-toggle${audioSettings.value.echoCancellation ? " on" : ""}`}
|
|
2601
|
+
onClick=${() => updateAudioSettings({ echoCancellation: !audioSettings.value.echoCancellation })}
|
|
2602
|
+
/>
|
|
2603
|
+
</div>
|
|
2604
|
+
|
|
2605
|
+
<div class="vm-settings-row">
|
|
2606
|
+
<div class="vm-settings-row-info">
|
|
2607
|
+
<div class="vm-settings-row-label">Noise suppression</div>
|
|
2608
|
+
<div class="vm-settings-row-desc">Filters background noise from microphone input</div>
|
|
2609
|
+
</div>
|
|
2610
|
+
<button
|
|
2611
|
+
class=${`vm-settings-toggle${audioSettings.value.noiseSuppression ? " on" : ""}`}
|
|
2612
|
+
onClick=${() => updateAudioSettings({ noiseSuppression: !audioSettings.value.noiseSuppression })}
|
|
2613
|
+
/>
|
|
2614
|
+
</div>
|
|
2615
|
+
|
|
2616
|
+
<div class="vm-settings-row">
|
|
2617
|
+
<div class="vm-settings-row-info">
|
|
2618
|
+
<div class="vm-settings-row-label">Auto gain control</div>
|
|
2619
|
+
<div class="vm-settings-row-desc">Automatically adjusts microphone volume level</div>
|
|
2620
|
+
</div>
|
|
2621
|
+
<button
|
|
2622
|
+
class=${`vm-settings-toggle${audioSettings.value.autoGainControl ? " on" : ""}`}
|
|
2623
|
+
onClick=${() => updateAudioSettings({ autoGainControl: !audioSettings.value.autoGainControl })}
|
|
2624
|
+
/>
|
|
2625
|
+
</div>
|
|
2626
|
+
</div>
|
|
2627
|
+
|
|
2628
|
+
<!-- Call info section -->
|
|
2629
|
+
<div class="vm-settings-section">
|
|
2630
|
+
<div class="vm-settings-section-title">📞 Call Info</div>
|
|
2631
|
+
<div class="vm-settings-row">
|
|
2632
|
+
<div class="vm-settings-row-info">
|
|
2633
|
+
<div class="vm-settings-row-label">Session</div>
|
|
2634
|
+
<div class="vm-settings-row-desc">${sessionId || "No active session"}</div>
|
|
2635
|
+
</div>
|
|
2636
|
+
</div>
|
|
2637
|
+
<div class="vm-settings-row">
|
|
2638
|
+
<div class="vm-settings-row-info">
|
|
2639
|
+
<div class="vm-settings-row-label">Agent</div>
|
|
2640
|
+
<div class="vm-settings-row-desc">${activeVoiceAgent?.name || "Default"}</div>
|
|
2641
|
+
</div>
|
|
2642
|
+
</div>
|
|
2643
|
+
<div class="vm-settings-row">
|
|
2644
|
+
<div class="vm-settings-row-info">
|
|
2645
|
+
<div class="vm-settings-row-label">Switch voice agent</div>
|
|
2646
|
+
<div class="vm-settings-row-desc">Change persona/toolset without leaving this call</div>
|
|
2647
|
+
</div>
|
|
2648
|
+
<select
|
|
2649
|
+
class="vm-settings-select"
|
|
2650
|
+
disabled=${switchingVoiceAgent}
|
|
2651
|
+
value=${selectedVoiceAgentId || ""}
|
|
2652
|
+
onChange=${(e) => handleVoiceAgentSelection(e.target.value)}
|
|
2653
|
+
>
|
|
2654
|
+
${(voiceAgents.length ? voiceAgents : [{ id: "voice-agent", name: "Voice Agent" }]).map((agent) => html`
|
|
2655
|
+
<option key=${agent.id} value=${agent.id}>${agent.name || agent.id}</option>
|
|
2656
|
+
`)}
|
|
2657
|
+
</select>
|
|
2658
|
+
</div>
|
|
2659
|
+
<div class="vm-settings-row">
|
|
2660
|
+
<div class="vm-settings-row-info">
|
|
2661
|
+
<div class="vm-settings-row-label">Model</div>
|
|
2662
|
+
<div class="vm-settings-row-desc">${model || "Default"}</div>
|
|
2663
|
+
</div>
|
|
2664
|
+
</div>
|
|
2665
|
+
<div class="vm-settings-row">
|
|
2666
|
+
<div class="vm-settings-row-info">
|
|
2667
|
+
<div class="vm-settings-row-label">Transport</div>
|
|
2668
|
+
<div class="vm-settings-row-desc">${usingSdk ? "Agents SDK" : "WebRTC Legacy"}</div>
|
|
2669
|
+
</div>
|
|
2670
|
+
</div>
|
|
2671
|
+
<div class="vm-settings-row">
|
|
2672
|
+
<div class="vm-settings-row-info">
|
|
2673
|
+
<div class="vm-settings-row-label">Duration</div>
|
|
2674
|
+
<div class="vm-settings-row-desc">${duration > 0 ? formatDuration(duration) : "Not started"}</div>
|
|
2675
|
+
</div>
|
|
2676
|
+
</div>
|
|
2677
|
+
</div>
|
|
2678
|
+
</div>
|
|
2679
|
+
</div>
|
|
2680
|
+
</div>
|
|
2681
|
+
`}
|
|
1900
2682
|
</div>
|
|
1901
2683
|
`}
|
|
1902
2684
|
|