diff --git a/apps/desktop/src/components/main/body/sessions/note-input/enhanced/config-error.tsx b/apps/desktop/src/components/main/body/sessions/note-input/enhanced/config-error.tsx new file mode 100644 index 0000000000..2c2deee1fe --- /dev/null +++ b/apps/desktop/src/components/main/body/sessions/note-input/enhanced/config-error.tsx @@ -0,0 +1,70 @@ +import { ArrowRightIcon } from "lucide-react"; + +import { commands as windowsCommands } from "@hypr/plugin-windows"; +import { Button } from "@hypr/ui/components/ui/button"; + +import type { LLMConnectionStatus } from "../../../../../../hooks/useLLMConnection"; + +export function ConfigError({ status }: { status: LLMConnectionStatus }) { + const handleConfigureClick = () => { + windowsCommands + .windowShow({ type: "settings" }) + .then(() => new Promise((resolve) => setTimeout(resolve, 1000))) + .then(() => + windowsCommands.windowEmitNavigate( + { type: "settings" }, + { + path: "/app/settings", + search: { tab: "intelligence" }, + }, + ), + ); + }; + + const message = getMessageForStatus(status); + + return ( +
+

+ {message} +

+ +
+ ); +} + +function getMessageForStatus(status: LLMConnectionStatus): string { + if (status.status === "pending" && status.reason === "missing_provider") { + return "You need to configure a language model to summarize this meeting"; + } + + if (status.status === "pending" && status.reason === "missing_model") { + return "You need to select a model to summarize this meeting"; + } + + if (status.status === "error" && status.reason === "unauthenticated") { + return "You need to sign in to use Hyprnote's language model"; + } + + if (status.status === "error" && status.reason === "missing_config") { + const missing = status.missing; + if (missing.includes("api_key") && missing.includes("base_url")) { + return "You need to configure the API key and base URL for your language model provider"; + } + if (missing.includes("api_key")) { + return "You need to configure the API key for your language model provider"; + } + if (missing.includes("base_url")) { + return "You need to configure the base URL for your language model provider"; + } + } + + return "You need to configure a language model to summarize this meeting"; +} diff --git a/apps/desktop/src/components/main/body/sessions/note-input/enhanced/index.tsx b/apps/desktop/src/components/main/body/sessions/note-input/enhanced/index.tsx index 6c7c0595ac..37d832553c 100644 --- a/apps/desktop/src/components/main/body/sessions/note-input/enhanced/index.tsx +++ b/apps/desktop/src/components/main/body/sessions/note-input/enhanced/index.tsx @@ -3,7 +3,9 @@ import { forwardRef } from "react"; import { type TiptapEditor } from "@hypr/tiptap/editor"; import { useAITaskTask } from "../../../../../../hooks/useAITaskTask"; +import { useLLMConnectionStatus } from "../../../../../../hooks/useLLMConnection"; import { createTaskId } from "../../../../../../store/zustand/ai-task/task-configs"; +import { ConfigError } from "./config-error"; import { EnhancedEditor } from "./editor"; import { StreamingView } from "./streaming"; @@ -12,9 +14,19 @@ export const Enhanced = forwardRef< { sessionId: string; enhancedNoteId: string } >(({ sessionId, enhancedNoteId }, ref) => { const taskId = createTaskId(enhancedNoteId, "enhance"); - + const llmStatus = useLLMConnectionStatus(); const { status } = useAITaskTask(taskId, "enhance"); + const isConfigError = + llmStatus.status === "pending" || + (llmStatus.status === "error" && + (llmStatus.reason === "missing_config" || + llmStatus.reason === "unauthenticated")); + + if (status === "idle" && isConfigError) { + return ; + } + if (status === "error") { return null; } diff --git a/apps/desktop/src/components/main/body/sessions/note-input/header.tsx b/apps/desktop/src/components/main/body/sessions/note-input/header.tsx index 000cd2a498..18be90e983 100644 --- a/apps/desktop/src/components/main/body/sessions/note-input/header.tsx +++ b/apps/desktop/src/components/main/body/sessions/note-input/header.tsx @@ -18,7 +18,10 @@ import { cn } from "@hypr/utils"; import { useListener } from "../../../../../contexts/listener"; import { useAITaskTask } from "../../../../../hooks/useAITaskTask"; import { useCreateEnhancedNote } from "../../../../../hooks/useEnhancedNotes"; -import { useLanguageModel } from "../../../../../hooks/useLLMConnection"; +import { + useLanguageModel, + useLLMConnectionStatus, +} from "../../../../../hooks/useLLMConnection"; import * as main from "../../../../../store/tinybase/main"; import { createTaskId } from "../../../../../store/zustand/ai-task/task-configs"; import { type EditorView } from "../../../../../store/zustand/tabs/schema"; @@ -409,6 +412,7 @@ function labelForEditorView(view: EditorView): string { function useEnhanceLogic(sessionId: string, enhancedNoteId: string) { const model = useLanguageModel(); + const llmStatus = useLLMConnectionStatus(); const taskId = createTaskId(enhancedNoteId, "enhance"); const [missingModelError, setMissingModelError] = useState( null, @@ -460,8 +464,17 @@ function useEnhanceLogic(sessionId: string, enhancedNoteId: string) { } }, [model, missingModelError]); + const isConfigError = + llmStatus.status === "pending" || + (llmStatus.status === "error" && + (llmStatus.reason === "missing_config" || + llmStatus.reason === "unauthenticated")); + + const isIdleWithConfigError = enhanceTask.isIdle && isConfigError; + const error = missingModelError ?? enhanceTask.error; - const isError = !!missingModelError || enhanceTask.isError; + const isError = + !!missingModelError || enhanceTask.isError || isIdleWithConfigError; return { isGenerating: enhanceTask.isGenerating, diff --git a/apps/desktop/src/components/main/body/sessions/note-input/index.tsx b/apps/desktop/src/components/main/body/sessions/note-input/index.tsx index 0033bb0200..f07638255d 100644 --- a/apps/desktop/src/components/main/body/sessions/note-input/index.tsx +++ b/apps/desktop/src/components/main/body/sessions/note-input/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import type { TiptapEditor } from "@hypr/tiptap/editor"; @@ -28,9 +28,15 @@ export function NoteInput({ useAutoEnhance(tab); useAutoTitle(tab); - const handleTabChange = (view: EditorView) => { - updateSessionTabState(tab, { editor: view }); - }; + const tabRef = useRef(tab); + tabRef.current = tab; + + const handleTabChange = useCallback( + (view: EditorView) => { + updateSessionTabState(tabRef.current, { editor: view }); + }, + [updateSessionTabState], + ); const currentTab: EditorView = useCurrentNoteTab(tab); diff --git a/apps/desktop/src/hooks/useAutoEnhance.ts b/apps/desktop/src/hooks/useAutoEnhance.ts index d6fc5772e0..79bd5b8c24 100644 --- a/apps/desktop/src/hooks/useAutoEnhance.ts +++ b/apps/desktop/src/hooks/useAutoEnhance.ts @@ -33,6 +33,8 @@ export function useAutoEnhance(tab: Extract) { ); const startedTasksRef = useRef>(new Set()); + const tabRef = useRef(tab); + tabRef.current = tab; const enhanceTaskId = autoEnhancedNoteId ? createTaskId(autoEnhancedNoteId, "enhance") @@ -54,7 +56,7 @@ export function useAutoEnhance(tab: Extract) { }); const createAndStartEnhance = useCallback(() => { - if (!model || !hasTranscript) { + if (!hasTranscript) { return; } @@ -63,17 +65,10 @@ export function useAutoEnhance(tab: Extract) { setAutoEnhancedNoteId(enhancedNoteId); - updateSessionTabState(tab, { + updateSessionTabState(tabRef.current, { editor: { type: "enhanced", id: enhancedNoteId }, }); - }, [ - hasTranscript, - model, - sessionId, - tab, - updateSessionTabState, - createEnhancedNote, - ]); + }, [hasTranscript, sessionId, updateSessionTabState, createEnhancedNote]); useEffect(() => { if ( diff --git a/apps/desktop/src/hooks/useLLMConnection.ts b/apps/desktop/src/hooks/useLLMConnection.ts index c2b070ab5a..1a0e20715f 100644 --- a/apps/desktop/src/hooks/useLLMConnection.ts +++ b/apps/desktop/src/hooks/useLLMConnection.ts @@ -26,7 +26,7 @@ type LLMConnectionInfo = { apiKey: string; }; -type LLMConnectionStatus = +export type LLMConnectionStatus = | { status: "pending"; reason: "missing_provider" } | { status: "pending"; reason: "missing_model"; providerId: ProviderId } | { status: "error"; reason: "provider_not_found"; providerId: string } @@ -240,6 +240,11 @@ export const useLLMConnection = (): LLMConnectionResult => { }, [auth, current_llm_model, current_llm_provider, providerConfig]); }; +export const useLLMConnectionStatus = (): LLMConnectionStatus => { + const { status } = useLLMConnection(); + return status; +}; + const wrapWithThinkingMiddleware = (model: Exclude) => { return wrapLanguageModel({ model, diff --git a/apps/desktop/src/hooks/useRunBatch.ts b/apps/desktop/src/hooks/useRunBatch.ts index e97237f044..4c3d55cae8 100644 --- a/apps/desktop/src/hooks/useRunBatch.ts +++ b/apps/desktop/src/hooks/useRunBatch.ts @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import { useCallback, useRef } from "react"; import type { BatchParams } from "@hypr/plugin-listener"; @@ -34,6 +34,9 @@ export const useRunBatch = (sessionId: string) => { }); const updateSessionTabState = useTabs((state) => state.updateSessionTabState); + const sessionTabRef = useRef(sessionTab); + sessionTabRef.current = sessionTab; + const { conn } = useSTTConnection(); const keywords = useKeywords(sessionId); const languages = useConfigValue("spoken_languages"); @@ -62,8 +65,10 @@ export const useRunBatch = (sessionId: string) => { return; } - if (sessionTab) { - updateSessionTabState(sessionTab, { editor: { type: "transcript" } }); + if (sessionTabRef.current) { + updateSessionTabState(sessionTabRef.current, { + editor: { type: "transcript" }, + }); } const transcriptId = id(); @@ -150,7 +155,6 @@ export const useRunBatch = (sessionId: string) => { languages, runBatch, sessionId, - sessionTab, store, updateSessionTabState, user_id,