diff --git a/apps/desktop/src/components/main/body/ai.tsx b/apps/desktop/src/components/main/body/ai.tsx index 11549a289f..5b49998fd1 100644 --- a/apps/desktop/src/components/main/body/ai.tsx +++ b/apps/desktop/src/components/main/body/ai.tsx @@ -60,42 +60,38 @@ function AIView({ tab }: { tab: Extract }) { [updateAiTabState, tab], ); - const headerAction = ( -
- - -
- ); - return ( -
- {activeTab === "transcription" ? ( - - ) : ( - - )} +
+
+ + +
+
+ {activeTab === "transcription" ? : } +
); } diff --git a/apps/desktop/src/components/main/sidebar/banner/component.tsx b/apps/desktop/src/components/main/sidebar/banner/component.tsx index 94b463082f..cea0abc572 100644 --- a/apps/desktop/src/components/main/sidebar/banner/component.tsx +++ b/apps/desktop/src/components/main/sidebar/banner/component.tsx @@ -13,7 +13,7 @@ export function Banner({ onDismiss?: () => void; }) { return ( -
+
diff --git a/apps/desktop/src/components/settings/ai/llm/configure.tsx b/apps/desktop/src/components/settings/ai/llm/configure.tsx index d78ab6b2d8..7589c50207 100644 --- a/apps/desktop/src/components/settings/ai/llm/configure.tsx +++ b/apps/desktop/src/components/settings/ai/llm/configure.tsx @@ -1,7 +1,3 @@ -import { useForm } from "@tanstack/react-form"; -import { useEffect } from "react"; - -import { type AIProvider, aiProviderSchema } from "@hypr/store"; import { Accordion, AccordionContent, @@ -12,13 +8,13 @@ import { Button } from "@hypr/ui/components/ui/button"; import { cn } from "@hypr/utils"; import { useBillingAccess } from "../../../../billing"; -import { FormField, StyledStreamdown, useProvider } from "../shared"; +import { NonHyprProviderCard, StyledStreamdown } from "../shared"; import { ProviderId, PROVIDERS } from "./shared"; export function ConfigureProviders() { return (
-

Configure Providers

+

Configure Providers

{PROVIDERS.filter((provider) => provider.id !== "hyprnote").map( (provider) => ( - + } + /> ), )} @@ -37,118 +39,6 @@ export function ConfigureProviders() { ); } -function NonHyprProviderCard({ - config, -}: { - config: (typeof PROVIDERS)[number]; -}) { - const billing = useBillingAccess(); - const [provider, setProvider] = useProvider(config.id); - const locked = config.requiresPro && !billing.isPro; - - useEffect(() => { - if (!provider && config.baseUrl && !config.apiKey) { - setProvider({ - type: "llm", - base_url: config.baseUrl, - api_key: "", - }); - } - }, [provider, config.baseUrl, config.apiKey, setProvider]); - - const form = useForm({ - onSubmit: ({ value }) => setProvider(value), - defaultValues: - provider ?? - ({ - type: "llm", - base_url: config.baseUrl ?? "", - api_key: "", - } satisfies AIProvider), - listeners: { - onChange: ({ formApi }) => { - queueMicrotask(() => { - const { - form: { errors }, - } = formApi.getAllErrors(); - if (errors.length > 0) { - console.log(errors); - } - - formApi.handleSubmit(); - }); - }, - }, - validators: { onChange: aiProviderSchema }, - }); - - return ( - - -
- {config.icon} - {config.displayName} -
-
- - - -
{ - e.preventDefault(); - e.stopPropagation(); - }} - > - {!config.baseUrl && ( - - {(field) => ( - - )} - - )} - {config?.apiKey && ( - - {(field) => ( - - )} - - )} - {config.baseUrl && ( -
- - Advanced - -
- - {(field) => ( - - )} - -
-
- )} -
-
-
- ); -} - function HyprProviderCard({ providerId, providerName, @@ -164,8 +54,11 @@ function HyprProviderCard({ return ( - - +
+
); diff --git a/apps/desktop/src/components/settings/ai/llm/select.tsx b/apps/desktop/src/components/settings/ai/llm/select.tsx index 16b905d2fe..df2b8c7a14 100644 --- a/apps/desktop/src/components/settings/ai/llm/select.tsx +++ b/apps/desktop/src/components/settings/ai/llm/select.tsx @@ -28,9 +28,7 @@ import { ModelCombobox } from "../shared/model-combobox"; import { HealthCheckForConnection } from "./health"; import { PROVIDERS } from "./shared"; -export function SelectProviderAndModel({ - headerAction, -}: { headerAction?: React.ReactNode } = {}) { +export function SelectProviderAndModel() { const configuredProviders = useConfiguredMapping(); const { current_llm_model, current_llm_provider } = useConfigValues([ @@ -77,10 +75,7 @@ export function SelectProviderAndModel({ return (
-
-

Model being used

- {headerAction} -
+

Model being used

)}
+ + {(!current_llm_provider || !current_llm_model) && ( +
+ + Language model is needed + to make Hyprnote summarize and chat about your conversations. + +
+ )}
); diff --git a/apps/desktop/src/components/settings/ai/shared/index.tsx b/apps/desktop/src/components/settings/ai/shared/index.tsx index 471becc815..104b643621 100644 --- a/apps/desktop/src/components/settings/ai/shared/index.tsx +++ b/apps/desktop/src/components/settings/ai/shared/index.tsx @@ -1,9 +1,15 @@ import { Icon } from "@iconify-icon/react"; -import { type AnyFieldApi } from "@tanstack/react-form"; +import { type AnyFieldApi, useForm } from "@tanstack/react-form"; +import type { ReactNode } from "react"; import { Streamdown } from "streamdown"; import type { AIProvider } from "@hypr/store"; import { aiProviderSchema } from "@hypr/store"; +import { + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@hypr/ui/components/ui/accordion"; import { InputGroup, InputGroupAddon, @@ -12,10 +18,182 @@ import { } from "@hypr/ui/components/ui/input-group"; import { cn } from "@hypr/utils"; +import { useBillingAccess } from "../../../../billing"; import * as settings from "../../../../store/tinybase/settings"; export * from "./model-combobox"; +type ProviderType = "stt" | "llm"; + +type ProviderConfig = { + id: string; + displayName: string; + icon: ReactNode; + badge?: string | null; + baseUrl?: string; + apiKey?: boolean; + disabled?: boolean; + requiresPro?: boolean; +}; + +function useIsProviderConfigured( + providerId: string, + providerType: ProviderType, + providers: readonly ProviderConfig[], +) { + const query = + providerType === "stt" + ? settings.QUERIES.sttProviders + : settings.QUERIES.llmProviders; + + const configuredProviders = settings.UI.useResultTable( + query, + settings.STORE_ID, + ); + const providerDef = providers.find((p) => p.id === providerId); + const config = configuredProviders[providerId]; + + if (!config) { + return false; + } + + if (providerType === "stt") { + if (!providerDef?.baseUrl && !config.base_url) { + return false; + } + if (!config.api_key) { + return false; + } + } else { + if (!config.base_url) { + return false; + } + if (providerDef?.apiKey && !config.api_key) { + return false; + } + } + + return true; +} + +export function NonHyprProviderCard({ + config, + providerType, + providers, + providerContext, +}: { + config: ProviderConfig; + providerType: ProviderType; + providers: readonly ProviderConfig[]; + providerContext?: ReactNode; +}) { + const billing = useBillingAccess(); + const [provider, setProvider] = useProvider(config.id); + const locked = config.requiresPro && !billing.isPro; + const isConfigured = useIsProviderConfigured( + config.id, + providerType, + providers, + ); + + const showApiKey = providerType === "stt" || config.apiKey; + + const form = useForm({ + onSubmit: ({ value }) => setProvider(value), + defaultValues: + provider ?? + ({ + type: providerType, + base_url: config.baseUrl ?? "", + api_key: "", + } satisfies AIProvider), + listeners: { + onChange: ({ formApi }) => { + queueMicrotask(() => { + formApi.handleSubmit(); + }); + }, + }, + validators: { onChange: aiProviderSchema }, + }); + + return ( + + +
+ {config.icon} + {config.displayName} + {config.badge && ( + + {config.badge} + + )} +
+
+ + {providerContext} + +
{ + e.preventDefault(); + e.stopPropagation(); + }} + > + {!config.baseUrl && ( + + {(field) => ( + + )} + + )} + {showApiKey && ( + + {(field) => ( + + )} + + )} + {config.baseUrl && ( +
+ + Advanced + +
+ + {(field) => ( + + )} + +
+
+ )} +
+
+
+ ); +} + const streamdownComponents = { ul: (props: React.HTMLAttributes) => { return ( @@ -57,7 +235,7 @@ export function StyledStreamdown({ ); } -export function useProvider(id: string) { +function useProvider(id: string) { const providerRow = settings.UI.useRow("ai_providers", id, settings.STORE_ID); const setProvider = settings.UI.useSetPartialRowCallback( "ai_providers", @@ -71,7 +249,7 @@ export function useProvider(id: string) { return [data, setProvider] as const; } -export function FormField({ +function FormField({ field, label, icon, diff --git a/apps/desktop/src/components/settings/ai/stt/configure.tsx b/apps/desktop/src/components/settings/ai/stt/configure.tsx index bff8853f31..f63006c521 100644 --- a/apps/desktop/src/components/settings/ai/stt/configure.tsx +++ b/apps/desktop/src/components/settings/ai/stt/configure.tsx @@ -1,5 +1,4 @@ import { Icon } from "@iconify-icon/react"; -import { useForm } from "@tanstack/react-form"; import { useQuery } from "@tanstack/react-query"; import { openPath } from "@tauri-apps/plugin-opener"; import { arch } from "@tauri-apps/plugin-os"; @@ -10,7 +9,6 @@ import { events as localSttEvents, type SupportedSttModel, } from "@hypr/plugin-local-stt"; -import { type AIProvider, aiProviderSchema } from "@hypr/store"; import { Accordion, AccordionContent, @@ -24,13 +22,13 @@ import { useBillingAccess } from "../../../../billing"; import { useListener } from "../../../../contexts/listener"; import { useIsMacos } from "../../../../hooks/usePlatform"; import * as settings from "../../../../store/tinybase/settings"; -import { FormField, StyledStreamdown, useProvider } from "../shared"; +import { NonHyprProviderCard, StyledStreamdown } from "../shared"; import { ProviderId, PROVIDERS, sttModelQueries } from "./shared"; export function ConfigureProviders() { return (
-

Configure Providers

+

Configure Providers

{PROVIDERS.filter((provider) => provider.id !== "hyprnote").map( (provider) => ( - + } + /> ), )} @@ -50,111 +54,6 @@ export function ConfigureProviders() { ); } -function NonHyprProviderCard({ - config, -}: { - config: (typeof PROVIDERS)[number]; -}) { - const billing = useBillingAccess(); - const [provider, setProvider] = useProvider(config.id); - const locked = config.requiresPro && !billing.isPro; - - const form = useForm({ - onSubmit: ({ value }) => setProvider(value), - defaultValues: - provider ?? - ({ - type: "stt", - base_url: config.baseUrl ?? "", - api_key: "", - } satisfies AIProvider), - listeners: { - onChange: ({ formApi }) => { - queueMicrotask(() => { - const { - form: { errors }, - } = formApi.getAllErrors(); - if (errors.length > 0) { - console.log(errors); - } - - formApi.handleSubmit(); - }); - }, - }, - validators: { onChange: aiProviderSchema }, - }); - - return ( - - -
- {config.icon} - {config.displayName} - {config.badge && ( - - {config.badge} - - )} -
-
- - - -
{ - e.preventDefault(); - e.stopPropagation(); - }} - > - {!config.baseUrl && ( - - {(field) => ( - - )} - - )} - - {(field) => ( - - )} - - {config.baseUrl && ( -
- - Advanced - -
- - {(field) => ( - - )} - -
-
- )} -
-
-
- ); -} - function HyprProviderCard({ providerId, providerName, @@ -177,7 +76,10 @@ function HyprProviderCard({ return (
diff --git a/apps/desktop/src/components/settings/ai/stt/index.tsx b/apps/desktop/src/components/settings/ai/stt/index.tsx index c8bca034b5..6769341fe5 100644 --- a/apps/desktop/src/components/settings/ai/stt/index.tsx +++ b/apps/desktop/src/components/settings/ai/stt/index.tsx @@ -1,12 +1,10 @@ import { ConfigureProviders } from "./configure"; -import { HealthCheckForAvailability } from "./health"; import { SelectProviderAndModel } from "./select"; -export function STT({ headerAction }: { headerAction?: React.ReactNode } = {}) { +export function STT() { return ( -
- - +
+
); diff --git a/apps/desktop/src/components/settings/ai/stt/select.tsx b/apps/desktop/src/components/settings/ai/stt/select.tsx index ee0e8f47bc..783ac55571 100644 --- a/apps/desktop/src/components/settings/ai/stt/select.tsx +++ b/apps/desktop/src/components/settings/ai/stt/select.tsx @@ -24,9 +24,7 @@ import { sttModelQueries, } from "./shared"; -export function SelectProviderAndModel({ - headerAction, -}: { headerAction?: React.ReactNode } = {}) { +export function SelectProviderAndModel() { const { current_stt_provider, current_stt_model } = useConfigValues([ "current_stt_provider", "current_stt_model", @@ -73,10 +71,7 @@ export function SelectProviderAndModel({ return (
-
-

Model being used

- {headerAction} -
+

Model being used

)}
+ + {(!current_stt_provider || !current_stt_model) && ( +
+ + Transcription model is + needed to make Hyprnote listen to your conversations. + +
+ )}
); diff --git a/apps/desktop/src/components/settings/calendar/index.tsx b/apps/desktop/src/components/settings/calendar/index.tsx index 49d65642f7..09c853fac9 100644 --- a/apps/desktop/src/components/settings/calendar/index.tsx +++ b/apps/desktop/src/components/settings/calendar/index.tsx @@ -7,4 +7,3 @@ export function SettingsCalendar() {
); } - diff --git a/apps/desktop/src/components/settings/calendar/shared.tsx b/apps/desktop/src/components/settings/calendar/shared.tsx index 1712d107d2..8292d44e9c 100644 --- a/apps/desktop/src/components/settings/calendar/shared.tsx +++ b/apps/desktop/src/components/settings/calendar/shared.tsx @@ -42,4 +42,3 @@ const _PROVIDERS = [ ] as const satisfies readonly CalendarProvider[]; export const PROVIDERS = [..._PROVIDERS]; - diff --git a/apps/desktop/src/store/tinybase/jsonPersister.ts b/apps/desktop/src/store/tinybase/jsonPersister.ts index 96abd956bc..417f1ba179 100644 --- a/apps/desktop/src/store/tinybase/jsonPersister.ts +++ b/apps/desktop/src/store/tinybase/jsonPersister.ts @@ -177,6 +177,13 @@ export async function migrateKeysJsonToSettings(): Promise { ); return true; } catch (error) { + const errorStr = String(error); + if ( + errorStr.includes("No such file or directory") || + errorStr.includes("ENOENT") + ) { + return false; + } console.error("[migrateKeysJsonToSettings] error:", error); return false; }