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,