Skip to content

Commit ca4b752

Browse files
authored
Chat abort (#1381)
* implemented chat aborting * ran tests * minor fixes - coderabbit
1 parent 1d8c689 commit ca4b752

File tree

7 files changed

+150
-96
lines changed

7 files changed

+150
-96
lines changed

apps/desktop/src/components/right-panel/components/chat/chat-input.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useQuery } from "@tanstack/react-query";
2-
import { ArrowUpIcon, BuildingIcon, FileTextIcon, UserIcon } from "lucide-react";
2+
import { ArrowUpIcon, BuildingIcon, FileTextIcon, Square, UserIcon } from "lucide-react";
33
import { useCallback, useEffect, useRef } from "react";
44

55
import { useHypr, useRightPanel } from "@/contexts";
@@ -21,6 +21,7 @@ interface ChatInputProps {
2121
entityType?: BadgeType;
2222
onNoteBadgeClick?: () => void;
2323
isGenerating?: boolean;
24+
onStop?: () => void;
2425
}
2526

2627
export function ChatInput(
@@ -34,6 +35,7 @@ export function ChatInput(
3435
entityType = "note",
3536
onNoteBadgeClick,
3637
isGenerating = false,
38+
onStop,
3739
}: ChatInputProps,
3840
) {
3941
const { userId } = useHypr();
@@ -383,10 +385,18 @@ export function ChatInput(
383385

384386
<Button
385387
size="icon"
386-
onClick={handleSubmit}
387-
disabled={!inputValue.trim() || isGenerating}
388+
onClick={isGenerating ? onStop : handleSubmit}
389+
disabled={isGenerating ? false : (!inputValue.trim())}
388390
>
389-
<ArrowUpIcon className="h-4 w-4" />
391+
{isGenerating
392+
? (
393+
<Square
394+
className="h-4 w-4"
395+
fill="currentColor"
396+
strokeWidth={0}
397+
/>
398+
)
399+
: <ArrowUpIcon className="h-4 w-4" />}
390400
</Button>
391401
</div>
392402
</div>

apps/desktop/src/components/right-panel/hooks/useChatLogic.ts

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { message } from "@tauri-apps/plugin-dialog";
2-
import { useRef, useState } from "react";
2+
import { useCallback, useEffect, useRef, useState } from "react";
33

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

64+
// Reset generation state and abort ongoing streams when session changes
65+
useEffect(() => {
66+
// Abort any ongoing generation when session changes
67+
if (abortControllerRef.current) {
68+
abortControllerRef.current.abort();
69+
abortControllerRef.current = null;
70+
}
71+
72+
// Reset generation state for new session
73+
setIsGenerating(false);
74+
setIsStreamingText(false);
75+
isGeneratingRef.current = false;
76+
}, [sessionId]);
77+
6378
const handleApplyMarkdown = async (markdownContent: string) => {
6479
if (!sessionId) {
6580
console.error("No session ID available");
@@ -92,14 +107,14 @@ export function useChatLogic({
92107

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

95-
if (userMessageCount >= 3 && !getLicense.data?.valid) {
110+
if (userMessageCount >= 4 && !getLicense.data?.valid) {
96111
if (userId) {
97112
await analyticsCommands.event({
98113
event: "pro_license_required_chat",
99114
distinct_id: userId,
100115
});
101116
}
102-
await message("3 messages are allowed per conversation for free users.", {
117+
await message("4 messages are allowed per conversation for free users.", {
103118
title: "Pro License Required",
104119
kind: "info",
105120
});
@@ -266,6 +281,9 @@ export function useChatLogic({
266281
},
267282
});
268283

284+
const abortController = new AbortController();
285+
abortControllerRef.current = abortController;
286+
269287
const { fullStream } = streamText({
270288
model,
271289
messages: await prepareMessageHistory(
@@ -295,6 +313,7 @@ export function useChatLogic({
295313
client.close();
296314
}
297315
},
316+
abortSignal: abortController.signal,
298317
});
299318

300319
let aiResponse = "";
@@ -456,29 +475,42 @@ export function useChatLogic({
456475
setIsGenerating(false);
457476
setIsStreamingText(false);
458477
isGeneratingRef.current = false;
478+
abortControllerRef.current = null; // Clear the abort controller on successful completion
459479
} catch (error) {
460-
console.error("AI error:", error);
461-
462-
const errorMsg = (error as any)?.error || "Unknown error";
480+
console.error(error);
481+
482+
let errorMsg = "Unknown error";
483+
if (typeof error === "string") {
484+
errorMsg = error;
485+
} else if (error instanceof Error) {
486+
errorMsg = error.message || error.name || "Unknown error";
487+
} else if ((error as any)?.error) {
488+
errorMsg = (error as any).error;
489+
} else if ((error as any)?.message) {
490+
errorMsg = (error as any).message;
491+
}
463492

464-
let finalErrorMesage = "";
493+
let finalErrorMessage = "";
465494

466495
if (String(errorMsg).includes("too large")) {
467-
finalErrorMesage =
496+
finalErrorMessage =
468497
"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."
469498
+ "\n\n" + errorMsg;
499+
} else if (String(errorMsg).includes("Request cancelled") || String(errorMsg).includes("Request canceled")) {
500+
finalErrorMessage = "Request was cancelled mid-stream. Try again with a different message.";
470501
} else {
471-
finalErrorMesage = "Sorry, I encountered an error. Please try again. " + "\n\n" + errorMsg;
502+
finalErrorMessage = "Sorry, I encountered an error. Please try again. " + "\n\n" + errorMsg;
472503
}
473504

474505
setIsGenerating(false);
475506
setIsStreamingText(false);
476507
isGeneratingRef.current = false;
508+
abortControllerRef.current = null; // Clear the abort controller on error
477509

478510
// Create error message
479511
const errorMessage: Message = {
480512
id: aiMessageId,
481-
content: finalErrorMesage,
513+
content: finalErrorMessage,
482514
isUser: false,
483515
timestamp: new Date(),
484516
type: "text-delta",
@@ -491,7 +523,7 @@ export function useChatLogic({
491523
group_id: groupId,
492524
created_at: new Date().toISOString(),
493525
role: "Assistant",
494-
content: finalErrorMesage,
526+
content: finalErrorMessage,
495527
type: "text-delta",
496528
});
497529
}
@@ -516,12 +548,20 @@ export function useChatLogic({
516548
}
517549
};
518550

551+
const handleStop = useCallback(() => {
552+
if (abortControllerRef.current) {
553+
abortControllerRef.current.abort();
554+
abortControllerRef.current = null;
555+
}
556+
}, []);
557+
519558
return {
520559
isGenerating,
521560
isStreamingText,
522561
handleSubmit,
523562
handleQuickAction,
524563
handleApplyMarkdown,
525564
handleKeyDown,
565+
handleStop,
526566
};
527567
}

apps/desktop/src/components/right-panel/views/chat-view.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export function ChatView() {
6464
handleQuickAction,
6565
handleApplyMarkdown,
6666
handleKeyDown,
67+
handleStop,
6768
} = useChatLogic({
6869
sessionId,
6970
userId,
@@ -186,6 +187,7 @@ export function ChatView() {
186187
entityType={activeEntity?.type}
187188
onNoteBadgeClick={handleNoteBadgeClick}
188189
isGenerating={isGenerating}
190+
onStop={handleStop}
189191
/>
190192
</div>
191193
);

0 commit comments

Comments
 (0)