Skip to content
Merged
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
@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import { ArrowUpIcon, BuildingIcon, FileTextIcon, UserIcon } from "lucide-react";
import { ArrowUpIcon, BuildingIcon, FileTextIcon, Square, UserIcon } from "lucide-react";
import { useCallback, useEffect, useRef } from "react";

import { useHypr, useRightPanel } from "@/contexts";
Expand All @@ -21,6 +21,7 @@ interface ChatInputProps {
entityType?: BadgeType;
onNoteBadgeClick?: () => void;
isGenerating?: boolean;
onStop?: () => void;
}

export function ChatInput(
Expand All @@ -34,6 +35,7 @@ export function ChatInput(
entityType = "note",
onNoteBadgeClick,
isGenerating = false,
onStop,
}: ChatInputProps,
) {
const { userId } = useHypr();
Expand Down Expand Up @@ -383,10 +385,18 @@ export function ChatInput(

<Button
size="icon"
onClick={handleSubmit}
disabled={!inputValue.trim() || isGenerating}
onClick={isGenerating ? onStop : handleSubmit}
Copy link

Choose a reason for hiding this comment

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

Ensure onClick never receives undefined so the Stop button always performs an action

Prompt for AI agents
Address the following comment on apps/desktop/src/components/right-panel/components/chat/chat-input.tsx at line 388:

<comment>Ensure onClick never receives undefined so the Stop button always performs an action</comment>

<file context>
@@ -383,10 +385,18 @@ export function ChatInput(
 
         &lt;Button
           size=&quot;icon&quot;
-          onClick={handleSubmit}
-          disabled={!inputValue.trim() || isGenerating}
+          onClick={isGenerating ? onStop : handleSubmit}
+          disabled={isGenerating ? false : (!inputValue.trim())}
         &gt;
</file context>

disabled={isGenerating ? false : (!inputValue.trim())}
>
<ArrowUpIcon className="h-4 w-4" />
{isGenerating
? (
<Square
className="h-4 w-4"
fill="currentColor"
strokeWidth={0}
/>
)
: <ArrowUpIcon className="h-4 w-4" />}
</Button>
Comment on lines 386 to 400
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Stop button can be enabled with no handler (click does nothing) — guard and add an a11y label.

If isGenerating is true and onStop is undefined, the button is enabled but has no onClick, resulting in a dead control. Also, the button lacks an accessible label.

Apply this diff to make behavior explicit and improve accessibility:

         <Button
           size="icon"
-          onClick={isGenerating ? onStop : handleSubmit}
-          disabled={isGenerating ? false : (!inputValue.trim())}
+          onClick={() => (isGenerating ? onStop?.() : handleSubmit())}
+          disabled={isGenerating ? !onStop : !inputValue.trim()}
+          aria-label={isGenerating ? "Stop generating" : "Send message"}
         >
           {isGenerating
             ? (
               <Square
                 className="h-4 w-4"
                 fill="currentColor"
                 strokeWidth={0}
               />
             )
             : <ArrowUpIcon className="h-4 w-4" />}
         </Button>
🤖 Prompt for AI Agents
In apps/desktop/src/components/right-panel/components/chat/chat-input.tsx around
lines 386 to 400, the send/stop Button becomes enabled when isGenerating is true
but onStop can be undefined, creating a dead control and lacks an accessible
label; fix by only wiring onClick to onStop when onStop is defined (e.g.,
onClick={onStop ?? handleSubmit} or conditionally set onClick) and disable the
button when isGenerating && !onStop, and add an aria-label (and optional title)
that reflects the action ("Stop generation" when isGenerating, otherwise "Send
message").

</div>
</div>
Expand Down
62 changes: 51 additions & 11 deletions apps/desktop/src/components/right-panel/hooks/useChatLogic.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { message } from "@tauri-apps/plugin-dialog";
import { useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";

import { useLicense } from "@/hooks/use-license";
import { commands as analyticsCommands } from "@hypr/plugin-analytics";
Expand Down Expand Up @@ -56,10 +56,25 @@ export function useChatLogic({
const [isGenerating, setIsGenerating] = useState(false);
const [isStreamingText, setIsStreamingText] = useState(false);
const isGeneratingRef = useRef(false);
const abortControllerRef = useRef<AbortController | null>(null);
const sessions = useSessions((state) => state.sessions);
const { getLicense } = useLicense();
const queryClient = useQueryClient();

// Reset generation state and abort ongoing streams when session changes
useEffect(() => {
Copy link

Choose a reason for hiding this comment

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

useEffect lacks a cleanup function, so ongoing streams aren’t aborted on component unmount which can leak resources and trigger setState on an unmounted component

Prompt for AI agents
Address the following comment on apps/desktop/src/components/right-panel/hooks/useChatLogic.ts at line 65:

<comment>useEffect lacks a cleanup function, so ongoing streams aren’t aborted on component unmount which can leak resources and trigger setState on an unmounted component</comment>

<file context>
@@ -56,10 +56,25 @@ export function useChatLogic({
   const [isGenerating, setIsGenerating] = useState(false);
   const [isStreamingText, setIsStreamingText] = useState(false);
   const isGeneratingRef = useRef(false);
+  const abortControllerRef = useRef&lt;AbortController | null&gt;(null);
   const sessions = useSessions((state) =&gt; state.sessions);
   const { getLicense } = useLicense();
   const queryClient = useQueryClient();
 
+  // Reset generation state and abort ongoing streams when session changes
</file context>

// Abort any ongoing generation when session changes
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
}

// Reset generation state for new session
setIsGenerating(false);
setIsStreamingText(false);
isGeneratingRef.current = false;
}, [sessionId]);

const handleApplyMarkdown = async (markdownContent: string) => {
if (!sessionId) {
console.error("No session ID available");
Expand Down Expand Up @@ -92,14 +107,14 @@ export function useChatLogic({

const userMessageCount = messages.filter(msg => msg.isUser).length;

if (userMessageCount >= 3 && !getLicense.data?.valid) {
if (userMessageCount >= 4 && !getLicense.data?.valid) {
if (userId) {
await analyticsCommands.event({
event: "pro_license_required_chat",
distinct_id: userId,
});
}
await message("3 messages are allowed per conversation for free users.", {
await message("4 messages are allowed per conversation for free users.", {
title: "Pro License Required",
kind: "info",
});
Expand Down Expand Up @@ -266,6 +281,9 @@ export function useChatLogic({
},
});

const abortController = new AbortController();
abortControllerRef.current = abortController;

const { fullStream } = streamText({
model,
messages: await prepareMessageHistory(
Expand Down Expand Up @@ -295,6 +313,7 @@ export function useChatLogic({
client.close();
}
},
abortSignal: abortController.signal,
});

let aiResponse = "";
Expand Down Expand Up @@ -456,29 +475,42 @@ export function useChatLogic({
setIsGenerating(false);
setIsStreamingText(false);
isGeneratingRef.current = false;
abortControllerRef.current = null; // Clear the abort controller on successful completion
} catch (error) {
console.error("AI error:", error);

const errorMsg = (error as any)?.error || "Unknown error";
console.error(error);

let errorMsg = "Unknown error";
if (typeof error === "string") {
errorMsg = error;
} else if (error instanceof Error) {
errorMsg = error.message || error.name || "Unknown error";
} else if ((error as any)?.error) {
errorMsg = (error as any).error;
} else if ((error as any)?.message) {
errorMsg = (error as any).message;
}

let finalErrorMesage = "";
let finalErrorMessage = "";

if (String(errorMsg).includes("too large")) {
finalErrorMesage =
finalErrorMessage =
"Sorry, I encountered an error. Please try again. Your transcript or meeting notes might be too large. Please try again with a smaller transcript or meeting notes."
+ "\n\n" + errorMsg;
} else if (String(errorMsg).includes("Request cancelled") || String(errorMsg).includes("Request canceled")) {
finalErrorMessage = "Request was cancelled mid-stream. Try again with a different message.";
} else {
finalErrorMesage = "Sorry, I encountered an error. Please try again. " + "\n\n" + errorMsg;
finalErrorMessage = "Sorry, I encountered an error. Please try again. " + "\n\n" + errorMsg;
}

setIsGenerating(false);
setIsStreamingText(false);
isGeneratingRef.current = false;
abortControllerRef.current = null; // Clear the abort controller on error

// Create error message
const errorMessage: Message = {
id: aiMessageId,
content: finalErrorMesage,
content: finalErrorMessage,
isUser: false,
timestamp: new Date(),
type: "text-delta",
Expand All @@ -491,7 +523,7 @@ export function useChatLogic({
group_id: groupId,
created_at: new Date().toISOString(),
role: "Assistant",
content: finalErrorMesage,
content: finalErrorMessage,
type: "text-delta",
});
}
Expand All @@ -516,12 +548,20 @@ export function useChatLogic({
}
};

const handleStop = useCallback(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
}, []);

return {
isGenerating,
isStreamingText,
handleSubmit,
handleQuickAction,
handleApplyMarkdown,
handleKeyDown,
handleStop,
};
}
2 changes: 2 additions & 0 deletions apps/desktop/src/components/right-panel/views/chat-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export function ChatView() {
handleQuickAction,
handleApplyMarkdown,
handleKeyDown,
handleStop,
} = useChatLogic({
sessionId,
userId,
Expand Down Expand Up @@ -186,6 +187,7 @@ export function ChatView() {
entityType={activeEntity?.type}
onNoteBadgeClick={handleNoteBadgeClick}
isGenerating={isGenerating}
onStop={handleStop}
/>
</div>
);
Expand Down
Loading
Loading