Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ export default function ListenButton({ sessionId }: { sessionId: string }) {
},
});

const anySttModelExists = useQuery({
queryKey: ["check-any-stt-model-downloaded"],
refetchInterval: 3000,
queryFn: async () => {
const supportedModels = await localSttCommands.listSupportedModels();
const sttDownloadStatuses = await Promise.all(
supportedModels.map((model) => localSttCommands.isModelDownloaded(model)),
);
return sttDownloadStatuses.some(Boolean);
},
enabled: isOnboarding,
});

const ongoingSessionStatus = useOngoingSession((s) => s.status);
const ongoingSessionId = useOngoingSession((s) => s.sessionId);
const ongoingSessionStore = useOngoingSession((s) => ({
Expand All @@ -88,6 +101,11 @@ export default function ListenButton({ sessionId }: { sessionId: string }) {
const handleStartSession = () => {
if (ongoingSessionStatus === "inactive") {
ongoingSessionStore.start(sessionId);

// Set mic muted after starting if it's onboarding
if (isOnboarding) {
listenerCommands.setMicMuted(true);
}
}
};

Expand Down Expand Up @@ -121,7 +139,9 @@ export default function ListenButton({ sessionId }: { sessionId: string }) {

if (ongoingSessionStatus === "inactive") {
const buttonProps = {
disabled: !modelDownloaded.data || (meetingEnded && isEnhancePending),
disabled: isOnboarding
? !anySttModelExists.data || (meetingEnded && isEnhancePending)
: !modelDownloaded.data || (meetingEnded && isEnhancePending),
onClick: handleStartSession,
};

Expand Down Expand Up @@ -199,13 +219,16 @@ function WhenInactiveAndMeetingNotEndedOnboarding({ disabled, onClick }: { disab
className={cn([
"w-24 h-9 rounded-full border-2 transition-all cursor-pointer outline-none p-0 flex items-center justify-center gap-1",
"bg-neutral-800 border-neutral-700 text-white text-xs font-medium",
!disabled
? "hover:scale-95"
: "opacity-50 cursor-progress",
])}
style={{
boxShadow: "0 0 0 2px rgba(255, 255, 255, 0.8) inset",
}}
>
<PlayIcon size={14} />
<Trans>Play video</Trans>
<Trans>{disabled ? "Wait..." : "Play video"}</Trans>
</ShinyButton>
);
}
Expand Down Expand Up @@ -326,12 +349,6 @@ function RecordingControls({
}
}, [configQuery.data]);

useEffect(() => {
if (sessionId === onboardingSessionId) {
listenerCommands.setMicMuted(true);
}
}, [ongoingSessionMuted.micMuted]);

const handleStopWithTemplate = () => {
const actualTemplateId = selectedTemplate === "auto" ? null : selectedTemplate;
onStop(actualTemplateId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export const sttModelMetadata: Record<SupportedModel, {
"QuantizedSmall": {
name: "Small",
description: "Higher accuracy, moderate speed for multilingual transcription.",
intelligence: 3,
intelligence: 2,
speed: 2,
size: "264 MB",
inputType: ["audio"],
Expand All @@ -91,7 +91,7 @@ export const sttModelMetadata: Record<SupportedModel, {
},
"QuantizedLargeTurbo": {
name: "Large",
description: "Highest accuracy, potentially faster than standard large. Resource intensive.",
description: "Highest accuracy, resource intensive. Only for Mac Pro M4 and above.",
intelligence: 3,
speed: 1,
size: "874 MB",
Expand Down
169 changes: 169 additions & 0 deletions apps/desktop/src/components/welcome-modal/audio-permissions-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { Trans, useLingui } from "@lingui/react/macro";
import { useMutation, useQuery } from "@tanstack/react-query";
import { CheckCircle2Icon, MicIcon, Volume2Icon } from "lucide-react";

import { commands as listenerCommands } from "@hypr/plugin-listener";
import { Button } from "@hypr/ui/components/ui/button";
import PushableButton from "@hypr/ui/components/ui/pushable-button";
import { Spinner } from "@hypr/ui/components/ui/spinner";
import { cn } from "@hypr/ui/lib/utils";

interface PermissionItemProps {
icon: React.ReactNode;
title: string;
description: string;
done: boolean | undefined;
isPending: boolean;
onRequest: () => void;
showSystemSettings?: boolean;
}

function PermissionItem({
icon,
title,
description,
done,
isPending,
onRequest,
showSystemSettings = false,
}: PermissionItemProps) {
return (
<div
className={cn(
"flex items-center justify-between rounded-lg border p-4 transition-all duration-200",
done ? "border-blue-500 bg-blue-50" : "bg-white border-neutral-200",
)}
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div
className={cn(
"flex size-10 items-center justify-center rounded-full flex-shrink-0",
done ? "bg-blue-100" : "bg-neutral-50",
)}
>
<div className={cn(done ? "text-blue-600" : "text-neutral-500")}>{icon}</div>
</div>
<div className="min-w-0 flex-1">
<div className="font-medium truncate">{title}</div>
<div className="text-sm text-muted-foreground">
{done
? (
<span className="text-blue-600 flex items-center gap-1">
<CheckCircle2Icon className="w-3.5 h-3.5 flex-shrink-0" />
<Trans>Access Granted</Trans>
</span>
)
: <span className="block truncate pr-2">{description}</span>}
</div>
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
{!done && (
<>
<Button
variant="outline"
size="sm"
onClick={onRequest}
disabled={isPending}
className="min-w-20"
>
{isPending
? (
<>
<Spinner className="mr-2" />
<Trans>Requesting...</Trans>
</>
)
: <Trans>Enable</Trans>}
</Button>
</>
)}
{done && (
<div className="flex size-8 items-center justify-center rounded-full bg-blue-100">
<CheckCircle2Icon className="w-4 h-4 text-blue-600" />
</div>
)}
</div>
</div>
);
}

interface AudioPermissionsViewProps {
onContinue: () => void;
}

export function AudioPermissionsView({ onContinue }: AudioPermissionsViewProps) {
const { t } = useLingui();

const micPermissionStatus = useQuery({
queryKey: ["micPermission"],
queryFn: () => listenerCommands.checkMicrophoneAccess(),
refetchInterval: 3000,
});

const systemAudioPermissionStatus = useQuery({
queryKey: ["systemAudioPermission"],
queryFn: () => listenerCommands.checkSystemAudioAccess(),
refetchInterval: 3000,
});

const micPermission = useMutation({
mutationFn: () => listenerCommands.requestMicrophoneAccess(),
onSuccess: () => micPermissionStatus.refetch(),
onError: console.error,
});

const capturePermission = useMutation({
mutationFn: () => listenerCommands.requestSystemAudioAccess(),
onSuccess: () => systemAudioPermissionStatus.refetch(),
onError: console.error,
});

const allPermissionsGranted = micPermissionStatus.data && systemAudioPermissionStatus.data;

return (
<div className="flex flex-col items-center min-w-[30rem]">
<h2 className="text-xl font-semibold mb-4">
<Trans>Audio Permissions</Trans>
</h2>

<p className="text-center text-sm text-muted-foreground mb-8">
<Trans>Grant access to audio so Hyprnote can transcribe your meetings</Trans>
</p>

<div className="w-full max-w-[30rem] space-y-3 mb-8">
<PermissionItem
icon={<MicIcon className="h-5 w-5" />}
title={t`Microphone Access`}
description={t`Required for meeting transcription`}
done={micPermissionStatus.data}
isPending={micPermission.isPending}
onRequest={() => micPermission.mutate({})}
/>

<PermissionItem
icon={<Volume2Icon className="h-5 w-5" />}
title={t`System Audio Access`}
description={t`Required for meeting transcription`}
done={systemAudioPermissionStatus.data}
isPending={capturePermission.isPending}
onRequest={() => capturePermission.mutate({})}
/>
</div>

<PushableButton
onClick={onContinue}
disabled={!allPermissionsGranted}
className="w-full max-w-sm"
>
<Trans>Continue</Trans>
</PushableButton>

{!allPermissionsGranted && (
<p className="text-xs text-muted-foreground text-center mt-4">
<Trans>Grant both permissions to continue</Trans>
</p>
)}
</div>
);
}
Loading
Loading