feat(frontend): always-visible Enable audio + Enable mic controls

Surface audio-playback unlock and mic publish as persistent buttons in the pod control row (alongside Test audio / Test PodMan voice / Share screen), replacing the conditional 'Sound is off' banner that only appeared after a refresh. Enable mic calls setMicrophoneEnabled, which triggers the browser permission prompt so users can verify their mic is set up right.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kartikeya
2026-06-27 23:55:22 -07:00
parent aaf1dc5ede
commit 46d277d203
+44 -16
View File
@@ -9,6 +9,8 @@ import {
GitBranchIcon,
ExternalLinkIcon,
MessageSquareIcon,
MicIcon,
MicOffIcon,
MonitorUpIcon,
PanelLeftIcon,
PanelRightIcon,
@@ -17,6 +19,7 @@ import {
SparklesIcon,
TriangleAlertIcon,
Volume2Icon,
VolumeXIcon,
WorkflowIcon,
XIcon,
} from 'lucide-react';
@@ -113,6 +116,7 @@ export function PodView({
const [testingVoice, setTestingVoice] = useState(false);
const [note, setNote] = useState<string | null>(null);
const [audioBlocked, setAudioBlocked] = useState(false);
const [micOn, setMicOn] = useState(false);
const [leftStreamOpen, setLeftStreamOpen] = useState(() =>
readStoredBool('podman.myStreamOpen', true),
);
@@ -168,6 +172,7 @@ export function PodView({
// are actually heard.
const onPlaybackChanged = () => setAudioBlocked(!room.canPlaybackAudio);
onPlaybackChanged();
setMicOn(room.localParticipant.isMicrophoneEnabled);
room
.on(RoomEvent.ParticipantConnected, refresh)
@@ -204,6 +209,20 @@ export function PodView({
}
}
// Publish/unpublish the local mic. Enabling triggers the browser permission
// prompt, so the button doubles as a "is my mic set up right?" check.
async function toggleMic() {
if (!room) return;
setNote(null);
try {
const next = !micOn;
await room.localParticipant.setMicrophoneEnabled(next);
setMicOn(next);
} catch (e) {
setNote(`Could not toggle mic: ${(e as Error).message}`);
}
}
useEffect(() => {
return () => {
screenTrackRef.current?.stop();
@@ -356,7 +375,31 @@ export function PodView({
</div>
</div>
<div className="grid grid-cols-2 gap-2 sm:flex sm:items-center">
<div className="grid grid-cols-2 gap-2 sm:flex sm:flex-wrap sm:items-center">
<Button
variant={audioBlocked ? 'default' : 'outline'}
onClick={() => void enableSound()}
disabled={!room}
>
{audioBlocked ? (
<VolumeXIcon data-icon="inline-start" />
) : (
<Volume2Icon data-icon="inline-start" />
)}
{audioBlocked ? 'Enable audio' : 'Audio on'}
</Button>
<Button
variant={micOn ? 'default' : 'outline'}
onClick={() => void toggleMic()}
disabled={!room}
>
{micOn ? (
<MicIcon data-icon="inline-start" />
) : (
<MicOffIcon data-icon="inline-start" />
)}
{micOn ? 'Mic on' : 'Enable mic'}
</Button>
<Button variant="outline" onClick={onToggleBeat} disabled={!room}>
<Volume2Icon data-icon="inline-start" />
{beat.on ? (beat.mine ? 'Stop audio' : `Stop (${beat.by})`) : 'Test audio'}
@@ -399,21 +442,6 @@ export function PodView({
</Alert>
)}
{room && audioBlocked && (
<Alert className="flex items-center justify-between gap-3">
<div className="flex items-start gap-3">
<Volume2Icon />
<div>
<AlertTitle>Sound is off</AlertTitle>
<AlertDescription>Click to hear PodMan&apos;s voice alerts.</AlertDescription>
</div>
</div>
<Button size="sm" onClick={() => void enableSound()}>
Enable sound
</Button>
</Alert>
)}
<main className="grid flex-1 gap-5 min-[1800px]:grid-cols-[minmax(0,1fr)_340px]">
<section className="flex min-w-0 flex-col gap-5">
<Card className="flex-1">