Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -17,7 +17,10 @@ export function Banner({
className={cn([
"relative group overflow-hidden rounded-lg",
"flex flex-col gap-2",
"bg-white border border-neutral-200 shadow-sm p-4",
"bg-white p-4",
banner.variant === "error"
? "border border-red-300 shadow-sm shadow-red-100"
: "border border-neutral-200 shadow-sm",
])}
>
{banner.dismissible && onDismiss && (
Expand Down
19 changes: 18 additions & 1 deletion apps/desktop/src/components/main/sidebar/banner/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { AnimatePresence, motion } from "motion/react";
import { useCallback, useEffect, useMemo, useState } from "react";

import type { ServerStatus } from "@hypr/plugin-local-stt";
import { cn } from "@hypr/utils";

import { useAuth } from "../../../../auth";
import { useConfigValues } from "../../../../config/use-config";
import { useSTTConnection } from "../../../../hooks/useSTTConnection";
import { useTabs } from "../../../../store/zustand/tabs";
import { Banner } from "./component";
import { createBannerRegistry, getBannerToShow } from "./registry";
Expand Down Expand Up @@ -32,7 +34,18 @@ export function BannerArea({
"current_stt_model",
] as const);
const hasLLMConfigured = !!(current_llm_provider && current_llm_model);
const hasSttConfigured = !!(current_stt_provider && current_stt_model);

const { conn: sttConnection, local: sttLocal } = useSTTConnection();
const sttServerStatus = sttLocal.data?.status as ServerStatus | undefined;

const isLocalSttModel =
current_stt_provider === "hyprnote" &&
!!current_stt_model &&
current_stt_model !== "cloud";
Comment on lines +41 to +44
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic Inconsistency: The definition of isLocalSttModel here is inconsistent with the helper function in useSTTConnection.ts.

This code defines local models as:

current_stt_provider === "hyprnote" && current_stt_model !== "cloud"

But the helper function defines them as:

provider === "hyprnote" && (model.startsWith("am-") || model.startsWith("Quantized"))

This will cause banner logic to behave inconsistently. For example, if a user selects a hyprnote model that doesn't start with "am-" or "Quantized" and isn't "cloud", the banner logic here will treat it as local, but select.tsx (which imports the helper) will not.

Fix: Import and use the isLocalSttModel helper function instead:

import { isLocalSttModel, useSTTConnection } from "../../../../hooks/useSTTConnection";

// Replace lines 41-44 with:
const isLocalSttModel = isLocalSttModel(current_stt_provider, current_stt_model);

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

This comment came from an experimental review—please leave feedback if it was helpful/unhelpful. Learn more about experimental comments here.


const hasSttConfigured = isLocalSttModel
? !!sttConnection
: !!(current_stt_provider && current_stt_model && sttConnection);

const currentTab = useTabs((state) => state.currentTab);
const isAiTranscriptionTabActive =
Expand Down Expand Up @@ -72,6 +85,8 @@ export function BannerArea({
isAuthenticated,
hasLLMConfigured,
hasSttConfigured,
sttServerStatus,
isLocalSttModel,
isAiTranscriptionTabActive,
isAiIntelligenceTabActive,
onSignIn: handleSignIn,
Expand All @@ -82,6 +97,8 @@ export function BannerArea({
isAuthenticated,
hasLLMConfigured,
hasSttConfigured,
sttServerStatus,
isLocalSttModel,
isAiTranscriptionTabActive,
isAiIntelligenceTabActive,
handleSignIn,
Expand Down
56 changes: 55 additions & 1 deletion apps/desktop/src/components/main/sidebar/banner/registry.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ServerStatus } from "@hypr/plugin-local-stt";

import type { BannerCondition, BannerType } from "./types";

type BannerRegistryEntry = {
Expand All @@ -9,6 +11,8 @@ type BannerRegistryParams = {
isAuthenticated: boolean;
hasLLMConfigured: boolean;
hasSttConfigured: boolean;
sttServerStatus: ServerStatus | undefined;
isLocalSttModel: boolean;
isAiTranscriptionTabActive: boolean;
isAiIntelligenceTabActive: boolean;
onSignIn: () => void | Promise<void>;
Expand All @@ -20,6 +24,8 @@ export function createBannerRegistry({
isAuthenticated,
hasLLMConfigured,
hasSttConfigured,
sttServerStatus,
isLocalSttModel,
isAiTranscriptionTabActive,
isAiIntelligenceTabActive,
onSignIn,
Expand All @@ -28,6 +34,53 @@ export function createBannerRegistry({
}: BannerRegistryParams): BannerRegistryEntry[] {
// order matters
return [
{
banner: {
id: "stt-loading",
description: (
<>
Transcription model is
<strong className="font-mono animate-ping text-amber-500">
loading
</strong>
. This may take a moment.
</>
),
primaryAction: {
label: "View status",
onClick: onOpenSTTSettings,
},
dismissible: false,
},
condition: () =>
isLocalSttModel &&
sttServerStatus === "loading" &&
!hasSttConfigured &&
!isAiTranscriptionTabActive,
},
{
banner: {
id: "stt-unreachable",
variant: "error",
description: (
<>
Transcription model{" "}
<strong className="font-mono text-red-500">failed to start</strong>.
Please try again.
</>
),
primaryAction: {
label: "Configure transcription",
onClick: onOpenSTTSettings,
},
dismissible: false,
},
condition: () =>
isLocalSttModel &&
sttServerStatus === "unreachable" &&
!hasSttConfigured &&
!isAiTranscriptionTabActive,
},
{
banner: {
id: "missing-stt",
Expand All @@ -43,7 +96,8 @@ export function createBannerRegistry({
},
dismissible: false,
},
condition: () => !hasSttConfigured && !isAiTranscriptionTabActive,
condition: () =>
!hasSttConfigured && !isLocalSttModel && !isAiTranscriptionTabActive,
},
{
banner: {
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/components/main/sidebar/banner/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type BannerType = {
primaryAction?: BannerAction;
secondaryAction?: BannerAction;
dismissible: boolean;
variant?: "default" | "error";
};

export type BannerCondition = () => boolean;
6 changes: 3 additions & 3 deletions apps/desktop/src/components/settings/ai/llm/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,10 @@ export function SelectProviderAndModel() {
<div
className={cn([
"flex flex-col gap-4",
"p-4 rounded-xl border border-neutral-200",
"p-4 rounded-xl border",
!!current_llm_provider && !!current_llm_model
? "bg-neutral-50"
: "bg-red-50",
? ["bg-neutral-50", "border-neutral-200"]
: ["bg-red-50", "border-red-200"],
])}
>
<div className="flex flex-row items-center gap-4">
Expand Down
30 changes: 25 additions & 5 deletions apps/desktop/src/components/settings/ai/stt/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { cn } from "@hypr/utils";

import { useBillingAccess } from "../../../../billing";
import { useConfigValues } from "../../../../config/use-config";
import {
isLocalSttModel,
useSTTConnection,
} from "../../../../hooks/useSTTConnection";
import * as settings from "../../../../store/tinybase/settings";
import {
getProviderSelectionBlockers,
Expand All @@ -35,6 +39,13 @@ export function SelectProviderAndModel() {
] as const);
const billing = useBillingAccess();
const configuredProviders = useConfiguredMapping();
const { local: sttLocal } = useSTTConnection();
const sttServerStatus = sttLocal.data?.status;

const isLocal = isLocalSttModel(current_stt_provider, current_stt_model);

const isUnreachable = isLocal && sttServerStatus === "unreachable";
const isNotConfigured = !current_stt_provider || !current_stt_model;

const handleSelectProvider = settings.UI.useSetValueCallback(
"current_stt_provider",
Expand Down Expand Up @@ -79,10 +90,10 @@ export function SelectProviderAndModel() {
<div
className={cn([
"flex flex-col gap-4",
"p-4 rounded-xl border border-neutral-200",
!!current_stt_provider && !!current_stt_model
? "bg-neutral-50"
: "bg-red-50",
"p-4 rounded-xl border",
isNotConfigured || isUnreachable
? ["bg-red-50", "border-red-200"]
: ["bg-neutral-50", "border-neutral-200"],
])}
>
<div className="flex flex-row items-center gap-4">
Expand Down Expand Up @@ -212,14 +223,23 @@ export function SelectProviderAndModel() {
)}
</div>

{(!current_stt_provider || !current_stt_model) && (
{isNotConfigured && (
<div className="flex items-center gap-2 pt-2 border-t border-red-200">
<span className="text-sm text-red-600">
<strong className="font-medium">Transcription model</strong> is
needed to make Hyprnote listen to your conversations.
</span>
</div>
)}

{isUnreachable && (
<div className="flex items-center gap-2 pt-2 border-t border-red-200">
<span className="text-sm text-red-600">
<strong className="font-medium">Transcription model</strong>{" "}
failed to start. Please try again.
</span>
</div>
)}
</div>
</div>
);
Expand Down
17 changes: 12 additions & 5 deletions apps/desktop/src/hooks/useSTTConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ import { ProviderId } from "../components/settings/ai/stt/shared";
import { env } from "../env";
import * as settings from "../store/tinybase/settings";

export function isLocalSttModel(
provider: string | undefined,
model: string | undefined,
): boolean {
return (
provider === "hyprnote" &&
!!model &&
(model.startsWith("am-") || model.startsWith("Quantized"))
);
}

export const useSTTConnection = () => {
const auth = useAuth();
const billing = useBillingAccess();
Expand All @@ -26,11 +37,7 @@ export const useSTTConnection = () => {
settings.STORE_ID,
) as AIProviderStorage | undefined;

const isLocalModel =
current_stt_provider === "hyprnote" &&
!!current_stt_model &&
(current_stt_model.startsWith("am-") ||
current_stt_model.startsWith("Quantized"));
const isLocalModel = isLocalSttModel(current_stt_provider, current_stt_model);

const isCloudModel =
current_stt_provider === "hyprnote" && current_stt_model === "cloud";
Expand Down
Loading