mirror of
https://github.com/block/buzz.git
synced 2026-11-05 08:51:10 +00:00
## Summary - add a visible Stop control for interrupting agent speech - make push-to-talk available by default while preserving manual mute controls - refine agent management, muted audio states, drawer layering, and return navigation - suppress duplicate notification sounds for Huddle messages ## Why Huddles could trap users behind long agent speech, hide useful agent controls, and leave temporary Huddle state visible after the call. The drawer also regressed when the terminal substrate began painting behind the rounded app surface. ## Validation - `just desktop-ci` - focused Huddle Playwright coverage for the drawer, speech interruption, agent picker, and leave navigation --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
187 lines
6.0 KiB
TypeScript
187 lines
6.0 KiB
TypeScript
import { ChevronDown, MoreVertical } from "lucide-react";
|
|
import * as React from "react";
|
|
|
|
import type { VoiceRegistryEntry } from "@/features/settings/ui/voiceSettingsLogic";
|
|
import {
|
|
voiceOptionLabel,
|
|
voicesForBackend,
|
|
} from "@/features/settings/ui/voiceSettingsLogic";
|
|
import { invokeTauri } from "@/shared/api/tauri";
|
|
import { cn } from "@/shared/lib/cn";
|
|
import { Button } from "@/shared/ui/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuRadioGroup,
|
|
DropdownMenuRadioItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/shared/ui/dropdown-menu";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
|
import { Separator } from "@/shared/ui/separator";
|
|
import { Switch } from "@/shared/ui/switch";
|
|
|
|
export type HuddleAgentVoiceSettings = {
|
|
enabled: boolean;
|
|
voice_key: string;
|
|
};
|
|
|
|
type AgentVoiceMenuProps = {
|
|
agentPubkey: string;
|
|
contentAlign?: React.ComponentProps<typeof PopoverContent>["align"];
|
|
contentClassName?: string;
|
|
contentSide?: React.ComponentProps<typeof PopoverContent>["side"];
|
|
displayName: string;
|
|
onRemoveAgent?: () => void;
|
|
registry: VoiceRegistryEntry[];
|
|
settings: HuddleAgentVoiceSettings | undefined;
|
|
onSettingsChange: (settings: HuddleAgentVoiceSettings) => void;
|
|
trigger?: React.ReactElement;
|
|
};
|
|
|
|
export function AgentVoiceMenu({
|
|
agentPubkey,
|
|
contentAlign = "end",
|
|
contentClassName,
|
|
contentSide,
|
|
displayName,
|
|
onRemoveAgent,
|
|
registry,
|
|
settings,
|
|
onSettingsChange,
|
|
trigger,
|
|
}: AgentVoiceMenuProps) {
|
|
const [busy, setBusy] = React.useState(false);
|
|
const [error, setError] = React.useState<string | null>(null);
|
|
const voices = voicesForBackend(registry, "pocket");
|
|
const selectedVoice =
|
|
voices.find((voice) => voice.key === settings?.voice_key) ?? voices[0];
|
|
|
|
const update = React.useCallback(
|
|
async (
|
|
command: "set_huddle_agent_tts_enabled" | "set_huddle_agent_voice",
|
|
payload: Record<string, unknown>,
|
|
) => {
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
const next = await invokeTauri<HuddleAgentVoiceSettings>(command, {
|
|
agentPubkey,
|
|
...payload,
|
|
});
|
|
onSettingsChange(next);
|
|
} catch (updateError) {
|
|
setError(
|
|
updateError instanceof Error
|
|
? updateError.message
|
|
: "Agent voice could not be updated.",
|
|
);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
},
|
|
[agentPubkey, onSettingsChange],
|
|
);
|
|
|
|
return (
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
{trigger ?? (
|
|
<Button
|
|
aria-label={`Voice settings for ${displayName}`}
|
|
className="absolute right-1.5 top-1.5 z-20 h-7 w-7 rounded-md bg-transparent text-muted-foreground shadow-none hover:bg-muted/70 hover:text-foreground"
|
|
data-testid="huddle-agent-voice-menu-trigger"
|
|
size="icon"
|
|
type="button"
|
|
variant="ghost"
|
|
>
|
|
<MoreVertical className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
align={contentAlign}
|
|
className={cn("w-64 space-y-3 p-3", contentClassName)}
|
|
data-testid="huddle-agent-voice-menu-content"
|
|
side={contentSide}
|
|
sideOffset={8}
|
|
>
|
|
<div className="flex items-center justify-between gap-3">
|
|
<label
|
|
className="text-sm font-medium"
|
|
htmlFor={`agent-tts-${agentPubkey}`}
|
|
>
|
|
Agent text-to-speech
|
|
</label>
|
|
<Switch
|
|
checked={settings?.enabled ?? true}
|
|
data-testid="huddle-agent-tts-toggle"
|
|
disabled={!settings || busy}
|
|
id={`agent-tts-${agentPubkey}`}
|
|
onCheckedChange={(enabled) => {
|
|
void update("set_huddle_agent_tts_enabled", { enabled });
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{settings?.enabled ? (
|
|
<div className="flex items-center justify-between gap-4">
|
|
<span className="text-sm font-medium">Agent voice</span>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
aria-label="Choose agent voice"
|
|
className="h-8 shrink-0 gap-1.5 px-2 text-sm text-muted-foreground"
|
|
data-testid="huddle-agent-voice-selector"
|
|
disabled={busy || voices.length === 0}
|
|
size="sm"
|
|
type="button"
|
|
variant="ghost"
|
|
>
|
|
{selectedVoice
|
|
? voiceOptionLabel(selectedVoice, voices)
|
|
: "Unavailable"}
|
|
<ChevronDown aria-hidden="true" className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent
|
|
align="end"
|
|
className="max-h-72 min-w-40 overflow-y-auto"
|
|
>
|
|
<DropdownMenuRadioGroup
|
|
onValueChange={(voiceKey) => {
|
|
void update("set_huddle_agent_voice", { voiceKey });
|
|
}}
|
|
value={selectedVoice?.key}
|
|
>
|
|
{voices.map((voice) => (
|
|
<DropdownMenuRadioItem key={voice.key} value={voice.key}>
|
|
{voiceOptionLabel(voice, voices)}
|
|
</DropdownMenuRadioItem>
|
|
))}
|
|
</DropdownMenuRadioGroup>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
) : null}
|
|
|
|
{onRemoveAgent ? (
|
|
<>
|
|
<Separator />
|
|
<Button
|
|
aria-label={`Remove ${displayName} from huddle`}
|
|
className="w-full justify-start text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
onClick={onRemoveAgent}
|
|
type="button"
|
|
variant="ghost"
|
|
>
|
|
Remove from huddle
|
|
</Button>
|
|
</>
|
|
) : null}
|
|
|
|
{error ? <p className="text-xs text-destructive">{error}</p> : null}
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
}
|